@pascal-app/viewer 1.0.1 → 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 (173) 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 +26 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +108 -29
  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 +19 -1
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +53 -5
  40. package/dist/index.d.ts +28 -15
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -18
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/csg-utils.d.ts +1 -1
  46. package/dist/lib/csg-utils.d.ts.map +1 -1
  47. package/dist/lib/csg-utils.js +1 -1
  48. package/dist/lib/display-state.d.ts +25 -0
  49. package/dist/lib/display-state.d.ts.map +1 -0
  50. package/dist/lib/display-state.js +1 -0
  51. package/dist/lib/hero-pose.d.ts +12 -2
  52. package/dist/lib/hero-pose.d.ts.map +1 -1
  53. package/dist/lib/hero-pose.js +72 -5
  54. package/dist/lib/ink-edges.d.ts +4 -2
  55. package/dist/lib/ink-edges.d.ts.map +1 -1
  56. package/dist/lib/ink-edges.js +17 -7
  57. package/dist/lib/key-light-override.d.ts +11 -0
  58. package/dist/lib/key-light-override.d.ts.map +1 -0
  59. package/dist/lib/key-light-override.js +14 -0
  60. package/dist/lib/live-frame-hold.d.ts +14 -0
  61. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  62. package/dist/lib/live-frame-hold.js +28 -0
  63. package/dist/lib/materials.d.ts +14 -7
  64. package/dist/lib/materials.d.ts.map +1 -1
  65. package/dist/lib/materials.js +31 -5
  66. package/dist/lib/node-top-surface-height.d.ts +6 -0
  67. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  68. package/dist/lib/node-top-surface-height.js +74 -0
  69. package/dist/lib/perf-probe-state.d.ts +39 -0
  70. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  71. package/dist/lib/perf-probe-state.js +17 -0
  72. package/dist/lib/pointer-events.d.ts +8 -0
  73. package/dist/lib/pointer-events.d.ts.map +1 -1
  74. package/dist/lib/pointer-events.js +27 -4
  75. package/dist/lib/procedural-emission.d.ts +7 -0
  76. package/dist/lib/procedural-emission.d.ts.map +1 -0
  77. package/dist/lib/procedural-emission.js +54 -0
  78. package/dist/lib/renderer-capability.d.ts +2 -1
  79. package/dist/lib/renderer-capability.d.ts.map +1 -1
  80. package/dist/lib/renderer-capability.js +2 -2
  81. package/dist/lib/scene-support-height.d.ts +4 -0
  82. package/dist/lib/scene-support-height.d.ts.map +1 -0
  83. package/dist/lib/scene-support-height.js +34 -0
  84. package/dist/lib/scene-visibility.d.ts +6 -0
  85. package/dist/lib/scene-visibility.d.ts.map +1 -1
  86. package/dist/lib/scene-visibility.js +15 -3
  87. package/dist/lib/scripted-opening.d.ts +11 -0
  88. package/dist/lib/scripted-opening.d.ts.map +1 -0
  89. package/dist/lib/scripted-opening.js +20 -0
  90. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  91. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  92. package/dist/lib/snapshot-pipeline.js +215 -105
  93. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  94. package/dist/lib/terrain-perimeter-fill.js +22 -14
  95. package/dist/lib/zone-geometry.d.ts +11 -0
  96. package/dist/lib/zone-geometry.d.ts.map +1 -0
  97. package/dist/lib/zone-geometry.js +46 -0
  98. package/dist/store/use-item-light-pool.d.ts +14 -10
  99. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  100. package/dist/store/use-item-light-pool.js +52 -26
  101. package/dist/store/use-viewer.d.ts +11 -0
  102. package/dist/store/use-viewer.d.ts.map +1 -1
  103. package/dist/store/use-viewer.js +13 -6
  104. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +86 -6
  107. package/dist/systems/door/door-system.d.ts.map +1 -1
  108. package/dist/systems/door/door-system.js +73 -22
  109. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  110. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  111. package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
  112. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  113. package/dist/systems/geometry/geometry-system.js +71 -11
  114. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  115. package/dist/systems/interactive/interactive-system.js +40 -15
  116. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  117. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  118. package/dist/systems/interactive/procedural-controls.js +17 -0
  119. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  120. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  121. package/dist/systems/interactive/scripted-clips.js +134 -0
  122. package/dist/systems/item/item-system.d.ts.map +1 -1
  123. package/dist/systems/item/item-system.js +9 -3
  124. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  125. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  126. package/dist/systems/item/object-cut-invalidation.js +68 -0
  127. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  128. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  129. package/dist/systems/item-light/item-light-system.js +47 -52
  130. package/dist/systems/level/level-system.d.ts.map +1 -1
  131. package/dist/systems/level/level-system.js +17 -19
  132. package/dist/systems/level/level-utils.d.ts +28 -1
  133. package/dist/systems/level/level-utils.d.ts.map +1 -1
  134. package/dist/systems/level/level-utils.js +36 -4
  135. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  136. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  137. package/dist/systems/level/shadow-only-levels.js +7 -0
  138. package/dist/systems/roof/roof-materials.d.ts +14 -2
  139. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  140. package/dist/systems/roof/roof-materials.js +54 -15
  141. package/dist/systems/roof/roof-system.d.ts +13 -3
  142. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  143. package/dist/systems/roof/roof-system.js +188 -13
  144. package/dist/systems/slab/slab-system.d.ts +1 -1
  145. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  146. package/dist/systems/slab/slab-system.js +49 -19
  147. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  148. package/dist/systems/stair/stair-system.js +133 -354
  149. package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
  150. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  151. package/dist/systems/wall/wall-cutout-cache.js +58 -20
  152. package/dist/systems/wall/wall-cutout.d.ts +9 -1
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -32
  155. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  156. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  157. package/dist/systems/wall/wall-finish-data.js +17 -0
  158. package/dist/systems/wall/wall-materials.d.ts +17 -3
  159. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  160. package/dist/systems/wall/wall-materials.js +109 -156
  161. package/dist/systems/wall/wall-system.d.ts +43 -4
  162. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  163. package/dist/systems/wall/wall-system.js +601 -163
  164. package/dist/systems/window/window-system.d.ts +1 -1
  165. package/dist/systems/window/window-system.d.ts.map +1 -1
  166. package/dist/systems/window/window-system.js +44 -24
  167. package/dist/xr/presentation-context.d.ts +7 -0
  168. package/dist/xr/presentation-context.d.ts.map +1 -0
  169. package/dist/xr/presentation-context.js +10 -0
  170. package/package.json +6 -6
  171. package/dist/systems/fence/fence-system.d.ts +0 -22
  172. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  173. 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,YAoHxB,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,16 +129,20 @@ 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
