@pascal-app/core 0.5.1 → 0.7.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 (134) hide show
  1. package/dist/events/bus.d.ts +74 -4
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +2 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +2 -0
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.js +164 -6
  9. package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
  10. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
  11. package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
  12. package/dist/index.d.ts +9 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +7 -11
  15. package/dist/lib/door-operation.d.ts +7 -0
  16. package/dist/lib/door-operation.d.ts.map +1 -0
  17. package/dist/lib/door-operation.js +25 -0
  18. package/dist/lib/polygon-geometry.d.ts +3 -0
  19. package/dist/lib/polygon-geometry.d.ts.map +1 -0
  20. package/dist/lib/polygon-geometry.js +90 -0
  21. package/dist/lib/slab-polygon.d.ts +3 -0
  22. package/dist/lib/slab-polygon.d.ts.map +1 -0
  23. package/dist/lib/slab-polygon.js +58 -0
  24. package/dist/lib/space-detection.d.ts +10 -17
  25. package/dist/lib/space-detection.d.ts.map +1 -1
  26. package/dist/lib/space-detection.js +666 -453
  27. package/dist/material-library.d.ts +20 -0
  28. package/dist/material-library.d.ts.map +1 -0
  29. package/dist/material-library.js +580 -0
  30. package/dist/schema/asset-url.d.ts +34 -0
  31. package/dist/schema/asset-url.d.ts.map +1 -0
  32. package/dist/schema/asset-url.js +79 -0
  33. package/dist/schema/asset-url.test.d.ts +2 -0
  34. package/dist/schema/asset-url.test.d.ts.map +1 -0
  35. package/dist/schema/asset-url.test.js +138 -0
  36. package/dist/schema/index.d.ts +14 -7
  37. package/dist/schema/index.d.ts.map +1 -1
  38. package/dist/schema/index.js +10 -7
  39. package/dist/schema/material.d.ts +112 -2
  40. package/dist/schema/material.d.ts.map +1 -1
  41. package/dist/schema/material.js +55 -1
  42. package/dist/schema/nodes/ceiling.d.ts +11 -1
  43. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  44. package/dist/schema/nodes/ceiling.js +6 -0
  45. package/dist/schema/nodes/column.d.ts +520 -0
  46. package/dist/schema/nodes/column.d.ts.map +1 -0
  47. package/dist/schema/nodes/column.js +385 -0
  48. package/dist/schema/nodes/door.d.ts +74 -1
  49. package/dist/schema/nodes/door.d.ts.map +1 -1
  50. package/dist/schema/nodes/door.js +39 -2
  51. package/dist/schema/nodes/fence.d.ts +34 -0
  52. package/dist/schema/nodes/fence.d.ts.map +1 -1
  53. package/dist/schema/nodes/fence.js +5 -0
  54. package/dist/schema/nodes/guide.d.ts +17 -0
  55. package/dist/schema/nodes/guide.d.ts.map +1 -1
  56. package/dist/schema/nodes/guide.js +11 -1
  57. package/dist/schema/nodes/item.d.ts +10 -2
  58. package/dist/schema/nodes/item.d.ts.map +1 -1
  59. package/dist/schema/nodes/item.js +18 -1
  60. package/dist/schema/nodes/level.d.ts +1 -1
  61. package/dist/schema/nodes/level.d.ts.map +1 -1
  62. package/dist/schema/nodes/level.js +6 -0
  63. package/dist/schema/nodes/roof-segment.d.ts +3 -1
  64. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  65. package/dist/schema/nodes/roof-segment.js +1 -0
  66. package/dist/schema/nodes/roof.d.ts +108 -0
  67. package/dist/schema/nodes/roof.d.ts.map +1 -1
  68. package/dist/schema/nodes/roof.js +58 -2
  69. package/dist/schema/nodes/scan.d.ts.map +1 -1
  70. package/dist/schema/nodes/scan.js +2 -1
  71. package/dist/schema/nodes/site.d.ts +2 -1
  72. package/dist/schema/nodes/site.d.ts.map +1 -1
  73. package/dist/schema/nodes/slab.d.ts +11 -1
  74. package/dist/schema/nodes/slab.d.ts.map +1 -1
  75. package/dist/schema/nodes/slab.js +7 -0
  76. package/dist/schema/nodes/spawn.d.ts +24 -0
  77. package/dist/schema/nodes/spawn.d.ts.map +1 -0
  78. package/dist/schema/nodes/spawn.js +8 -0
  79. package/dist/schema/nodes/stair-segment.d.ts +3 -1
  80. package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
  81. package/dist/schema/nodes/stair-segment.js +1 -0
  82. package/dist/schema/nodes/stair.d.ts +122 -2
  83. package/dist/schema/nodes/stair.d.ts.map +1 -1
  84. package/dist/schema/nodes/stair.js +72 -2
  85. package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
  86. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
  87. package/dist/schema/nodes/surface-hole-metadata.js +5 -0
  88. package/dist/schema/nodes/wall.d.ts +87 -1
  89. package/dist/schema/nodes/wall.d.ts.map +1 -1
  90. package/dist/schema/nodes/wall.js +45 -4
  91. package/dist/schema/nodes/window.d.ts +57 -1
  92. package/dist/schema/nodes/window.d.ts.map +1 -1
  93. package/dist/schema/nodes/window.js +29 -0
  94. package/dist/schema/types.d.ts +653 -12
  95. package/dist/schema/types.d.ts.map +1 -1
  96. package/dist/schema/types.js +4 -0
  97. package/dist/store/actions/node-actions.d.ts +1 -1
  98. package/dist/store/actions/node-actions.d.ts.map +1 -1
  99. package/dist/store/actions/node-actions.js +181 -5
  100. package/dist/store/history-control.d.ts +14 -0
  101. package/dist/store/history-control.d.ts.map +1 -0
  102. package/dist/store/history-control.js +22 -0
  103. package/dist/store/use-interactive.d.ts +43 -0
  104. package/dist/store/use-interactive.d.ts.map +1 -1
  105. package/dist/store/use-interactive.js +66 -0
  106. package/dist/store/use-scene.d.ts.map +1 -1
  107. package/dist/store/use-scene.js +307 -3
  108. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  109. package/dist/systems/ceiling/ceiling-system.js +7 -0
  110. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  111. package/dist/systems/fence/fence-system.js +106 -39
  112. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  113. package/dist/systems/roof/roof-system.js +31 -1
  114. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  115. package/dist/systems/slab/slab-system.js +45 -8
  116. package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
  117. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
  118. package/dist/systems/stair/stair-opening-sync.js +576 -0
  119. package/dist/systems/stair/stair-opening-sync.test.d.ts +2 -0
  120. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +1 -0
  121. package/dist/systems/stair/stair-opening-sync.test.js +65 -0
  122. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  123. package/dist/systems/stair/stair-system.js +119 -2
  124. package/dist/systems/wall/wall-curve.d.ts +43 -0
  125. package/dist/systems/wall/wall-curve.d.ts.map +1 -0
  126. package/dist/systems/wall/wall-curve.js +176 -0
  127. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  128. package/dist/systems/wall/wall-footprint.js +16 -2
  129. package/dist/systems/wall/wall-mitering.d.ts +7 -0
  130. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  131. package/dist/systems/wall/wall-mitering.js +76 -3
  132. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  133. package/dist/systems/wall/wall-system.js +202 -2
  134. package/package.json +33 -5
