1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
//! Composition graph: arena-backed node tree and strength ordering.
//!
//! [`PrimIndexGraph`] stores the composition [`Node`]s of a single prim in an
//! append-only arena plus a separate strength-order projection. See the
//! [module-level docs](super) for the composition overview and
//! [`PrimIndex`](crate::pcp::PrimIndex) for value resolution over the graph.
use std::cmp::Ordering;
use bitflags::bitflags;
use crate::sdf::{LayerOffset, Path};
use super::mapping::MapFunction;
use super::LayerId;
/// Whether an arc introduces a class hierarchy node — an inherit or a
/// specializes (C++ `PcpIsClassBasedArc`).
pub(crate) fn is_class_based_arc(arc: ArcType) -> bool {
matches!(arc, ArcType::Inherit | ArcType::Specialize)
}
/// The type of composition arc that introduced a [`Node`].
///
/// Variants are ordered by LIVERPS strength (strongest first). The
/// derived `PartialOrd`/`Ord` use the discriminant, so
/// `Root < Inherit < … < Specialize`.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ArcType {
/// Direct opinions from the root layer stack (sublayers).
Root,
/// Inherited from a class prim.
Inherit,
/// Contributed by a selected variant.
Variant,
/// Contributed by a relocate (non-destructive namespace remapping).
Relocate,
/// Brought in via a reference arc.
Reference,
/// Brought in via a payload arc.
Payload,
/// Contributed by a specializes arc (weakest).
Specialize,
}
/// Stable handle to a [`Node`] within a [`PrimIndex`](crate::pcp::PrimIndex)'s
/// composition graph (C++ `PcpNodeRef`).
///
/// A handle stays valid for the life of the index: the node arena is only
/// ever appended to, never reordered, so a `NodeId` is safe to store in
/// another node's `parent`/`children`/`origin`. The sentinel value
/// [`INVALID`](Self::INVALID) represents the absence of a node.
///
/// `Ord` is by arena index, which the task queue uses for a deterministic
/// tiebreak among equal-priority tasks.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NodeId(pub(crate) u32);
impl NodeId {
/// Sentinel: no node.
pub const INVALID: Self = Self(u32::MAX);
/// Returns `true` if this handle points to an actual node.
pub fn is_valid(self) -> bool {
self.0 != u32::MAX
}
/// Converts to a `usize` for indexing into the arena.
pub(crate) fn idx(self) -> usize {
self.0 as usize
}
}
impl Default for NodeId {
/// The default handle is the [`INVALID`](Self::INVALID) sentinel.
fn default() -> Self {
Self::INVALID
}
}
bitflags! {
/// Per-node composition state, mirroring the booleans carried by C++
/// `PcpNodeRef`.
///
/// Most bits are reserved for parity features not yet wired up; only the
/// flags set during composition today affect resolution. Reserving the
/// full surface now keeps later work from re-editing [`Node`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct NodeFlags: u16 {
/// Contributes no opinions; kept only for graph structure.
const INERT = 1 << 0;
/// Hidden from value resolution but retained for change tracking.
const CULLED = 1 << 1;
/// Subtree namespace-restricted by a relocate.
const RESTRICTED = 1 << 2;
/// Blocked by `permission = private` on a stronger site.
const PERMISSION_DENIED = 1 << 3;
/// This site is itself `permission = private`.
const PERMISSION_PRIVATE = 1 << 4;
/// Children are prohibited (e.g. an unloaded payload).
const PROHIBITED_CHILDREN = 1 << 5;
/// Added by implied inherit/specialize propagation.
const IMPLIED_CLASS = 1 << 6;
/// Introduced by a directly-authored arc (not implied).
const DIRECT = 1 << 7;
/// Introduced through a specializes arc; globally weak (spec 10.4.1).
const HAS_SPECIALIZES = 1 << 8;
/// Introduced by a relocate.
const RELOCATE_SOURCE = 1 << 9;
/// Lies inside a selected variant branch.
const VARIANT_BRANCH = 1 << 10;
}
}
/// A single node in the composition graph (C++ `PcpNodeRef`).
///
/// Each node represents a site (layer + path) contributing opinions to a
/// composed prim. The identity fields (`layer_index`, `path`, `arc`)
/// define *what* this node contributes. The namespace mappings
/// (`map_to_parent`, `map_to_root`) translate paths across composition arcs.
/// The structure fields (`parent`, `children`, `origin`) record the node's
/// place in the composition tree; access them through
/// [`PrimIndex`](crate::pcp::PrimIndex).
#[derive(Debug, Clone)]
pub struct Node {
/// The layer stack contributing opinions at this site (C++
/// `PcpNode::GetLayerStack`): each member is a `(layer id, sublayer offset)`
/// pair, strongest sublayer first. A member's sublayer offset is composed
/// atop `map_to_root`'s arc offset during value resolution (see
/// [`layers`](Self::layers)).
pub(crate) layer_stack: Vec<(LayerId, LayerOffset)>,
/// The path within that layer (may differ from composed path due to remapping).
pub path: Path,
/// The composition arc that introduced this node.
pub arc: ArcType,
/// Maps paths from this node's namespace to its parent's namespace.
pub map_to_parent: MapFunction,
/// Maps paths from this node's namespace directly to the root namespace.
/// Computed as `parent.map_to_root.compose(self.map_to_parent)`.
pub map_to_root: MapFunction,
/// Structural parent in the composition tree, or `None` for a root node.
pub(crate) parent: Option<NodeId>,
/// Structural children, in the order they were introduced (strength order
/// among siblings).
pub(crate) children: Vec<NodeId>,
/// Node that introduced this arc (C++ `GetOriginNode`): the parent for a
/// direct arc (set at [`add_child`](PrimIndexGraph::add_child) time), or
/// the propagated-from node for an implied class or graft. `None` only for
/// the synthetic root, which has no parent.
pub(crate) origin: Option<NodeId>,
/// Namespace depth at which the introducing arc was authored (C++
/// `PcpNode::GetNamespaceDepth`): the prim-element count of the parent
/// site's path when this node was added. Used by implied inherits/specializes
/// that propagate toward the root, and by
/// [`depth_below_introduction`](Self::depth_below_introduction).
pub(crate) namespace_depth: u16,
/// This node's index among the same-arc-type siblings at its origin (C++
/// `GetSiblingNumAtOrigin`): the arc number a class-based arc was authored
/// with. Carried onto implied copies so their relative strength is preserved;
/// reference/payload arcs leave it 0.
pub(crate) sibling_num_at_origin: u16,
/// Whether any layer in [`layer_stack`](Self::layer_stack) authors a spec at
/// [`path`](Self::path) (C++ `PcpNode::HasSpecs`). Under the full-site-stack
/// model a node may carry its whole layer stack yet author no spec at its
/// path — a cloned ancestral site deepened to a child that has no opinion
/// there. Such a node is kept for structure (it may still introduce arcs at
/// the deepened path) but contributes nothing to value resolution and is not
/// counted by [`is_empty`](crate::pcp::PrimIndex::is_empty). Defaults to
/// `true`: the recursive indexer only ever creates a node when a layer
/// authors, so its nodes always have specs.
pub(crate) has_specs: bool,
/// Namespace depth at which this node's spec contribution was restricted (C++
/// `PcpNode::GetSpecContributionRestrictedDepth`), or 0 when unrestricted.
///
/// A relocate source / salted-earth root is restricted at its own depth
/// (`path.element_count()`): its direct site contributes nothing, but the
/// ancestral opinions above it — the "spooky ancestors" a relocation pulls
/// through — still do. A non-zero depth allows ancestral opinions only at
/// paths shallower than it (see
/// [`node_can_contribute_ancestral`](crate::pcp::prim_indexer)). An inert node
/// left at depth 0 is treated as fully restricted.
pub(crate) restriction_depth: u16,
/// Composition state bits (see [`NodeFlags`]).
pub(crate) flags: NodeFlags,
}
impl Node {
/// Builds a standalone node with no structural links.
///
/// Callers that append through [`PrimIndexGraph::add_child`] have their
/// `parent`/`children` populated by the indexer; the relocate inserts and
/// grafts set the links explicitly afterward.
pub(crate) fn new(
layer_id: LayerId,
path: Path,
arc: ArcType,
map_to_parent: MapFunction,
map_to_root: MapFunction,
introduced_by_specialize: bool,
) -> Self {
let flags = if introduced_by_specialize {
NodeFlags::HAS_SPECIALIZES
} else {
NodeFlags::empty()
};
Self {
// A node lists one layer until the per-site model folds a whole
// sublayer stack into it; that lone layer's sublayer offset is
// already baked into `map_to_root`, so its entry offset is identity.
layer_stack: vec![(layer_id, LayerOffset::IDENTITY)],
path,
arc,
map_to_parent,
map_to_root,
parent: None,
children: Vec::new(),
origin: None,
namespace_depth: 0,
sibling_num_at_origin: 0,
has_specs: true,
restriction_depth: 0,
flags,
}
}
/// Id of the strongest layer contributing at this site. A representative
/// for callers that key on a single layer (dependencies, dumps); value
/// resolution iterates [`layers`](Self::layers) instead.
pub fn layer_id(&self) -> LayerId {
self.layer_stack[0].0
}
/// The site's contributing layers as stored: `(layer id, sublayer offset)`
/// members, strongest first — the node's `(layerStack, path)` site (C++
/// `PcpNodeRef::GetLayerStack`'s layers and their offsets). The offsets are
/// the raw sublayer offsets; for value resolution use [`layers`](Self::layers),
/// which folds the arc offset on top. This raw form is for structural
/// introspection.
pub fn layer_stack(&self) -> &[(LayerId, LayerOffset)] {
&self.layer_stack
}
/// Iterates the site's contributing layers strongest first, each paired
/// with its effective time offset to the root namespace: `map_to_root`'s
/// arc offset with the member's sublayer offset composed on top. Value
/// resolution reads opinions through this iterator so one per-site node can
/// fold every sublayer.
pub fn layers(&self) -> impl Iterator<Item = (LayerId, LayerOffset)> + '_ {
let arc_offset = self.map_to_root.time_offset();
self.layer_stack
.iter()
.map(move |&(li, sub)| (li, arc_offset.concatenate(&sub)))
}
/// Structural parent, or `None` for a root node.
pub fn parent(&self) -> Option<NodeId> {
self.parent
}
/// Structural children, in strength order among siblings.
pub fn children(&self) -> &[NodeId] {
&self.children
}
/// Node that introduced this arc (C++ `GetOriginNode`): the parent for a
/// direct arc, the propagated-from node for an implied class or graft, or
/// `None` for the synthetic root.
pub fn origin(&self) -> Option<NodeId> {
self.origin
}
/// Namespace depth at which the introducing arc was authored.
pub fn namespace_depth(&self) -> u16 {
self.namespace_depth
}
/// Number of namespace levels this node's site sits below the level at which
/// its arc was introduced (C++ `PcpNode::GetDepthBelowIntroduction`): the
/// node path's prim-element count minus its namespace depth. A direct arc
/// node has depth 0; a node reached by extending that arc to a child has 1,
/// and so on. Implied-class propagation uses this to tell a class's true
/// namespace descendants from the arc that continues an ancestral chain.
pub(crate) fn depth_below_introduction(&self) -> u16 {
(self.path.prim_element_count() as u16).saturating_sub(self.namespace_depth)
}
/// This node's path at the level where its arc was introduced (C++
/// `PcpNode::GetPathAtIntroduction`): the node path with its
/// [`depth_below_introduction`](Self::depth_below_introduction) trailing
/// elements stripped.
pub(crate) fn path_at_introduction(&self) -> Path {
let mut path = self.path.clone();
for _ in 0..self.depth_below_introduction() {
match path.parent() {
Some(parent) => path = parent,
None => break,
}
}
path
}
/// Whether any layer in this node's stack authors a spec at its path (C++
/// `PcpNode::HasSpecs`). A node carrying its full site layer stack may
/// author nothing at a deepened child path; such a node contributes no
/// opinions and is not counted as content.
pub fn has_specs(&self) -> bool {
self.has_specs
}
/// Composition state bits.
pub fn flags(&self) -> NodeFlags {
self.flags
}
/// True when this node contributes no opinions and is kept only for graph
/// structure: the synthetic tree root, or a non-contributing class
/// placeholder added by implied-class propagation (C++ `SetInert`).
pub fn is_inert(&self) -> bool {
self.flags.contains(NodeFlags::INERT)
}
/// True when this node is retained for composition structure (so an editor
/// or change tracking can see its arc) but contributes no opinions to value
/// resolution. Set on an arc whose target site authors no spec (C++
/// `PcpPrimIndex` culling).
pub fn is_culled(&self) -> bool {
self.flags.contains(NodeFlags::CULLED)
}
/// True when this node was introduced through a specializes arc (directly
/// or transitively), making it globally weak per spec section 10.4.1.
pub(crate) fn introduced_by_specialize(&self) -> bool {
self.flags.contains(NodeFlags::HAS_SPECIALIZES)
}
/// True when this node carries a relocation source site (C++
/// `PcpNodeRef::IsRestricted` relocate placeholder). It is inert for value
/// resolution, but its source site must still be tracked as a dependency so
/// an edit there recomposes the relocated prim.
pub(crate) fn is_relocate_source(&self) -> bool {
self.flags.contains(NodeFlags::RELOCATE_SOURCE)
}
/// True when this node is a direct arc to a `permission = private` site, or
/// lies in such an arc's subtree (spec 10.3.3). It stays visible
/// structurally (`nodes`, `has_spec`, child names) but contributes no
/// opinions to value resolution — the C++ `_InertSubtree` behavior.
pub(crate) fn is_permission_denied(&self) -> bool {
self.flags.contains(NodeFlags::PERMISSION_DENIED)
}
}
/// Arena-based composition graph.
///
/// `nodes` is the node arena in insertion (structural) order: it is only ever
/// appended to, never reordered, so a [`NodeId`] (an index into it) stays
/// valid for the life of the index and is safe to store in another node's
/// `parent`/`children`/`origin`. Strongest-to-weakest strength order is a
/// separate projection, `strength_order`, holding the same handles permuted —
/// value resolution walks it, not the arena. Dereferencing the graph yields
/// the arena slice for the indexer's structural lookups.
///
/// `root` is the synthetic, inert tree root created by
/// [`init_root`](Self::init_root): every otherwise-parentless node attaches
/// under it, so the graph is a single rooted tree rather than a forest. It is
/// [`NodeId::INVALID`] for the hand-built test graphs that never call
/// `init_root`.
#[derive(Debug, Clone, Default)]
pub(crate) struct PrimIndexGraph {
pub(crate) nodes: Vec<Node>,
pub(crate) strength_order: Vec<NodeId>,
pub(crate) root: NodeId,
}
impl PrimIndexGraph {
/// Returns the prim's local root node — the `Root`-arc child of the
/// synthetic inert root, carrying the prim's own (sublayer) opinions. An
/// implied class anchors here so it ranks among the prim's direct arcs (C++
/// `_AddClassBasedArc` adds it under the node owning the prim), regardless
/// of how deep the arc that implied it sits or whether an ancestral arc was
/// grafted as a separate root-level sibling. [`NodeId::INVALID`] when the
/// prim has no local opinion (composed purely through arcs).
pub(crate) fn local_root(&self) -> NodeId {
if !self.root.is_valid() {
return NodeId::INVALID;
}
self.nodes[self.root.idx()]
.children
.iter()
.copied()
.find(|&c| self.nodes[c.idx()].arc == ArcType::Root)
.unwrap_or(NodeId::INVALID)
}
/// Creates the synthetic, inert tree root (C++ unified graph root) and
/// records it as [`root`](Self::root). Must be the first node, so it takes
/// [`NodeId`] 0.
///
/// Its identity `map_to_parent`/`map_to_root` make re-parenting an
/// otherwise-parentless node under it offset-neutral
/// (`identity ∘ child.map_to_parent == child.map_to_parent`), so a former
/// forest root keeps the `map_to_root` it had with no parent. The node is
/// flagged [`INERT`](NodeFlags::INERT) and skipped by value resolution.
pub(crate) fn init_root(&mut self, layer_id: LayerId, path: Path) -> NodeId {
debug_assert!(self.nodes.is_empty(), "synthetic root must be the first node");
let id = NodeId(self.nodes.len() as u32);
let depth = path.prim_element_count() as u16;
let mut node = Node::new(
layer_id,
path,
ArcType::Root,
MapFunction::identity(),
MapFunction::identity(),
false,
);
node.namespace_depth = depth;
node.flags |= NodeFlags::INERT;
self.nodes.push(node);
self.root = id;
id
}
/// Appends a node to the graph, linking it under `parent`.
///
/// `parent` identifies the structural parent in the composition graph. An
/// [`INVALID`](NodeId::INVALID) `parent` attaches the node under the
/// synthetic [`root`](Self::root) when one exists, keeping the graph a
/// single tree; `map_to_root` then composes through that identity-mapped
/// root, leaving it equal to `map_to_parent`. The new node is recorded in
/// its parent's children and appended to the arena; the strength projection
/// is built once at the end of the build by
/// [`finalize_strength_order`](Self::finalize_strength_order). The returned
/// handle stays valid for the life of the index.
pub(crate) fn add_child(
&mut self,
parent: NodeId,
layer_id: LayerId,
path: Path,
arc: ArcType,
map_to_parent: MapFunction,
introduced_by_specialize: bool,
) -> NodeId {
// A caller-supplied `INVALID` parent means "a root site": the arc is
// introduced at this prim. Such nodes re-parent under the synthetic
// root for structure, but their namespace depth still derives from
// their own path (the conceptual parent site), not the synthetic root.
let root_site = !parent.is_valid();
let struct_parent = if root_site { self.root } else { parent };
let map_to_root = if struct_parent.is_valid() {
self.nodes[struct_parent.idx()].map_to_root.compose(&map_to_parent)
} else {
map_to_parent.clone()
};
// Namespace depth is the prim-element count of the parent site path at
// arc introduction (C++ `PcpNode_GetNonVariantPathElementCount`); a
// root site uses its own path.
let namespace_depth = if root_site {
path.prim_element_count()
} else {
self.nodes[parent.idx()].path.prim_element_count()
} as u16;
let idx = NodeId(self.nodes.len() as u32);
let mut node = Node::new(
layer_id,
path,
arc,
map_to_parent,
map_to_root,
introduced_by_specialize,
);
node.namespace_depth = namespace_depth;
if struct_parent.is_valid() {
node.parent = Some(struct_parent);
// A directly-authored arc's origin is its parent (C++
// `GetOriginNode`). Implied-class and graft propagation overwrite
// this afterward; setting it here makes `origin` total.
node.origin = Some(struct_parent);
self.nodes[struct_parent.idx()].children.push(idx);
}
self.nodes.push(node);
idx
}
/// Like [`add_child`](Self::add_child) but for a site spanning several
/// sublayers: `layer_stack` lists every contributing `(layer index,
/// sublayer offset)`, strongest sublayer first. The first member is the
/// site representative; the remaining members are folded into the node's
/// layer stack so value resolution reads each sublayer in turn. Panics on
/// an empty `layer_stack`.
pub(crate) fn add_site_child(
&mut self,
parent: NodeId,
layer_stack: Vec<(LayerId, LayerOffset)>,
path: Path,
arc: ArcType,
map_to_parent: MapFunction,
introduced_by_specialize: bool,
) -> NodeId {
let id = self.add_child(
parent,
layer_stack[0].0,
path,
arc,
map_to_parent,
introduced_by_specialize,
);
self.nodes[id.idx()].layer_stack = layer_stack;
id
}
/// Finds a non-inert, non-culled node already on this graph whose site
/// matches `(root_layer, path)` (C++ `PcpPrimIndex_Graph::GetNodeUsingSite`).
///
/// The representative layer index is the root sublayer of a node's layer
/// stack, which uniquely identifies that stack, so matching it together with
/// the path is equivalent to C++'s `layerStack == site.layerStack` test. The
/// task-queue indexer uses this for opt-in duplicate-node skipping by the
/// class-based arcs (inherits/specializes); reference/payload arcs keep
/// diamonds.
pub(crate) fn node_using_site(&self, root_layer: LayerId, path: &Path) -> Option<NodeId> {
self.nodes
.iter()
.position(|node| {
!node.flags.intersects(NodeFlags::INERT | NodeFlags::CULLED)
&& node.layer_id() == root_layer
&& &node.path == path
})
.map(|i| NodeId(i as u32))
}
/// Deepens every node's site path by one namespace element, from the parent
/// prim to a child (C++ `PcpPrimIndex_Graph::AppendChildNameToAllSites`).
///
/// A node sitting exactly at the parent path moves to `child_path`; every
/// other node has the child name appended to its own (already deeper or
/// arc-mapped) path. The namespace mappings are untouched — deepening does
/// not change how paths translate across arcs, only the depth — so this does
/// not require re-finalizing strength order. Used to adapt a cloned parent
/// graph into the seed for its child's index.
pub(crate) fn append_child_name_to_all_sites(&mut self, child_path: &Path) {
let Some(parent_path) = child_path.parent() else {
return;
};
let Some(child_name) = child_path.name() else {
return;
};
for node in &mut self.nodes {
if node.path == parent_path {
node.path = child_path.clone();
} else if let Ok(deeper) = node.path.append_path(child_name) {
node.path = deeper;
}
}
}
/// Whether `x` is a namespace ancestor of `y` in the composition tree,
/// walking `y` up its parent chain and stopping before the local root (C++
/// `Task::PriorityOrder`'s `isAncestorAndDescendant`). The local root and the
/// synthetic tree root are never reported as ancestors.
pub(crate) fn is_ancestor_of(&self, x: NodeId, y: NodeId) -> bool {
let root = self.local_root();
let mut cur = y;
while cur.is_valid() && cur != root && cur != self.root {
if cur == x {
return true;
}
cur = self.nodes[cur.idx()].parent.unwrap_or(NodeId::INVALID);
}
false
}
/// Collects the chain of nodes from `id` up to its tree root, node first
/// and root last.
fn chain_to_root(&self, id: NodeId) -> Vec<NodeId> {
let mut chain = vec![id];
let mut cur = id;
while let Some(parent) = self.nodes[cur.idx()].parent {
chain.push(parent);
cur = parent;
}
chain
}
/// Compares two sibling nodes (nodes sharing a parent) by strength,
/// porting C++ `PcpCompareSiblingNodeStrength`. Returns [`Ordering::Less`]
/// when `a` is the stronger sibling.
///
/// Keys, in priority order (spec 10.3): arc type (lower discriminant
/// stronger); namespace depth (deeper stronger); origin strength (the
/// stronger origin wins); finally the sibling arc number at the origin (C++
/// `GetSiblingNumAtOrigin`, lower stronger). Two specializes use the
/// specializes comparator ([`compare_specialize_siblings`]), which reads the
/// copy-to-root structure [`propagate_node_to_root`](crate::pcp::prim_indexer)
/// produces.
pub(crate) fn compare_sibling_node_strength(&self, a: NodeId, b: NodeId) -> Ordering {
let na = &self.nodes[a.idx()];
let nb = &self.nodes[b.idx()];
// 1. Arc type: lower discriminant (e.g. Inherit) is stronger.
if na.arc != nb.arc {
return na.arc.cmp(&nb.arc);
}
// Two specializes are copied under the local root (C++
// `_PropagateNodeToRoot`), so they sort by the specializes branch of C++
// `PcpCompareSiblingNodeStrength`, which reads that copy-to-root structure.
if na.introduced_by_specialize() && nb.introduced_by_specialize() {
return self.compare_specialize_siblings(a, b);
}
// 2. Namespace depth: a deeper arc-introduction site is stronger (C++
// uses higher namespace depth).
if na.namespace_depth != nb.namespace_depth {
return nb.namespace_depth.cmp(&na.namespace_depth);
}
// 3. Origin strength, only when the two origins differ. `origin` is
// total (C++ `GetOriginNode`): the node an implied arc was propagated
// from, or the parent for a directly-authored arc, so two direct
// siblings share an origin and fall through to the tiebreak below. The
// synthetic root is the sole node without an origin; it stands in for
// itself. C++ `_OriginIsStronger` walks the tree in strength order to
// find which origin comes first; [`compare_node_strength`] is the
// order-independent equivalent, well-defined here because it never reads
// the strength projection being computed. It recurses only over
// strictly-older nodes (an origin is always created before the node
// carrying it), except when both nodes default to themselves (no origin
// authored): recursing on the same `(a, b)` would not progress, so skip
// straight to the sibling-number tiebreak.
let oa = na.origin.unwrap_or(a);
let ob = nb.origin.unwrap_or(b);
if oa != ob && (oa != a || ob != b) {
let ord = self.compare_node_strength(oa, ob);
if ord != Ordering::Equal {
return ord;
}
}
// 4. Origin sibling arc number, then arena index (C++ `GetSiblingNumAtOrigin`,
// with the arena index as the deterministic tiebreak where C++ returns equal).
self.sibling_then_index(a, b)
}
/// Compares two specializes siblings under the copy-to-root model, porting
/// the specializes branch of C++ `PcpCompareSiblingNodeStrength`. Returns
/// [`Ordering::Less`] when `a` is stronger.
// TODO(perf): invoked O(n log n) times from `finalize_strength_order`'s sort,
// and `origin_root_node` / `origins_are_nested` / `namespace_depth_for_class_hierarchy`
// re-walk origin/parent chains while `is_propagated_specializes` re-scans the
// root's children (`local_root`) on each call. The per-node chain results
// could be precomputed into a side table once before the sort.
fn compare_specialize_siblings(&self, a: NodeId, b: NodeId) -> Ordering {
let (a_root, a_hops) = self.origin_root_node(a);
let (b_root, b_hops) = self.origin_root_node(b);
// Origin namespace depth (skipped when the origin roots are nested arcs:
// a specializes source must stay weaker than its target regardless of
// namespace depth, C++ `_OriginsAreNestedArcs`).
if !self.origins_are_nested(a_root, b_root) {
let da = self.nodes[a.idx()].namespace_depth;
let db = self.nodes[b.idx()].namespace_depth;
if da != db {
return db.cmp(&da);
}
}
let oa = self.origin_of(a);
let ob = self.origin_of(b);
let a_authored = oa == self.parent_of(a);
let b_authored = ob == self.parent_of(b);
if oa == ob {
// Same origin: either both authored arcs (fall through to sibling
// number) or both copied to the root — the implied node (site differs
// from its origin's) is the more local, stronger opinion.
if !a_authored && !b_authored {
return self
.implied_beats_propagated(a, oa, b, ob)
.unwrap_or_else(|| a.0.cmp(&b.0));
}
} else if a_root != b_root {
// Different authored specialize arcs: order by the strength of the
// origin roots. C++ `_OriginIsStronger` walks the tree in strength
// order; [`compare_node_strength`] is the order-independent
// equivalent, safe to call mid-sort (it never reads the projection).
return self.compare_node_strength(a_root, b_root).then(a.0.cmp(&b.0));
} else {
// Same origin root, different origins: both children of the root.
// First the namespace depth of the node that inherits/specializes the
// class hierarchy the origin belongs to (accounts for specializes
// implied to ancestral hierarchies).
let a_depth = if a_authored {
0
} else {
self.namespace_depth_for_class_hierarchy(oa)
};
let b_depth = if b_authored {
0
} else {
self.namespace_depth_for_class_hierarchy(ob)
};
if a_depth != b_depth {
return a_depth.cmp(&b_depth);
}
// Then the longer origin chain (implied further up, more local) wins.
if a_hops != b_hops {
return b_hops.cmp(&a_hops);
}
// An implied opinion local to the root layer stack beats a propagated
// one (C++ `TrickySpecializesAndInherits3`).
if !a_authored && !b_authored && self.same_layer_stack_as_root(a) && self.same_layer_stack_as_root(b) {
if let Some(ord) = self.implied_beats_propagated(a, oa, b, ob) {
return ord;
}
}
// Finally, order by the strength of the two origins themselves.
return self.compare_node_strength(oa, ob).then(a.0.cmp(&b.0));
}
self.sibling_then_index(a, b)
}
/// When exactly one of `a`/`b` is an implied copy — its site differs from
/// its origin's — and the other a propagated original, the implied node is
/// the more local, stronger opinion (the implied-vs-propagated tiebreak in
/// C++ `PcpCompareSiblingNodeStrength`). `None` when both or neither are
/// implied.
fn implied_beats_propagated(&self, a: NodeId, oa: NodeId, b: NodeId, ob: NodeId) -> Option<Ordering> {
match (!self.same_site(a, oa), !self.same_site(b, ob)) {
(true, false) => Some(Ordering::Less),
(false, true) => Some(Ordering::Greater),
_ => None,
}
}
/// Sibling arc number at the origin (C++ `GetSiblingNumAtOrigin`, lower
/// stronger), with the arena index as the final deterministic tiebreak.
fn sibling_then_index(&self, a: NodeId, b: NodeId) -> Ordering {
self.nodes[a.idx()]
.sibling_num_at_origin
.cmp(&self.nodes[b.idx()].sibling_num_at_origin)
.then(a.0.cmp(&b.0))
}
/// Arc type of a node.
fn arc_of(&self, id: NodeId) -> ArcType {
self.nodes[id.idx()].arc
}
/// Structural parent, or [`NodeId::INVALID`] for a root node.
fn parent_of(&self, id: NodeId) -> NodeId {
self.nodes[id.idx()].parent.unwrap_or(NodeId::INVALID)
}
/// Introducing node (C++ `GetOriginNode`), or [`NodeId::INVALID`].
fn origin_of(&self, id: NodeId) -> NodeId {
self.nodes[id.idx()].origin.unwrap_or(NodeId::INVALID)
}
/// Whether two nodes carry the same site: same representative layer and path
/// (C++ `GetSite() ==`).
pub(crate) fn same_site(&self, a: NodeId, b: NodeId) -> bool {
a.is_valid()
&& b.is_valid()
&& self.nodes[a.idx()].layer_id() == self.nodes[b.idx()].layer_id()
&& self.nodes[a.idx()].path == self.nodes[b.idx()].path
}
/// Whether `a`'s layer stack is the local root's layer stack (C++
/// `GetLayerStack() == GetRootNode().GetLayerStack()`).
fn same_layer_stack_as_root(&self, a: NodeId) -> bool {
let root = self.local_root();
root.is_valid() && self.nodes[a.idx()].layer_stack == self.nodes[root.idx()].layer_stack
}
/// Whether a node is a specializes node copied under the local root for
/// strength ordering (C++ `Pcp_IsPropagatedSpecializesNode`): a specialize
/// arc whose parent is the local root and whose site equals its origin's.
pub(crate) fn is_propagated_specializes(&self, id: NodeId) -> bool {
self.arc_of(id) == ArcType::Specialize
&& self.parent_of(id) == self.local_root()
&& self.same_site(id, self.origin_of(id))
}
/// Start of a node's origin chain and the number of hops to it (C++
/// `PcpNodeRef::GetOriginRootNode`): follows `origin` while it differs from
/// `parent` (an authored arc has `origin == parent`).
fn origin_root_node(&self, id: NodeId) -> (NodeId, usize) {
let mut cur = id;
let mut hops = 0;
loop {
let origin = self.origin_of(cur);
if !origin.is_valid() || origin == self.parent_of(cur) {
break;
}
cur = origin;
hops += 1;
}
(cur, hops)
}
/// Whether `a` is a descendant of `b` or vice versa in the mixed
/// origin/parent chain (C++ `_OriginsAreNestedArcs`): a propagated
/// specializes node walks up via its origin, every other node via its parent.
fn origins_are_nested(&self, a: NodeId, b: NodeId) -> bool {
self.is_nested_under(a, b) || self.is_nested_under(b, a)
}
fn is_nested_under(&self, x: NodeId, y: NodeId) -> bool {
let mut n = x;
while n.is_valid() {
if n == y {
return true;
}
n = if self.is_propagated_specializes(n) {
self.origin_of(n)
} else {
self.parent_of(n)
};
}
false
}
/// Namespace depth of the node that inherits or specializes the class
/// hierarchy `n` belongs to (C++ `_GetNamespaceDepthForClassHierarchy`),
/// skipping past relocate placeholders.
fn namespace_depth_for_class_hierarchy(&self, n: NodeId) -> u16 {
let (mut instance, _class) = self.starting_node_of_class_hierarchy(n);
while instance.is_valid() && self.arc_of(instance) == ArcType::Relocate {
instance = self.parent_of(instance);
}
if instance.is_valid() {
self.nodes[instance.idx()].namespace_depth
} else {
0
}
}
/// Walks up the chain of class arcs at the same depth-below-introduction from
/// `n`, returning `(instance node, topmost class node)` (C++
/// `Pcp_FindStartingNodeOfClassHierarchy`), stepping across a propagated
/// specializes node via its origin.
pub(crate) fn starting_node_of_class_hierarchy(&self, n: NodeId) -> (NodeId, NodeId) {
let mut instance = n;
if self.is_propagated_specializes(instance) {
instance = self.origin_of(instance);
}
let mut class_node = NodeId::INVALID;
let depth = self.nodes[instance.idx()].depth_below_introduction();
while instance.is_valid()
&& is_class_based_arc(self.arc_of(instance))
&& self.nodes[instance.idx()].depth_below_introduction() == depth
{
class_node = instance;
let parent = self.parent_of(instance);
if !parent.is_valid() {
break;
}
instance = parent;
if self.is_propagated_specializes(instance) {
instance = self.origin_of(instance);
}
}
(instance, class_node)
}
/// The specializes node copied under the local root for `node`, if any (C++
/// `_GetPropagatedSpecializesNode`): a local-root child whose origin is
/// `node` and which is itself a propagated specializes node. Returns `None`
/// for a non-specializes node.
pub(crate) fn get_propagated_specializes_node(&self, node: NodeId) -> Option<NodeId> {
if self.arc_of(node) != ArcType::Specialize {
return None;
}
let root = self.local_root();
if !root.is_valid() {
return None;
}
self.nodes[root.idx()]
.children
.iter()
.copied()
.find(|&rc| self.origin_of(rc) == node && self.is_propagated_specializes(rc))
}
/// Compares two arbitrary nodes in the same tree by strength, porting C++
/// `PcpCompareNodeStrength`. Returns [`Ordering::Less`] when `a` is stronger.
///
/// Walks each node's chain to the shared root to find their lowest common
/// ancestor. When one node is an ancestor of the other, the ancestor is
/// stronger; otherwise the two diverging children are siblings, compared by
/// [`compare_sibling_node_strength`](Self::compare_sibling_node_strength).
pub(crate) fn compare_node_strength(&self, a: NodeId, b: NodeId) -> Ordering {
if a == b {
return Ordering::Equal;
}
let chain_a = self.chain_to_root(a);
let chain_b = self.chain_to_root(b);
// Walk both chains from the root downward (chains are leaf-first) until
// they diverge. The last shared node is the lowest common ancestor.
let mut ia = chain_a.len();
let mut ib = chain_b.len();
while ia > 0 && ib > 0 {
let ca = chain_a[ia - 1];
let cb = chain_b[ib - 1];
if ca != cb {
// First divergence: `ca` and `cb` are siblings under the LCA.
return self.compare_sibling_node_strength(ca, cb);
}
ia -= 1;
ib -= 1;
}
// One chain is a prefix of the other: the shorter (the ancestor) wins.
match (ia, ib) {
(0, 0) => Ordering::Equal,
(0, _) => Ordering::Less,
(_, 0) => Ordering::Greater,
_ => Ordering::Equal,
}
}
/// Rebuilds the strength-order projection as a pre-order DFS of the tree.
/// The result runs strongest to weakest; reversing it gives weak-to-strong.
///
/// Each node's `children` are first sorted by
/// [`compare_sibling_node_strength`](Self::compare_sibling_node_strength), so
/// the DFS yields strength order among siblings. Specializes are copied under
/// the local root (C++ `_PropagateNodeToRoot` / `_EvalImpliedSpecializes`)
/// and specialize is the weakest arc, so the DFS already trails the
/// globally-weak band (spec 10.4.1) at the end.
///
/// The comparator reads only arc type, namespace depth, origin chains, and
/// arena order — never the projection being computed — so it is well-defined
/// here.
pub(crate) fn finalize_strength_order(&mut self) {
if !self.root.is_valid() {
return;
}
// Order every node's children by sibling strength. `children` is taken
// out so the comparator can borrow the arena immutably during the sort.
for i in 0..self.nodes.len() {
let mut children = std::mem::take(&mut self.nodes[i].children);
children.sort_by(|&a, &b| self.compare_sibling_node_strength(a, b));
self.nodes[i].children = children;
}
// Pre-order DFS from the root: visit a node, then its children in
// strength order. Pushing children reversed makes the strongest pop
// first. Specializes were copied under the local root (C++
// `_PropagateNodeToRoot`); specialize is the weakest arc, so those copies
// and their subtrees sort last among the root's children and the DFS
// already places the globally-weak band (spec 10.4.1) at the end.
let mut dfs = Vec::with_capacity(self.nodes.len());
let mut stack = vec![self.root];
while let Some(id) = stack.pop() {
dfs.push(id);
for &child in self.nodes[id.idx()].children.iter().rev() {
stack.push(child);
}
}
self.strength_order = dfs;
}
}
impl std::ops::Deref for PrimIndexGraph {
/// Dereferences to the node arena in insertion (structural) order. The
/// indexer relies on this for positional lookups while composing; strength
/// order is reached through [`PrimIndex::nodes`].
type Target = [Node];
fn deref(&self) -> &[Node] {
&self.nodes
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(path: &str, namespace_depth: u16) -> Node {
let mut n = Node::new(
LayerId::from_raw(0),
Path::from(path),
ArcType::Inherit,
MapFunction::identity(),
MapFunction::identity(),
false,
);
n.namespace_depth = namespace_depth;
n
}
#[test]
fn introduction_depth_and_path() {
// An arc introduced at the prim's own level: depth 0, path unchanged.
let direct = node("/Model", 1);
assert_eq!(direct.depth_below_introduction(), 0);
assert_eq!(direct.path_at_introduction(), Path::from("/Model"));
// The same arc extended two levels into a child: depth 2, and the
// introduction path strips the two trailing elements.
let extended = node("/Model/Rig/Anim", 1);
assert_eq!(extended.depth_below_introduction(), 2);
assert_eq!(extended.path_at_introduction(), Path::from("/Model"));
}
}