World & props
Entity gizmos
Build native 3D placement editors with Open77.gizmos: Lua functions, target capabilities, snapping, undo/redo, lifecycle events, safe restoration and server-authorized edits. Development-build feature with documented validation limits.
On this page9 sections
Open77.gizmos is a resource-owned, native 3D manipulation service. Use it for
placement editors, spawn-point tools, vehicle staging, scene composition or a
custom editor that listens to transform changes. Handles are drawn by the native
overlay, not a WebUI page. Opening a gizmo does not pause the server.
Permission and execution side
permissions { 'gizmos.edit' }
All methods require this permission, including queries. The service exists on the client only. It is available to bundled and server-downloaded client resources; handles and events belong to one exact resource VM and generation. A resource cannot control another resource's handle, including a same-named resource on a different host. Handles are invalid after resource stop/reload or world teardown.
The permission grants local editor access, not server authority. Never accept a client transform on the server without checking staff rights, ownership, distance, routing bucket, target lifetime and value bounds.
Quick start: a virtual placement point
local G = Open77.gizmos
local handle
-- Call from your editor's open action, after releasing any WebUI cursor focus.
local function editPoint(position)
if handle then G.destroy(handle); handle = nil end
local why
handle, why = G.create({
transform = { position = position },
mode = 'translate', space = 'world',
snap = { translation = 0.25, rotation = 15, scale = 0.1 },
timeoutMs = 120000,
})
if not handle then return false, why end
local ok, error = G.activate(handle)
if not ok then G.destroy(handle); handle = nil end
return ok, error
end
AddEventHandler('open77:gizmo:change', function(state)
if state.id ~= handle then return end
-- Update a placement ghost or numeric inspector here.
-- state.transform.position, .rotation and .scale are full values.
end)
AddEventHandler('open77:gizmo:commit', function(state)
if state.id ~= handle then return end
local p = state.transform.position
print(('Selected point: %.3f, %.3f, %.3f'):format(p.x, p.y, p.z))
G.destroy(handle)
handle = nil
end)
A virtual target transforms data only. It does not spawn or move an entity.
The transform defaults to identity rotation and unit scale; position defaults
to {x=0,y=0,z=0}. Give it an explicit position in front of your editor camera.
Target identities and capabilities
local target = { kind = 'vehicle', id = vehicleId }
local caps, error = Open77.gizmos.capabilities(target)
local id, error = Open77.gizmos.create({ target = target })
kind |
id refers to |
|---|---|
transform |
No entity; omit id or use 0; requires transform |
entity |
Receiver-local Open77 entity-registry handle; 0 is own player |
puppet |
Receiver-local entity handle, checked to be a puppet |
player |
Network player ID; 0 means own player |
npc |
Server NPC ID |
vehicle |
Server vehicle ID |
prop |
Server prop ID |
localProp |
Local decorative prop ID owned by the calling resource |
IDs accept unsigned decimal strings (full 64-bit) or exact safe numbers up to
9007199254740991. Snapshot target IDs are always decimal strings. Keep IDs from
existing APIs as strings; never use tonumber to round-trip a network ID. Engine
pointers, TweakDB record names and engine EntityIDs are not accepted identities.
The target must be streamed/attached when created and activated. The same native
incarnation is checked before changes and at every active tick; a replacement is
not silently edited. Attached props and occupied vehicles are refused. Vehicle
occupancy checks include native NPC passengers and mounting relations, not only
network player seats; a generic entity alias cannot bypass them. An unavailable
occupancy query is a refusal, not evidence that a car is empty. A player
must leave vehicles/workspots/stand-in animations before entering the editor.
Capabilities are returned as:
{
translate = {x=true, y=true, z=true},
rotate = {x=false, y=false, z=true},
scale = {x=false, y=false, z=false},
preview = true, commit = false, networked = true,
}
preview means the native adapter can move the target locally. commit means
it can retain that change locally after accepting. networked means the target
has a replicated identity. A network target's commit=false does not disable
the commit() method: acceptance emits a proposal for your server logic and
requests restoration of the local preview. An own-player teleport is queued:
the commit event is a proposal, not proof that the native body has finished
restoring or that a server accepted the proposed placement.
For own-player previews, the client keeps sending the last normal position as
an idle keepalive, never the editor position. When editing ends, movement stays
blocked briefly until independent native reads confirm restoration. Starting a
new gizmo during this interval returns gizmo_restore_pending. A newer normal
teleport takes priority (travel_superseded); a restoration that fails for five
seconds ends a network session with gizmo_restore_failed rather than publishing
the preview pose. Completion is reported separately through settled/error;
the validation status above remains applicable.
Vehicle previews similarly fence native motion capture and use baseline
keepalives for parked owners. Restoration keeps the editor's physics hold until
the destination is observed, then observes the resumed rigid body before
reopening motion capture. A server freeze or observer mask remains independent.
An unsuccessful five-second vehicle restoration ends a network session with
gizmo_vehicle_restore_failed. commit, cancel and deactivated events do
not acknowledge completion of these asynchronous restoration fences; wait for
settled/error and read nativeTransform as described below. Motion fencing is
not a collision sandbox: avoid previewing a vehicle through nearby actors, and
reserve a clear editing area on the server. Local collision isolation is still
under investigation.
An explicit server vehicle placement (Open77.vehicles.setTransform) takes
priority over an active preview: cancel and deactivated carry
reason='transform_superseded', and no commit proposal is emitted. If placement
arrives just after Enter/commit(), while native restoration is still pending,
the fence switches to the new server destination instead of restoring the old
origin. This is not a server lock: your editor must still validate ownership,
revision and occupancy before accepting any proposed transform.
An NPC enterVehicle task also takes priority: it cancels a vehicle preview with
vehicle_entry_requested, then waits for native restoration before attempting
the mount. This guard is in the client NPC task driver, not a global seat lock;
reserve the vehicle on your server when other players or native AI could board.
With preview=false, only editor data changes: the native car is never moved or
frozen, and accepting it still requires your own server validation. There is no
asynchronous settled event for that callback-only mode.
Capabilities are refreshed before activation. If the target's capabilities or
authority model change during an edit, writes/commit fail with
target_capabilities_changed, and the next tick cancels that edit. Explicitly
reactivate after inspecting the new capabilities; a stale handle never retains
the former ability to commit locally.
Current native adapters provide XYZ translation; puppets, players and managed props support yaw rotation only. Vehicle and generic non-puppet root transforms can use quaternion rotation. Arbitrary native entity scaling is not exposed as a working capability: REDengine render proxies and physics do not share a safe live scale setter. Virtual targets support all three scale axes and uniform scale, letting an author connect the result to their own rebuild/resize API. Do not treat a scaling handle as evidence that a physical collider was resized.
Options
{
target = {kind='transform', id='0'}, -- default target
transform = { -- virtual targets only
position = {x=1, y=2, z=3},
rotation = {x=0, y=0, z=0, w=1}, -- normalized quaternion, not Euler
scale = {x=1, y=1, z=1},
},
mode = 'translate', -- translate | rotate | scale
space = 'world', -- world | local
axes = {x=true, y=true, z=true},
snap = {translation=0, rotation=0, scale=0},
pivotOffset = {x=0, y=0, z=0}, -- target-local offset
minimumPosition = false, -- false clears a bound
maximumPosition = false, -- or {x=...,y=...,z=...}
minimumScale = 0.01,
maximumScale = 100,
preview = true,
visible = true,
size = 100, -- 32..240 logical pixels at 1080p
timeoutMs = 300000, -- 1000..1800000, starts at activation
}
All values must be finite. Positions are bounded to Β±1,000,000; quaternion length
must be within 0.001 of one. Scale components must be 0.01..100 and positive.
Unknown option keys, malformed vectors and invalid enum names are errors, not
ignored fallbacks. Sparse axes/snap patches preserve unspecified values.
Translation snap is in metres (0..1000); rotation is in degrees (0..180); scale snap is a relative factor (0..10). Zero disables that snap. Snapping is relative to the transform at drag start, avoiding accumulated rounding errors. Position bounds are world-space. Scale uses local axes even in world mode, because non-uniform world scale would require shear outside the transform format.
axes selects pointer handles, while capability checks constrain both pointer
and programmatic edits. setTransform intentionally does not snap the supplied
numeric value or restrict it to the selected manipulation mode.
API reference
Queries return a value or nil, reason. Mutations return true or false, reason.
| Method | Result / effect |
|---|---|
create(options) |
Returns opaque string handle; does not capture input |
get(id) |
Full snapshot of one owned handle |
list() |
Array of all this VM's handles, including completed handles |
active() |
This VM's active snapshot, or nil (never another owner's) |
capabilities([target]) |
Capability object; omitted target means virtual |
activate(id) |
Acquire exclusive input, read fresh target pose, reset history |
update(id, patch) |
Change mutable options; refused during a pointer drag |
setTransform(id, patch) |
Edit active transform; one undo entry; refused mid-drag |
undo(id) / redo(id) |
Apply previous/next accepted edit in bounded history |
commit(id) |
Accept proposal, release input and emit result |
cancel(id) |
Restore initial preview and release input |
deactivate(id) |
Alias for cancellation, not implicit acceptance |
destroy(id) |
Cancel if active, remove handle and free its quota |
update accepts mode, space, axes, snap, pivotOffset, position/scale
bounds, visible, and size. Target, initial transform, preview policy and
timeout are fixed at creation. An active gizmo cannot be hidden. Unsupported
modes fail with mode_not_supported_by_target.
setTransform accepts a sparse {position, rotation, scale} patch. Present
vectors/quaternions must be complete. It validates capabilities and bounds and
reports native write failures before changing the logical transform.
There can be eight handles per VM, 64 total and one active gizmo per client. Undo/redo retains 32 edits; one pointer drag is one edit, not one per frame. Accepting/cancelling does not destroy the handle. Re-activation resets the edit baseline/history; destroy completed handles when your editor no longer needs them.
Built-in controls
| Input | Action |
|---|---|
| Left-drag axis arrow / plane | Constrained translation |
| Left-drag centre square | Camera-plane translation or uniform scale |
| Left-drag rotation ring | Rotation around the selected axis |
T / R / S |
Translation / rotation / scale, if supported |
L |
Toggle local/world axes |
Hold Shift |
Precision movement (0.1 multiplier) |
Hold Alt |
Temporarily bypass snap |
Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z |
Undo / redo |
Enter |
Accept current transform, including an active drag |
Escape while dragging |
Revert only this drag; keep editor open |
Escape otherwise |
Cancel entire edit and release input |
An inactive handle captures no input. Activation refuses an existing keyboard or cursor owner. A WebUI menu taking focus, focus loss, player invalidation or camera loss cancels the editor and releases only its own restrictions. Other resources' action blocks, server freezes and observer physics must remain intact.
Events and snapshot shape
Register with AddEventHandler('open77:gizmo:<name>', function(state) ... end).
Every event supplies one snapshot, with the same fields as get(id).
| Event suffix | Meaning |
|---|---|
created |
Handle allocated |
activated |
Input acquired; fresh baseline captured |
configured |
Options changed |
hover |
Hovered handle changed |
dragStart |
Pointer drag begins |
change |
Valid transform applied; also used by undo/redo and drag cancellation |
dragEnd |
Drag ended; reason released, cancelled or committed |
undo / redo |
History operation completed |
commit |
User/script accepted; network authority is still the server's job |
settled |
Asynchronous local puppet placement or own-player/vehicle restoration was observed on the same native body |
cancel |
Edit cancelled; asynchronous restoration may still be pending |
targetLost |
Stream-out, invalidation, incarnation or capability change |
error |
Native restoration/release reported failure |
deactivated |
Editor/cursor released after commit/cancel/error; native restoration may still hold movement |
destroyed |
Handle removed |
Snapshot fields: id, phase, reason, revision, target, capabilities,
transform, initial, nativeTransform, mode, space, axes, snap, pivotOffset,
minimumPosition, maximumPosition, minimumScale, maximumScale, size,
timeoutMs, active, dragging, visible, preview, hover, handle,
undoCount, redoCount. Absent position bounds are false. Handle names are
none, x, y, z, xy, yz, zx, screen, uniform.
Phases are idle, editing, committed, cancelled, failed. Native pointers,
VM cookies and incarnation tokens are never included. revision orders snapshots
for one handle and advances on events; it is not a server entity revision.
Events are local and delivered on the resource host's next dispatch. The native
queue is bounded to 512 snapshots; consecutive change/hover events can be
coalesced, and an abusive create/destroy loop can overflow old lifecycle events.
Use get/list for current state, not an assumption that every rendered frame
produces a separate event. Stop/reload clears the retiring VM's pending events.
Local puppet commits use a queued native NPC teleport after releasing the editor's
mover hold. commit/deactivated acknowledge acceptance, not native completion.
Wait for settled (native_transform_settled) or error
(gizmo_puppet_placement_failed / target_replaced) before treating that placement
as observed. The watch lasts at most five seconds and blocks new activation with
gizmo_restore_pending, but does not keep the player's input captured. Destroying
the handle or stopping its resource ends retries and future completion delivery.
Own-player and vehicle previews also emit settled after native restoration,
including on cancellation. Vehicles must pass both frozen-placement and resumed
physics readbacks. reason identifies what was observed:
| Reason | Meaning |
|---|---|
native_transform_settled |
Local placement/restoration completed |
native_preview_restored |
Network preview restored; this does not accept the proposed edit on the server |
authority_transform_settled |
A newer normal travel/server placement superseded the destination and was observed |
nativeTransform is false until successful asynchronous completion, then a
transform table with independently read native position/orientation and the
known unchanged scale. It is included in get() and events, and cleared on
reactivation. transform remains the edit proposal (or initial baseline on
cancel); do not use it as proof of the final native position. For example:
AddEventHandler('open77:gizmo:settled', function(state)
print(state.reason, state.nativeTransform.position.x)
-- Server acceptance needs its own response, even after this event.
end)
Native timeouts report gizmo_restore_failed / gizmo_vehicle_restore_failed;
body replacement reports target_replaced. Failed readbacks have no
nativeTransform. Stopping the resource/destroying the handle suppresses its
future events; network preview cleanup still finishes safely. No settled event
is generated for virtual edits, callback-only edits, synchronous prop edits or
network NPC/remote-player previews. Completion is an observation, not a promise
that an autonomous NPC or a physics vehicle will remain stationary afterwards.
Authoritative server integration
See the complete opt-in server-authorized prop editor for the client/server implementation and runnable rejection tests. Its policy has automated coverage and a one-client/server pointer-driven acceptance run. This does not by itself prove cross-client presentation or persistence.
- The server checks permission and creates a short-lived edit ticket bound to player identity, resource, target and its current revision/routing bucket.
- It reserves/freezes the target as appropriate and asks that client to open it.
- The client previews locally. Edited player motion is replaced by baseline keepalives; parked-owner vehicle motion likewise uses baseline keepalives, while locally simulated NPCs send stationary baseline keepalives and suspend client task advancement. Terminal NPC task reports are also baseline-fenced; they must never submit the edited body as canonical placement. This is not a server freeze. Other clients continue receiving canonical state. A newer server NPC/prop pose, attachment/body change, NPC task or authority change preempts the preview. Cancellation runs before normal replication applies that state, so the old preview cannot overwrite it. Observer motion from another NPC authority likewise preempts an edit when the pose changes.
commitsends the ticket and transform back. Treat this as untrusted input.- The server revalidates the ticket, identity, target, revision, finite values,
bounds and distance, then calls its existing
players/npcs/vehicles/propsmutation API. It acknowledges success or failure and always releases its lease. - Cancellation, disconnect, resource stop and ticket expiry release the server lease. A client-side timeout is not sufficient server cleanup.
Do not stream every change over the network by default. Keep previews local;
send accepted results or a deliberately rate-limited collaborative-editor stream.
Native cancellation attempts restoration but cannot recreate a streamed-out or
deleted target. Such failures still release input and surface an error event.
Common errors
permission_denied:gizmos.edit: add the manifest permission to a trusted editor.gizmos_unavailable_on_this_host: client feature absent / server or validation host.gizmo_not_owned,gizmo_not_found: wrong VM/generation or stale/destroyed handle.gizmo_input_busy: another editor or menu owns input; release its focus first.gizmo_restore_pending: own-player/vehicle restoration or local puppet placement is still being observed; retry later.game_not_focused,player_not_alive_in_world: wait for a valid in-world player.target_not_streamed,target_not_attached,target_replaced: query the target anew.local_prop_not_owned_or_missing: local props can only be edited by their owner.target_vehicle_occupied,target_is_attached: leave occupants/attachments intact.target_vehicle_occupancy_unavailable: native occupancy could not be verified; retry when the vehicle is fully attached, never assume it is empty.mode_not_supported_by_target,*_axis_unsupported: use capability reporting.target_capabilities_changed: the active target's editing/authority model changed; cancellation follows, then inspect capabilities before reactivating.position_out_of_bounds,scale_out_of_bounds: value outside configured limits.drag_in_progress,gizmo_not_active,nothing_to_undo,nothing_to_redo: state-dependent refusal; refresh your inspector fromget.
No method silently grants admin rights, changes a routing bucket, detaches an occupant, deletes a target or persists server data.
// For agents & LLMsthis page as Markdownllms.txtllms-full.txt
