A composition can now sit in a scene like a container and play there.
Its items become real objects while they run, so behaviors, colliders
and events work on them as on any other object.
- A composition is placeable with its own symbol, transform and render
layer. The scene view shows its picture at frame 0. A click on that
picture selects the placement, not a single item.
- CompositionSystem spawns each item as an object under the placement
when it begins, with the id <placement>~<item>. On start and the
other events run. The object is destroyed when the item ends.
- The top track lies in front no matter which item started first.
World.arrange() orders the entity list and the scene objects.
- Clips and driven videos follow the same rules as compositionAt().
Loop, restart, stop and the game pause work.
- New nodes: Play composition (with restart), Stop composition and the
event On composition finished. They were planned for step 4 but come
now, because without autoplay nothing could play otherwise.
- The editor run ignores Play composition and says so in the log. It
writes into the saved project, and the items would stay behind.
- contentId() moved to instance.ts. The animation system uses it for
nested ids too.
- Compositions get no "Sort with parent" for now.
Tests: the gate passed with 301 files and 4643 tests, 18 more than
step 1. 13 of them are in tests/core/composition-play.test.ts. The
first gate run failed in event-order.test.ts, which counts the event
nodes and now finds 19. Five counter-checks each turned exactly their
test red. Two order tests stayed green without the fix at first and
were rebuilt. e2e 68 is new and passed with 5 own checks. Its
counter-check without arrange() failed as expected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two findings from #269. A reference from a container into a placement
nested in it was dead from the start, and deleting an asset left
references to the objects it took along showing "(missing)".
- A reference is stored the way its holder shows the object. In a
container tab that is q~x for content of a nested placement q.
- referenceCandidates translates it when the container is placed: in
placement r, q~x becomes r~q/x. pathOfId and idOfPath in instance.ts
do the mapping, objectChoices offers the same spelling the other way.
The rule holds at any depth.
- Storing q/x as proposed in #285 was dropped. The container tab would
then have had to translate its own ids.
- No migration. All projects and snapshots hold three references, none
of them with ~ or /.
- Create container lets a reference to content of a member placement
follow it. It was left dead before.
- Paste lets such a reference point into the copy. It kept pointing at
the original before. Both go through the new renamedReference.
- Delete asset records what every holder shows before the cascade and
clears every reference to an object that is gone afterwards, with
dropReferencesToGone from #269. The row shows None, undo restores
the references (#286).
Tests:
- Gate: 198 files, 2882 tests (2873 + 9), exit 0
- Unit object-references: 26 passed (17 + 9)
- e2e 1-17,57: 566 checks passed, 57 has +57i and +57j
- Six unit counter-probes fail between 1 and 3 tests each
- The browser counter-probe fails 57i and 57j as described in the
issues
- e2e total 1609 is computed, not measured in a full run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recurring cleanup round over the code since round 6 (056a622), 105
commits. Behaviour is unchanged. Duplicated code in src went from 29
clones with 229 lines (0.32 %) to 14 clones with 109 lines (0.15 %),
measured with jscpd and the same settings as round 6.
- BehaviorSystem.ts shrinks from 941 to 589 lines. Collecting the
pulses (keys, timers, collisions, animations) moved to
behaviorPulses.ts. The NodeContext the nodes receive moved to
behaviorContext.ts. Imports from outside stay the same, TimeScaled
is still exported from BehaviorSystem.ts.
- PointKindButton and drawPointGrip replace the "Point:" button with
its menu and the point and handle drawing that the curve and the
vector art tab each had on their own.
- lerpVec, cubicAt and splitCubic in src/core/util/vec.ts replace four
copies of the cubic curve and two de Casteljau splits.
- mapNested in Properties.ts is the one walk over lists and structs
that liveObjects and objectRefs did separately. withFieldRenamed
and withoutField replace three copies of renaming a struct field
and two of removing one.
- jsonBody and staleRevision build the JSON body and turn a 409 into
StaleRevisionError for all stores. The auth store turns a 401 into
its error in one place.
- Smaller helpers: structAssetFrom for the two "Make struct type"
commands, shownObject in the inspector, selectedItem for the size
gizmo and the polygon corners, solidTiles in the tilemap collision,
requireVariablesAsset and requireBehaviorVariable.
- Left as is on purpose: the plain forwarding from the collision and
physics systems to Solids, the pointer wiring of the canvases and
the identical last lines of the two make commands.
- Removed unused code: tilemapTileIds and tilemapsUsing were never
called since #128 and #131. EditorState.runtimeProject became unused
with GameRun in #266. The assetId getter of the make commands was
read only by a test.
- 60 values used only in their own file lost their export. The shader
and particle node catalogues stay exported as in round 6.
- Five comments that told history now describe the current state. The
doc comment of BehaviorSystem sits on the class again.
- README lists the struct, clip area and curve assets and mentions the
game link from #266.
Findings:
- Deleting a tile that is painted on a tilemap leaves its cells in
place, and they draw nothing. The delete dialog does not mention the
tilemaps. That was probably the job of the unused tilemapsUsing.
Left as is, because fixing it changes behaviour.
- The solid filter for tile surfaces was untested. A bent filter
stayed green. A new test in tests/core/surface.test.ts now fails
against it.
Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (one test removed with
tilemapsUsing, one added for tile surfaces).
- Full e2e run: ALL PASSED, 1580 checks.
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks, all passed.
The example project files were restored byte for byte afterwards.
- Counter-checks, 12, all fail as expected: 10 in unit tests (among
them mapNested, splitCubic, cubicAt, staleRevision, the stay pulse,
the late timer and the time scale), 2 in the browser (Point button
always enabled fails 47, selectedItem without a hit fails 29).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scene with a hundred crates fills the hierarchy with a hundred rows.
Groups bundle top-level objects under one folder row that folds away.
They exist for the editor only. Parenting, positions and the game do
not see them.
- Model: a scene or container template holds a list of groups, an
object may carry a groupId. Old projects load unchanged. Loading
drops empty groups and members that are no longer top level.
- Only top-level objects can join a group. Parenting a member takes it
out of the group, and Undo puts it back. There are no groups in
groups.
- Order: treeOrder lists the members together, where the first one
stands. The same order decides drawing at equal z, in the editor and
in the game, so the hierarchy and the picture cannot disagree. The
stored object list stays untouched when grouping.
- Hierarchy: a group row shows a folder icon, the name and the member
count. Clicking it selects all members and keeps it folded. Selecting
a single member elsewhere unfolds it. Folding lasts for the session.
- Create with "Group..." in the object menu. A name dialog asks first,
so creating is one Undo step and Cancel creates nothing.
- The group menu has Select members, Rename... and Ungroup. The object
menu adds "Remove from group".
- Drag and drop: onto the group row appends, into a gap between members
inserts there, next to a row outside leaves the group. Dragging the
group row moves all members as a block. The drop rules live in
hierarchyDrop.ts as a pure function.
- Copy and paste keeps a top-level copy in the group of its original.
A container built from group members takes their place in the group.
- Clicking an object that is already selected together with others now
keeps only that object selected. ListView narrows the selection on
release, so a drag starting on such a row keeps the selection.
Not checked in the browser: groups in the container tab, "Remove from
group", Ctrl-click on a group row and a drag out of a multi-selection.
The code path covers them.
Tests:
- check.sh: 177 files, 2552 tests, CHECK EXIT 0
- e2e 1, 51, 52: ALL PASSED, 31 checks (new module 52)
- 19 new unit tests in object-groups.test.ts and hierarchy-drop.test.ts
- 9 counter-probes, all fail as expected (7 unit, 2 in the browser)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.
Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing
Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
findPropertyUsages walks override sites, so Project.ts imported instance.ts
while instance.ts imported Project.ts – the repo's only value-level import
cycle. The queries now sit in usages.ts and Project.ts is a base type module
again. requireScene has had no caller since holderId replaced sceneId.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A variable could only be a number, a text or a yes/no, so every set of values had
to be smuggled through a string - which is where Tetris got its unreadable
"03066326048763714" colours from. A list is now a value of its own, with an item
kind (number, text, yes/no, colour) that decides how the inspector reads the one
comma-separated line it is typed as, and eight nodes to work with: List, Fill,
Count, Item at, Set item, Sub list, Join lists, Index of.
Two traps came with it. An array is typeof 'object', so every place that told a
value from an entity had to learn the difference, or a list would have been
passed around as a game object. And a value was compared by identity in three
places - with lists that marks everything as an overridden property after each
reload, so those comparisons now compare contents.
There is no list of objects: a list is written to the project file and an object
is a live handle that no reload survives. A set of objects stays a list of names.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Two regressions from the template-resolution rework, plus the harness gap
that hid them:
- `instanceTargetOf` decided from the shape of the id. Unpacking keeps the
derived ids, so unpacked content still counted as template content. It now
asks whether the object is spelled out in the holder.
- The parent picker captured its holder when the row was built. Since
`viewOf()` returns a derived copy, the menu listed the objects of that
moment (measured: 40 in the model, 17 in the menu). It resolves on open.
- `evaluate()` swallowed page exceptions, so a mutate that died half-way let
every later check pass or fail for the wrong reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scene now stores only the container object; its content is resolved from
the template on every read, with deviations kept as sparse instanceEdits on
the container object. This drops the propagation bookkeeping entirely --
templateSync.ts is gone and the three structural gaps (add, remove, reparent)
cannot diverge any more.
Values stay editable on an instance, structure does not; Unpack writes the
content into the scene, keeping ids. Play mode bakes containers into the
throwaway copy before entities are built. No migration: formatVersion is 2
and the seeders write the new format.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>