@pascal-app/viewer 0.9.1 → 1.0.0-beta.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 (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -1,5 +1,22 @@
1
1
  import { type FenceNode } from '@pascal-app/core';
2
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>;
3
19
  export declare function generateFenceGeometry(fence: FenceNode): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
4
20
  export declare const FenceSystem: () => null;
21
+ export {};
5
22
  //# sourceMappingURL=fence-system.d.ts.map
@@ -1 +1 @@
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;AA2O9B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,oFAcrD;AAgBD,eAAO,MAAM,WAAW,YAiBvB,CAAA"}
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,10 +1,16 @@
1
- import { getWallCurveFrameAt, getWallCurveLength, sceneRegistry, useScene, } from '@pascal-app/core';
1
+ import { getFenceCenterlineFrameAt, getFenceCenterlineLength, sceneRegistry, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
5
5
  const MIN_CURVE_SEGMENT_LENGTH = 0.18;
6
+ const HORIZONTAL_FENCE_CURVE_SEGMENT_LENGTH = 0.2;
6
7
  function createFencePartGeometry(part) {
7
- const geometry = new THREE.BoxGeometry(1, 1, 1);
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);
8
14
  geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
9
15
  if (part.rotationY) {
10
16
  geometry.rotateY(part.rotationY);
@@ -14,37 +20,79 @@ function createFencePartGeometry(part) {
14
20
  return geometry;
15
21
  }
16
22
  function getFencePointAt(fence, t) {
17
- const frame = getWallCurveFrameAt(fence, t);
23
+ const frame = getFenceCenterlineFrameAt(fence, t);
18
24
  return {
19
25
  point: frame.point,
20
26
  tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
21
27
  };
22
28
  }
23
- function createStraightFenceSpanPart(start, end, centerY, height, depth) {
24
- const dx = end[0] - start[0];
25
- const dz = end[1] - start[1];
26
- const length = Math.hypot(dx, dz);
27
- if (length <= 1e-4) {
29
+ function createFenceCurveBlockPart(fence, startT, endT, centerY, height, depth) {
30
+ if (endT - startT <= 1e-5)
28
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]);
29
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();
30
80
  return {
31
- position: [(start[0] + end[0]) / 2, centerY, (start[1] + end[1]) / 2],
32
- rotationY: -Math.atan2(dz, dx),
33
- scale: [length, height, depth],
81
+ geometry,
82
+ position: [0, 0, 0],
83
+ scale: [1, 1, 1],
34
84
  };
35
85
  }
36
- function createFenceCurveSpanParts(fence, startT, endT, centerY, height, depth) {
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)));
37
89
  const parts = [];
38
- const frameCount = Math.max(1, Math.ceil((getWallCurveLength(fence) * Math.max(1e-4, endT - startT)) / MIN_CURVE_SEGMENT_LENGTH));
39
- let previous = getFencePointAt(fence, startT);
40
- for (let index = 1; index <= frameCount; index += 1) {
41
- const t = startT + (endT - startT) * (index / frameCount);
42
- const current = getFencePointAt(fence, t);
43
- const segment = createStraightFenceSpanPart([previous.point.x, previous.point.y], [current.point.x, current.point.y], centerY, height, depth);
44
- if (segment) {
45
- parts.push(segment);
46
- }
47
- previous = current;
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);
48
96
  }
49
97
  return parts;
50
98
  }
@@ -53,15 +101,13 @@ function applyFenceUVs(geometry) {
53
101
  const normal = geometry.getAttribute('normal');
54
102
  if (!(position && normal))
55
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).
56
110
  const uvs = new Float32Array(position.count * 2);
