Skip to content

Scope entity references to the enclosing entity hierarchy - #430

Merged
willeastcott merged 2 commits into
mainfrom
scoped-entity-references
Aug 30, 2026
Merged

Scope entity references to the enclosing entity hierarchy#430
willeastcott merged 2 commits into
mainfrom
scoped-entity-references

Conversation

@willeastcott

Copy link
Copy Markdown
Contributor

Fixes #429

Entity references resolved document-wide, so a <template> whose content cross-references its own entities could not be cloned more than once: every clone's references bound to the first instance's entities — silently, because the lookup succeeds, on the wrong element. The unresolved-reference warnings from #425/#426 cannot see this failure class at all.

The contract

An exact scoped entity-name match wins first; otherwise the existing document resolver interprets the reference as selector, id, or name — unchanged.

When an element resolves a reference, an exact pc-entity name match is looked up lexically through the entity hierarchy first: the closest entity-fronting ancestor's inclusive subtree, then each outer entity-fronting ancestor, then the containing <pc-app>. Only then does the existing document-wide selector/id/name triple run, verbatim, as the fallback (and it is the entire behavior when no resolving element is supplied).

Deliberate consequences:

  • The scoped phase queries only pc-entity[name=...], so a non-entity element can never block or divert it, and selectors/ids keep their document.querySelector/getElementById meaning in practice (an entity whose exact name is the ref text scopes, by design — the docs say so rather than claiming selectors "never" scope).
  • The inclusive subtree check lets a clone's descendant name the clone's own root.
  • A template prefab with internal references needs one root pc-entity — the <template> boundary disappears after cloning, so a multi-root clone's names would resolve through the shared parent into an earlier clone. Documented on the elements that resolve references.
  • Nearest-name-first also fixes two long-standing traps for in-scope names: a page-level id shadowing an entity name, and a name like Head losing to <head> matched as a type selector.

No timing changes: a cloned subtree's entities are all created synchronously during appendChild (tree-order upgrade), while component elements defer behind ready(), so every entity in a clone exists before any reference resolves — regardless of order inside the template.

What changed

  • findEntityElement/getEntity gain an optional from: Element; resolveEntity requires it, derives the message tag from it (byte-identical at all 16 call sites), and now looks up once instead of twice on the failure path. All warning strings are unchanged.
  • The 16 resolveEntity call sites (pc-joint, pc-button, pc-scrollbar, pc-scroll-view) pass this; the script Conversion type carries the declaring element so entity: references scope the same way.
  • pc-node already scopes its name search to the enclosing subtree in the entity graph — this extends the same semantics to DOM references.

Behavior changes

Only bare-name references with a resolving element can resolve differently, and only when the name is ambiguous today:

  • A duplicated entity name resolves to the nearest enclosing entity scope instead of first-in-document — the requested semantics.
  • A bare ref shadowed cross-form (far id, or an HTML tag name as type selector) resolves the in-scope entity name first.
  • A nearest same-name pc-entity backing no entity yet wins over a farther live one, reported with the existing timing cause rather than silently skipped (pinned by a test, so a future "helpful" fallback cannot land silently).

Unique references — the overwhelming case, including all shipped examples — resolve identically.

The example (second commit)

The AR Wiener Storm chain — six capsule segments and five 6dof flex joints, previously ~90 lines of addComponent calls — becomes a <template> wired by bare names, cloned per throw. The script scales each clone to its random size before it upgrades, appends it under the owning app's <pc-scene> (entities parent to the application root, keeping the spawn world-space), and launches once the clone's components report ready. The spawn is guarded: clones stay in a pending set until the launch commits, readiness is raced against a destroy promise (a removed element's ready() never settles), and teardown removes pending and live clones alike.

Verified live in the browser (?sim mode): four concurrent clones, every joint bound within its own clone (none cross-bound), all constraints created, adjacent-segment gaps matching authored spacing × each clone's scale, hits registering, and retirement keeping DOM elements, engine entities and the live list in lockstep.

Test plan

  • get-entity.test.ts: nine new scoped-resolution cases — nearest-scope-first, inclusive scope root, the <pc-app> ring (distinguished from the document by an entity-less decoy ahead of the app), in-scope name over document id, selector/id forms staying document-wide from inside a scope, the no-skip rule with its verbatim warning, document fallback (connected and disconnected callers), an inexpressible-in-CSS reference surviving the scoped phase, and an escaped name resolving through it.
  • template-clone.test.ts: the headline case — a prefab cloned twice, each clone's joint bound to its own entities and explicitly not the first clone's (fails against the old resolver, verified).
  • pc-joint and pc-script each gain a nearest-enclosing-match case (init + setter paths; entity: via the attributes JSON).
  • All pre-existing warning-message assertions pass unchanged. Full suite: 1035 tests across 49 files; lint, type-check and build (CEM validation) clean.

🤖 Generated with Claude Code

willeastcott and others added 2 commits August 31, 2026 00:07
An exact entity-name match now resolves lexically through the entity
hierarchy first - the closest entity-fronting ancestor's inclusive
subtree, then each outer one, then the containing pc-app - before the
existing document-wide selector/id/name lookup runs unchanged as the
fallback. A <template> prefab with one root pc-entity can therefore
wire its joints and script attributes by bare name and stay
self-contained when cloned: every clone's references bind within
itself, where the document-wide lookup bound them all to the first
instance.

The scoped phase queries only pc-entity[name=...], so selectors and
ids keep their document-wide meaning and a non-entity element can
never divert it. resolveEntity now takes the resolving element instead
of a tag string - it scopes the lookup and names the message - and
resolves once instead of twice on the failure path. All warning
messages are byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The six capsule segments and five 6dof flex joints move from ~90 lines
of addComponent calls in wiener-storm.mjs into a <template> in the
example page, wired by bare entity names: names resolve against the
nearest enclosing entity first, so every clone's joints bind that
clone's own segments - one template, many wieners. The single root
pc-entity is what keeps each clone's names self-contained.

The script now clones the template per throw, scales the clone's
positions and capsules to its random size before it upgrades, appends
it under the owning application's pc-scene (entities parent to the
application root, keeping the spawn pose world space), and launches
once the clone's components report ready. The throw is guarded: a
pending set tracks clones until the launch commits to the live list,
readiness is raced against a destroy promise so a clone removed
mid-flight cannot strand the await, and teardown removes pending and
live clones alike. What has no declarative surface stays in script:
the solver tuning, the stop-ERP stiffening, the bone riding and the
launch solve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@willeastcott
willeastcott merged commit f96153a into main Aug 30, 2026
3 checks passed
@willeastcott
willeastcott deleted the scoped-entity-references branch August 30, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scope entity references inside cloned templates

1 participant