@pascal-app/viewer 1.0.3 → 1.0.4

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 (164) hide show
  1. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/node-renderer.js +8 -3
  3. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  4. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  5. package/dist/components/viewer/frame-limiter.js +22 -3
  6. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  7. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  8. package/dist/components/viewer/glb-interactive.js +252 -85
  9. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  10. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  11. package/dist/components/viewer/glb-mechanisms.js +19 -0
  12. package/dist/components/viewer/glb-scene.d.ts +45 -27
  13. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  14. package/dist/components/viewer/glb-scene.js +420 -506
  15. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  16. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
  18. package/dist/components/viewer/index.d.ts +14 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +84 -26
  21. package/dist/components/viewer/lights.d.ts.map +1 -1
  22. package/dist/components/viewer/lights.js +6 -1
  23. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  24. package/dist/components/viewer/perf-monitor.js +6 -0
  25. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  26. package/dist/components/viewer/post-processing.js +8 -3
  27. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  28. package/dist/components/viewer/registered-systems.js +16 -2
  29. package/dist/components/viewer/render-error.d.ts +18 -0
  30. package/dist/components/viewer/render-error.d.ts.map +1 -0
  31. package/dist/components/viewer/render-error.js +29 -0
  32. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  33. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  34. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  35. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  36. package/dist/components/viewer/selection-manager.js +10 -0
  37. package/dist/components/viewer/viewer-camera.d.ts +4 -0
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +11 -1
  40. package/dist/index.d.ts +22 -11
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +20 -14
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/display-state.d.ts +25 -0
  46. package/dist/lib/display-state.d.ts.map +1 -0
  47. package/dist/lib/display-state.js +1 -0
  48. package/dist/lib/hero-pose.d.ts +12 -2
  49. package/dist/lib/hero-pose.d.ts.map +1 -1
  50. package/dist/lib/hero-pose.js +72 -5
  51. package/dist/lib/ink-edges.d.ts +4 -2
  52. package/dist/lib/ink-edges.d.ts.map +1 -1
  53. package/dist/lib/ink-edges.js +17 -7
  54. package/dist/lib/key-light-override.d.ts +11 -0
  55. package/dist/lib/key-light-override.d.ts.map +1 -0
  56. package/dist/lib/key-light-override.js +14 -0
  57. package/dist/lib/live-frame-hold.d.ts +14 -0
  58. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  59. package/dist/lib/live-frame-hold.js +28 -0
  60. package/dist/lib/materials.d.ts +14 -7
  61. package/dist/lib/materials.d.ts.map +1 -1
  62. package/dist/lib/materials.js +31 -5
  63. package/dist/lib/node-top-surface-height.d.ts +6 -0
  64. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  65. package/dist/lib/node-top-surface-height.js +74 -0
  66. package/dist/lib/perf-probe-state.d.ts +39 -0
  67. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  68. package/dist/lib/perf-probe-state.js +17 -0
  69. package/dist/lib/pointer-events.d.ts +8 -0
  70. package/dist/lib/pointer-events.d.ts.map +1 -1
  71. package/dist/lib/pointer-events.js +27 -4
  72. package/dist/lib/procedural-emission.d.ts +7 -0
  73. package/dist/lib/procedural-emission.d.ts.map +1 -0
  74. package/dist/lib/procedural-emission.js +54 -0
  75. package/dist/lib/scene-support-height.d.ts +4 -0
  76. package/dist/lib/scene-support-height.d.ts.map +1 -0
  77. package/dist/lib/scene-support-height.js +34 -0
  78. package/dist/lib/scene-visibility.d.ts +6 -0
  79. package/dist/lib/scene-visibility.d.ts.map +1 -1
  80. package/dist/lib/scene-visibility.js +15 -3
  81. package/dist/lib/scripted-opening.d.ts +11 -0
  82. package/dist/lib/scripted-opening.d.ts.map +1 -0
  83. package/dist/lib/scripted-opening.js +20 -0
  84. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  85. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  86. package/dist/lib/snapshot-pipeline.js +215 -105
  87. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  88. package/dist/lib/terrain-perimeter-fill.js +22 -14
  89. package/dist/lib/zone-geometry.d.ts +11 -0
  90. package/dist/lib/zone-geometry.d.ts.map +1 -0
  91. package/dist/lib/zone-geometry.js +46 -0
  92. package/dist/store/use-item-light-pool.d.ts +14 -10
  93. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  94. package/dist/store/use-item-light-pool.js +52 -26
  95. package/dist/store/use-viewer.d.ts +11 -0
  96. package/dist/store/use-viewer.d.ts.map +1 -1
  97. package/dist/store/use-viewer.js +13 -6
  98. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  99. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  100. package/dist/systems/ceiling/ceiling-system.js +86 -6
  101. package/dist/systems/door/door-system.d.ts.map +1 -1
  102. package/dist/systems/door/door-system.js +55 -17
  103. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  104. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  105. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  106. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  107. package/dist/systems/geometry/geometry-system.js +71 -11
  108. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  109. package/dist/systems/interactive/interactive-system.js +40 -15
  110. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  111. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  112. package/dist/systems/interactive/procedural-controls.js +17 -0
  113. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  114. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  115. package/dist/systems/interactive/scripted-clips.js +134 -0
  116. package/dist/systems/item/item-system.d.ts.map +1 -1
  117. package/dist/systems/item/item-system.js +9 -3
  118. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  119. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  120. package/dist/systems/item/object-cut-invalidation.js +68 -0
  121. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  122. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  123. package/dist/systems/item-light/item-light-system.js +47 -52
  124. package/dist/systems/level/level-system.d.ts.map +1 -1
  125. package/dist/systems/level/level-system.js +3 -5
  126. package/dist/systems/level/level-utils.d.ts +7 -4
  127. package/dist/systems/level/level-utils.d.ts.map +1 -1
  128. package/dist/systems/level/level-utils.js +17 -6
  129. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  130. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  131. package/dist/systems/level/shadow-only-levels.js +7 -0
  132. package/dist/systems/roof/roof-materials.d.ts +14 -2
  133. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  134. package/dist/systems/roof/roof-materials.js +54 -15
  135. package/dist/systems/roof/roof-system.d.ts +13 -3
  136. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  137. package/dist/systems/roof/roof-system.js +188 -13
  138. package/dist/systems/slab/slab-system.d.ts +1 -1
  139. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  140. package/dist/systems/slab/slab-system.js +49 -19
  141. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  142. package/dist/systems/stair/stair-system.js +133 -354
  143. package/dist/systems/wall/wall-cutout-cache.d.ts +7 -0
  144. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  145. package/dist/systems/wall/wall-cutout-cache.js +46 -17
  146. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  147. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  148. package/dist/systems/wall/wall-cutout.js +42 -28
  149. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  150. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  151. package/dist/systems/wall/wall-finish-data.js +17 -0
  152. package/dist/systems/wall/wall-materials.d.ts +9 -4
  153. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-materials.js +99 -152
  155. package/dist/systems/wall/wall-system.d.ts +28 -2
  156. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-system.js +582 -143
  158. package/dist/systems/window/window-system.d.ts +1 -1
  159. package/dist/systems/window/window-system.d.ts.map +1 -1
  160. package/dist/systems/window/window-system.js +41 -22
  161. package/package.json +5 -5
  162. package/dist/systems/fence/fence-system.d.ts +0 -22
  163. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  164. package/dist/systems/fence/fence-system.js +0 -357
