@pascal-app/core 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/events/bus.d.ts +4 -2
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  5. package/dist/hooks/scene-registry/scene-registry.js +3 -0
  6. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +1 -1
  7. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  8. package/dist/hooks/spatial-grid/spatial-grid-sync.js +11 -3
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -8
  12. package/dist/materials.d.ts +10 -0
  13. package/dist/materials.d.ts.map +1 -0
  14. package/dist/materials.js +22 -0
  15. package/dist/schema/index.d.ts +3 -1
  16. package/dist/schema/index.d.ts.map +1 -1
  17. package/dist/schema/index.js +3 -1
  18. package/dist/schema/nodes/level.d.ts +1 -1
  19. package/dist/schema/nodes/level.d.ts.map +1 -1
  20. package/dist/schema/nodes/level.js +2 -0
  21. package/dist/schema/nodes/stair-segment.d.ts +81 -0
  22. package/dist/schema/nodes/stair-segment.d.ts.map +1 -0
  23. package/dist/schema/nodes/stair-segment.js +42 -0
  24. package/dist/schema/nodes/stair.d.ts +56 -0
  25. package/dist/schema/nodes/stair.d.ts.map +1 -0
  26. package/dist/schema/nodes/stair.js +22 -0
  27. package/dist/schema/types.d.ts +119 -1
  28. package/dist/schema/types.d.ts.map +1 -1
  29. package/dist/schema/types.js +4 -0
  30. package/dist/store/actions/node-actions.d.ts.map +1 -1
  31. package/dist/store/actions/node-actions.js +25 -29
  32. package/dist/store/use-live-transforms.d.ts +14 -0
  33. package/dist/store/use-live-transforms.d.ts.map +1 -0
  34. package/dist/store/use-live-transforms.js +20 -0
  35. package/dist/store/use-scene.d.ts +2 -5
  36. package/dist/store/use-scene.d.ts.map +1 -1
  37. package/dist/store/use-scene.js +25 -15
  38. package/dist/systems/door/door-system.d.ts.map +1 -1
  39. package/dist/systems/door/door-system.js +1 -17
  40. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  41. package/dist/systems/roof/roof-system.js +18 -0
  42. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  43. package/dist/systems/slab/slab-system.js +71 -26
  44. package/dist/systems/stair/stair-system.d.ts +2 -0
  45. package/dist/systems/stair/stair-system.d.ts.map +1 -0
  46. package/dist/systems/stair/stair-system.js +354 -0
  47. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  48. package/dist/systems/wall/wall-system.js +2 -0
  49. package/dist/systems/window/window-system.d.ts.map +1 -1
  50. package/dist/systems/window/window-system.js +8 -24
  51. package/dist/utils/clone-scene-graph.d.ts +25 -1
  52. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  53. package/dist/utils/clone-scene-graph.js +160 -5
  54. package/package.json +6 -1