137
141
  // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
138
142
  // The editor tools will request a final wall rebuild on commit.
139
- const isTransient = !!node.metadata?.isTransient;
140
- if (!isTransient && effectiveNode.parentId) {
143
+ const metadata = effectiveNode.metadata;
144
+ const deferParentRebuild = !!metadata?.isTransient || !!metadata?.deferParentRebuild;
145
+ if (!deferParentRebuild && effectiveNode.parentId) {
141
146
  useScene.getState().dirtyNodes.add(effectiveNode.parentId);
142
147
  }
143
148
  }
@@ -191,16 +196,19 @@ function windowSlotDefault(slotId) {
191
196
  getBaseMaterial(currentShading));
192
197
  }
193
198
  // Resolve a window's slot to a material: the `node.slots` override (colored mode
194
- // only) → the role/base default. Textures-off ignores overrides — the monochrome
195
- // 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.
196
201
  function resolveWindowSlotMaterial(node, slotId) {
197
202
  const fallback = windowSlotDefault(slotId);
198
203
  if (!currentTextures)
199
204
  return fallback;
200
205
  const ref = node.slots?.[slotId];
201
- if (!ref)
202
- return fallback;
203
- 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;
204
212
  }
205
213
  function addBox(parent, material, w, h, d, x, y, z) {
206
214
  const geometry = new THREE.BoxGeometry(w, h, d);
@@ -1525,8 +1533,19 @@ function updateWindowMesh(node, mesh) {
1525
1533
  mesh.geometry = new THREE.BoxGeometry(node.width, node.height, node.frameDepth);
1526
1534
  mesh.material = hitboxMaterial;
1527
1535
  // Sync transform from node (React may lag behind the system by a frame during drag)
1528
- mesh.position.set(node.position[0], node.position[1], node.position[2]);
1529
- 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
+ }
1530
1549
  // Dispose and remove all old visual children; preserve 'cutout'
1531
1550
  for (const child of [...mesh.children]) {
1532
1551
  if (child.name === 'cutout')
@@ -1542,62 +1561,62 @@ function updateWindowMesh(node, mesh) {
1542
1561
  const { width, height, frameDepth, frameThickness, columnRatios, rowRatios, columnDividerThickness, rowDividerThickness, sill, sillDepth, sillThickness, openingKind, openingShape, windowType, } = node;
1543
1562
  if (openingKind === 'opening') {
1544
1563
  syncWindowCutout(node, mesh);
1545
- return;
1564
+ return true;
1546
1565
  }
1547
1566
  if (windowType === 'sliding') {
1548
1567
  addSlidingWindowVisuals(node, mesh);
1549
1568
  syncWindowCutout(node, mesh);
1550
- return;
1569
+ return true;
1551
1570
  }
1552
1571
  if (windowType === 'casement') {
1553
1572
  addCasementWindowVisuals(node, mesh);
1554
1573
  syncWindowCutout(node, mesh);
1555
- return;
1574
+ return true;
1556
1575
  }
1557
1576
  if (windowType === 'awning') {
1558
1577
  addAwningWindowVisuals(node, mesh);
1559
1578
  syncWindowCutout(node, mesh);
1560
- return;
1579
+ return true;
1561
1580
  }
1562
1581
  if (windowType === 'hopper') {
1563
1582
  addAwningWindowVisuals(node, mesh);
1564
1583
  syncWindowCutout(node, mesh);
1565
- return;
1584
+ return true;
1566
1585
  }
1567
1586
  if (windowType === 'single-hung') {
1568
1587
  addSingleHungWindowVisuals(node, mesh);
1569
1588
  syncWindowCutout(node, mesh);
1570
- return;
1589
+ return true;
1571
1590
  }
1572
1591
  if (windowType === 'double-hung') {
1573
1592
  addDoubleHungWindowVisuals(node, mesh);
1574
1593
  syncWindowCutout(node, mesh);
1575
- return;
1594
+ return true;
1576
1595
  }
1577
1596
  if (windowType === 'bay') {
1578
1597
  addBayWindowVisuals(node, mesh);
1579
1598
  syncWindowCutout(node, mesh);
1580
- return;
1599
+ return true;
1581
1600
  }
1582
1601
  if (windowType === 'bow') {
1583
1602
  addBowWindowVisuals(node, mesh);
1584
1603
  syncWindowCutout(node, mesh);
1585
- return;
1604
+ return true;
1586
1605
  }
1587
1606
  if (windowType === 'louvered') {
1588
1607
  addLouveredWindowVisuals(node, mesh);
1589
1608
  syncWindowCutout(node, mesh);
1590
- return;
1609
+ return true;
1591
1610
  }
1592
1611
  if (openingShape === 'arch') {
1593
1612
  addArchedWindowVisuals(node, mesh);
1594
1613
  syncWindowCutout(node, mesh);
1595
- return;
1614
+ return true;
1596
1615
  }
1597
1616
  if (openingShape === 'rounded') {
1598
1617
  addRoundedWindowVisuals(node, mesh);
1599
1618
  syncWindowCutout(node, mesh);
1600
- return;
1619
+ return true;
1601
1620
  }
1602
1621
  const innerW = width - 2 * frameThickness;
1603
1622
  const innerH = height - 2 * frameThickness;
@@ -1672,6 +1691,7 @@ function updateWindowMesh(node, mesh) {
1672
1691
  addBox(mesh, baseMaterial, sillW, sillThickness, sillDepth, 0, -height / 2 - sillThickness / 2, sillZ);
1673
1692
  }
1674
1693
  syncWindowCutout(node, mesh);
1694
+ return true;
1675
1695
  }
1676
1696
  function syncWindowCutout(node, mesh) {
1677
1697
  // ── Cutout: invisible raycast hit target for the whole opening ──
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare function ImmersiveXRPresentationProvider({ children, enabled, }: {
3
+ children: ReactNode;
4
+ enabled: boolean;
5
+ }): import("react").JSX.Element;
6
+ export declare function useImmersiveXRPresentation(): boolean;
7
+ //# sourceMappingURL=presentation-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presentation-context.d.ts","sourceRoot":"","sources":["../../src/xr/presentation-context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAIjE,wBAAgB,+BAA+B,CAAC,EAC9C,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,SAAS,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB,+BAMA;AAED,wBAAgB,0BAA0B,YAEzC"}
@@ -0,0 +1,10 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ const ImmersiveXRPresentationContext = createContext(false);
5
+ export function ImmersiveXRPresentationProvider({ children, enabled, }) {
6
+ return (_jsx(ImmersiveXRPresentationContext.Provider, { value: enabled, children: children }));
7
+ }
8
+ export function useImmersiveXRPresentation() {
9
+ return useContext(ImmersiveXRPresentationContext);
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.1",
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.1",
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.1",
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
  },
@@ -67,7 +67,7 @@
67
67
  ],
68
68
  "repository": {
69
69
  "type": "git",
70
- "url": "https://github.com/pascalorg/editor.git",
70
+ "url": "git+https://github.com/pascalorg/editor.git",
71
71
  "directory": "packages/viewer"
72
72
  },
73
73
  "license": "MIT",
@@ -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
- };