@@ -5,7 +5,233 @@ import { BuildingNode } from '../schema';
5
5
  import { generateCollectionId } from '../schema/collections';
6
6
  import { LevelNode } from '../schema/nodes/level';
7
7
  import { SiteNode } from '../schema/nodes/site';
8
+ import { StairNode as StairNodeSchema } from '../schema/nodes/stair';
9
+ import { StairSegmentNode as StairSegmentNodeSchema } from '../schema/nodes/stair-segment';
8
10
  import * as nodeActions from './actions/node-actions';
11
+ import { resetSceneHistoryPauseDepth } from './history-control';
12
+ function getFiniteNumber(value, fallback) {
13
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
14
+ }
15
+ function getBoolean(value, fallback) {
16
+ return typeof value === 'boolean' ? value : fallback;
17
+ }
18
+ function getEnumValue(value, allowed, fallback) {
19
+ return typeof value === 'string' && allowed.includes(value) ? value : fallback;
20
+ }
21
+ function getNullableString(value) {
22
+ return typeof value === 'string' ? value : null;
23
+ }
24
+ function getStringArray(value) {
25
+ return Array.isArray(value)
26
+ ? value.filter((entry) => typeof entry === 'string')
27
+ : [];
28
+ }
29
+ function getVector3(value, fallback) {
30
+ if (!Array.isArray(value) || value.length < 3) {
31
+ return fallback;
32
+ }
33
+ return [
34
+ getFiniteNumber(value[0], fallback[0]),
35
+ getFiniteNumber(value[1], fallback[1]),
36
+ getFiniteNumber(value[2], fallback[2]),
37
+ ];
38
+ }
39
+ function normalizeStairNode(node) {
40
+ const sanitized = {
41
+ ...node,
42
+ position: getVector3(node.position, [0, 0, 0]),
43
+ rotation: getFiniteNumber(node.rotation, 0),
44
+ stairType: getEnumValue(node.stairType, ['straight', 'curved', 'spiral'], 'straight'),
45
+ fromLevelId: getNullableString(node.fromLevelId),
46
+ toLevelId: getNullableString(node.toLevelId),
47
+ slabOpeningMode: getEnumValue(node.slabOpeningMode, ['none', 'destination'], 'none'),
48
+ openingOffset: getFiniteNumber(node.openingOffset, 0),
49
+ width: getFiniteNumber(node.width, 1),
50
+ totalRise: getFiniteNumber(node.totalRise, 2.5),
51
+ stepCount: getFiniteNumber(node.stepCount, 10),
52
+ thickness: getFiniteNumber(node.thickness, 0.25),
53
+ fillToFloor: getBoolean(node.fillToFloor, true),
54
+ innerRadius: getFiniteNumber(node.innerRadius, 0.9),
55
+ sweepAngle: getFiniteNumber(node.sweepAngle, Math.PI / 2),
56
+ topLandingMode: getEnumValue(node.topLandingMode, ['none', 'integrated'], 'none'),
57
+ topLandingDepth: getFiniteNumber(node.topLandingDepth, 0.9),
58
+ showCenterColumn: getBoolean(node.showCenterColumn, true),
59
+ showStepSupports: getBoolean(node.showStepSupports, true),
60
+ railingMode: getEnumValue(node.railingMode, ['none', 'left', 'right', 'both'], 'none'),
61
+ railingHeight: getFiniteNumber(node.railingHeight, 0.92),
62
+ children: getStringArray(node.children),
63
+ };
64
+ const parsed = StairNodeSchema.safeParse(sanitized);
65
+ return parsed.success ? parsed.data : null;
66
+ }
67
+ function normalizeStairSegmentNode(node) {
68
+ const sanitized = {
69
+ ...node,
70
+ position: getVector3(node.position, [0, 0, 0]),
71
+ rotation: getFiniteNumber(node.rotation, 0),
72
+ segmentType: getEnumValue(node.segmentType, ['stair', 'landing'], 'stair'),
73
+ width: getFiniteNumber(node.width, 1),
74
+ length: getFiniteNumber(node.length, 3),
75
+ height: getFiniteNumber(node.height, 2.5),
76
+ stepCount: getFiniteNumber(node.stepCount, 10),
77
+ attachmentSide: getEnumValue(node.attachmentSide, ['front', 'left', 'right'], 'front'),
78
+ fillToFloor: getBoolean(node.fillToFloor, true),
79
+ thickness: getFiniteNumber(node.thickness, 0.25),
80
+ };
81
+ const parsed = StairSegmentNodeSchema.safeParse(sanitized);
82
+ return parsed.success ? parsed.data : null;
83
+ }
84
+ function migrateWallSurfaceMaterials(node) {
85
+ const hasInterior = node.interiorMaterial !== undefined || typeof node.interiorMaterialPreset === 'string';
86
+ const hasExterior = node.exteriorMaterial !== undefined || typeof node.exteriorMaterialPreset === 'string';
87
+ const legacyFinish = {
88
+ material: node.material,
89
+ materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
90
+ };
91
+ if (!hasInterior && !hasExterior) {
92
+ if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
93
+ return node;
94
+ }
95
+ return {
96
+ ...node,
97
+ interiorMaterial: legacyFinish.material,
98
+ interiorMaterialPreset: legacyFinish.materialPreset,
99
+ exteriorMaterial: legacyFinish.material,
100
+ exteriorMaterialPreset: legacyFinish.materialPreset,
101
+ };
102
+ }
103
+ if (!hasInterior) {
104
+ return {
105
+ ...node,
106
+ interiorMaterial: node.exteriorMaterial,
107
+ interiorMaterialPreset: node.exteriorMaterialPreset,
108
+ };
109
+ }
110
+ if (!hasExterior) {
111
+ return {
112
+ ...node,
113
+ exteriorMaterial: node.interiorMaterial,
114
+ exteriorMaterialPreset: node.interiorMaterialPreset,
115
+ };
116
+ }
117
+ return node;
118
+ }
119
+ function migrateStairSurfaceMaterials(node) {
120
+ const hasRailing = node.railingMaterial !== undefined || typeof node.railingMaterialPreset === 'string';
121
+ const hasTread = node.treadMaterial !== undefined || typeof node.treadMaterialPreset === 'string';
122
+ const hasSide = node.sideMaterial !== undefined || typeof node.sideMaterialPreset === 'string';
123
+ const legacyFinish = {
124
+ material: node.material,
125
+ materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
126
+ };
127
+ const resolveBodyFallback = () => {
128
+ if (node.treadMaterial !== undefined || typeof node.treadMaterialPreset === 'string') {
129
+ return {
130
+ material: node.treadMaterial,
131
+ materialPreset: typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
132
+ };
133
+ }
134
+ if (node.sideMaterial !== undefined || typeof node.sideMaterialPreset === 'string') {
135
+ return {
136
+ material: node.sideMaterial,
137
+ materialPreset: typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
138
+ };
139
+ }
140
+ return legacyFinish;
141
+ };
142
+ if (!hasRailing && !hasTread && !hasSide) {
143
+ if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
144
+ return node;
145
+ }
146
+ return {
147
+ ...node,
148
+ railingMaterial: legacyFinish.material,
149
+ railingMaterialPreset: legacyFinish.materialPreset,
150
+ treadMaterial: legacyFinish.material,
151
+ treadMaterialPreset: legacyFinish.materialPreset,
152
+ sideMaterial: legacyFinish.material,
153
+ sideMaterialPreset: legacyFinish.materialPreset,
154
+ };
155
+ }
156
+ const next = { ...node };
157
+ if (!hasTread) {
158
+ const fallback = node.sideMaterial !== undefined || typeof node.sideMaterialPreset === 'string'
159
+ ? {
160
+ material: node.sideMaterial,
161
+ materialPreset: typeof node.sideMaterialPreset === 'string' ? node.sideMaterialPreset : undefined,
162
+ }
163
+ : resolveBodyFallback();
164
+ next.treadMaterial = fallback.material;
165
+ next.treadMaterialPreset = fallback.materialPreset;
166
+ }
167
+ if (!hasSide) {
168
+ const fallback = node.treadMaterial !== undefined || typeof node.treadMaterialPreset === 'string'
169
+ ? {
170
+ material: node.treadMaterial,
171
+ materialPreset: typeof node.treadMaterialPreset === 'string' ? node.treadMaterialPreset : undefined,
172
+ }
173
+ : resolveBodyFallback();
174
+ next.sideMaterial = fallback.material;
175
+ next.sideMaterialPreset = fallback.materialPreset;
176
+ }
177
+ if (!hasRailing) {
178
+ const fallback = resolveBodyFallback();
179
+ next.railingMaterial = fallback.material;
180
+ next.railingMaterialPreset = fallback.materialPreset;
181
+ }
182
+ return next;
183
+ }
184
+ function migrateRoofSurfaceMaterials(node) {
185
+ const hasTop = node.topMaterial !== undefined || typeof node.topMaterialPreset === 'string';
186
+ const hasEdge = node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string';
187
+ const hasWall = node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string';
188
+ const legacyFinish = {
189
+ material: node.material,
190
+ materialPreset: typeof node.materialPreset === 'string' ? node.materialPreset : undefined,
191
+ };
192
+ if (!hasTop && !hasEdge && !hasWall) {
193
+ if (legacyFinish.material === undefined && legacyFinish.materialPreset === undefined) {
194
+ return node;
195
+ }
196
+ return {
197
+ ...node,
198
+ topMaterial: legacyFinish.material,
199
+ topMaterialPreset: legacyFinish.materialPreset,
200
+ edgeMaterial: legacyFinish.material,
201
+ edgeMaterialPreset: legacyFinish.materialPreset,
202
+ wallMaterial: legacyFinish.material,
203
+ wallMaterialPreset: legacyFinish.materialPreset,
204
+ };
205
+ }
206
+ const next = { ...node };
207
+ if (!hasTop) {
208
+ next.topMaterial = legacyFinish.material;
209
+ next.topMaterialPreset = legacyFinish.materialPreset;
210
+ }
211
+ if (!hasEdge) {
212
+ if (node.wallMaterial !== undefined || typeof node.wallMaterialPreset === 'string') {
213
+ next.edgeMaterial = node.wallMaterial;
214
+ next.edgeMaterialPreset =
215
+ typeof node.wallMaterialPreset === 'string' ? node.wallMaterialPreset : undefined;
216
+ }
217
+ else {
218
+ next.edgeMaterial = legacyFinish.material;
219
+ next.edgeMaterialPreset = legacyFinish.materialPreset;
220
+ }
221
+ }
222
+ if (!hasWall) {
223
+ if (node.edgeMaterial !== undefined || typeof node.edgeMaterialPreset === 'string') {
224
+ next.wallMaterial = node.edgeMaterial;
225
+ next.wallMaterialPreset =
226
+ typeof node.edgeMaterialPreset === 'string' ? node.edgeMaterialPreset : undefined;
227
+ }
228
+ else {
229
+ next.wallMaterial = legacyFinish.material;
230
+ next.wallMaterialPreset = legacyFinish.materialPreset;
231
+ }
232
+ }
233
+ return next;
234
+ }
9
235
  function migrateNodes(nodes) {
10
236
  const patchedNodes = { ...nodes };
11
237
  for (const [id, node] of Object.entries(patchedNodes)) {
@@ -43,9 +269,75 @@ function migrateNodes(nodes) {
43
269
  children: [segmentId],
44
270
  };
45
271
  }
272
+ if (node.type === 'stair') {
273
+ const normalized = normalizeStairNode(migrateStairSurfaceMaterials(node));
274
+ if (normalized) {
275
+ patchedNodes[id] = normalized;
276
+ }
277
+ }
278
+ if (node.type === 'stair-segment') {
279
+ const normalized = normalizeStairSegmentNode(node);
280
+ if (normalized) {
281
+ patchedNodes[id] = normalized;
282
+ }
283
+ }
284
+ if (node.type === 'wall') {
285
+ patchedNodes[id] = migrateWallSurfaceMaterials(patchedNodes[id]);
286
+ }
287
+ if (node.type === 'roof') {
288
+ patchedNodes[id] = migrateRoofSurfaceMaterials(patchedNodes[id]);
289
+ }
46
290
  }
