@pascal-app/viewer 0.8.0 → 0.9.1

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 (194) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  10. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  11. package/dist/components/viewer/ground-occluder.js +12 -24
  12. package/dist/components/viewer/index.d.ts +37 -1
  13. package/dist/components/viewer/index.d.ts.map +1 -1
  14. package/dist/components/viewer/index.js +186 -63
  15. package/dist/components/viewer/lights.d.ts +1 -1
  16. package/dist/components/viewer/lights.d.ts.map +1 -1
  17. package/dist/components/viewer/lights.js +170 -43
  18. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  19. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  20. package/dist/components/viewer/perf-monitor.js +99 -11
  21. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  22. package/dist/components/viewer/post-processing.js +231 -54
  23. package/dist/components/viewer/registered-systems.d.ts +12 -0
  24. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  25. package/dist/components/viewer/registered-systems.js +47 -0
  26. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  27. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  28. package/dist/components/viewer/scene-bvh.js +94 -0
  29. package/dist/components/viewer/selection-manager.d.ts +1 -1
  30. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  31. package/dist/components/viewer/selection-manager.js +20 -5
  32. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  33. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  34. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  35. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  36. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  37. package/dist/hooks/use-node-events.d.ts +5 -73
  38. package/dist/hooks/use-node-events.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.js +19 -7
  40. package/dist/index.d.ts +36 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +67 -3
  43. package/dist/lib/csg-utils.d.ts +16 -0
  44. package/dist/lib/csg-utils.d.ts.map +1 -0
  45. package/dist/lib/csg-utils.js +33 -0
  46. package/dist/lib/drawable-geometry.d.ts +21 -0
  47. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  48. package/dist/lib/drawable-geometry.js +22 -0
  49. package/dist/lib/edge-style.d.ts +3 -0
  50. package/dist/lib/edge-style.d.ts.map +1 -0
  51. package/dist/lib/edge-style.js +11 -0
  52. package/dist/lib/gpu-perf.d.ts +4 -0
  53. package/dist/lib/gpu-perf.d.ts.map +1 -0
  54. package/dist/lib/gpu-perf.js +23 -0
  55. package/dist/lib/ink-edges.d.ts +9 -0
  56. package/dist/lib/ink-edges.d.ts.map +1 -0
  57. package/dist/lib/ink-edges.js +43 -0
  58. package/dist/lib/isolation.d.ts +39 -0
  59. package/dist/lib/isolation.d.ts.map +1 -0
  60. package/dist/lib/isolation.js +103 -0
  61. package/dist/lib/layers.d.ts +18 -0
  62. package/dist/lib/layers.d.ts.map +1 -1
  63. package/dist/lib/layers.js +18 -0
  64. package/dist/lib/materials.d.ts +25 -15
  65. package/dist/lib/materials.d.ts.map +1 -1
  66. package/dist/lib/materials.js +277 -62
  67. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  68. package/dist/lib/merged-outline-node.js +7 -0
  69. package/dist/lib/polygon-union.d.ts +4 -0
  70. package/dist/lib/polygon-union.d.ts.map +1 -0
  71. package/dist/lib/polygon-union.js +289 -0
  72. package/dist/lib/polygon-union.test.d.ts +2 -0
  73. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  74. package/dist/lib/polygon-union.test.js +105 -0
  75. package/dist/lib/scene-themes.d.ts +29 -0
  76. package/dist/lib/scene-themes.d.ts.map +1 -0
  77. package/dist/lib/scene-themes.js +193 -0
  78. package/dist/store/use-viewer.d.ts +42 -4
  79. package/dist/store/use-viewer.d.ts.map +1 -1
  80. package/dist/store/use-viewer.js +26 -3
  81. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  82. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  83. package/dist/systems/ceiling/ceiling-system.js +62 -12
  84. package/dist/systems/door/door-system.d.ts.map +1 -1
  85. package/dist/systems/door/door-system.js +373 -22
  86. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  87. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  88. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  89. package/dist/systems/fence/fence-system.d.ts +3 -0
  90. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  91. package/dist/systems/fence/fence-system.js +9 -5
  92. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  93. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  94. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  95. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  96. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  97. package/dist/systems/geometry/geometry-system.js +279 -0
  98. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  99. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  100. package/dist/systems/item/item-system.d.ts +7 -0
  101. package/dist/systems/item/item-system.d.ts.map +1 -1
  102. package/dist/systems/item/item-system.js +8 -11
  103. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  104. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  105. package/dist/systems/item-light/item-light-system.js +18 -5
  106. package/dist/systems/roof/roof-materials.d.ts +3 -2
  107. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  108. package/dist/systems/roof/roof-materials.js +38 -14
  109. package/dist/systems/roof/roof-system.d.ts +27 -0
  110. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  111. package/dist/systems/roof/roof-system.js +396 -59
  112. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  113. package/dist/systems/slab/slab-system.js +167 -49
  114. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  115. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  116. package/dist/systems/slab/slab-system.test.js +62 -0
  117. package/dist/systems/stair/stair-materials.d.ts +4 -3
  118. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  119. package/dist/systems/stair/stair-materials.js +30 -15
  120. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  121. package/dist/systems/stair/stair-system.js +50 -61
  122. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  123. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  124. package/dist/systems/surface-hole-geometry.js +4 -0
  125. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  126. package/dist/systems/wall/wall-cutout.js +18 -2
  127. package/dist/systems/wall/wall-materials.d.ts +4 -3
  128. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  129. package/dist/systems/wall/wall-materials.js +52 -28
  130. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  131. package/dist/systems/wall/wall-system.js +171 -26
  132. package/dist/systems/window/window-system.d.ts.map +1 -1
  133. package/dist/systems/window/window-system.js +71 -7
  134. package/package.json +3 -4
  135. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  136. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  137. package/dist/components/renderers/building/building-renderer.js +0 -11
  138. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  139. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  140. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  141. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  142. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  143. package/dist/components/renderers/column/column-renderer.js +0 -597
  144. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  145. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  146. package/dist/components/renderers/door/door-renderer.js +0 -16
  147. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  148. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  149. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  150. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  151. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  152. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  153. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  154. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  155. package/dist/components/renderers/item/item-renderer.js +0 -172
  156. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  157. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  158. package/dist/components/renderers/level/level-renderer.js +0 -11
  159. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  160. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  161. package/dist/components/renderers/roof/roof-materials.js +0 -16
  162. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  163. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  164. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  165. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  166. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  167. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  168. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  169. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  170. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  171. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  172. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  173. package/dist/components/renderers/site/site-renderer.js +0 -106
  174. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  175. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  176. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  177. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  178. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  179. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  180. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  181. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  182. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  183. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  184. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  185. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  186. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  187. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  188. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  189. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  190. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  191. package/dist/components/renderers/window/window-renderer.js +0 -21
  192. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  193. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  194. package/dist/components/renderers/zone/zone-renderer.js +0 -181
