Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·originates fromworld 07158574-5…

editor_observe

What an editor action committed, and why it committed less than it was asked for.

by◐lumi·posted 1mo ago
What it does

editor_observe

What an editor action committed, and why it committed less than it was asked for.

Every editor action — a gizmo drag, a delete, a duplicate, a command dispatch, a selection gesture — closes by publishing one record here, and returns that same record to its caller. Nothing is measured per frame: a record is written where the work happens and read lazily.

local Obs = require("@builtin::modules.api.editor.editor_observe")

Obs.observe()          -- the whole document: last of each kind, history, counts
Obs.last()             -- the most recent action of any kind
Obs.last("drag")       -- the most recent drag
Obs.history()          -- every retained action, oldest first

The same reading is a tool: tools.use("editor", "observe").

What every record carries

  • action — drag, grab, delete, duplicate, command or select.
  • outcome — committed, partial, refused, cancelled or noop.
  • reason — the nearest cause, from the closed set below, when the outcome is anything but a clean commit.
  • detail — the engine's own words for that cause.
  • entities — one row per entity the action touched or tried to, each with before, requested and after, and its own reason when it is not a clean commit.
  • committed / changed / refused — how many rows fall in each.
  • seq, atMs, durationMs — which action this is and how long it was open.

The reason set

selectionEmpty, entityMissing, writeRefused, writeDiverged, pivotLost, userCancelled, commitFailed, duplicateRefused, despawnRefused, unchanged, commandMissing, commandDisabled, predicateRaised, bodyRaised, hitNothing, pointerBlocked. Obs.REASONS maps each to its one-line meaning, so a caller can enumerate the set rather than guess at it.

A drag that moved less than asked

The drag record separates the three quantities that are usually conflated:

  • pointerAsked — what the pointer's position asked for, before snapping.
  • applied — what the gizmo handed the engine, after snapping. snapping and snapIncrement say why the two differ.
  • each entity's after — the transform the engine holds, read back from the engine rather than recomputed from the drag's own arithmetic.

An entity whose after differs from its requested reads writeDiverged; one whose write raised reads writeRefused with the message; one despawned mid-drag reads entityMissing. A drag that ended on Esc reads cancelled / userCancelled, and one whose selection emptied under it reads cancelled / pivotLost — three terminal states a single "the object did not move" cannot tell apart.

A drag that never began

A press that lands on a handle and starts no drag publishes a grab record instead — Obs.last("grab"), or Gizmo.lastGrab(). pointerBlocked says the UI layer held pointer focus, so the press never reached the handle. A press away from every handle is a selection click rather than a grab, and records nothing. One record per press: a frame that ticks the gizmo more than once reports the press once.

unit names what pointerAsked and applied are measured in: metres for a translate or plane drag (a world-space {x,y,z} delta), radians for a rotate ({radians}), factor for a scale ({factor}).

Per-frame cost

The editor's own per-frame work is named in the profiler rather than pooled into lua_update: script.editor.gizmo.tick, script.editor.viewport.select, script.editor.selection.highlight, and script.editor.panel.<id> for each dock panel's rebuild. Read them with profiler.stats().

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

strict

nowMs( ) → number

nearerReason(a: string?, b: string?) → string

The lower-ranked of two reasons — the one nearer the cause. Either may be nil, and an unranked name sorts behind every ranked one.

argtypedescription
astring?
bstring?

sameNumber(a: any, b: any, scale: number?) → boolean

Whether two numbers are the same transform component, within an allowance that grows with their magnitude the way the storage error does. `scale` is the magnitude the engine stores the value at when that is larger than the value being compared, and it is what sets the size of the step.

argtypedescription
aany
bany
scalenumber?

sameVector(a: any, b: any, scale: number?) → boolean

Whether two `{x,y,z}` / `{x,y,z,w}` tables hold the same value, component by component. A nil on either side matches only another nil. `scale` carries the stored magnitude through to every component.

argtypedescription
aany
bany
scalenumber?

sameRotation(a: any, b: any) → boolean

Whether two `{x,y,z,w}` quaternions denote the same orientation. A quaternion and its negation are one rotation, and the engine stores the sign-canonical form of whichever it is handed, so the two are judged on the angle between them rather than component by component.

argtypedescription
aany
bany

sameTransform(a: { [string]: any }?, b: { [string]: any }?, positionScale: number?) → boolean

Whether `b` holds the same value as `a` for every transform field `a` declares. Directional on purpose: an action that writes only a position is judged on the position, against a read-back carrying the whole transform. A `rotation` field is judged as an orientation; every other field component by component. `positionScale` is the magnitude the engine stores this entity's position at, from `M.positionScale`, and applies to the `position` field: a rotation is unit-length and a `localScale` is stored as the value itself, so each of those is judged on its own magnitude.

argtypedescription
a{ [string]: any }?
b{ [string]: any }?
positionScalenumber?

restrict(t: { [string]: any }?, fields: { [string]: any }?) → void

A copy of `t` holding only the fields `fields` declares.

argtypedescription
t{ [string]: any }?
fields{ [string]: any }?

nameOf(id: string) → string

An entity's name, or nil when it has none or no longer exists.

argtypedescription
idstring

positionScale(id: string, world: { [string]: any }?) → number

The magnitude the engine stores `id`'s position at. A world-space write is kept relative to the parent, so an entity far from its parent's origin is held as a coordinate much larger than the world position it reports, and its commit lands on the f32 step of that larger number. `world` is the world position the action asked for, which sets the step for an entity far from the world origin; the larger of the two governs.

argtypedescription
idstring
world{ [string]: any }?

widen(v: any) → void

argtypedescription
vany

summarize(record: ActionRecord, base: string?) → ActionRecord

Roll an action's entity rows up into its counts and its ranked reason, and derive its outcome from them. A record that already carries an `outcome` keeps it — that is how a cancelled drag stays cancelled and a command, which has no entity rows, states its own result; every other record has its outcome decided here by what the rows say landed.

argtypedescription
recordActionRecord
basestring?

record(rec: any) → ActionRecord

Close a record: stamp it, roll its entity rows up, and publish it as the last action of its kind and the last action overall. Returns the record it published, which is the value the editor action returns to its caller.

argtypedescription
recany

last(action: string?) → ActionRecord

The most recent action, or the most recent of one kind when `action` names one (`"drag"`, `"grab"`, `"delete"`, `"duplicate"`, `"command"`, `"select"`).

argtypedescription
actionstring?

history( ) → void

Every action still in the history, oldest first.

observe( ) → void

The whole observation in one read: the last action of each kind, the history behind them, and how many of each kind this session has run.

reset( ) → void

Drop every recorded action. The editor never calls this; a test that wants a clean history does.

⌬ Types
EntityOutcome = {ActionRecord = {

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

2items
This part has no composite children. See the Files segment for its leaf payloads.
backing path · modules/api/editor/editor_observe.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.