@@ -10,7 +10,7 @@ export declare const DOUBLE_HUNG_BOTTOM_SASH_NAME = "double-hung-bottom-sash";
10
10
  export declare const LOUVERED_WINDOW_SLATS_NAME = "louvered-window-slats";
11
11
  export declare const AWNING_WINDOW_SASH_NAME = "awning-window-sash";
12
12
  export declare const HOPPER_WINDOW_SASH_NAME = "hopper-window-sash";
13
- export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
13
+ export declare const pendingWindowAnimationRebuilds: Set<`building_${string}` | `level_${string}` | `elevator_${string}` | `unit_${string}` | `block_${string}` | `ceiling_${string}` | `item_${string}` | `procedural-item_${string}` | `column_${string}` | `construction-dimension_${string}` | `wall_${string}` | `roof_${string}` | `slab_${string}` | `separator_${string}` | `floor-opening_${string}` | `fence_${string}` | `structural-grid_${string}` | `imesh_${string}` | `zone_${string}` | `stair_${string}` | `scan_${string}` | `guide_${string}` | `measurement_${string}` | `spawn_${string}` | `shelf_${string}` | `duct-segment_${string}` | `duct-fitting_${string}` | `duct-terminal_${string}` | `hvac-equipment_${string}` | `lineset_${string}` | `liquid-line_${string}` | `pipe-segment_${string}` | `pipe-fitting_${string}` | `pipe-trap_${string}` | `site_${string}` | `leanto_${string}` | `door_${string}` | `window_${string}` | `fence-gate_${string}` | `fence-opening_${string}` | `cabinet_${string}` | `cabinet-module_${string}` | `rseg_${string}` | `sseg_${string}` | `bvent_${string}` | `rvent_${string}` | `tvent_${string}` | `cupola_${string}` | `eyebrow-vent_${string}` | `gutter_${string}` | `chimney_${string}` | `solarpanel_${string}` | `skylight_${string}` | `dormer_${string}` | `downspout_${string}`>;
14
14
  export declare const WindowSystem: () => null;