@@ -0,0 +1,4 @@
1
+ import { unionPolygons } from '../lib/polygon-union';
2
+ export function mergeSurfaceHolePolygons(holes) {
3
+ return unionPolygons(holes);
4
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAsCA,eAAO,MAAM,UAAU,YAkHtB,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAsCA,eAAO,MAAM,UAAU,YAwItB,CAAA"}
@@ -33,10 +33,18 @@ export const WallCutout = () => {
33
33
  const lastCameraTarget = useRef(new Vector3());
34
34
  const lastUpdateTime = useRef(0);
35
35
  const lastWallMode = useRef(useViewer.getState().wallMode);
36
+ const lastShading = useRef(useViewer.getState().shading);
36
37
  const lastNumberOfWalls = useRef(0);
37
38
  const lastHighlightKey = useRef('');
39
+ const lastTextures = useRef(useViewer.getState().textures);
40
+ const lastColorPreset = useRef(useViewer.getState().colorPreset);
41
+ const lastSceneTheme = useRef(useViewer.getState().sceneTheme);
38
42
  useFrame(({ camera, clock }) => {
39
43
  const wallMode = useViewer.getState().wallMode;
44
+ const shading = useViewer.getState().shading;
45
+ const textures = useViewer.getState().textures;
46
+ const colorPreset = useViewer.getState().colorPreset;
47
+ const sceneTheme = useViewer.getState().sceneTheme;
40
48
  const selectedIds = useViewer.getState().selection.selectedIds;
41
49
  const previewSelectedIds = useViewer.getState().previewSelectedIds;
42
50
  const hoveredId = useViewer.getState().hoveredId;
@@ -57,6 +65,10 @@ export const WallCutout = () => {
57
65
  const timeSinceUpdate = currentTime - lastUpdateTime.current;
58
66
  if (((distanceMoved > 0.5 || directionChanged > 0.3) && timeSinceUpdate > 0.1) ||
59
67
  lastWallMode.current !== wallMode ||
68
+ lastShading.current !== shading ||
69
+ lastTextures.current !== textures ||
70
+ lastColorPreset.current !== colorPreset ||
71
+ lastSceneTheme.current !== sceneTheme ||
60
72
  sceneRegistry.byType.wall.size !== lastNumberOfWalls.current ||
61
73
  lastHighlightKey.current !== highlightKey) {
62
74
  lastCameraPosition.current.copy(currentCameraPosition);
@@ -74,7 +86,7 @@ export const WallCutout = () => {
74
86
  const hideWall = getWallHideState(wallNode, wallMesh, wallMode, u);
75
87
  const isDeleteHighlighted = deleteHoveredWallId === wallId;
76
88
  const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId);
77
- const materials = getMaterialsForWall(wallNode);
89
+ const materials = getMaterialsForWall(wallNode, shading, textures, colorPreset, sceneTheme);
78
90
  if (hideWall) {
79
91
  ;
80
92
  wallMesh.material = isDeleteHighlighted
@@ -93,6 +105,10 @@ export const WallCutout = () => {
93
105
  }
94
106
  });
95
107
  lastWallMode.current = wallMode;
108
+ lastShading.current = shading;
109
+ lastTextures.current = textures;
110
+ lastColorPreset.current = colorPreset;
111
+ lastSceneTheme.current = sceneTheme;
96
112
  lastNumberOfWalls.current = sceneRegistry.byType.wall.size;
97
113
  lastHighlightKey.current = highlightKey;
98
114
  }
@@ -107,7 +123,7 @@ export const WallCutout = () => {
107
123
  const wallNode = useScene.getState().nodes[wallId];
108
124
  if (!wallNode || wallNode.type !== 'wall')
109
125
  return;
110
- const mats = getMaterialsForWall(wallNode);
126
+ const mats = getMaterialsForWall(wallNode, useViewer.getState().shading, useViewer.getState().textures, useViewer.getState().colorPreset, useViewer.getState().sceneTheme);
111
127
  const current = wallMesh.material;
112
128
  snapshot.set(wallMesh, current);
113
129
  if (current === mats.highlightedVisible || current === mats.deleteVisible) {
@@ -1,5 +1,6 @@
1
1
  import { type WallNode } from '@pascal-app/core';
2
2
  import { type Material } from 'three';
3
+ import { type ColorPreset, type RenderShading } from '../../lib/materials';
3
4
  export type WallMaterialArray = [Material, Material, Material];
4
5
  export interface WallMaterials {
5
6
  visible: WallMaterialArray;
@@ -10,7 +11,7 @@ export interface WallMaterials {
10
11
  highlightedInvisible: WallMaterialArray;
11
12
  materialHash: string;
12
13
  }
13
- export declare function getWallMaterialHash(wallNode: WallNode): string;
14
- export declare function getMaterialsForWall(wallNode: WallNode): WallMaterials;
15
- export declare function getVisibleWallMaterials(wallNode: WallNode): WallMaterialArray;
14
+ export declare function getWallMaterialHash(wallNode: WallNode, shading: RenderShading): string;
15
+ export declare function getMaterialsForWall(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string): WallMaterials;
16
+ export declare function getVisibleWallMaterials(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string): WallMaterialArray;
16
17
  //# sourceMappingURL=wall-materials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAwB5C,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,kBAAkB,EAAE,iBAAiB,CAAA;IACrC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;CACrB;AA4GD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS9D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CA2DrE;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,iBAAiB,CAE7E"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAIhB,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAA;AAqB5B,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,kBAAkB,EAAE,iBAAiB,CAAA;IACrC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;CACrB;AA6HD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAUtF;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,aAAa,CA6Ef;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,CAEnB"}
@@ -1,8 +1,8 @@
1
1
  import { getEffectiveWallSurfaceMaterial, getMaterialPresetByRef, getWallSurfaceMaterialSignature, resolveMaterial, } from '@pascal-app/core';
2
2
  import { Color } from 'three';
3
3
  import { Fn, float, fract, length, mix, positionLocal, smoothstep, step, vec2 } from 'three/tsl';
4
- import { MeshStandardNodeMaterial } from 'three/webgpu';
5
- import { baseMaterial, createMaterial, createMaterialFromPresetRef } from '../../lib/materials';
4
+ import { MeshLambertNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
5
+ import { baseMaterial, createMaterial, createMaterialFromPresetRef, createSurfaceRoleMaterial, resolveSurfaceColor, } from '../../lib/materials';
6
6
  const DEFAULT_WALL_COLOR = '#f2f0ed';
7
7
  const WALL_HIGHLIGHT_PROFILES = {
8
8
  delete: {
@@ -30,14 +30,17 @@ const dotPattern = Fn(() => {
30
30
  const yFade = float(1).sub(smoothstep(float(0), fadeHeight, positionLocal.y));
31
31
  return dots.mul(yFade);
32
32
  });
33
- function getSurfaceVisibleMaterial(spec) {
33
+ function getSurfaceVisibleMaterial(spec, shading) {
34
34
  if (spec.materialPreset) {
35
- return createMaterialFromPresetRef(spec.materialPreset) ?? baseMaterial;
35
+ return createMaterialFromPresetRef(spec.materialPreset, shading) ?? baseMaterial(shading);
36
36
  }
37
37
  if (spec.material) {
38
- return createMaterial(spec.material);
38
+ return createMaterial(spec.material, shading);
39
39
  }
40
- return baseMaterial;
40
+ return baseMaterial(shading);
41
+ }
42
+ function hasExplicitMaterial(spec) {
43
+ return Boolean(spec.materialPreset || spec.material);
41
44
  }
42
45
  function getSurfaceColor(spec, fallback = DEFAULT_WALL_COLOR) {
43
46
  const preset = getMaterialPresetByRef(spec.materialPreset);
@@ -70,14 +73,22 @@ function createHighlightedWallMaterial(material, kind) {
70
73
  highlightedMaterial.needsUpdate = true;
71
74
  return highlightedMaterial;
72
75
  }
73
- function createInvisibleWallMaterial(color) {
74
- return new MeshStandardNodeMaterial({
75
- transparent: true,
76
- opacityNode: mix(float(0.0), float(0.24), dotPattern()),
77
- color,
78
- depthWrite: false,
79
- emissive: color,
80
- });
76
+ function createInvisibleWallMaterial(color, shading) {
77
+ const material = shading === 'solid'
78
+ ? new MeshLambertNodeMaterial({
79
+ transparent: true,
80
+ color,
81
+ depthWrite: false,
82
+ emissive: color,
83
+ })
84
+ : new MeshStandardNodeMaterial({
85
+ transparent: true,
86
+ color,
87
+ depthWrite: false,
88
+ emissive: color,
89
+ });
90
+ material.opacityNode = mix(float(0.0), float(0.24), dotPattern());
91
+ return material;
81
92
  }
82
93
  function mapWallMaterialArray(materials, iteratee) {
83
94
  return materials.map(iteratee);
@@ -93,15 +104,18 @@ function disposeOwnedMaterials(materials) {
93
104
  material.dispose();
94
105
  });
95
106
  }
96
- export function getWallMaterialHash(wallNode) {
107
+ export function getWallMaterialHash(wallNode, shading) {
97
108
  return JSON.stringify({
109
+ shading,
98
110
  interior: getWallSurfaceMaterialSignature(getEffectiveWallSurfaceMaterial(wallNode, 'interior')),
99
111
  exterior: getWallSurfaceMaterialSignature(getEffectiveWallSurfaceMaterial(wallNode, 'exterior')),
100
112
  });
101
113
  }
102
- export function getMaterialsForWall(wallNode) {
103
- const cacheKey = wallNode.id;
104
- const materialHash = getWallMaterialHash(wallNode);
114
+ export function getMaterialsForWall(wallNode, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme) {
115
+ const cacheKey = `${wallNode.id}-${shading}-${textures}-${colorPreset}-${sceneTheme ?? 'base'}`;
116
+ const materialHash = textures
117
+ ? getWallMaterialHash(wallNode, shading)
118
+ : JSON.stringify({ textures, colorPreset, sceneTheme });
105
119
  const existing = wallMaterialCache.get(cacheKey);
106
120
  if (existing && existing.materialHash === materialHash) {
107
121
  return existing;
@@ -117,15 +131,25 @@ export function getMaterialsForWall(wallNode) {
117
131
  }
118
132
  const interiorSpec = getEffectiveWallSurfaceMaterial(wallNode, 'interior');
119
133
  const exteriorSpec = getEffectiveWallSurfaceMaterial(wallNode, 'exterior');
120
- const visible = [
121
- baseMaterial,
122
- getSurfaceVisibleMaterial(interiorSpec),
123
- getSurfaceVisibleMaterial(exteriorSpec),
124
- ];
134
+ const wallRoleMaterial = createSurfaceRoleMaterial('wall', colorPreset, undefined, sceneTheme);
135
+ // Untextured surfaces take the themed wall role colour even with textures on;
136
+ // only surfaces with an explicit preset/material keep their texture.
137
+ const visible = textures
138
+ ? [
139
+ wallRoleMaterial,
140
+ hasExplicitMaterial(interiorSpec)
141
+ ? getSurfaceVisibleMaterial(interiorSpec, shading)
142
+ : wallRoleMaterial,
143
+ hasExplicitMaterial(exteriorSpec)
144
+ ? getSurfaceVisibleMaterial(exteriorSpec, shading)
145
+ : wallRoleMaterial,
146
+ ]
147
+ : [wallRoleMaterial, wallRoleMaterial, wallRoleMaterial];
148
+ const wallRoleColor = resolveSurfaceColor('wall', colorPreset, sceneTheme);
125
149
  const invisible = [
126
- createInvisibleWallMaterial(DEFAULT_WALL_COLOR),
127
- createInvisibleWallMaterial(getSurfaceColor(interiorSpec, DEFAULT_WALL_COLOR)),
128
- createInvisibleWallMaterial(getSurfaceColor(exteriorSpec, DEFAULT_WALL_COLOR)),
150
+ createInvisibleWallMaterial(wallRoleColor, textures ? shading : 'solid'),
151
+ createInvisibleWallMaterial(textures ? getSurfaceColor(interiorSpec, wallRoleColor) : wallRoleColor, textures ? shading : 'solid'),
152
+ createInvisibleWallMaterial(textures ? getSurfaceColor(exteriorSpec, wallRoleColor) : wallRoleColor, textures ? shading : 'solid'),
129
153
  ];
130
154
  const highlightedVisible = mapWallMaterialArray(visible, (material) => createHighlightedWallMaterial(material, 'selection'));
131
155
  const highlightedInvisible = mapWallMaterialArray(invisible, (material) => createHighlightedWallMaterial(material, 'selection'));
@@ -143,6 +167,6 @@ export function getMaterialsForWall(wallNode) {
143
167
  wallMaterialCache.set(cacheKey, result);
144
168
  return result;
145
169
  }
146
- export function getVisibleWallMaterials(wallNode) {
147
- return getMaterialsForWall(wallNode).visible;
170
+ export function getVisibleWallMaterials(wallNode, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme) {
171
+ return getMaterialsForWall(wallNode, shading, textures, colorPreset, sceneTheme).visible;
148
172
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAkBZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAiS9B,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,GAChB,KAAK,CAAC,cAAc,CAyGtB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAqBZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA8T9B,eAAO,MAAM,UAAU,YAwJtB,CAAA;AAkGD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,GAChB,KAAK,CAAC,cAAc,CAyGtB"}
@@ -1,4 +1,4 @@
1
- import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useScene, } from '@pascal-app/core';
1
+ import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
@@ -208,49 +208,158 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
208
208
  // WALL SYSTEM
209
209
  // ============================================================================
210
210
  let useFrameNb = 0;
211
+ // ─── Drag-throttle state (singleton — one WallSystem mounted globally) ──
212
+ //
213
+ // Endpoint drags fire `markDirty(wallId)` on every pointermove tick. Without
214
+ // throttling, each tick rebuilds the dragged wall (~1 CSG + miter pass) AND
215
+ // every adjacent wall sharing a corner (3–4× in a t-junction or room).
216
+ // Visible as drag lag, especially on walls with door/window cutouts.
217
+ //
218
+ // Strategy: rebuild the dragged wall every tick (so the drag follows the
219
+ // cursor with full fidelity), but defer adjacent rebuilds to a trailing-
220
+ // edge flush DRAG_FLUSH_MS after the dirty stream stops. Visually, neighbor
221
+ // corners stay at their pre-drag miter until release, then snap into place
222
+ // within ~80ms. Standard CAD-app behavior. Speeds up t-junction drags ~3×,
223
+ // 4-corner-room drags ~4×.
224
+ const DRAG_FLUSH_MS = 80;
225
+ const MAX_WALL_REBUILDS_PER_FRAME = 8;
226
+ const WALL_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WALL_REBUILDS_PER_FRAME;
227
+ const WALL_PROGRESSIVE_TIME_BUDGET_MS = 8;
228
+ let lastWallDirtyAtMs = 0;
229
+ const pendingAdjacentByLevel = new Map();
230
+ function getPendingAdjacentCount() {
231
+ let count = 0;
232
+ for (const ids of pendingAdjacentByLevel.values()) {
233
+ count += ids.size;
234
+ }
235
+ return count;
236
+ }
211
237
  export const WallSystem = () => {
212
238
  const dirtyNodes = useScene((state) => state.dirtyNodes);
213
239
  const clearDirty = useScene((state) => state.clearDirty);
240
+ // Subscribe so override-only changes (no scene write) still re-run
241
+ // this component, which lets the gate below pick up the latest
242
+ // `dirtyNodes` set from the same render pass that received the
243
+ // override-publishing `markDirty` call. Without this, very fast
244
+ // drags could land an override and a markDirty in the same React
245
+ // tick and the next `useFrame` would still see the stale closure.
246
+ useLiveNodeOverrides((s) => s.overrides);
214
247
  useFrame(() => {
215
- if (dirtyNodes.size === 0)
248
+ const hasDirty = dirtyNodes.size > 0;
249
+ const hasPending = pendingAdjacentByLevel.size > 0;
250
+ if (!hasDirty && !hasPending)
216
251
  return;
217
252
  const nodes = useScene.getState().nodes;
253
+ const now = performance.now();
218
254
  // Collect dirty walls and their levels
219
255
  const dirtyWallsByLevel = new Map();
256
+ let dirtyWallCount = 0;
220
257
  useFrameNb += 1;
221
- dirtyNodes.forEach((id) => {
222
- const node = nodes[id];
223
- if (!node || node.type !== 'wall')
224
- return;
225
- const levelId = node.parentId;
226
- if (!levelId)
227
- return;
228
- if (!dirtyWallsByLevel.has(levelId)) {
229
- dirtyWallsByLevel.set(levelId, new Set());
230
- }
231
- dirtyWallsByLevel.get(levelId)?.add(id);
232
- });
258
+ if (hasDirty) {
259
+ dirtyNodes.forEach((id) => {
260
+ const node = nodes[id];
261
+ if (!node || node.type !== 'wall')
262
+ return;
263
+ const levelId = node.parentId;
264
+ if (!levelId)
265
+ return;
266
+ if (!dirtyWallsByLevel.has(levelId)) {
267
+ dirtyWallsByLevel.set(levelId, new Set());
268
+ }
269
+ dirtyWallsByLevel.get(levelId)?.add(id);
270
+ dirtyWallCount += 1;
271
+ });
272
+ }
273
+ const hasDirtyWalls = dirtyWallsByLevel.size > 0;
274
+ if (hasDirtyWalls) {
275
+ lastWallDirtyAtMs = now;
276
+ }
277
+ const useProgressiveWallRebuilds = dirtyWallCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
278
+ let rebuiltWallsThisFrame = 0;
279
+ const rebuildFrameStartedAt = now;
233
280
  // Process each level that has dirty walls
234
281
  for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
282
+ if (useProgressiveWallRebuilds && rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
283
+ break;
284
+ }
235
285
  const levelWalls = getLevelWalls(levelId);
236
286
  const miterData = calculateLevelMiters(levelWalls);
237
- // Update dirty walls
287
+ const rebuiltWallIds = new Set();
288
+ // Update dirty walls — always, no throttling. The dragged wall must
289
+ // follow the cursor with full fidelity (cutouts and all). Large imports
290
+ // enter the progressive path so initial load can't lock the tab.
238
291
  for (const wallId of dirtyWallIds) {
292
+ if (useProgressiveWallRebuilds) {
293
+ if (rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
294
+ break;
295
+ }
296
+ if (rebuiltWallsThisFrame > 0 &&
297
+ performance.now() - rebuildFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
298
+ break;
299
+ }
300
+ }
239
301
  const mesh = sceneRegistry.nodes.get(wallId);
240
302
  if (mesh) {
241
303
  updateWallGeometry(wallId, miterData);
242
304
  clearDirty(wallId);
305
+ rebuiltWallIds.add(wallId);
306
+ rebuiltWallsThisFrame += 1;
243
307
  }
244
308
  // If mesh not found, keep it dirty for next frame
245
309
  }
246
- // Update adjacent walls that share junctions
247
- const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
310
+ if (rebuiltWallIds.size === 0) {
311
+ continue;
312
+ }
313
+ // Adjacent walls sharing junctions — *defer* during active drag
314
+ // (dirty arrived this frame), flush on the trailing edge.
315
+ const adjacentWallIds = getAdjacentWallIds(levelWalls, rebuiltWallIds);
316
+ let pending = pendingAdjacentByLevel.get(levelId);
317
+ if (!pending) {
318
+ pending = new Set();
319
+ pendingAdjacentByLevel.set(levelId, pending);
320
+ }
248
321
  for (const wallId of adjacentWallIds) {
249
322
  if (!dirtyWallIds.has(wallId)) {
323
+ pending.add(wallId);
324
+ }
325
+ }
326
+ }
327
+ // Trailing-edge flush: if no new dirty marks for DRAG_FLUSH_MS, the
328
+ // drag has ended — rebuild the queued neighbors so corners snap into
329
+ // their correct miter joins.
330
+ const quiet = !hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS;
331
+ if (quiet && pendingAdjacentByLevel.size > 0) {
332
+ const pendingCount = getPendingAdjacentCount();
333
+ const useProgressiveAdjacentRebuilds = pendingCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
334
+ let rebuiltAdjacentThisFrame = 0;
335
+ const adjacentFrameStartedAt = performance.now();
336
+ for (const [levelId, pendingIds] of pendingAdjacentByLevel) {
337
+ if (pendingIds.size === 0)
338
+ continue;
339
+ const levelWalls = getLevelWalls(levelId);
340
+ const miterData = calculateLevelMiters(levelWalls);
341
+ for (const wallId of Array.from(pendingIds)) {
342
+ if (useProgressiveAdjacentRebuilds) {
343
+ if (rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
344
+ break;
345
+ }
346
+ if (rebuiltAdjacentThisFrame > 0 &&
347
+ performance.now() - adjacentFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
348
+ break;
349
+ }
350
+ }
250
351
  const mesh = sceneRegistry.nodes.get(wallId);
251
- if (mesh) {
352
+ if (mesh)
252
353
  updateWallGeometry(wallId, miterData);
253
- }
354
+ pendingIds.delete(wallId);
355
+ rebuiltAdjacentThisFrame += 1;
356
+ }
357
+ if (pendingIds.size === 0) {
358
+ pendingAdjacentByLevel.delete(levelId);
359
+ }
360
+ if (useProgressiveAdjacentRebuilds &&
361
+ rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
362
+ break;
254
363
  }
255
364
  }
256
365
  }
@@ -258,7 +367,22 @@ export const WallSystem = () => {
258
367
  return null;
259
368
  };
260
369
  /**
261
- * Gets all walls that belong to a level
370
+ * Merge any live override for a wall into the scene record. Lets the
371
+ * 2D move handler publish `{ start, end, curveOffset }` to
372
+ * `useLiveNodeOverrides` and have the geometry / miter pipeline use
373
+ * those values without zustand churn during the drag. When no
374
+ * override is set, the wall is returned unchanged.
375
+ */
376
+ function getEffectiveWall(wall) {
377
+ const override = useLiveNodeOverrides.getState().get(wall.id);
378
+ if (!override || Object.keys(override).length === 0)
379
+ return wall;
380
+ return { ...wall, ...override };
381
+ }
382
+ /**
383
+ * Gets all walls that belong to a level, with any live overrides
384
+ * merged in so miters compute against the cursor-driven positions
385
+ * (not the pre-drag scene state).
262
386
  */
263
387
  function getLevelWalls(levelId) {
264
388
  const { nodes } = useScene.getState();
@@ -269,28 +393,49 @@ function getLevelWalls(levelId) {
269
393
  for (const childId of level.children) {
270
394
  const child = nodes[childId];
271
395
  if (child?.type === 'wall') {
272
- walls.push(child);
396
+ walls.push(getEffectiveWall(child));
273
397
  }
274
398
  }
275
399
  return walls;
276
400
  }
277
401
  /**
278
- * Updates the geometry for a single wall
402
+ * Updates the geometry for a single wall. Reads the effective node
403
+ * (override-merged) so a 2D drag visibly moves the 3D mesh without
404
+ * having touched `useScene` mid-drag.
279
405
  */
280
406
  function updateWallGeometry(wallId, miterData) {
281
407
  const nodes = useScene.getState().nodes;
282
- const node = nodes[wallId];
283
- if (!node || node.type !== 'wall')
408
+ const sceneNode = nodes[wallId];
409
+ if (!sceneNode || sceneNode.type !== 'wall')
284
410
  return;
411
+ const node = getEffectiveWall(sceneNode);
285
412
  const mesh = sceneRegistry.nodes.get(wallId);
286
413
  if (!mesh)
287
414
  return;
288
415
  const levelId = resolveLevelId(node, nodes);
289
- const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end);
416
+ const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
290
417
  const childrenIds = node.children || [];
418
+ // Merge live overrides into door / window children so cutouts track an
419
+ // in-flight resize drag (door width arrow, window height arrow, etc.)
420
+ // without waiting on the scene store. Non-cutout children pass through
421
+ // unchanged.
291
422
  const childrenNodes = childrenIds
292
423
  .map((childId) => nodes[childId])
293
- .filter((n) => n !== undefined);
424
+ .filter((n) => n !== undefined)
425
+ .map((child) => {
426
+ if (child.type !== 'door' && child.type !== 'window')
427
+ return child;
428
+ // `getEffectiveNode` folds in resize overrides (width/height arrows).
429
+ // Position moves publish to `useLiveTransforms` instead, so fold that
430
+ // in too — otherwise shaped openings (arch/rounded/`opening`), whose
431
+ // cutout brush is rebuilt from `node.position`, lag the live move
432
+ // (rectangular cutouts already track via the live mesh matrixWorld).
433
+ const effective = getEffectiveNode(child);
434
+ const live = useLiveTransforms.getState().get(child.id);
435
+ if (!live?.position)
436
+ return effective;
437
+ return { ...effective, position: live.position };
438
+ });
294
439
  const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
295
440
  mesh.geometry.dispose();
296
441
  mesh.geometry = newGeo;
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAaA,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D,eAAO,MAAM,YAAY,YA2BxB,CAAA"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAuBA,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAM3D,eAAO,MAAM,YAAY,YA0FxB,CAAA"}
@@ -1,9 +1,13 @@
1
- import { sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect, useRef } from 'react';
3
4
  import * as THREE from 'three';
4
- import { baseMaterial, glassMaterial } from '../../lib/materials';
5
+ import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, } from '../../lib/materials';
6
+ import useViewer from '../../store/use-viewer';
5
7
  // Invisible material for root mesh — used as selection hitbox only
6
8
  const hitboxMaterial = new THREE.MeshBasicMaterial({ visible: false });
9
+ let baseMaterial = getBaseMaterial();
10
+ let glassMaterial = defaultGlassMaterial;
7
11
  export const CASEMENT_WINDOW_SASH_NAME = 'casement-window-sash';
8
12
  export const FRENCH_CASEMENT_LEFT_SASH_NAME = 'french-casement-left-sash';
9
13
  export const FRENCH_CASEMENT_RIGHT_SASH_NAME = 'french-casement-right-sash';
@@ -14,27 +18,87 @@ export const DOUBLE_HUNG_BOTTOM_SASH_NAME = 'double-hung-bottom-sash';
14
18
  export const LOUVERED_WINDOW_SLATS_NAME = 'louvered-window-slats';
15
19
  export const AWNING_WINDOW_SASH_NAME = 'awning-window-sash';
16
20
  export const HOPPER_WINDOW_SASH_NAME = 'hopper-window-sash';
21
+ const MAX_WINDOW_REBUILDS_PER_FRAME = 16;
22
+ const WINDOW_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WINDOW_REBUILDS_PER_FRAME;
23
+ const WINDOW_PROGRESSIVE_TIME_BUDGET_MS = 8;
17
24
  export const WindowSystem = () => {
18
25
  const dirtyNodes = useScene((state) => state.dirtyNodes);
19
26
  const clearDirty = useScene((state) => state.clearDirty);
27
+ const shading = useViewer((state) => state.shading);
28
+ const textures = useViewer((state) => state.textures);
29
+ const colorPreset = useViewer((state) => state.colorPreset);
30
+ const materialRevisionRef = useRef(null);
31
+ // Subscribe so override-only updates re-run this component. Mirrors
32
+ // WallSystem + DoorSystem.
33
+ useLiveNodeOverrides((s) => s.overrides);
34
+ baseMaterial = textures
35
+ ? getBaseMaterial(shading)
36
+ : createSurfaceRoleMaterial('joinery', colorPreset);
37
+ glassMaterial = textures
38
+ ? defaultGlassMaterial
39
+ : createSurfaceRoleMaterial('glazing', colorPreset);
40
+ useEffect(() => {
41
+ const materialRevision = `${shading}:${textures ? 'textures' : 'solid'}:${colorPreset}`;
42
+ if (materialRevisionRef.current === materialRevision)
43
+ return;
44
+ materialRevisionRef.current = materialRevision;
45
+ const nodes = useScene.getState().nodes;
46
+ for (const node of Object.values(nodes)) {
47
+ if (node?.type === 'window') {
48
+ useScene.getState().dirtyNodes.add(node.id);
49
+ }
50
+ }
51
+ });
20
52
  useFrame(() => {
21
53
  if (dirtyNodes.size === 0)
22
54
  return;
55
+ baseMaterial = textures
56
+ ? getBaseMaterial(shading)
57
+ : createSurfaceRoleMaterial('joinery', colorPreset);
58
+ glassMaterial = textures
59
+ ? defaultGlassMaterial
60
+ : createSurfaceRoleMaterial('glazing', colorPreset);
23
61
  const nodes = useScene.getState().nodes;
62
+ const dirtyWindowIds = [];
24
63
  dirtyNodes.forEach((id) => {
25
64
  const node = nodes[id];
26
65
  if (!node || node.type !== 'window')
27
66
  return;
67
+ dirtyWindowIds.push(id);
68
+ });
69
+ const useProgressiveWindowRebuilds = dirtyWindowIds.length > WINDOW_PROGRESSIVE_DIRTY_THRESHOLD;
70
+ const frameStartedAt = performance.now();
71
+ let rebuiltWindowsThisFrame = 0;
72
+ for (const id of dirtyWindowIds) {
73
+ if (useProgressiveWindowRebuilds) {
74
+ if (rebuiltWindowsThisFrame >= MAX_WINDOW_REBUILDS_PER_FRAME) {
75
+ break;
76
+ }
77
+ if (rebuiltWindowsThisFrame > 0 &&
78
+ performance.now() - frameStartedAt >= WINDOW_PROGRESSIVE_TIME_BUDGET_MS) {
79
+ break;
80
+ }
81
+ }
82
+ const node = nodes[id];
83
+ if (!node || node.type !== 'window')
84
+ continue;
28
85
  const mesh = sceneRegistry.nodes.get(id);
29
86
  if (!mesh)
30
- return; // Keep dirty until mesh mounts
31
- updateWindowMesh(node, mesh);
87
+ continue; // Keep dirty until mesh mounts
88
+ // Merge any live override (width / height / position) so the mesh
89
+ // rebuild reflects the in-flight drag without zustand churn.
90
+ const effectiveNode = getEffectiveNode(node);
91
+ updateWindowMesh(effectiveNode, mesh);
32
92
  clearDirty(id);
93
+ rebuiltWindowsThisFrame += 1;
33
94
  // Rebuild the parent wall so its cutout reflects the updated window geometry
34
- if (node.parentId) {
35
- useScene.getState().dirtyNodes.add(node.parentId);
95
+ // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
96
+ // The editor tools will request a final wall rebuild on commit.
97
+ const isTransient = !!node.metadata?.isTransient;
98
+ if (!isTransient && effectiveNode.parentId) {
99
+ useScene.getState().dirtyNodes.add(effectiveNode.parentId);
36
100
  }
37
- });
101
+ }
38
102
  }, 3);
39
103
  return null;
40
104
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,14 +22,13 @@
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "peerDependencies": {
25
- "@pascal-app/core": "^0.7.0",
25
+ "@pascal-app/core": "^0.9.1",
26
26
  "@react-three/drei": "^10",
27
27
  "@react-three/fiber": "^9",
28
28
  "react": "^18 || ^19",
29
29
  "three": "^0.184"
30
30
  },
31
31
  "dependencies": {
32
- "polygon-clipping": "^0.15.7",
33
32
  "three-bvh-csg": "^0.0.18",
34
33
  "three-mesh-bvh": "^0.9.8",
35
34
  "zustand": "^5"
@@ -39,7 +38,7 @@
39
38
  "@types/node": "^22",
40
39
  "@types/react": "^19.2.2",
41
40
  "@types/three": "^0.184.0",
42
- "typescript": "6.0.2"
41
+ "typescript": "6.0.3"
43
42
  },
44
43
  "keywords": [
45
44
  "3d",
@@ -1,5 +0,0 @@
1
- import { type BuildingNode } from '@pascal-app/core';
2
- export declare const BuildingRenderer: ({ node }: {
3
- node: BuildingNode;
4
- }) => import("react/jsx-runtime").JSX.Element;
5
- //# sourceMappingURL=building-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"building-renderer.d.ts","sourceRoot":"","sources":["../../../../src/components/renderers/building/building-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,kBAAkB,CAAA;AAMjE,eAAO,MAAM,gBAAgB,GAAI,UAAU;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,4CAkBhE,CAAA"}