11"""
22PartIndex: owns stable runtime part-ID assignment for a dataset.
33
4- Built once from a vtkDataObject at add_dataset time. IDs are seeded from
5- the scene-graph node IDs (same JavaScript-safe integer convention) so that
4+ Built once from a vtkDataObject at add_dataset time. IDs are seeded
5+ positionally: the seed is a sequence of scene-graph node IDs (same
6+ JavaScript-safe integer convention) indexed by flat_index, so that
67part_id == scene-graph node ID -- the contract the frontend relies on to
78apply per-part state (opacity, visibility, color) to the right VTK actor.
8- If no seed is provided for a given name, a fresh random ID is generated as
9- a fallback. IDs are session-scoped -- they do not survive a server restart.
9+ A position with no seed entry takes a fresh random ID as a fallback.
10+ IDs are session-scoped -- they do not survive a server restart.
1011The name <-> id bridge exists so that VisorStateMapper can translate between
1112the runtime layer (IDs) and the persistence layer (names) for
1213save_state / load_state.
1314"""
1415from __future__ import annotations
1516
17+ from collections .abc import Sequence
1618from dataclasses import dataclass
1719
1820from vtkmodules .vtkCommonDataModel import vtkCompositeDataSet , vtkDataObject
@@ -43,16 +45,20 @@ class PartIndex:
4345 --------
4446 - part_id values are assigned once and never change for the lifetime
4547 of the dataset.
48+ - part_id is seeded positionally from the scene-graph node ID at the
49+ same flat_index, so part_id == scene-graph node ID. Part names play
50+ no part in runtime identity.
4651 - Part names must be unique within a dataset for save_state / load_state
4752 round-trips to be reliable (the same constraint already applies to
4853 dataset names in the registry).
4954 - Part IDs are NOT stable across server restarts.
5055 """
5156
52- def __init__ (self , data : vtkDataObject , dataset_name : str = "" , seed_ids : dict [ str , int ] | None = None ):
57+ def __init__ (self , data : vtkDataObject , dataset_name : str = "" , seed_ids : Sequence [ int ] | None = None ):
5358 self ._parts : dict [int , PartEntry ] = {} # part_id -> entry
5459 self ._name_to_id : dict [str , int ] = {}
55- self ._seed_ids : dict [str , int ] = seed_ids or {}
60+ # Positional seed: scene-graph node IDs in leaf order, indexed by flat_index.
61+ self ._seed_ids : tuple [int , ...] | None = tuple (seed_ids ) if seed_ids is not None else None
5662 self ._build (data , dataset_name )
5763
5864 # ------------------------------------------------------------------
@@ -63,6 +69,7 @@ def _build(self, data: vtkDataObject, dataset_name: str) -> None:
6369 """Build the part IDs and names."""
6470 if not is_composite_dataset (data ):
6571 self ._add_entry (name = dataset_name , flat_index = 0 )
72+ leaf_count = 1
6673 else :
6774 composite = data # type: vtkCompositeDataSet
6875 it = composite .NewIterator ()
@@ -80,16 +87,29 @@ def _build(self, data: vtkDataObject, dataset_name: str) -> None:
8087 logger .warning (
8188 f"Duplicate part name '{ name } ' detected in dataset. "
8289 f"The name→id map will only retain the last part with this name, "
83- f"which means save_state/load_state will be unreliable for these parts. "
84- f"Part IDs remain unique and all runtime operations are unaffected."
90+ f"which means save_state/load_state will be unreliable for these parts."
8591 )
8692 self ._add_entry (name = name , flat_index = flat_idx )
8793 flat_idx += 1
8894 it .GoToNextItem ()
95+ leaf_count = flat_idx
96+
97+ # A seed that does not cover exactly one position per leaf means the seed
98+ # source and this traversal disagree; that is a broken invariant.
99+ if self ._seed_ids is not None and len (self ._seed_ids ) != leaf_count :
100+ logger .error (
101+ f"Part ID seed length { len (self ._seed_ids )} does not match leaf count { leaf_count } "
102+ f"for dataset '{ dataset_name } '. Unseeded positions were assigned random part IDs, "
103+ f"so part_id == scene-graph node ID does not hold for them."
104+ )
89105
90106 def _add_entry (self , name : str , flat_index : int ) -> PartEntry :
91107 """Add a new entry to the part IDs and names."""
92- pid = self ._seed_ids .get (name ) or get_random_javascript_safe_id ()
108+ seed = self ._seed_ids
109+ if seed is not None and 0 <= flat_index < len (seed ):
110+ pid = seed [flat_index ] # 0 is a valid JavaScript-safe ID; do not use `or`
111+ else :
112+ pid = get_random_javascript_safe_id ()
93113 entry = PartEntry (part_id = pid , name = name , flat_index = flat_index )
94114 self ._parts [pid ] = entry
95115 self ._name_to_id [name ] = pid # last writer wins on collision
@@ -133,7 +153,7 @@ def get_leaf_block(self, part_id: int, data: vtkDataObject) -> vtkDataObject | N
133153
134154 @property
135155 def name_to_id_map (self ) -> dict [str , int ]:
136- """Return a dictionary that maps part IDs to ids ."""
156+ """Return a dictionary that maps part names to part IDs (last writer wins) ."""
137157 return dict (self ._name_to_id )
138158
139159 @property
0 commit comments