Path → Object3D index for a scene subtree (B9a, tracking #2944).
The viewer resolves a node by its PATH (its name) on every per-node step of
an update pass: the sweep's eligibility probe, each type's process and
commit, the lazy-LOD release hooks, the layers panel. Object3D.getObjectByName
answers that with a depth-first walk of the whole subtree, so one pass over N
nodes costs O(N²) visits — millions per slice move on a 2000-part partition
tree. findObjectByName answers it from a map instead.
Maintained by the scene graph itself, not by the call sites that build it,
so no add or remove path can forget it: every member node carries
childadded / childremoved listeners (dispatched by three's
add / remove / attach / clear / removeFromParent), and a member's
name becomes an accessor that re-keys the entry on a rename. A new subtree
is indexed as it is added; a removed one (a node disposal, a dataset switch's
teardown) is dropped as it is removed.
Identical answers to getObjectByName, by construction:
A UNIQUE name returns the one indexed object after an O(depth) check that it
is still attached below the lookup root. That check is what keeps the index
safe against graph edits that bypass three's events (e.g. the eager-child
loader's slot flattening splices children arrays directly): a stale hit is
dropped and the lookup falls back to the walk.
A name held by TWO OR MORE live objects is ambiguous, and three's answer is
the FIRST in depth-first pre-order — an ordering the map does not keep. Such
lookups fall back to getObjectByName itself, so the first-match semantic is
preserved exactly rather than approximated. (Loader paths are unique, so this
is the exception, not the hot path.)
A name the index does not hold falls back to the walk too (a node inserted
without an event, or a genuinely absent path), and a hit found that way is
indexed so the next lookup is O(1).
A walk that MISSES is remembered per lookup root, so a path that is not
built yet (a lazy LOD level, a deferred partition part — looked up on every
pass) costs one walk, not one per lookup. The memory is invalidated by an
EPOCH bumped on every event that could turn a miss into a hit: a
childadded anywhere in the tree (a new node, or a member moved into the
looked-up root) and a member rename. A removal, and the eager-child
loader's event-less slot flattening, can only SHRINK the set of nodes below
a root, so they never stale a miss. (A node inserted with no event at all
would — nothing in the viewer does that, and the property test in
tests/unit/utils/scene-graph-index.test.ts checks every lookup against
the walk after each edit, flattening included.)
The empty name is never indexed (every unnamed group would collide).
A lookup rooted at a node of an indexed tree (not only at the indexed root) is
served by the same index, with the ancestor check bounded at that node.
Path →
Object3Dindex for a scene subtree (B9a, tracking #2944).The viewer resolves a node by its PATH (its
name) on every per-node step of an update pass: the sweep's eligibility probe, each type's process and commit, the lazy-LOD release hooks, the layers panel.Object3D.getObjectByNameanswers that with a depth-first walk of the whole subtree, so one pass over N nodes costs O(N²) visits — millions per slice move on a 2000-part partition tree. findObjectByName answers it from a map instead.Maintained by the scene graph itself, not by the call sites that build it, so no add or remove path can forget it: every member node carries
childadded/childremovedlisteners (dispatched by three'sadd/remove/attach/clear/removeFromParent), and a member'snamebecomes an accessor that re-keys the entry on a rename. A new subtree is indexed as it is added; a removed one (a node disposal, a dataset switch's teardown) is dropped as it is removed.Identical answers to
getObjectByName, by construction:childrenarrays directly): a stale hit is dropped and the lookup falls back to the walk.getObjectByNameitself, so the first-match semantic is preserved exactly rather than approximated. (Loader paths are unique, so this is the exception, not the hot path.)childaddedanywhere in the tree (a new node, or a member moved into the looked-up root) and a member rename. A removal, and the eager-child loader's event-less slot flattening, can only SHRINK the set of nodes below a root, so they never stale a miss. (A node inserted with no event at all would — nothing in the viewer does that, and the property test intests/unit/utils/scene-graph-index.test.tschecks every lookup against the walk after each edit, flattening included.)A lookup rooted at a node of an indexed tree (not only at the indexed root) is served by the same index, with the ancestor check bounded at that node.