57
- let minX = Number.POSITIVE_INFINITY;
58
- let minY = Number.POSITIVE_INFINITY;
59
- let minZ = Number.POSITIVE_INFINITY;
60
- for (let index = 0; index < position.count; index += 1) {
61
- minX = Math.min(minX, position.getX(index));
62
- minY = Math.min(minY, position.getY(index));
63
- minZ = Math.min(minZ, position.getZ(index));
64
- }
65
111
  for (let index = 0; index < position.count; index += 1) {
66
112
  const px = position.getX(index);
67
113
  const py = position.getY(index);
@@ -72,16 +118,16 @@ function applyFenceUVs(geometry) {
72
118
  let u = 0;
73
119
  let v = 0;
74
120
  if (ny >= nx && ny >= nz) {
75
- u = px - minX;
76
- v = pz - minZ;
121
+ u = px;
122
+ v = pz;
77
123
  }
78
124
  else if (nx >= nz) {
79
- u = pz - minZ;
80
- v = py - minY;
125
+ u = pz;
126
+ v = py;
81
127
  }
82
128
  else {
83
- u = px - minX;
84
- v = py - minY;
129
+ u = px;
130
+ v = py;
85
131
  }
86
132
  uvs[index * 2] = u;
87
133
  uvs[index * 2 + 1] = v;
@@ -98,9 +144,104 @@ function getStyleDefaults(style) {
98
144
  }
99
145
  return { spacingFactor: 0.3, postFactor: 0.55, baseFactor: 1, topFactor: 0.75 };
100
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
+ }
101
237
  function createFenceParts(fence) {
102
- const parts = [];
103
- const length = Math.max(getWallCurveLength(fence), 0.01);
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);
104
245
  const panelDepth = Math.max(fence.thickness, 0.03);
105
246
  const clearance = Math.max(fence.groundClearance, 0);
106
247
  const styleDefaults = getStyleDefaults(fence.style);
@@ -117,34 +258,42 @@ function createFenceParts(fence) {
117
258
  const startInsetT = Math.min(0.499, edgeInset / length);
118
259
  const endInsetT = Math.max(0.501, 1 - edgeInset / length);
119
260
  if (!isFloating) {
120
- parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
121
- parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
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));
122
263
  }
123
264
  const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2;
124
265
  const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
125
266
  for (let index = 0; index < count; index += 1) {
126
267
  const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
127
- const frame = getFencePointAt(fence, t);
128
268
  const isEdgePost = index === 0 || index === count - 1;
129
269
  const fullHeightPost = !showInfill || (isFloating && isEdgePost);
130
270
  const postHeight = fullHeightPost
131
271
  ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
132
272
  : verticalHeight;
133
273
  const postY = fullHeightPost ? postHeight / 2 : verticalY;
134
- parts.push({
135
- position: [frame.point.x, postY, frame.point.y],
136
- rotationY: -frame.tangentAngle,
137
- scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
138
- });
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
+ }
139
284
  }
140
- parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
285
+ rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
141
286
  if (isFloating) {
142
- parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
287
+ rail.push(...createFenceCurveBlockParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
143
288
  }
144
- return parts;
289
+ return { posts, infill, base, rail };
145
290
  }