15
15
  /**
16
16
  * Build a fresh window mesh for preview/ghost rendering.
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyB9B,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;AAS3D,eAAO,MAAM,8BAA8B,uoCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAqHxB,CAAA;AA28GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAcL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA2B9B,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;AAS3D,eAAO,MAAM,8BAA8B,gvCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAuHxB,CAAA;AA29GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
@@ -1,10 +1,11 @@
1
- import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getWallThickness, sceneRegistry, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
1
+ import { DEFAULT_WALL_THICKNESS, getEffectiveNode, getOpeningWallPlacement, getWallThickness, sceneRegistry, scriptedSize, useInteractive, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect, useRef } from 'react';
4
4
  import * as THREE from 'three';
5
5
  import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
6
- import { createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
6
+ import { createMaterial, createSurfaceRoleMaterial, glassMaterial as defaultGlassMaterial, baseMaterial as getBaseMaterial, resolveMaterialRef, } from '../../lib/materials';
7
7
  import { timeSpan } from '../../lib/perf-tracks';
8
+ import { settleScriptedOpening } from '../../lib/scripted-opening';
8
9
  import useViewer from '../../store/use-viewer';
9
10
  import { getOpeningCutoutProxyDepth } from '../wall/opening-cutout-geometry';
10
11
  // Invisible material for root mesh — used as selection hitbox only
@@ -128,9 +129,12 @@ export const WindowSystem = () => {
128
129
  // Merge any live override (width / height / position) so the mesh
129
130
  // rebuild reflects the in-flight drag without zustand churn.
130
131
  const effectiveNode = getEffectiveNode(node);
131
- timeSpan('window', () => updateWindowMesh(effectiveNode, mesh), {
132
+ const built = timeSpan('window', () => updateWindowMesh(effectiveNode, mesh), {
132
133
  properties: [['node', id]],
133
134
  });
135
+ // A scripted opening stays dirty until its artifact has loaded.
136
+ if (!built)
137
+ continue;
134
138
  clearDirty(id);
135
139
  rebuiltWindowsThisFrame += 1;
136
140
  // Rebuild the parent wall so its cutout reflects the updated window geometry
@@ -192,16 +196,19 @@ function windowSlotDefault(slotId) {
192
196
  getBaseMaterial(currentShading));
193
197
  }
194
198
  // Resolve a window's slot to a material: the `node.slots` override (colored mode
195
- // only) → the role/base default. Textures-off ignores overrides — the monochrome
196
- // escape hatch.
199
+ // only) → the window-wide `material` for the frame (glass keeps its own) → the
200
+ // role/base default. Textures-off ignores overrides — the monochrome escape hatch.
197
201
  function resolveWindowSlotMaterial(node, slotId) {
198
202
  const fallback = windowSlotDefault(slotId);
199
203
  if (!currentTextures)
200
204
  return fallback;
201
205
  const ref = node.slots?.[slotId];
202
- if (!ref)
203
- return fallback;
204
- return resolveMaterialRef(ref, currentSceneMaterials, currentShading) ?? fallback;
206
+ const painted = ref ? resolveMaterialRef(ref, currentSceneMaterials, currentShading) : null;
207
+ if (painted)
208
+ return painted;
209
+ if (node.material && slotId === 'frame')
210
+ return createMaterial(node.material, currentShading);
211
+ return fallback;
205
212
  }
206
213
  function addBox(parent, material, w, h, d, x, y, z) {
207
214
  const geometry = new THREE.BoxGeometry(w, h, d);
@@ -1526,8 +1533,19 @@ function updateWindowMesh(node, mesh) {
1526
1533
  mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
1527
1534
  mesh.material = hitboxMaterial;
1528
1535
  // Sync transform from node (React may lag behind the system by a frame during drag)
1529
- mesh.position.set(node.position[0], node.position[1], node.position[2]);
1530
- mesh.rotation.set(node.rotation[0], node.rotation[1], node.rotation[2]);
1536
+ const parent = node.parentId ? useScene.getState().nodes[node.parentId] : undefined;
1537
+ const placement = parent?.type === 'wall' && !node.roofSegmentId
1538
+ ? getOpeningWallPlacement(getEffectiveNode(parent), node, useScene.getState().nodes)
1539
+ : node;
1540
+ mesh.position.set(...placement.position);
1541
+ mesh.rotation.set(...placement.rotation);
1542
+ // Built from a script: the renderer shows its artifact, not the parametric
1543
+ // frame, and the hit box is what the script built.
1544
+ if (node.source) {
1545
+ mesh.geometry.dispose();
1546
+ mesh.geometry = new THREE.BoxGeometry(...scriptedSize(node.source.manifest));
1547
+ return settleScriptedOpening(mesh);
1548
+ }
1531
1549
  // Dispose and remove all old visual children; preserve 'cutout'
1532
1550
  for (const child of [...mesh.children]) {
1533
1551
  if (child.name === 'cutout')
@@ -1543,62 +1561,62 @@ function updateWindowMesh(node, mesh) {
1543
1561
  const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
1544
1562
  if (openingKind === 'opening') {
1545
1563
  syncWindowCutout(node, mesh);
1546
- return;
1564
+ return true;
1547
1565
  }
1548
1566
  if (windowType === 'sliding') {
1549
1567
  addSlidingWindowVisuals(node, mesh);
1550
1568
  syncWindowCutout(node, mesh);
1551
- return;
1569
+ return true;
1552
1570
  }
1553
1571
  if (windowType === 'casement') {
1554
1572
  addCasementWindowVisuals(node, mesh);
1555
1573
  syncWindowCutout(node, mesh);
1556
- return;
1574
+ return true;
1557
1575
  }
1558
1576
  if (windowType === 'awning') {
1559
1577
  addAwningWindowVisuals(node, mesh);
1560
1578
  syncWindowCutout(node, mesh);
1561
- return;
1579
+ return true;
1562
1580
  }
1563
1581
  if (windowType === 'hopper') {
1564
1582
  addAwningWindowVisuals(node, mesh);
1565
1583
  syncWindowCutout(node, mesh);
1566
- return;
1584
+ return true;
1567
1585
  }
1568
1586
  if (windowType === 'single-hung') {
1569
1587
  addSingleHungWindowVisuals(node, mesh);
1570
1588
  syncWindowCutout(node, mesh);
1571
- return;
1589
+ return true;
1572
1590
  }
1573
1591
  if (windowType === 'double-hung') {
1574
1592
  addDoubleHungWindowVisuals(node, mesh);
1575
1593
  syncWindowCutout(node, mesh);
1576
- return;
1594
+ return true;
1577
1595
  }
1578
1596
  if (windowType === 'bay') {
1579
1597
  addBayWindowVisuals(node, mesh);
1580
1598
  syncWindowCutout(node, mesh);
1581
- return;
1599
+ return true;
1582
1600
  }
1583
1601
  if (windowType === 'bow') {
1584
1602
  addBowWindowVisuals(node, mesh);
1585
1603
  syncWindowCutout(node, mesh);
1586
- return;
1604
+ return true;
1587
1605
  }
1588
1606
  if (windowType === 'louvered') {
1589
1607
  addLouveredWindowVisuals(node, mesh);
1590
1608
  syncWindowCutout(node, mesh);
1591
- return;
1609
+ return true;
1592
1610
  }
1593
1611
  if (openingShape === 'arch') {
1594
1612
  addArchedWindowVisuals(node, mesh);
1595
1613
  syncWindowCutout(node, mesh);
1596
- return;
1614
+ return true;
1597
1615
  }
1598
1616
  if (openingShape === 'rounded') {
1599
1617
  addRoundedWindowVisuals(node, mesh);
1600
1618
  syncWindowCutout(node, mesh);
1601
- return;
1619
+ return true;
1602
1620
  }
1603
1621
  const innerW = width - 2 * frameThickness;
1604
1622
  const innerH = height - 2 * frameThickness;
@@ -1673,6 +1691,7 @@ function updateWindowMesh(node, mesh) {
1673
1691
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1674
1692
  }
1675
1693
  syncWindowCutout(node, mesh);
1694
+ return true;
1676
1695
  }
1677
1696
  function syncWindowCutout(node, mesh) {
1678
1697
  // ── Cutout: invisible raycast hit target for the whole opening ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "peerDependencies": {
36
- "@pascal-app/core": "^1.0.3",
36
+ "@pascal-app/core": "^1.0.4",
37
37
  "@react-three/drei": "^10",
38
38
  "@react-three/fiber": "^9",
39
39
  "react": "^18 || ^19",
@@ -46,12 +46,12 @@
46
46
  "zustand": "^5"
47
47
  },
48
48
  "devDependencies": {
49
- "@pascal-app/core": "^1.0.3",
49
+ "@pascal-app/core": "^1.0.4",
50
50
  "@pascal/typescript-config": "*",
51
51
  "@react-three/test-renderer": "^9.1.0",
52
52
  "@types/node": "^22",
53
- "@types/react": "^19.2.2",
54
- "@types/react-dom": "19.2.2",
53
+ "@types/react": "19.3.0",
54
+ "@types/react-dom": "19.3.0",
55
55
  "@types/three": "^0.184.0",
56
56
  "typescript": "6.0.3"
57
57
  },
@@ -1,22 +0,0 @@
1
- import { type FenceNode } from '@pascal-app/core';
2
- import * as THREE from 'three';
3
- type FencePart = {
4
- geometry?: THREE.BufferGeometry;
5
- position: [number, number, number];
6
- rotationY?: number;
7
- scale: [number, number, number];
8
- shape?: 'box' | 'pyramid';
9
- };
10
- export type FenceSlotId = 'posts' | 'infill' | 'base' | 'rail';
11
- export type FenceSlotParts = Record<FenceSlotId, FencePart[]>;
12
- /**
13
- * Geometry split by paint slot — posts, infill, base, rail — each a separate
14
- * merged BufferGeometry (empty ones included) so the fence renderer can give
15
- * each its own material + `userData.slotId`. Slots match the panel's build
16
- * options 1:1.
17
- */
18
- export declare function generateFenceSlotGeometries(fence: FenceNode): Record<FenceSlotId, THREE.BufferGeometry>;
19
- export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
20
- export declare const FenceSystem: () => null;
21
- export {};
22
- //# sourceMappingURL=fence-system.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"fence-system.d.ts","sourceRoot":"","sources":["../../../src/systems/fence/fence-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,KAAK,SAAS,GAAG;IACf,QAAQ,CAAC,EAAE,KAAK,CAAC,cAAc,CAAA;IAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAG/B,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAA;CAC1B,CAAA;AAqPD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAE9D,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;AAsQ7D;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,GACf,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,cAAc,CAAC,CAQ3C;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,oFAGrD;AAgBD,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
@@ -1,357 +0,0 @@
1
- import { getFenceCenterlineFrameAt, getFenceCenterlineLength, sceneRegistry, useScene, } from '@pascal-app/core';
2
- import { useFrame } from '@react-three/fiber';
3
- import * as THREE from 'three';
4
- import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
5
- const MIN_CURVE_SEGMENT_LENGTH = 0.18;
6
- const HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH = 0.2;
7
- function createFencePartGeometry(part) {
8
- if (part.geometry) {
9
- return part.geometry;
10
- }
11
- const geometry = part.shape === 'pyramid'
12
- ? new THREE.ConeGeometry(0.5, 1, 4, 1, false, Math.PI / 4)
13
- : new THREE.BoxGeometry(1, 1, 1);
14
- geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
15
- if (part.rotationY) {
16
- geometry.rotateY(part.rotationY);
17
- }
18
- geometry.translate(part.position[0], part.position[1], part.position[2]);
19
- applyFenceUVs(geometry);
20
- return geometry;
21
- }
22
- function getFencePointAt(fence, t) {
23
- const frame = getFenceCenterlineFrameAt(fence, t);
24
- return {
25
- point: frame.point,
26
- tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
27
- };
28
- }
29
- function createFenceCurveBlockPart(fence, startT, endT, centerY, height, depth) {
30
- if (endT - startT <= 1e-5)
31
- return null;
32
- const halfHeight = height / 2;
33
- const halfDepth = depth / 2;
34
- const centerlineLength = getFenceCenterlineLength(fence);
35
- const startDistance = startT * centerlineLength;
36
- const endDistance = endT * centerlineLength;
37
- const bottomY = centerY - halfHeight;
38
- const topY = centerY + halfHeight;
39
- const corners = [];
40
- for (const t of [startT, endT]) {
41
- const frame = getFencePointAt(fence, t);
42
- const normalX = -Math.sin(frame.tangentAngle);
43
- const normalZ = Math.cos(frame.tangentAngle);
44
- const outerX = frame.point.x + normalX * halfDepth;
45
- const outerZ = frame.point.y + normalZ * halfDepth;
46
- const innerX = frame.point.x - normalX * halfDepth;
47
- const innerZ = frame.point.y - normalZ * halfDepth;
48
- corners.push([outerX, bottomY, outerZ], [innerX, bottomY, innerZ], [outerX, topY, outerZ], [innerX, topY, innerZ]);
49
- }
50
- const positions = [];
51
- const uvs = [];
52
- const pushVertex = (index, uv) => {
53
- positions.push(...corners[index]);
54
- uvs.push(...uv);
55
- };
56
- const pushQuad = (a, b, c, d, uvA, uvB, uvC, uvD) => {
57
- pushVertex(a, uvA);
58
- pushVertex(b, uvB);
59
- pushVertex(c, uvC);
60
- pushVertex(a, uvA);
61
- pushVertex(c, uvC);
62
- pushVertex(d, uvD);
63
- };
64
- const topOuterV = topY;
65
- const topInnerV = topY + depth;
66
- const innerTopV = topInnerV;
67
- const innerBottomV = topInnerV + height;
68
- const bottomInnerV = bottomY - depth;
69
- pushQuad(0, 4, 6, 2, [startDistance, bottomY], [endDistance, bottomY], [endDistance, topY], [startDistance, topY]);
70
- pushQuad(1, 3, 7, 5, [startDistance, innerBottomV], [startDistance, innerTopV], [endDistance, innerTopV], [endDistance, innerBottomV]);
71
- pushQuad(2, 6, 7, 3, [startDistance, topOuterV], [endDistance, topOuterV], [endDistance, topInnerV], [startDistance, topInnerV]);
72
- pushQuad(0, 1, 5, 4, [startDistance, bottomY], [startDistance, bottomInnerV], [endDistance, bottomInnerV], [endDistance, bottomY]);
73
- pushQuad(0, 2, 3, 1, [0, bottomY], [0, topY], [depth, innerTopV], [depth, innerBottomV]);
74
- pushQuad(4, 5, 7, 6, [0, bottomY], [depth, innerBottomV], [depth, innerTopV], [0, topY]);
75
- const geometry = new THREE.BufferGeometry();
76
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(positions), 3));
77
- geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
78
- geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
79
- geometry.computeVertexNormals();
80
- return {
81
- geometry,
82
- position: [0, 0, 0],
83
- scale: [1, 1, 1],
84
- };
85
- }
86
- function createFenceCurveBlockParts(fence, startT, endT, centerY, height, depth, maxSegmentLength = MIN_CURVE_SEGMENT_LENGTH) {
87
- const length = getFenceCenterlineLength(fence) * Math.max(1e-4, endT - startT);
88
- const segmentCount = Math.max(1, Math.ceil(length / Math.max(1e-4, maxSegmentLength)));
89
- const parts = [];
90
- for (let index = 0; index < segmentCount; index += 1) {
91
- const segmentStartT = startT + (endT - startT) * (index / segmentCount);
92
- const segmentEndT = startT + (endT - startT) * ((index + 1) / segmentCount);
93
- const part = createFenceCurveBlockPart(fence, segmentStartT, segmentEndT, centerY, height, depth);
94
- if (part)
95
- parts.push(part);
96
- }
97
- return parts;
98
- }
99
- function applyFenceUVs(geometry) {
100
- const position = geometry.getAttribute('position');
101
- const normal = geometry.getAttribute('normal');
102
- if (!(position && normal))
103
- return;
104
- // World-scale triplanar UVs: 1 UV unit = 1 metre, sampled from the part's
105
- // local-space (already translated into fence space) coordinates with NO
106
- // per-part origin shift. A shared origin keeps a tiled finish continuous
107
- // across posts, rails, and infill instead of restarting the tile at each
108
- // part's own min corner (the previous behaviour, which broke the 1 m
109
- // contract and made adjacent parts mistile).
110
- const uvs = new Float32Array(position.count * 2);
111
- for (let index = 0; index < position.count; index += 1) {
112
- const px = position.getX(index);
113
- const py = position.getY(index);
114
- const pz = position.getZ(index);
115
- const nx = Math.abs(normal.getX(index));
116
- const ny = Math.abs(normal.getY(index));
117
- const nz = Math.abs(normal.getZ(index));
118
- let u = 0;
119
- let v = 0;
120
- if (ny >= nx && ny >= nz) {
121
- u = px;
122
- v = pz;
123
- }
124
- else if (nx >= nz) {
125
- u = pz;
126
- v = py;
127
- }
128
- else {
129
- u = px;
130
- v = py;
131
- }
132
- uvs[index * 2] = u;
133
- uvs[index * 2 + 1] = v;
134
- }
135
- geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
136
- geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
137
- }
138
- function getStyleDefaults(style) {
139
- if (style === 'privacy') {
140
- return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 };
141
- }
142
- if (style === 'rail') {
143
- return { spacingFactor: 0.68, postFactor: 0.8, baseFactor: 0.85, topFactor: 0.85 };
144
- }
145
- return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 };
146
- }
147
- /**
148
- * Horizontal-board fence — composite cladding boards stacked between square
149
- * intermediate posts (each capped), instead of the vertical pickets the other
150
- * styles draw. Posts march along the whole span at `postSpacing` (not just the
151
- * two ends), the boards run full-length so they curve with the fence, and a
152
- * thin reveal between boards leaves the groove shadow that reads as cladding.
153
- */
154
- function createHorizontalFenceParts(fence) {
155
- const posts = [];
156
- const infill = [];
157
- const base = [];
158
- const rail = [];
159
- const length = Math.max(getFenceCenterlineLength(fence), 0.01);
160
- const panelDepth = Math.max(fence.thickness, 0.03);
161
- const clearance = Math.max(fence.groundClearance, 0);
162
- const isFloating = fence.baseStyle === 'floating';
163
- const showInfill = fence.showInfill ?? true;
164
- const baseHeight = Math.max(fence.baseHeight, 0.04);
165
- const topRailHeight = Math.max(fence.topRailHeight, 0.01);
166
- const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
167
- const baseY = isFloating ? clearance : 0;
168
- // Square posts stand proud of the recessed boards on both faces.
169
- const postWidth = Math.max(fence.postSize * 1.4, 0.04);
170
- const postDepth = postWidth;
171
- const boardDepth = Math.min(panelDepth, postDepth - 0.012);
172
- // Stop the horizontal boards / base / rail at the inner faces of the
173
- // end posts. Letting curved spans run all the way to t=0/1 makes them
174
- // overlap the terminal post mesh and creates the broken seam/notch seen
175
- // at curve ends.
176
- const edgeInset = Math.max(fence.edgeInset ?? 0.015, postWidth * 0.5);
177
- const startInsetT = Math.min(0.499, edgeInset / length);
178
- const endInsetT = Math.max(0.501, 1 - edgeInset / length);
179
- // Grounded fences get a kickboard along the bottom; floating ones don't.
180
- if (!isFloating) {
181
- base.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight / 2, baseHeight, postDepth * 0.92, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
182
- }
183
- // Stack full-length boards between the kickboard and the top rail. The board
184
- // height is derived to evenly fill the panel around a ~0.145 m target, with a
185
- // constant reveal between each so the count adapts to any fence height.
186
- if (showInfill) {
187
- const reveal = Math.max(fence.slatGap ?? 0.01, 0);
188
- const infillBottom = baseY + baseHeight;
189
- if (reveal < 0.002) {
190
- // No reveal → one flush panel, so the stacked-board edge seams don't
191
- // read as faint lines where the user asked for a smooth surface.
192
- infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, infillBottom + verticalHeight / 2, verticalHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
193
- }
194
- else {
195
- const boardCount = Math.max(1, Math.round(verticalHeight / (0.145 + reveal)));
196
- const slabHeight = Math.max((verticalHeight - reveal * (boardCount - 1)) / boardCount, 0.02);
197
- for (let index = 0; index < boardCount; index += 1) {
198
- const centerY = infillBottom + slabHeight / 2 + index * (slabHeight + reveal);
199
- infill.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, centerY, slabHeight, boardDepth, HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
200
- }
201
- }
202
- }
203
- // Top rail caps the boards.
204
- rail.push(...createFenceCurveBlockParts(fence, startInsetT, endInsetT, baseY + baseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(postDepth * 0.78, 0.02), HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH));
205
- // Posts at every `postSpacing`, anchored at both ends, each with a flat cap.
206
- const spacing = Math.max(fence.postSpacing, postWidth * 1.4);
207
- const postCount = Math.max(2, Math.floor(length / spacing) + 1);
208
- const postHeight = baseHeight + verticalHeight + topRailHeight + clearance;
209
- const capHeight = Math.max(postWidth * 0.32, 0.03);
210
- const cap = fence.postCap ?? 'pyramid';
211
- for (let index = 0; index < postCount; index += 1) {
212
- const t = postCount === 1 ? 0.5 : index / (postCount - 1);
213
- const frame = getFencePointAt(fence, t);
214
- posts.push({
215
- position: [frame.point.x, postHeight / 2, frame.point.y],
216
- rotationY: -frame.tangentAngle,
217
- scale: [postWidth, postHeight, postDepth],
218
- });
219
- if (cap === 'flat') {
220
- posts.push({
221
- position: [frame.point.x, postHeight + capHeight / 2, frame.point.y],
222
- rotationY: -frame.tangentAngle,
223
- scale: [postWidth * 1.22, capHeight, postDepth * 1.22],
224
- });
225
- }
226
- else if (cap === 'pyramid') {
227
- posts.push({
228
- position: [frame.point.x, postHeight + capHeight * 0.9, frame.point.y],
229
- rotationY: -frame.tangentAngle,
230
- scale: [postWidth * 1.18, capHeight * 1.8, postDepth * 1.18],
231
- shape: 'pyramid',
232
- });
233
- }
234
- }
235
- return { posts, infill, base, rail };
236
- }
237
- function createFenceParts(fence) {
238
- if (fence.style === 'horizontal')
239
- return createHorizontalFenceParts(fence);
240
- const posts = [];
241
- const infill = [];
242
- const base = [];
243
- const rail = [];
244
- const length = Math.max(getFenceCenterlineLength(fence), 0.01);
245
- const panelDepth = Math.max(fence.thickness, 0.03);
246
- const clearance = Math.max(fence.groundClearance, 0);
247
- const styleDefaults = getStyleDefaults(fence.style);
248
- const baseHeight = Math.max(fence.baseHeight * styleDefaults.baseFactor, 0.04);
249
- const topRailHeight = Math.max(fence.topRailHeight * styleDefaults.topFactor, 0.01);
250
- const verticalHeight = Math.max(fence.height - baseHeight - topRailHeight, 0.08);
251
- const postWidth = Math.max(fence.postSize * styleDefaults.postFactor, 0.01);
252
- const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2);
253
- const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005);
254
- const isFloating = fence.baseStyle === 'floating';
255
- const showInfill = fence.showInfill ?? true;
256
- const baseY = isFloating ? clearance : 0;
257
- const effectiveBaseHeight = baseHeight;
258
- const startInsetT = Math.min(0.499, edgeInset / length);
259
- const endInsetT = Math.max(0.501, 1 - edgeInset / length);
260
- if (!isFloating) {
261
- base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
262
- base.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
263
- }
264
- const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2;
265
- const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
266
- for (let index = 0; index < count; index += 1) {
267
- const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
268
- const isEdgePost = index === 0 || index === count - 1;
269
- const fullHeightPost = !showInfill || (isFloating && isEdgePost);
270
- const postHeight = fullHeightPost
271
- ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
272
- : verticalHeight;
273
- const postY = fullHeightPost ? postHeight / 2 : verticalY;
274
- // End posts are the structural `posts` slot; the intermediate verticals are
275
- // the `infill` slats (only present when showInfill adds them).
276
- const slatHalfT = Math.max(0.0005, postWidth / (2 * length));
277
- const slatStartT = Math.max(0, t - slatHalfT);
278
- const slatEndT = Math.min(1, t + slatHalfT);
279
- const slat = createFenceCurveBlockPart(fence, slatStartT, slatEndT, postY, postHeight, Math.max(panelDepth * 0.35 - 0.001, 0.011));
280
- if (slat) {
281
- ;
282
- (isEdgePost ? posts : infill).push(slat);
283
- }
284
- }
285
- rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
286
- if (isFloating) {
287
- rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
288
- }
289
- return { posts, infill, base, rail };
290
- }
291
- function mergeFenceParts(parts) {
292
- // An empty slot group (e.g. infill with showInfill off, or base on a floating
293
- // fence) must not reach mergeGeometries — it throws on an empty array. The
294
- // empty geometry has no position attribute, so the renderer skips its mesh.
295
- if (parts.length === 0)
296
- return new THREE.BufferGeometry();
297
- const geometries = parts.map(createFencePartGeometry);
298
- const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
299
- geometries.forEach((geometry) => {
300
- geometry.dispose();
301
- });
302
- const mergedUv = merged.getAttribute('uv');
303
- if (mergedUv) {
304
- merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
305
- }
306
- merged.computeVertexNormals();
307
- return merged;
308
- }
309
- /**
310
- * Geometry split by paint slot — posts, infill, base, rail — each a separate
311
- * merged BufferGeometry (empty ones included) so the fence renderer can give
312
- * each its own material + `userData.slotId`. Slots match the panel's build
313
- * options 1:1.
314
- */
315
- export function generateFenceSlotGeometries(fence) {
316
- const parts = createFenceParts(fence);
317
- return {
318
- posts: mergeFenceParts(parts.posts),
319
- infill: mergeFenceParts(parts.infill),
320
- base: mergeFenceParts(parts.base),
321
- rail: mergeFenceParts(parts.rail),
322
- };
323
- }
324
- export function generateFenceGeometry(fence) {
325
- const { posts, infill, base, rail } = createFenceParts(fence);
326
- return mergeFenceParts([...posts, ...infill, ...base, ...rail]);
327
- }
328
- function updateFenceGeometry(fenceId) {
329
- const node = useScene.getState().nodes[fenceId];
330
- if (node?.type !== 'fence')
331
- return;
332
- const mesh = sceneRegistry.nodes.get(fenceId);
333
- if (!mesh)
334
- return;
335
- const newGeometry = generateFenceGeometry(node);
336
- mesh.geometry.dispose();
337
- mesh.geometry = newGeometry;
338
- mesh.position.set(0, 0, 0);
339
- mesh.rotation.set(0, 0, 0);
340
- }
341
- export const FenceSystem = () => {
342
- const dirtyNodes = useScene((state) => state.dirtyNodes);
343
- const clearDirty = useScene((state) => state.clearDirty);
344
- useFrame(() => {
345
- if (dirtyNodes.size === 0)
346
- return;
347
- const nodes = useScene.getState().nodes;
348
- dirtyNodes.forEach((id) => {
349
- const node = nodes[id];
350
- if (node?.type !== 'fence')
351
- return;
352
- updateFenceGeometry(id);
353
- clearDirty(id);
354
- });
355
- }, 4);
356
- return null;
357
- };