@@ -175,7 +175,7 @@ export declare const AnyNode: z.ZodDiscriminatedUnion<[z.ZodObject<{
175
175
  metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
176
176
  id: z.ZodDefault<z.ZodTemplateLiteral<`level_${string}`>>;
177
177
  type: z.ZodDefault<z.ZodLiteral<"level">>;
178
- children: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodDefault<z.ZodTemplateLiteral<`wall_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`zone_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`slab_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`ceiling_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`roof_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`scan_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`guide_${string}`>>]>>>;
178
+ children: z.ZodDefault<z.ZodArray<z.ZodUnion<readonly [z.ZodDefault<z.ZodTemplateLiteral<`wall_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`zone_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`slab_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`ceiling_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`roof_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`stair_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`scan_${string}`>>, z.ZodDefault<z.ZodTemplateLiteral<`guide_${string}`>>]>>>;
179
179
  level: z.ZodDefault<z.ZodNumber>;
180
180
  }, z.core.$strip>, z.ZodObject<{
181
181
  object: z.ZodDefault<z.ZodLiteral<"node">>;
@@ -579,6 +579,124 @@ export declare const AnyNode: z.ZodDiscriminatedUnion<[z.ZodObject<{
579
579
  deckThickness: z.ZodDefault<z.ZodNumber>;
580
580
  overhang: z.ZodDefault<z.ZodNumber>;
581
581
  shingleThickness: z.ZodDefault<z.ZodNumber>;
582
+ }, z.core.$strip>, z.ZodObject<{
583
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
584
+ name: z.ZodOptional<z.ZodString>;
585
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
586
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
587
+ camera: z.ZodOptional<z.ZodObject<{
588
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
589
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
590
+ mode: z.ZodDefault<z.ZodEnum<{
591
+ perspective: "perspective";
592
+ orthographic: "orthographic";
593
+ }>>;
594
+ fov: z.ZodOptional<z.ZodNumber>;
595
+ zoom: z.ZodOptional<z.ZodNumber>;
596
+ }, z.core.$strip>>;
597
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
598
+ id: z.ZodDefault<z.ZodTemplateLiteral<`stair_${string}`>>;
599
+ type: z.ZodDefault<z.ZodLiteral<"stair">>;
600
+ material: z.ZodOptional<z.ZodObject<{
601
+ preset: z.ZodOptional<z.ZodEnum<{
602
+ custom: "custom";
603
+ white: "white";
604
+ brick: "brick";
605
+ concrete: "concrete";
606
+ wood: "wood";
607
+ glass: "glass";
608
+ metal: "metal";
609
+ plaster: "plaster";
610
+ tile: "tile";
611
+ marble: "marble";
612
+ }>>;
613
+ properties: z.ZodOptional<z.ZodObject<{
614
+ color: z.ZodDefault<z.ZodString>;
615
+ roughness: z.ZodDefault<z.ZodNumber>;
616
+ metalness: z.ZodDefault<z.ZodNumber>;
617
+ opacity: z.ZodDefault<z.ZodNumber>;
618
+ transparent: z.ZodDefault<z.ZodBoolean>;
619
+ side: z.ZodDefault<z.ZodEnum<{
620
+ front: "front";
621
+ back: "back";
622
+ double: "double";
623
+ }>>;
624
+ }, z.core.$strip>>;
625
+ texture: z.ZodOptional<z.ZodObject<{
626
+ url: z.ZodString;
627
+ repeat: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
628
+ scale: z.ZodOptional<z.ZodNumber>;
629
+ }, z.core.$strip>>;
630
+ }, z.core.$strip>>;
631
+ position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
632
+ rotation: z.ZodDefault<z.ZodNumber>;
633
+ children: z.ZodDefault<z.ZodArray<z.ZodDefault<z.ZodTemplateLiteral<`sseg_${string}`>>>>;
634
+ }, z.core.$strip>, z.ZodObject<{
635
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
636
+ name: z.ZodOptional<z.ZodString>;
637
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
638
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
639
+ camera: z.ZodOptional<z.ZodObject<{
640
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
641
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
642
+ mode: z.ZodDefault<z.ZodEnum<{
643
+ perspective: "perspective";
644
+ orthographic: "orthographic";
645
+ }>>;
646
+ fov: z.ZodOptional<z.ZodNumber>;
647
+ zoom: z.ZodOptional<z.ZodNumber>;
648
+ }, z.core.$strip>>;
649
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
650
+ id: z.ZodDefault<z.ZodTemplateLiteral<`sseg_${string}`>>;
651
+ type: z.ZodDefault<z.ZodLiteral<"stair-segment">>;
652
+ material: z.ZodOptional<z.ZodObject<{
653
+ preset: z.ZodOptional<z.ZodEnum<{
654
+ custom: "custom";
655
+ white: "white";
656
+ brick: "brick";
657
+ concrete: "concrete";
658
+ wood: "wood";
659
+ glass: "glass";
660
+ metal: "metal";
661
+ plaster: "plaster";
662
+ tile: "tile";
663
+ marble: "marble";
664
+ }>>;
665
+ properties: z.ZodOptional<z.ZodObject<{
666
+ color: z.ZodDefault<z.ZodString>;
667
+ roughness: z.ZodDefault<z.ZodNumber>;
668
+ metalness: z.ZodDefault<z.ZodNumber>;
669
+ opacity: z.ZodDefault<z.ZodNumber>;
670
+ transparent: z.ZodDefault<z.ZodBoolean>;
671
+ side: z.ZodDefault<z.ZodEnum<{
672
+ front: "front";
673
+ back: "back";
674
+ double: "double";
675
+ }>>;
676
+ }, z.core.$strip>>;
677
+ texture: z.ZodOptional<z.ZodObject<{
678
+ url: z.ZodString;
679
+ repeat: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
680
+ scale: z.ZodOptional<z.ZodNumber>;
681
+ }, z.core.$strip>>;
682
+ }, z.core.$strip>>;
683
+ position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
684
+ rotation: z.ZodDefault<z.ZodNumber>;
685
+ segmentType: z.ZodDefault<z.ZodEnum<{
686
+ stair: "stair";
687
+ landing: "landing";
688
+ }>>;
689
+ width: z.ZodDefault<z.ZodNumber>;
690
+ length: z.ZodDefault<z.ZodNumber>;
691
+ height: z.ZodDefault<z.ZodNumber>;
692
+ stepCount: z.ZodDefault<z.ZodNumber>;
693
+ attachmentSide: z.ZodDefault<z.ZodEnum<{
694
+ front: "front";
695
+ left: "left";
696
+ right: "right";
697
+ }>>;
698
+ fillToFloor: z.ZodDefault<z.ZodBoolean>;
699
+ thickness: z.ZodDefault<z.ZodNumber>;
582
700
  }, z.core.$strip>, z.ZodObject<{
583
701
  object: z.ZodDefault<z.ZodLiteral<"node">>;
584
702
  name: z.ZodOptional<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,KAAK,CAAA;AAgBnB,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAelB,CAAA;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAA;AAC7C,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AACzC,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,KAAK,CAAA;AAkBnB,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAiBlB,CAAA;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAA;AAC7C,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AACzC,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA"}
@@ -10,6 +10,8 @@ import { RoofSegmentNode } from './nodes/roof-segment';
10
10
  import { ScanNode } from './nodes/scan';
11
11
  import { SiteNode } from './nodes/site';
12
12
  import { SlabNode } from './nodes/slab';
13
+ import { StairNode } from './nodes/stair';
14
+ import { StairSegmentNode } from './nodes/stair-segment';
13
15
  import { WallNode } from './nodes/wall';
14
16
  import { WindowNode } from './nodes/window';
15
17
  import { ZoneNode } from './nodes/zone';
@@ -24,6 +26,8 @@ export const AnyNode = z.discriminatedUnion('type', [
24
26
  CeilingNode,
25
27
  RoofNode,
26
28
  RoofSegmentNode,
29
+ StairNode,
30
+ StairSegmentNode,
27
31
  ScanNode,
28
32
  GuideNode,
29
33
  WindowNode,
@@ -1 +1 @@
1
- {"version":3,"file":"node-actions.d.ts","sourceRoot":"","sources":["../../../src/store/actions/node-actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAQ9C,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,EAAE,SA2C/C,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,SAAS;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,SAsErD,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK,SAAS,EAAE,SA4EjB,CAAA"}
1
+ {"version":3,"file":"node-actions.d.ts","sourceRoot":"","sources":["../../../src/store/actions/node-actions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAQ9C,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,EAAE,SA4C/C,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,SAAS;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,SA+DrD,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,EAC7D,KAAK,MAAM,UAAU,EACrB,KAAK,SAAS,EAAE,SAuEjB,CAAA"}
@@ -2,6 +2,8 @@
2
2
  let pendingRafId = null;
3
3
  let pendingUpdates = new Set();
4
4
  export const createNodesAction = (set, get, ops) => {
5
+ if (get().readOnly)
6
+ return;
5
7
  set((state) => {
6
8
  const nextNodes = { ...state.nodes };
7
9
  const nextRootIds = [...state.rootNodeIds];
@@ -41,8 +43,9 @@ export const createNodesAction = (set, get, ops) => {
41
43
  });
42
44
  };
43
45
  export const updateNodesAction = (set, get, updates) => {
46
+ if (get().readOnly)
47
+ return;
44
48
  const parentsToUpdate = new Set();
45
- const idsToMarkDirty = new Set();
46
49
  set((state) => {
47
50
  const nextNodes = { ...state.nodes };
48
51
  for (const { id, data } of updates) {
@@ -77,23 +80,17 @@ export const updateNodesAction = (set, get, updates) => {
77
80
  }
78
81
  return { nodes: nextNodes };
79
82
  });
80
- // Collect all IDs that need to be marked dirty
83
+ // Batch dirty-marking into a single RAF to avoid redundant callbacks during rapid updates
81
84
  for (const u of updates) {
82
- idsToMarkDirty.add(u.id);
85
+ pendingUpdates.add(u.id);
83
86
  }
84
87
  for (const pId of parentsToUpdate) {
85
- idsToMarkDirty.add(pId);
88
+ pendingUpdates.add(pId);
86
89
  }
87
- // Add to pending updates set
88
- for (const id of idsToMarkDirty) {
89
- pendingUpdates.add(id);
90
- }
91
- // Cancel any pending RAF and schedule a new one
92
90
  if (pendingRafId !== null) {
93
91
  cancelAnimationFrame(pendingRafId);
94
92
  }
95
93
  pendingRafId = requestAnimationFrame(() => {
96
- // Mark all pending updates as dirty
97
94
  pendingUpdates.forEach((id) => {
98
95
  get().markDirty(id);
99
96
  });
@@ -102,36 +99,35 @@ export const updateNodesAction = (set, get, updates) => {
102
99
  });
103
100
  };
104
101
  export const deleteNodesAction = (set, get, ids) => {
102
+ if (get().readOnly)
103
+ return;
105
104
  const parentsToMarkDirty = new Set();
106
105
  set((state) => {
107
106
  const nextNodes = { ...state.nodes };
108
107
  const nextCollections = { ...state.collections };
109
108
  let nextRootIds = [...state.rootNodeIds];
110
- // Collect all IDs to delete (including descendants) in a first pass
111
- // This avoids issues with recursive calls during state mutation
112
- const allIdsToDelete = new Set();
113
- const collectDescendants = (id) => {
114
- const node = nextNodes[id];
115
- if (!node)
109
+ // Collect all ids to delete (the requested ids + all their descendants) before
110
+ // mutating anything, so the recursive walk reads consistent state.
111
+ const allIds = new Set();
112
+ const collect = (id) => {
113
+ if (allIds.has(id))
116
114
  return;
117
- allIdsToDelete.add(id);
118
- if ('children' in node && node.children) {
119
- for (const childId of node.children) {
120
- collectDescendants(childId);
121
- }
115
+ allIds.add(id);
116
+ const node = nextNodes[id];
117
+ if (node && 'children' in node) {
118
+ for (const cid of node.children)
119
+ collect(cid);
122
120
  }
123
121
  };
124
- for (const id of ids) {
125
- collectDescendants(id);
126
- }
127
- // Now process all nodes for deletion
128
- for (const id of allIdsToDelete) {
122
+ for (const id of ids)
123
+ collect(id);
124
+ for (const id of allIds) {
129
125
  const node = nextNodes[id];
130
126
  if (!node)
131
127
  continue;
132
- // 1. Remove reference from Parent
128
+ // 1. Remove reference from parent — only if the parent itself is NOT also being deleted
133
129
  const parentId = node.parentId;
134
- if (parentId && nextNodes[parentId]) {
130
+ if (parentId && nextNodes[parentId] && !allIds.has(parentId)) {
135
131
  const parent = nextNodes[parentId];
136
132
  if (parent.children) {
137
133
  nextNodes[parent.id] = {
@@ -141,7 +137,7 @@ export const deleteNodesAction = (set, get, ids) => {
141
137
  parentsToMarkDirty.add(parent.id);
142
138
  }
143
139
  }
144
- // 2. Remove from Root list
140
+ // 2. Remove from root list
145
141
  nextRootIds = nextRootIds.filter((rid) => rid !== id);
146
142
  // 3. Remove from any collections it belongs to
147
143
  if ('collectionIds' in node && node.collectionIds) {
@@ -0,0 +1,14 @@
1
+ export type LiveTransform = {
2
+ position: [number, number, number];
3
+ rotation: number;
4
+ };
5
+ type LiveTransformState = {
6
+ transforms: Map<string, LiveTransform>;
7
+ set(nodeId: string, transform: LiveTransform): void;
8
+ get(nodeId: string): LiveTransform | undefined;
9
+ clear(nodeId: string): void;
10
+ clearAll(): void;
11
+ };
12
+ declare const useLiveTransforms: import("zustand").UseBoundStore<import("zustand").StoreApi<LiveTransformState>>;
13
+ export default useLiveTransforms;
14
+ //# sourceMappingURL=use-live-transforms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-live-transforms.d.ts","sourceRoot":"","sources":["../../src/store/use-live-transforms.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACtC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI,CAAA;IACnD,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAA;IAC9C,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,IAAI,IAAI,CAAA;CACjB,CAAA;AAED,QAAA,MAAM,iBAAiB,iFAgBpB,CAAA;AAEH,eAAe,iBAAiB,CAAA"}
@@ -0,0 +1,20 @@
1
+ // Ephemeral live transform state for nodes being actively dragged/moved.
2
+ // This decouples 2D (floorplan) and 3D (viewer) so neither needs to peek
3
+ // into the other's scene graph during drag operations.
4
+ import { create } from 'zustand';
5
+ const useLiveTransforms = create((set, get) => ({
6
+ transforms: new Map(),
7
+ set: (nodeId, transform) => set((state) => {
8
+ const next = new Map(state.transforms);
9
+ next.set(nodeId, transform);
10
+ return { transforms: next };
11
+ }),
12
+ get: (nodeId) => get().transforms.get(nodeId),
13
+ clear: (nodeId) => set((state) => {
14
+ const next = new Map(state.transforms);
15
+ next.delete(nodeId);
16
+ return { transforms: next };
17
+ }),
18
+ clearAll: () => set({ transforms: new Map() }),
19
+ }));
20
+ export default useLiveTransforms;
@@ -7,6 +7,8 @@ export type SceneState = {
7
7
  rootNodeIds: AnyNodeId[];
8
8
  dirtyNodes: Set<AnyNodeId>;
9
9
  collections: Record<CollectionId, Collection>;
10
+ readOnly: boolean;
11
+ setReadOnly: (readOnly: boolean) => void;
10
12
  loadScene: () => void;
11
13
  clearScene: () => void;
12
14
  unloadScene: () => void;
@@ -36,10 +38,5 @@ type UseSceneStore = UseBoundStore<StoreApi<SceneState>> & {
36
38
  };
37
39
  declare const useScene: UseSceneStore;
38
40
  export default useScene;
39
- /**
40
- * Clears temporal history tracking variables to prevent memory leaks.
41
- * Should be called when unloading a scene to release node references.
42
- */
43
- export declare function clearTemporalTracking(): void;
44
41
  export declare function clearSceneHistory(): void;
45
42
  //# sourceMappingURL=use-scene.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAIrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AA8CzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aA2Mf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB;;;GAGG;AACH,wBAAgB,qBAAqB,SAIpC;AAED,wBAAgB,iBAAiB,SAGhC"}
1
+ {"version":3,"file":"use-scene.d.ts","sourceRoot":"","sources":["../../src/store/use-scene.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAE1C,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAIrE,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AA8CzD,MAAM,MAAM,UAAU,GAAG;IAEvB,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAGjC,WAAW,EAAE,SAAS,EAAE,CAAA;IAGxB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IAG1B,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IAG7C,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAGxC,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAE/E,SAAS,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IAEnC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,KAAK,IAAI,CAAA;IACzD,WAAW,EAAE,CAAC,GAAG,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAErE,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC3D,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,KAAK,IAAI,CAAA;IAE3E,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,IAAI,CAAA;IAGvC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,YAAY,CAAA;IACvE,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAA;IAC5C,gBAAgB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAA;IACnF,eAAe,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;IAC9D,oBAAoB,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,KAAK,IAAI,CAAA;CACpE,CAAA;AAID,KAAK,aAAa,GAAG,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,GAAG;IACzD,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;CAC7F,CAAA;AAED,QAAA,MAAM,QAAQ,EAAE,aA8Nf,CAAA;AAED,eAAe,QAAQ,CAAA;AAOvB,wBAAgB,iBAAiB,SAKhC"}
@@ -55,11 +55,10 @@ const useScene = create()(temporal((set, get) => ({
55
55
  dirtyNodes: new Set(),
56
56
  // 4. Collections
57
57
  collections: {},
58
+ // 5. Read-only lock
59
+ readOnly: false,
60
+ setReadOnly: (readOnly) => set({ readOnly }),
58
61
  unloadScene: () => {
59
- // Clear temporal tracking to prevent memory leaks from stale node references
60
- prevPastLength = 0;
61
- prevFutureLength = 0;
62
- prevNodesSnapshot = null;
63
62
  set({
64
63
  nodes: {},
65
64
  rootNodeIds: [],
@@ -74,14 +73,22 @@ const useScene = create()(temporal((set, get) => ({
74
73
  setScene: (nodes, rootNodeIds) => {
75
74
  // Apply backward compatibility migrations
76
75
  const patchedNodes = migrateNodes(nodes);
76
+ // Remove orphans: nodes whose parentId points to a non-existent node
77
+ const cleanedNodes = { ...patchedNodes };
78
+ for (const node of Object.values(cleanedNodes)) {
79
+ if (node.parentId && !cleanedNodes[node.parentId]) {
80
+ console.warn('[Scene] Removing orphan node', node.id, '(parentId', node.parentId, 'not found)');
81
+ delete cleanedNodes[node.id];
82
+ }
83
+ }
77
84
  set({
78
- nodes: patchedNodes,
85
+ nodes: cleanedNodes,
79
86
  rootNodeIds,
80
87
  dirtyNodes: new Set(),
81
88
  collections: {},
82
89
  });
83
90
  // Mark all nodes as dirty to trigger re-validation
84
- Object.values(patchedNodes).forEach((node) => {
91
+ Object.values(cleanedNodes).forEach((node) => {
85
92
  get().markDirty(node.id);
86
93
  });
87
94
  },
@@ -129,6 +136,8 @@ const useScene = create()(temporal((set, get) => ({
129
136
  deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]),
130
137
  // --- COLLECTIONS ---
131
138
  createCollection: (name, nodeIds = []) => {
139
+ if (get().readOnly)
140
+ return '';
132
141
  const id = generateCollectionId();
133
142
  const collection = { id, name, nodeIds };
134
143
  set((state) => {
@@ -147,6 +156,8 @@ const useScene = create()(temporal((set, get) => ({
147
156
  return id;
148
157
  },
149
158
  deleteCollection: (id) => {
159
+ if (get().readOnly)
160
+ return;
150
161
  set((state) => {
151
162
  const col = state.collections[id];
152
163
  const nextCollections = { ...state.collections };
@@ -166,6 +177,8 @@ const useScene = create()(temporal((set, get) => ({
166
177
  });
167
178
  },
168
179
  updateCollection: (id, data) => {
180
+ if (get().readOnly)
181
+ return;
169
182
  set((state) => {
170
183
  const col = state.collections[id];
171
184
  if (!col)
@@ -174,6 +187,8 @@ const useScene = create()(temporal((set, get) => ({
174
187
  });
175
188
  },
176
189
  addToCollection: (id, nodeId) => {
190
+ if (get().readOnly)
191
+ return;
177
192
  set((state) => {
178
193
  const col = state.collections[id];
179
194
  if (!col || col.nodeIds.includes(nodeId))
@@ -194,6 +209,8 @@ const useScene = create()(temporal((set, get) => ({
194
209
  });
195
210
  },
196
211
  removeFromCollection: (id, nodeId) => {
212
+ if (get().readOnly)
213
+ return;
197
214
  set((state) => {
198
215
  const col = state.collections[id];
199
216
  if (!col)
@@ -227,19 +244,12 @@ export default useScene;
227
244
  let prevPastLength = 0;
228
245
  let prevFutureLength = 0;
229
246
  let prevNodesSnapshot = null;
230
- /**
231
- * Clears temporal history tracking variables to prevent memory leaks.
232
- * Should be called when unloading a scene to release node references.
233
- */
234
- export function clearTemporalTracking() {
247
+ export function clearSceneHistory() {
248
+ useScene.temporal.getState().clear();
235
249
  prevPastLength = 0;
236
250
  prevFutureLength = 0;
237
251
  prevNodesSnapshot = null;
238
252
  }
239
- export function clearSceneHistory() {
240
- useScene.temporal.getState().clear();
241
- clearTemporalTracking();
242
- }
243
253
  // Subscribe to the temporal store (Undo/Redo events)
244
254
  useScene.temporal.subscribe((state) => {
245
255
  const currentPastLength = state.pastStates.length;
@@ -1 +1 @@
1
- {"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AA4BA,eAAO,MAAM,UAAU,YA2BtB,CAAA"}
1
+ {"version":3,"file":"door-system.d.ts","sourceRoot":"","sources":["../../../src/systems/door/door-system.tsx"],"names":[],"mappings":"AAUA,eAAO,MAAM,UAAU,YA2BtB,CAAA"}
@@ -1,24 +1,8 @@
1
1
  import { useFrame } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
- import { DoubleSide, MeshStandardNodeMaterial } from 'three/webgpu';
4
3
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
+ import { baseMaterial, glassMaterial } from '../../materials';
5
5
  import useScene from '../../store/use-scene';
6
- const baseMaterial = new MeshStandardNodeMaterial({
7
- name: 'door-base',
8
- color: '#f2f0ed',
9
- roughness: 0.5,
10
- metalness: 0,
11
- });
12
- const glassMaterial = new MeshStandardNodeMaterial({
13
- name: 'door-glass',
14
- color: 'lightblue',
15
- roughness: 0.05,
16
- metalness: 0.1,
17
- transparent: true,
18
- opacity: 0.35,
19
- side: DoubleSide,
20
- depthWrite: false,
21
- });
22
6
  // Invisible material for root mesh — used as selection hitbox only
23
7
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
24
8
  export const DoorSystem = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AAwBjF,eAAO,MAAM,UAAU,YAqFtB,CAAA;AAmLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAkRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAoDvF"}
1
+ {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AA+BjF,eAAO,MAAM,UAAU,YAuFtB,CAAA;AAuLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAsRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAoDvF"}
@@ -7,7 +7,13 @@ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
7
7
  import useScene from '../../store/use-scene';
8
8
  const csgEvaluator = new Evaluator();
9
9
  csgEvaluator.useGroups = true;
10
+ csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
10
11
  csgEvaluator.attributes = ['position', 'normal'];
12
+ function prepareBrushForCSG(brush) {
13
+ brush.geometry.computeBoundsTree = computeBoundsTree;
14
+ brush.geometry.computeBoundsTree({ maxLeafSize: 10 });
15
+ brush.updateMatrixWorld();
16
+ }
11
17
  // Pooled objects to avoid per-frame allocation in updateMergedRoofGeometry
12
18
  const _matrix = new THREE.Matrix4();
13
19
  const _position = new THREE.Vector3();
@@ -71,6 +77,9 @@ export const RoofSystem = () => {
71
77
  }
72
78
  clearDirty(id);
73
79
  }
80
+ else {
81
+ clearDirty(id);
82
+ }
74
83
  // Queue the parent roof for a merged geometry update
75
84
  if (node.parentId) {
76
85
  pendingRoofUpdates.add(node.parentId);
@@ -151,6 +160,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
151
160
  const next = csgEvaluator.evaluate(totalShinSlab, brushes.shinSlab, ADDITION);
152
161
  totalShinSlab.geometry.dispose();
153
162
  brushes.shinSlab.geometry.dispose();
163
+ prepareBrushForCSG(next);
154
164
  totalShinSlab = next;
155
165
  }
156
166
  else {
@@ -160,6 +170,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
160
170
  const next = csgEvaluator.evaluate(totalDeckSlab, brushes.deckSlab, ADDITION);
161
171
  totalDeckSlab.geometry.dispose();
162
172
  brushes.deckSlab.geometry.dispose();
173
+ prepareBrushForCSG(next);
163
174
  totalDeckSlab = next;
164
175
  }
165
176
  else {
@@ -169,6 +180,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
169
180
  const next = csgEvaluator.evaluate(totalWall, brushes.wallBrush, ADDITION);
170
181
  totalWall.geometry.dispose();
171
182
  brushes.wallBrush.geometry.dispose();
183
+ prepareBrushForCSG(next);
172
184
  totalWall = next;
173
185
  }
174
186
  else {
@@ -178,6 +190,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
178
190
  const next = csgEvaluator.evaluate(totalInner, brushes.innerBrush, ADDITION);
179
191
  totalInner.geometry.dispose();
180
192
  brushes.innerBrush.geometry.dispose();
193
+ prepareBrushForCSG(next);
181
194
  totalInner = next;
182
195
  }
183
196
  else {
@@ -380,6 +393,11 @@ export function getRoofSegmentBrushes(node) {
380
393
  return null;
381
394
  if (!geo.index)
382
395
  return null;
396
+ // Strip zero-count groups — three-bvh-csg crashes with groupIndices[i] undefined
397
+ // when a group exists but covers no triangles (can happen after mergeVertices)
398
+ geo.groups = geo.groups.filter((g) => g.count > 0);
399
+ if (geo.groups.length === 0)
400
+ return null;
383
401
  geo.computeBoundsTree = computeBoundsTree;
384
402
  geo.computeBoundsTree({ maxLeafSize: 10 });
385
403
  const brush = new Brush(geo, dummyMats);
@@ -1 +1 @@
1
- {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAkED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAmD7E"}
1
+ {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAuED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}