146
- export function generateFenceGeometry(fence) {
147
- const parts = createFenceParts(fence);
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();
148
297
  const geometries = parts.map(createFencePartGeometry);
149
298
  const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
150
299
  geometries.forEach((geometry) => {
@@ -157,9 +306,28 @@ export function generateFenceGeometry(fence) {
157
306
  merged.computeVertexNormals();
158
307
  return merged;
159
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
+ }
160
328
  function updateFenceGeometry(fenceId) {
161
329
  const node = useScene.getState().nodes[fenceId];
162
- if (!node || node.type !== 'fence')
330
+ if (node?.type !== 'fence')
163
331
  return;
164
332
  const mesh = sceneRegistry.nodes.get(fenceId);
165
333
  if (!mesh)
@@ -179,7 +347,7 @@ export const FenceSystem = () => {
179
347
  const nodes = useScene.getState().nodes;
180
348
  dirtyNodes.forEach((id) => {
181
349
  const node = nodes[id];
182
- if (!node || node.type !== 'fence')
350
+ if (node?.type !== 'fence')
183
351
  return;
184
352
  updateFenceGeometry(id);
185
353
  clearDirty(id);
@@ -1 +1 @@
1
- {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YA6ChC,CAAA"}
1
+ {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAyCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YAoEhC,CAAA"}
@@ -1,7 +1,6 @@
1
- import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
- function withLiveTransform(node, id) {
4
- const liveTransform = useLiveTransforms.getState().get(id);
3
+ function withLiveTransform(node, liveTransform) {
5
4
  if (!liveTransform)
6
5
  return node;
7
6
  const currentRotation = node.rotation;
@@ -38,10 +37,18 @@ export const FloorElevationSystem = () => {
38
37
  const dirtyNodes = useScene((s) => s.dirtyNodes);
39
38
  const clearDirty = useScene((s) => s.clearDirty);
40
39
  useFrame(() => {
41
- if (dirtyNodes.size === 0)
40
+ // Nodes with a live preview (override / transform) are reapplied EVERY
41
+ // frame, not only while dirty: the React commit that rebinds the group's
42
+ // base-Y position can land between frames, after the dirty mark was
43
+ // already consumed by the priority-2 systems — without this the lift
44
+ // vanishes until the next pointer tick re-dirties (visible Y blink
45
+ // during group drags over elevated slabs).
46
+ const overrides = useLiveNodeOverrides.getState().overrides;
47
+ const transforms = useLiveTransforms.getState().transforms;
48
+ if (dirtyNodes.size === 0 && overrides.size === 0 && transforms.size === 0)
42
49
  return;
43
50
  const nodes = useScene.getState().nodes;
44
- dirtyNodes.forEach((id) => {
51
+ const applyLift = (id) => {
45
52
  const node = nodes[id];
46
53
  if (!node)
47
54
  return;
@@ -52,7 +59,8 @@ export const FloorElevationSystem = () => {
52
59
  const mesh = sceneRegistry.nodes.get(id);
53
60
  if (!mesh)
54
61
  return;
55
- const effectiveNode = withLiveTransform(getEffectiveNode(node), id);
62
+ const liveTransform = useLiveTransforms.getState().get(id);
63
+ const effectiveNode = withLiveTransform(getEffectiveNode(node), liveTransform);
56
64
  const position = effectiveNode.position;
57
65
  if (!position)
58
66
  return;
@@ -67,11 +75,26 @@ export const FloorElevationSystem = () => {
67
75
  node: effectiveNode,
68
76
  nodes: resolverNodes,
69
77
  position,
78
+ // 3D drags publish the pointer-decided surface cap with their live
79
+ // transform; honoring it here keeps this system's per-frame Y in
80
+ // agreement with the tool's preview (no deck/floor flicker).
81
+ maxElevation: liveTransform?.supportElevationCap,
70
82
  });
71
83
  mesh.position.y = visualPosition[1];
72
- if (!(def.geometry || def.system)) {
84
+ if (!(def.geometry || def.system) && dirtyNodes.has(id)) {
73
85
  clearDirty(id);
74
86
  }
87
+ };
88
+ dirtyNodes.forEach((id) => {
89
+ applyLift(id);
90
+ });
91
+ overrides.forEach((_values, id) => {
92
+ if (!dirtyNodes.has(id))
93
+ applyLift(id);
94
+ });
95
+ transforms.forEach((_transform, id) => {
96
+ if (!dirtyNodes.has(id) && !overrides.has(id))
97
+ applyLift(id);
75
98
  });
76
99
  }, 1);
77
100
  return null;
@@ -1,3 +1,4 @@
1
+ import { type Group, type Object3D } from 'three';
1
2
  /**
2
3
  * Generic geometry system.
3
4
  *
@@ -31,4 +32,10 @@
31
32
  */
32
33
  export declare const GeometrySystem: () => null;
33
34
  export default GeometrySystem;
35
+ export type GeometryBuildCacheEntry = {
36
+ group: Group;
37
+ key: string;
38
+ };
39
+ export declare function markGeometryBuildOutput(child: Object3D): void;
40
+ export declare function shouldReuseGeometryBuild(cache: Map<string, GeometryBuildCacheEntry>, id: string, group: Group, key: string): boolean;
34
41
  //# sourceMappingURL=geometry-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YAwK1B,CAAA;AAyHD,eAAe,cAAc,CAAA"}
1
+ {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAQtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YA8L1B,CAAA;AAmJD,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}