7
Containers
meleki edited this page 2026-10-01 07:28:57 +02:00

The Container tab shows one container's template with the same viewport the Scene tab uses — ContainerCanvas subclasses SceneViewport and swaps nothing but which asset holds the objects, so picking, rubber band, dragging, gizmos, labels and parent lines are not written twice. It adds a cross at the origin, because that is the point the container is held by when placed: where the content sits relative to it is the thing being authored. A template has no background of its own — painting one would invent the scene it is not in.

The Scene objects panel lists whatever the visible tab edits, so in the Container tab it lists the template, with the same drag-to-parent, context menu and folding as a scene; the inspector, Delete and Ctrl+G work on template objects like on placed ones. None of that is a special case: a scene asset and a container asset are both ObjectHolders, and every object command takes a holderId that is either one.

Header bar: Fit (frames the content, origin always included) and the size of the template as "N objects". Placing needs no button of its own: an asset dragged from the panel onto this view goes into the template, and a container dragged onto its own template is refused while it hovers rather than at the drop. Each container opens as a document in a tab of its own (see The tab row in Concepts); several can be open at once. Selecting a template object brings its container's tab up, the rule a graph node and a texture frame already follow: it exists in no other view. Selecting the asset itself does not; double-clicking it does.

Placing a container anywhere creates one object. Its content is never copied: resolveObjects (src/core/model/instance.ts) weaves the template in whenever the holder is read, one level per nesting step, and the resolved objects get derived ids — <instance id>~<template id>, joined by / for a container inside a container. That is what the hierarchy lists, what the viewport draws and what a command addresses. A container that reaches this one, directly or through another, is 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 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.

A removal reaches past the holder it happens in when something else names what goes (#269). RemoveSceneObjectsCommand takes what every holder shows before and after (shownObjectIds, the resolved ids) and empties each object reference that named an object shown before and gone after (dropReferencesToGone in core/model/objectRefs.ts). A reference is looked up from where it sits, with referenceCandidates, just as the running game does, so the same rule covers the template, a placement's edits in any scene or container, nested placements and full derived ids on objects outside. An emptied edit that now equals its base is dropped like any other. All of it goes through the command's Recorder, so one undo brings every reference back. DeleteAssetCommand does the same around its cascade (#286), so deleting an asset empties the references to every object it takes along.

A reference is stored as the holder that holds it shows the object (#285). In a container D, the content x of a placement q nested in D shows as q~x, and that is what a pick in D's tab stores. In a placement r of D the same object is r~q/x: the first ~ of the reference becomes a / (pathOfId in core/model/instance.ts) before the scope of the placement goes in front. The inspector's choices on a placement go the other way (idOfPath), so the template and its placements spell the same object the same way. Before #285 the lookup tried r~q~x, and such a reference was dead from the start. renamedReference lets a reference to q~x follow when q is renamed: by Create container (to q'~x inside the new template, to c~q'/x from outside) and by pasting q together with the object holding the reference.

Two places do spell the content out. Unpack does it on purpose — the objects become ordinary ones and stop following. And play mode bakes every placed container into stored objects, in the throwaway copy of the model, because a running behavior may destroy one of them and a resolved object has nowhere to remember that. Both keep the derived ids, so the selection and a check script survive them. A world built without baking (the editor run, WorldOptions.bakeContainers) runs that content all the same and writes through objectRef into the instance edits; only a removal has nowhere to go there.

Screens without scene switching

A screen (title, win, lose, a pause overlay) is just a container whose enabled is switched on and off. It keeps working while it is off in one important way: Object by name still finds it, so a graph can switch it back on. A behavior inside a disabled screen has no interpreter yet — it gets one, and its On start, the moment the screen is switched on, which makes "the screen initialises itself when shown" the default. Its variables survive being switched off and on again; every later show fires On enable, every hide On disable (see Behaviors, Lifecycle). A switched-off object is hidden in the Scene tab as well, so the editor shows what the game shows — selected, it keeps a faint outline, so where it sits is still visible, but it cannot be dragged. It stays listed in the hierarchy panel, which is where it is selected and switched back on.

Restart game throws the run away and starts the scene again from the saved model — what the "Play again" buttons of the example game do.