Measure a deviation against the base, not the innermost template

An instance edit was dropped as soon as its value matched the template
object. With nested containers the template is not what the value falls
back to: the outer container holds an edit for the object below it, and
that one wins. If the inner template happened to carry the same value the
user typed, the edit was thrown away and the value jumped to whatever the
container in between says.

Reported against the Jump-and-Run project: a coin narrowed to 8 inside
"level", then set back to the sprite's own 24 in the scene, snapped back
to 8. Reproduced through the inspector before touching anything.

InstanceTarget now offers `base`: the template with the edits of every
container in between woven in. Everything that judges deviation uses it -
setOverride/setName/setPosition, both Revert commands, and the gold marks
in the inspector. Writing still goes into the real template, which is
where "apply to container" belongs.

`base` is built on every read. Apply writes into the template and then
asks whether the edit still deviates, so a base captured beforehand would
answer for the template as it was - the e2e suite caught exactly that.

Also in here, because it corrects the commit before it: placing an asset
by double-clicking its row is gone, as asked for in asset-doppelklick.md.
Dragging is the only way to place now. That took AssetPanel.place, the
PlacementTarget interface, Editor.placementTarget and the two worldCenter()
methods that existed only to feed it. A double-click on a scene asset still
makes it the active scene.
This commit is contained in:
root committed 2026-09-09 13:45:27 +02:00
1 parent 75d377af39
commit d7c8d3e579
16 files changed
+180 -77

No files matched your search

+9 -4
View File
@@ -21,7 +21,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| **Collider** | A shape (box or circle) for collision detection, placed as a **child** of the object it belongs to (its *body*). Drawn as a green outline in edit mode only. |
| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. |
| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. |
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is always the **template**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
| **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. |
| **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. |
| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** – the saved model is never touched. |
@@ -120,7 +120,8 @@ be refused, which today means a container that would end up inside itself (`plac
`src/core/model/placement.ts` – the same function the command runs, so the preview cannot promise
a placement the drop then denies). Picking a container up selects it, but the Container tab does
*not* follow that while a drag is in flight: the target must not swap out from under the pointer.
A double-click on the row still places at the middle of the visible view.
Dragging is the only way to place: a double-click on a scene makes it the active one, on anything
else it does nothing.
**Editor symbols are always on top.** The viewport draws scene content (sprites) in an
`objects` layer and editor symbols (camera icon, labels) in an `overlay` layer above it, so a
@@ -319,8 +320,12 @@ still refused – that cycle would resolve forever.
So **editing a template changes every instance**, structure included: add, delete or re-parent a
template object and every placed container shows it immediately. What an instance may deviate in
is values, and only per slot: an override key, one position axis, the name. Those live in
`instanceEdits` on the container object and are dropped again the moment the value matches the
template, which is what keeps "untouched follows, touched keeps" exact without any bookkeeping.
`instanceEdits` on the container object and are dropped again the moment the value matches its
base, which is what keeps "untouched follows, touched keeps" exact without any bookkeeping. That
base is the template **with the edits of every container in between woven in** (`InstanceTarget.base`):
inside nested containers the innermost template is not what a value falls back to, so comparing
against it would drop an edit that does deviate – and the value would jump to the value of the
container in between.
The other direction is therefore just as small: **Apply to container** writes the value into the
template and drops the edit, **Reset to container** drops the edit alone.