47
291
  return patchedNodes;
48
292
  }
293
+ function getNodeChildIds(node) {
294
+ if (!('children' in node) || !Array.isArray(node.children)) {
295
+ return [];
296
+ }
297
+ return node.children
298
+ .map((child) => {
299
+ if (typeof child === 'string')
300
+ return child;
301
+ if (child && typeof child === 'object' && 'id' in child && typeof child.id === 'string') {
302
+ return child.id;
303
+ }
304
+ return null;
305
+ })
306
+ .filter((id) => typeof id === 'string');
307
+ }
308
+ function normalizeRootNodeIds(nodes, rootNodeIds) {
309
+ const existingRootIds = rootNodeIds.filter((id) => Boolean(nodes[id]));
310
+ const siteRootIds = existingRootIds.filter((id) => nodes[id]?.type === 'site');
311
+ if (siteRootIds.length > 0) {
312
+ return siteRootIds;
313
+ }
314
+ return existingRootIds.filter((id) => nodes[id]?.parentId === null);
315
+ }
316
+ function collectReachableNodeIds(nodes, rootNodeIds) {
317
+ const reachable = new Set();
318
+ const stack = [...rootNodeIds];
319
+ const childIdsByParentId = new Map();
320
+ for (const node of Object.values(nodes)) {
321
+ if (!node.parentId)
322
+ continue;
323
+ const parentId = node.parentId;
324
+ const children = childIdsByParentId.get(parentId) ?? [];
325
+ children.push(node.id);
326
+ childIdsByParentId.set(parentId, children);
327
+ }
328
+ while (stack.length > 0) {
329
+ const id = stack.pop();
330
+ if (!id || reachable.has(id))
331
+ continue;
332
+ const node = nodes[id];
333
+ if (!node)
334
+ continue;
335
+ reachable.add(id);
336
+ stack.push(...getNodeChildIds(node));
337
+ stack.push(...(childIdsByParentId.get(id) ?? []));
338
+ }
339
+ return reachable;
340
+ }
49
341
  const useScene = create()(temporal((set, get) => ({
50
342
  // 1. Flat dictionary of all nodes
51
343
  nodes: {},
@@ -81,9 +373,19 @@ const useScene = create()(temporal((set, get) => ({
81
373
  delete cleanedNodes[node.id];
82
374
  }
83
375
  }
376
+ const normalizedRootNodeIds = normalizeRootNodeIds(cleanedNodes, rootNodeIds);
377
+ const reachableNodeIds = collectReachableNodeIds(cleanedNodes, normalizedRootNodeIds);
378
+ if (normalizedRootNodeIds.length > 0) {
379
+ for (const node of Object.values(cleanedNodes)) {
380
+ if (reachableNodeIds.has(node.id))
381
+ continue;
382
+ console.warn('[Scene] Removing unreachable node', node.id);
383
+ delete cleanedNodes[node.id];
384
+ }
385
+ }
84
386
  set({
85
387
  nodes: cleanedNodes,
86
- rootNodeIds,
388
+ rootNodeIds: normalizedRootNodeIds,
87
389
  dirtyNodes: new Set(),
88
390
  collections: {},
89
391
  });
@@ -246,6 +548,7 @@ let prevFutureLength = 0;
246
548
  let prevNodesSnapshot = null;
247
549
  export function clearSceneHistory() {
248
550
  useScene.temporal.getState().clear();
551
+ resetSceneHistoryPauseDepth();
249
552
  prevPastLength = 0;
250
553
  prevFutureLength = 0;
251
554
  prevNodesSnapshot = null;
@@ -261,8 +564,9 @@ useScene.temporal.subscribe((state) => {
261
564
  if (didUndo || didRedo) {
262
565
  // Capture the previous snapshot before RAF fires
263
566
  const snapshotBefore = prevNodesSnapshot;
264
- // Use RAF to ensure all middleware and store updates are complete
265
- requestAnimationFrame(() => {
567
+ // Defer to a microtask so the scene store has settled before we diff,
568
+ // but still mark walls/items dirty before the next paint.
569
+ queueMicrotask(() => {
266
570
  const currentNodes = useScene.getState().nodes;
267
571
  const { markDirty } = useScene.getState();
268
572
  if (snapshotBefore) {
@@ -1 +1 @@
1
- {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAO1D,eAAO,MAAM,aAAa,YAuBzB,CAAA;AAqBD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,KAAK,CAAC,cAAc,CA+CtF"}
1
+ {"version":3,"file":"ceiling-system.d.ts","sourceRoot":"","sources":["../../../src/systems/ceiling/ceiling-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,cAAc,CAAA;AAc1D,eAAO,MAAM,aAAa,YAuBzB,CAAA;AAqBD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,GAAG,KAAK,CAAC,cAAc,CAgDtF"}
@@ -2,6 +2,12 @@ import { useFrame } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
3
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
4
  import useScene from '../../store/use-scene';
5
+ function ensureUv2Attribute(geometry) {
6
+ const uv = geometry.getAttribute('uv');
7
+ if (!uv)
8
+ return;
9
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
10
+ }
5
11
  // ============================================================================
6
12
  // CEILING SYSTEM
7
13
  // ============================================================================
@@ -81,5 +87,6 @@ export function generateCeilingGeometry(ceilingNode) {
81
87
  // Rotate so the shape lies flat in X-Z plane
82
88
  geometry.rotateX(-Math.PI / 2);
83
89
  geometry.computeVertexNormals();
90
+ ensureUv2Attribute(geometry);
84
91
  return geometry;
85
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AA+HA,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
1
+ {"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAyQA,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
@@ -3,6 +3,94 @@ import * as THREE from 'three';
3
3
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
4
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
5
5
  import useScene from '../../store/use-scene';
6
+ import { getWallCurveFrameAt, getWallCurveLength } from '../wall/wall-curve';
7
+ const MIN_CURVE_SEGMENT_LENGTH = 0.18;
8
+ function createFencePartGeometry(part) {
9
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
10
+ geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
11
+ if (part.rotationY) {
12
+ geometry.rotateY(part.rotationY);
13
+ }
14
+ geometry.translate(part.position[0], part.position[1], part.position[2]);
15
+ applyFenceUVs(geometry);
16
+ return geometry;
17
+ }
18
+ function getFencePointAt(fence, t) {
19
+ const frame = getWallCurveFrameAt(fence, t);
20
+ return {
21
+ point: frame.point,
22
+ tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
23
+ };
24
+ }
25
+ function createStraightFenceSpanPart(start, end, centerY, height, depth) {
26
+ const dx = end[0] - start[0];
27
+ const dz = end[1] - start[1];
28
+ const length = Math.hypot(dx, dz);
29
+ if (length <= 1e-4) {
30
+ return null;
31
+ }
32
+ return {
33
+ position: [(start[0] + end[0]) / 2, centerY, (start[1] + end[1]) / 2],
34
+ rotationY: -Math.atan2(dz, dx),
35
+ scale: [length, height, depth],
36
+ };
37
+ }
38
+ function createFenceCurveSpanParts(fence, startT, endT, centerY, height, depth) {
39
+ const parts = [];
40
+ const frameCount = Math.max(1, Math.ceil((getWallCurveLength(fence) * Math.max(1e-4, endT - startT)) / MIN_CURVE_SEGMENT_LENGTH));
41
+ let previous = getFencePointAt(fence, startT);
42
+ for (let index = 1; index <= frameCount; index += 1) {
43
+ const t = startT + (endT - startT) * (index / frameCount);
44
+ const current = getFencePointAt(fence, t);
45
+ const segment = createStraightFenceSpanPart([previous.point.x, previous.point.y], [current.point.x, current.point.y], centerY, height, depth);
46
+ if (segment) {
47
+ parts.push(segment);
48
+ }
49
+ previous = current;
50
+ }
51
+ return parts;
52
+ }
53
+ function applyFenceUVs(geometry) {
54
+ const position = geometry.getAttribute('position');
55
+ const normal = geometry.getAttribute('normal');
56
+ if (!(position && normal))
57
+ return;
58
+ const uvs = new Float32Array(position.count * 2);
59
+ let minX = Number.POSITIVE_INFINITY;
60
+ let minY = Number.POSITIVE_INFINITY;
61
+ let minZ = Number.POSITIVE_INFINITY;
62
+ for (let index = 0; index < position.count; index += 1) {
63
+ minX = Math.min(minX, position.getX(index));
64
+ minY = Math.min(minY, position.getY(index));
65
+ minZ = Math.min(minZ, position.getZ(index));
66
+ }
67
+ for (let index = 0; index < position.count; index += 1) {
68
+ const px = position.getX(index);
69
+ const py = position.getY(index);
70
+ const pz = position.getZ(index);
71
+ const nx = Math.abs(normal.getX(index));
72
+ const ny = Math.abs(normal.getY(index));
73
+ const nz = Math.abs(normal.getZ(index));
74
+ let u = 0;
75
+ let v = 0;
76
+ if (ny >= nx && ny >= nz) {
77
+ u = px - minX;
78
+ v = pz - minZ;
79
+ }
80
+ else if (nx >= nz) {
81
+ u = pz - minZ;
82
+ v = py - minY;
83
+ }
84
+ else {
85
+ u = px - minX;
86
+ v = py - minY;
87
+ }
88
+ uvs[index * 2] = u;
89
+ uvs[index * 2 + 1] = v;
90
+ }
91
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
92
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
93
+ }
6
94
  function getStyleDefaults(style) {
7
95
  if (style === 'privacy') {
8
96
  return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 };
@@ -14,7 +102,7 @@ function getStyleDefaults(style) {
14
102
  }
15
103
  function createFenceParts(fence) {
16
104
  const parts = [];
17
- const length = Math.max(Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]), 0.01);
105
+ const length = Math.max(getWallCurveLength(fence), 0.01);
18
106
  const panelDepth = Math.max(fence.thickness, 0.03);
19
107
  const clearance = Math.max(fence.groundClearance, 0);
20
108
  const styleDefaults = getStyleDefaults(fence.style);
@@ -27,61 +115,43 @@ function createFenceParts(fence) {
27
115
  const isFloating = fence.baseStyle === 'floating';
28
116
  const baseY = isFloating ? clearance : 0;
29
117
  const effectiveBaseHeight = baseHeight;
118
+ const startInsetT = Math.min(0.499, edgeInset / length);
119
+ const endInsetT = Math.max(0.501, 1 - edgeInset / length);
30
120
  if (!isFloating) {
31
- parts.push({
32
- position: [0, baseY + effectiveBaseHeight / 2, 0],
33
- scale: [length, effectiveBaseHeight, panelDepth * 1.05],
34
- });
35
- parts.push({
36
- position: [0, baseY + effectiveBaseHeight + verticalHeight * 0.15, 0],
37
- scale: [length, topRailHeight * 0.8, panelDepth * 0.35],
38
- });
121
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
122
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
39
123
  }
40
124
  const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1);
41
- const step = count > 1 ? (length - edgeInset * 2) / (count - 1) : 0;
42
- const startX = -length / 2 + edgeInset;
43
125
  const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
44
126
  for (let index = 0; index < count; index += 1) {
45
- const x = count === 1 ? 0 : startX + step * index;
46
- let posX = x;
127
+ const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
128
+ const frame = getFencePointAt(fence, t);
47
129
  const isEdgePost = index === 0 || index === count - 1;
48
- if (count > 1) {
49
- if (index === 0)
50
- posX = -length / 2 + edgeInset + postWidth / 2;
51
- else if (index === count - 1)
52
- posX = length / 2 - edgeInset - postWidth / 2;
53
- }
54
130
  const postHeight = isFloating && isEdgePost
55
131
  ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
56
132
  : verticalHeight;
57
133
  const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY;
58
134
  parts.push({
59
- position: [posX, postY, 0],
135
+ position: [frame.point.x, postY, frame.point.y],
136
+ rotationY: -frame.tangentAngle,
60
137
  scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
61
138
  });
62
139
  }
63
- parts.push({
64
- position: [0, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, 0],
65
- scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)],
66
- });
140
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
67
141
  if (isFloating) {
68
- parts.push({
69
- position: [0, baseY + effectiveBaseHeight + topRailHeight / 2, 0],
70
- scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)],
71
- });
142
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
72
143
  }
73
144
  return parts;
74
145
  }
75
146
  function generateFenceGeometry(fence) {
76
147
  const parts = createFenceParts(fence);
77
- const geometries = parts.map((part) => {
78
- const geometry = new THREE.BoxGeometry(1, 1, 1);
79
- geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
80
- geometry.translate(part.position[0], part.position[1], part.position[2]);
81
- return geometry;
82
- });
148
+ const geometries = parts.map(createFencePartGeometry);
83
149
  const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
84
150
  geometries.forEach((geometry) => geometry.dispose());
151
+ const mergedUv = merged.getAttribute('uv');
152
+ if (mergedUv) {
153
+ merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
154
+ }
85
155
  merged.computeVertexNormals();
86
156
  return merged;
87
157
  }
@@ -95,11 +165,8 @@ function updateFenceGeometry(fenceId) {
95
165
  const newGeometry = generateFenceGeometry(node);
96
166
  mesh.geometry.dispose();
97
167
  mesh.geometry = newGeometry;
98
- const centerX = (node.start[0] + node.end[0]) / 2;
99
- const centerZ = (node.start[1] + node.end[1]) / 2;
100
- const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
101
- mesh.position.set(centerX, 0, centerZ);
102
- mesh.rotation.set(0, -angle, 0);
168
+ mesh.position.set(0, 0, 0);
169
+ mesh.rotation.set(0, 0, 0);
103
170
  }
104
171
  export const FenceSystem = () => {
105
172
  const dirtyNodes = useScene((state) => state.dirtyNodes);
@@ -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;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"}
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;AAgCjF,eAAO,MAAM,UAAU,YAuFtB,CAAA;AAwLD;;;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,CAqDvF"}
@@ -8,7 +8,7 @@ import useScene from '../../store/use-scene';
8
8
  const csgEvaluator = new Evaluator();
9
9
  csgEvaluator.useGroups = true;
10
10
  csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
11
- csgEvaluator.attributes = ['position', 'normal'];
11
+ csgEvaluator.attributes = ['position', 'normal', 'uv'];
12
12
  function prepareBrushForCSG(brush) {
13
13
  brush.geometry.computeBoundsTree = computeBoundsTree;
14
14
  brush.geometry.computeBoundsTree({ maxLeafSize: 10 });
@@ -20,6 +20,7 @@ const _position = new THREE.Vector3();
20
20
  const _quaternion = new THREE.Quaternion();
21
21
  const _scale = new THREE.Vector3(1, 1, 1);
22
22
  const _yAxis = new THREE.Vector3(0, 1, 0);
23
+ const _uvFaceNormal = new THREE.Vector3();
23
24
  // Pending merged-roof updates carried across frames (for throttling)
24
25
  const pendingRoofUpdates = new Set();
25
26
  const MAX_ROOFS_PER_FRAME = 1;
@@ -217,6 +218,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
217
218
  for (const g of resultGeo.groups) {
218
219
  g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
219
220
  }
221
+ ensureUv2Attribute(resultGeo);
220
222
  resultGeo.computeVertexNormals();
221
223
  mergedMesh.geometry.dispose();
222
224
  mergedMesh.geometry = resultGeo;
@@ -500,6 +502,7 @@ export function generateRoofSegmentGeometry(node) {
500
502
  shinSlab.geometry.dispose();
501
503
  wallBrush.geometry.dispose();
502
504
  innerBrush.geometry.dispose();
505
+ ensureUv2Attribute(resultGeo);
503
506
  resultGeo.computeVertexNormals();
504
507
  return resultGeo;
505
508
  }
@@ -709,6 +712,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
709
712
  function createGeometryFromFaces(faces, matRule = null) {
710
713
  const positions = [];
711
714
  const normals = [];
715
+ const uvs = [];
712
716
  const indices = [];
713
717
  const groups = [];
714
718
  let vertexCount = 0;
@@ -743,6 +747,9 @@ function createGeometryFromFaces(faces, matRule = null) {
743
747
  normals.push(normal.x, normal.y, normal.z);
744
748
  normals.push(normal.x, normal.y, normal.z);
745
749
  normals.push(normal.x, normal.y, normal.z);
750
+ pushRoofUv(uvs, p0, normal);
751
+ pushRoofUv(uvs, fi, normal);
752
+ pushRoofUv(uvs, fi1, normal);
746
753
  indices.push(vertexCount, vertexCount + 1, vertexCount + 2);
747
754
  faceVertexCount += 3;
748
755
  vertexCount += 3;
@@ -756,6 +763,7 @@ function createGeometryFromFaces(faces, matRule = null) {
756
763
  const geometry = new THREE.BufferGeometry();
757
764
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
758
765
  geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
766
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
759
767
  geometry.setIndex(indices);
760
768
  for (const g of groups) {
761
769
  geometry.addGroup(g.start, g.count, g.materialIndex);
@@ -763,5 +771,27 @@ function createGeometryFromFaces(faces, matRule = null) {
763
771
  // Merge identical vertices to optimize geometry for CSG and create clean topology
764
772
  const mergedGeo = mergeVertices(geometry, 1e-4);
765
773
  geometry.dispose();
774
+ ensureUv2Attribute(mergedGeo);
766
775
  return mergedGeo;
767
776
  }
777
+ function pushRoofUv(uvs, point, normal) {
778
+ _uvFaceNormal.copy(normal).normalize();
779
+ const absX = Math.abs(_uvFaceNormal.x);
780
+ const absY = Math.abs(_uvFaceNormal.y);
781
+ const absZ = Math.abs(_uvFaceNormal.z);
782
+ if (absY >= absX && absY >= absZ) {
783
+ uvs.push(point.x, point.z);
784
+ return;
785
+ }
786
+ if (absX >= absZ) {
787
+ uvs.push(point.z, point.y);
788
+ return;
789
+ }
790
+ uvs.push(point.x, point.y);
791
+ }
792
+ function ensureUv2Attribute(geometry) {
793
+ const uv = geometry.getAttribute('uv');
794
+ if (!uv)
795
+ return;
796
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
797
+ }
@@ -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;AAuED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
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;AAG9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAcvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAmFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}