@pascal-app/core 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/events/bus.d.ts +37 -3
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
  5. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
  7. package/dist/index.d.ts +4 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -1
  10. package/dist/lib/polygon-geometry.d.ts +3 -0
  11. package/dist/lib/polygon-geometry.d.ts.map +1 -0
  12. package/dist/lib/polygon-geometry.js +90 -0
  13. package/dist/lib/space-detection.d.ts +10 -17
  14. package/dist/lib/space-detection.d.ts.map +1 -1
  15. package/dist/lib/space-detection.js +666 -453
  16. package/dist/material-library.d.ts +18 -0
  17. package/dist/material-library.d.ts.map +1 -0
  18. package/dist/material-library.js +603 -0
  19. package/dist/schema/index.d.ts +9 -4
  20. package/dist/schema/index.d.ts.map +1 -1
  21. package/dist/schema/index.js +5 -4
  22. package/dist/schema/material.d.ts +109 -0
  23. package/dist/schema/material.d.ts.map +1 -1
  24. package/dist/schema/material.js +52 -0
  25. package/dist/schema/nodes/ceiling.d.ts +10 -0
  26. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  27. package/dist/schema/nodes/ceiling.js +6 -0
  28. package/dist/schema/nodes/door.d.ts +1 -0
  29. package/dist/schema/nodes/door.d.ts.map +1 -1
  30. package/dist/schema/nodes/fence.d.ts +34 -0
  31. package/dist/schema/nodes/fence.d.ts.map +1 -1
  32. package/dist/schema/nodes/fence.js +5 -0
  33. package/dist/schema/nodes/item.d.ts +2 -2
  34. package/dist/schema/nodes/roof-segment.d.ts +2 -0
  35. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  36. package/dist/schema/nodes/roof-segment.js +1 -0
  37. package/dist/schema/nodes/roof.d.ts +108 -0
  38. package/dist/schema/nodes/roof.d.ts.map +1 -1
  39. package/dist/schema/nodes/roof.js +58 -2
  40. package/dist/schema/nodes/site.d.ts +1 -1
  41. package/dist/schema/nodes/slab.d.ts +10 -0
  42. package/dist/schema/nodes/slab.d.ts.map +1 -1
  43. package/dist/schema/nodes/slab.js +7 -0
  44. package/dist/schema/nodes/stair-segment.d.ts +2 -0
  45. package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
  46. package/dist/schema/nodes/stair-segment.js +1 -0
  47. package/dist/schema/nodes/stair.d.ts +122 -2
  48. package/dist/schema/nodes/stair.d.ts.map +1 -1
  49. package/dist/schema/nodes/stair.js +72 -2
  50. package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
  51. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
  52. package/dist/schema/nodes/surface-hole-metadata.js +5 -0
  53. package/dist/schema/nodes/wall.d.ts +87 -1
  54. package/dist/schema/nodes/wall.d.ts.map +1 -1
  55. package/dist/schema/nodes/wall.js +45 -4
  56. package/dist/schema/nodes/window.d.ts +1 -0
  57. package/dist/schema/nodes/window.d.ts.map +1 -1
  58. package/dist/schema/types.d.ts +343 -5
  59. package/dist/schema/types.d.ts.map +1 -1
  60. package/dist/store/actions/node-actions.d.ts +1 -1
  61. package/dist/store/actions/node-actions.d.ts.map +1 -1
  62. package/dist/store/actions/node-actions.js +175 -0
  63. package/dist/store/history-control.d.ts +14 -0
  64. package/dist/store/history-control.d.ts.map +1 -0
  65. package/dist/store/history-control.js +22 -0
  66. package/dist/store/use-scene.d.ts.map +1 -1
  67. package/dist/store/use-scene.js +248 -2
  68. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  69. package/dist/systems/ceiling/ceiling-system.js +7 -0
  70. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  71. package/dist/systems/fence/fence-system.js +106 -39
  72. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  73. package/dist/systems/roof/roof-system.js +31 -1
  74. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  75. package/dist/systems/slab/slab-system.js +45 -8
  76. package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
  77. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
  78. package/dist/systems/stair/stair-opening-sync.js +515 -0
  79. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  80. package/dist/systems/stair/stair-system.js +119 -2
  81. package/dist/systems/wall/wall-curve.d.ts +43 -0
  82. package/dist/systems/wall/wall-curve.d.ts.map +1 -0
  83. package/dist/systems/wall/wall-curve.js +176 -0
  84. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  85. package/dist/systems/wall/wall-footprint.js +16 -2
  86. package/dist/systems/wall/wall-mitering.d.ts +7 -0
  87. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  88. package/dist/systems/wall/wall-mitering.js +76 -3
  89. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  90. package/dist/systems/wall/wall-system.js +202 -2
  91. package/package.json +3 -3
@@ -3,6 +3,94 @@ import * as THREE from 'three';
3
3
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
4
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
5
5
  import useScene from '../../store/use-scene';
6
+ import { getWallCurveFrameAt, getWallCurveLength } from '../wall/wall-curve';
7
+ const MIN_CURVE_SEGMENT_LENGTH = 0.18;
8
+ function createFencePartGeometry(part) {
9
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
10
+ geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
11
+ if (part.rotationY) {
12
+ geometry.rotateY(part.rotationY);
13
+ }
14
+ geometry.translate(part.position[0], part.position[1], part.position[2]);
15
+ applyFenceUVs(geometry);
16
+ return geometry;
17
+ }
18
+ function getFencePointAt(fence, t) {
19
+ const frame = getWallCurveFrameAt(fence, t);
20
+ return {
21
+ point: frame.point,
22
+ tangentAngle: Math.atan2(frame.tangent.y, frame.tangent.x),
23
+ };
24
+ }
25
+ function createStraightFenceSpanPart(start, end, centerY, height, depth) {
26
+ const dx = end[0] - start[0];
27
+ const dz = end[1] - start[1];
28
+ const length = Math.hypot(dx, dz);
29
+ if (length <= 1e-4) {
30
+ return null;
31
+ }
32
+ return {
33
+ position: [(start[0] + end[0]) / 2, centerY, (start[1] + end[1]) / 2],
34
+ rotationY: -Math.atan2(dz, dx),
35
+ scale: [length, height, depth],
36
+ };
37
+ }
38
+ function createFenceCurveSpanParts(fence, startT, endT, centerY, height, depth) {
39
+ const parts = [];
40
+ const frameCount = Math.max(1, Math.ceil((getWallCurveLength(fence) * Math.max(1e-4, endT - startT)) / MIN_CURVE_SEGMENT_LENGTH));
41
+ let previous = getFencePointAt(fence, startT);
42
+ for (let index = 1; index <= frameCount; index += 1) {
43
+ const t = startT + (endT - startT) * (index / frameCount);
44
+ const current = getFencePointAt(fence, t);
45
+ const segment = createStraightFenceSpanPart([previous.point.x, previous.point.y], [current.point.x, current.point.y], centerY, height, depth);
46
+ if (segment) {
47
+ parts.push(segment);
48
+ }
49
+ previous = current;
50
+ }
51
+ return parts;
52
+ }
53
+ function applyFenceUVs(geometry) {
54
+ const position = geometry.getAttribute('position');
55
+ const normal = geometry.getAttribute('normal');
56
+ if (!(position && normal))
57
+ return;
58
+ const uvs = new Float32Array(position.count * 2);
59
+ let minX = Number.POSITIVE_INFINITY;
60
+ let minY = Number.POSITIVE_INFINITY;
61
+ let minZ = Number.POSITIVE_INFINITY;
62
+ for (let index = 0; index < position.count; index += 1) {
63
+ minX = Math.min(minX, position.getX(index));
64
+ minY = Math.min(minY, position.getY(index));
65
+ minZ = Math.min(minZ, position.getZ(index));
66
+ }
67
+ for (let index = 0; index < position.count; index += 1) {
68
+ const px = position.getX(index);
69
+ const py = position.getY(index);
70
+ const pz = position.getZ(index);
71
+ const nx = Math.abs(normal.getX(index));
72
+ const ny = Math.abs(normal.getY(index));
73
+ const nz = Math.abs(normal.getZ(index));
74
+ let u = 0;
75
+ let v = 0;
76
+ if (ny >= nx && ny >= nz) {
77
+ u = px - minX;
78
+ v = pz - minZ;
79
+ }
80
+ else if (nx >= nz) {
81
+ u = pz - minZ;
82
+ v = py - minY;
83
+ }
84
+ else {
85
+ u = px - minX;
86
+ v = py - minY;
87
+ }
88
+ uvs[index * 2] = u;
89
+ uvs[index * 2 + 1] = v;
90
+ }
91
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
92
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
93
+ }
6
94
  function getStyleDefaults(style) {
7
95
  if (style === 'privacy') {
8
96
  return { spacingFactor: 0.42, postFactor: 1.35, baseFactor: 1.2, topFactor: 1.2 };
@@ -14,7 +102,7 @@ function getStyleDefaults(style) {
14
102
  }
15
103
  function createFenceParts(fence) {
16
104
  const parts = [];
17
- const length = Math.max(Math.hypot(fence.end[0] - fence.start[0], fence.end[1] - fence.start[1]), 0.01);
105
+ const length = Math.max(getWallCurveLength(fence), 0.01);
18
106
  const panelDepth = Math.max(fence.thickness, 0.03);
19
107
  const clearance = Math.max(fence.groundClearance, 0);
20
108
  const styleDefaults = getStyleDefaults(fence.style);
@@ -27,61 +115,43 @@ function createFenceParts(fence) {
27
115
  const isFloating = fence.baseStyle === 'floating';
28
116
  const baseY = isFloating ? clearance : 0;
29
117
  const effectiveBaseHeight = baseHeight;
118
+ const startInsetT = Math.min(0.499, edgeInset / length);
119
+ const endInsetT = Math.max(0.501, 1 - edgeInset / length);
30
120
  if (!isFloating) {
31
- parts.push({
32
- position: [0, baseY + effectiveBaseHeight / 2, 0],
33
- scale: [length, effectiveBaseHeight, panelDepth * 1.05],
34
- });
35
- parts.push({
36
- position: [0, baseY + effectiveBaseHeight + verticalHeight * 0.15, 0],
37
- scale: [length, topRailHeight * 0.8, panelDepth * 0.35],
38
- });
121
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
122
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
39
123
  }
40
124
  const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1);
41
- const step = count > 1 ? (length - edgeInset * 2) / (count - 1) : 0;
42
- const startX = -length / 2 + edgeInset;
43
125
  const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
44
126
  for (let index = 0; index < count; index += 1) {
45
- const x = count === 1 ? 0 : startX + step * index;
46
- let posX = x;
127
+ const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
128
+ const frame = getFencePointAt(fence, t);
47
129
  const isEdgePost = index === 0 || index === count - 1;
48
- if (count > 1) {
49
- if (index === 0)
50
- posX = -length / 2 + edgeInset + postWidth / 2;
51
- else if (index === count - 1)
52
- posX = length / 2 - edgeInset - postWidth / 2;
53
- }
54
130
  const postHeight = isFloating && isEdgePost
55
131
  ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
56
132
  : verticalHeight;
57
133
  const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY;
58
134
  parts.push({
59
- position: [posX, postY, 0],
135
+ position: [frame.point.x, postY, frame.point.y],
136
+ rotationY: -frame.tangentAngle,
60
137
  scale: [postWidth, postHeight, Math.max(panelDepth * 0.35, 0.012)],
61
138
  });
62
139
  }
63
- parts.push({
64
- position: [0, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, 0],
65
- scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)],
66
- });
140
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
67
141
  if (isFloating) {
68
- parts.push({
69
- position: [0, baseY + effectiveBaseHeight + topRailHeight / 2, 0],
70
- scale: [length, topRailHeight, Math.max(panelDepth * 0.55, 0.018)],
71
- });
142
+ parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + topRailHeight / 2, topRailHeight, Math.max(panelDepth * 0.55, 0.018)));
72
143
  }
73
144
  return parts;
74
145
  }
75
146
  function generateFenceGeometry(fence) {
76
147
  const parts = createFenceParts(fence);
77
- const geometries = parts.map((part) => {
78
- const geometry = new THREE.BoxGeometry(1, 1, 1);
79
- geometry.scale(part.scale[0], part.scale[1], part.scale[2]);
80
- geometry.translate(part.position[0], part.position[1], part.position[2]);
81
- return geometry;
82
- });
148
+ const geometries = parts.map(createFencePartGeometry);
83
149
  const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
84
150
  geometries.forEach((geometry) => geometry.dispose());
151
+ const mergedUv = merged.getAttribute('uv');
152
+ if (mergedUv) {
153
+ merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
154
+ }
85
155
  merged.computeVertexNormals();
86
156
  return merged;
87
157
  }
@@ -95,11 +165,8 @@ function updateFenceGeometry(fenceId) {
95
165
  const newGeometry = generateFenceGeometry(node);
96
166
  mesh.geometry.dispose();
97
167
  mesh.geometry = newGeometry;
98
- const centerX = (node.start[0] + node.end[0]) / 2;
99
- const centerZ = (node.start[1] + node.end[1]) / 2;
100
- const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
101
- mesh.position.set(centerX, 0, centerZ);
102
- mesh.rotation.set(0, -angle, 0);
168
+ mesh.position.set(0, 0, 0);
169
+ mesh.rotation.set(0, 0, 0);
103
170
  }
104
171
  export const FenceSystem = () => {
105
172
  const dirtyNodes = useScene((state) => state.dirtyNodes);
@@ -1 +1 @@
1
- {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AA+BjF,eAAO,MAAM,UAAU,YAuFtB,CAAA;AAuLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAsRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAoDvF"}
1
+ {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAGvE,OAAO,KAAK,EAAgC,eAAe,EAAE,MAAM,cAAc,CAAA;AAgCjF,eAAO,MAAM,UAAU,YAuFtB,CAAA;AAwLD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,KAAK,CAAA;CAAE,GAAG,IAAI,CAsRlF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,eAAe,GAAG,KAAK,CAAC,cAAc,CAqDvF"}
@@ -8,7 +8,7 @@ import useScene from '../../store/use-scene';
8
8
  const csgEvaluator = new Evaluator();
9
9
  csgEvaluator.useGroups = true;
10
10
  csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
11
- csgEvaluator.attributes = ['position', 'normal'];
11
+ csgEvaluator.attributes = ['position', 'normal', 'uv'];
12
12
  function prepareBrushForCSG(brush) {
13
13
  brush.geometry.computeBoundsTree = computeBoundsTree;
14
14
  brush.geometry.computeBoundsTree({ maxLeafSize: 10 });
@@ -20,6 +20,7 @@ const _position = new THREE.Vector3();
20
20
  const _quaternion = new THREE.Quaternion();
21
21
  const _scale = new THREE.Vector3(1, 1, 1);
22
22
  const _yAxis = new THREE.Vector3(0, 1, 0);
23
+ const _uvFaceNormal = new THREE.Vector3();
23
24
  // Pending merged-roof updates carried across frames (for throttling)
24
25
  const pendingRoofUpdates = new Set();
25
26
  const MAX_ROOFS_PER_FRAME = 1;
@@ -217,6 +218,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
217
218
  for (const g of resultGeo.groups) {
218
219
  g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
219
220
  }
221
+ ensureUv2Attribute(resultGeo);
220
222
  resultGeo.computeVertexNormals();
221
223
  mergedMesh.geometry.dispose();
222
224
  mergedMesh.geometry = resultGeo;
@@ -500,6 +502,7 @@ export function generateRoofSegmentGeometry(node) {
500
502
  shinSlab.geometry.dispose();
501
503
  wallBrush.geometry.dispose();
502
504
  innerBrush.geometry.dispose();
505
+ ensureUv2Attribute(resultGeo);
503
506
  resultGeo.computeVertexNormals();
504
507
  return resultGeo;
505
508
  }
@@ -709,6 +712,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
709
712
  function createGeometryFromFaces(faces, matRule = null) {
710
713
  const positions = [];
711
714
  const normals = [];
715
+ const uvs = [];
712
716
  const indices = [];
713
717
  const groups = [];
714
718
  let vertexCount = 0;
@@ -743,6 +747,9 @@ function createGeometryFromFaces(faces, matRule = null) {
743
747
  normals.push(normal.x, normal.y, normal.z);
744
748
  normals.push(normal.x, normal.y, normal.z);
745
749
  normals.push(normal.x, normal.y, normal.z);
750
+ pushRoofUv(uvs, p0, normal);
751
+ pushRoofUv(uvs, fi, normal);
752
+ pushRoofUv(uvs, fi1, normal);
746
753
  indices.push(vertexCount, vertexCount + 1, vertexCount + 2);
747
754
  faceVertexCount += 3;
748
755
  vertexCount += 3;
@@ -756,6 +763,7 @@ function createGeometryFromFaces(faces, matRule = null) {
756
763
  const geometry = new THREE.BufferGeometry();
757
764
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
758
765
  geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
766
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
759
767
  geometry.setIndex(indices);
760
768
  for (const g of groups) {
761
769
  geometry.addGroup(g.start, g.count, g.materialIndex);
@@ -763,5 +771,27 @@ function createGeometryFromFaces(faces, matRule = null) {
763
771
  // Merge identical vertices to optimize geometry for CSG and create clean topology
764
772
  const mergedGeo = mergeVertices(geometry, 1e-4);
765
773
  geometry.dispose();
774
+ ensureUv2Attribute(mergedGeo);
766
775
  return mergedGeo;
767
776
  }
777
+ function pushRoofUv(uvs, point, normal) {
778
+ _uvFaceNormal.copy(normal).normalize();
779
+ const absX = Math.abs(_uvFaceNormal.x);
780
+ const absY = Math.abs(_uvFaceNormal.y);
781
+ const absZ = Math.abs(_uvFaceNormal.z);
782
+ if (absY >= absX && absY >= absZ) {
783
+ uvs.push(point.x, point.z);
784
+ return;
785
+ }
786
+ if (absX >= absZ) {
787
+ uvs.push(point.z, point.y);
788
+ return;
789
+ }
790
+ uvs.push(point.x, point.y);
791
+ }
792
+ function ensureUv2Attribute(geometry) {
793
+ const uv = geometry.getAttribute('uv');
794
+ if (!uv)
795
+ return;
796
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
797
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAuED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
1
+ {"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAG9B,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAcvD,eAAO,MAAM,UAAU,YAwBtB,CAAA;AAmFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
@@ -1,7 +1,14 @@
1
1
  import { useFrame } from '@react-three/fiber';
2
2
  import * as THREE from 'three';
3
3
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
4
+ import { insetPolygonFromCentroid, simplifyClosedPolygon } from '../../lib/polygon-geometry';
4
5
  import useScene from '../../store/use-scene';
6
+ function ensureUv2Attribute(geometry) {
7
+ const uv = geometry.getAttribute('uv');
8
+ if (!uv)
9
+ return;
10
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
11
+ }
5
12
  // ============================================================================
6
13
  // SLAB SYSTEM
7
14
  // ============================================================================
@@ -32,6 +39,7 @@ export const SlabSystem = () => {
32
39
  */
33
40
  function updateSlabGeometry(node, mesh) {
34
41
  const newGeo = generateSlabGeometry(node);
42
+ ensureUv2Attribute(newGeo);
35
43
  mesh.geometry.dispose();
36
44
  mesh.geometry = newGeo;
37
45
  // For negative elevation, shift the mesh down so the top face sits at Y=elevation
@@ -41,6 +49,13 @@ function updateSlabGeometry(node, mesh) {
41
49
  }
42
50
  /** Half of default wall thickness — used to extend slab geometry under walls */
43
51
  const SLAB_OUTSET = 0.05;
52
+ const AUTO_SLAB_INSET = 0.02;
53
+ const AUTO_SLAB_SIMPLIFY_TOLERANCE = 0.08;
54
+ function getRenderableSlabPolygon(slabNode) {
55
+ return slabNode.autoFromWalls
56
+ ? simplifyClosedPolygon(insetPolygonFromCentroid(slabNode.polygon, AUTO_SLAB_INSET), AUTO_SLAB_SIMPLIFY_TOLERANCE)
57
+ : outsetPolygon(slabNode.polygon, SLAB_OUTSET);
58
+ }
44
59
  /**
45
60
  * Expand a polygon outward by a uniform distance.
46
61
  * Offsets each edge outward then intersects consecutive offset edges.
@@ -100,7 +115,7 @@ export function generateSlabGeometry(slabNode) {
100
115
  * Standard slab: flat extrusion upward from Y=0 by elevation thickness.
101
116
  */
102
117
  function generatePositiveSlabGeometry(slabNode) {
103
- const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET);
118
+ const polygon = getRenderableSlabPolygon(slabNode);
104
119
  const elevation = slabNode.elevation ?? 0.05;
105
120
  if (polygon.length < 3)
106
121
  return new THREE.BufferGeometry();
@@ -134,21 +149,41 @@ function generatePositiveSlabGeometry(slabNode) {
134
149
  * - walls from Y=0 to Y=depth, inward-facing normals (visible from inside pool)
135
150
  */
136
151
  function generatePoolGeometry(slabNode) {
137
- const polygon = outsetPolygon(slabNode.polygon, SLAB_OUTSET);
152
+ const polygon = getRenderableSlabPolygon(slabNode);
138
153
  const depth = Math.abs(slabNode.elevation ?? 0.05);
139
154
  if (polygon.length < 3)
140
155
  return new THREE.BufferGeometry();
141
156
  const positions = [];
157
+ const uvs = [];
142
158
  const indices = [];
143
159
  const n = polygon.length;
160
+ const bounds = new THREE.Box2();
161
+ for (const [x, z] of polygon) {
162
+ bounds.expandByPoint(new THREE.Vector2(x, z));
163
+ }
164
+ for (const hole of slabNode.holes ?? []) {
165
+ for (const [x, z] of hole) {
166
+ bounds.expandByPoint(new THREE.Vector2(x, z));
167
+ }
168
+ }
169
+ const floorWidth = Math.max(bounds.max.x - bounds.min.x, 0.001);
170
+ const floorHeight = Math.max(bounds.max.y - bounds.min.y, 0.001);
171
+ const pushFloorVertex = (x, y, z) => {
172
+ positions.push(x, y, z);
173
+ uvs.push((x - bounds.min.x) / floorWidth, (z - bounds.min.y) / floorHeight);
174
+ };
175
+ const pushWallVertex = (x, y, z, u, v) => {
176
+ positions.push(x, y, z);
177
+ uvs.push(u, v);
178
+ };
144
179
  // --- Floor at Y=0 ---
145
180
  for (const [x, z] of polygon)
146
- positions.push(x, 0, z);
181
+ pushFloorVertex(x, 0, z);
147
182
  const pts2d = polygon.map(([x, z]) => new THREE.Vector2(x, z));
148
183
  const holesPts2d = (slabNode.holes ?? []).map((h) => h.map(([x, z]) => new THREE.Vector2(x, z)));
149
184
  for (const hole of slabNode.holes ?? []) {
150
185
  for (const [x, z] of hole)
151
- positions.push(x, 0, z);
186
+ pushFloorVertex(x, 0, z);
152
187
  }
153
188
  const floorTris = THREE.ShapeUtils.triangulateShape(pts2d, holesPts2d);
154
189
  for (const tri of floorTris) {
@@ -162,15 +197,17 @@ function generatePoolGeometry(slabNode) {
162
197
  const [x0, z0] = polygon[i];
163
198
  const [x1, z1] = polygon[j];
164
199
  const vBase = positions.length / 3;
165
- positions.push(x0, 0, z0); // v0 — floor level
166
- positions.push(x1, 0, z1); // v1 — floor level
167
- positions.push(x1, depth, z1); // v2ground level
168
- positions.push(x0, depth, z0); // v3 — ground level
200
+ const segmentLength = Math.max(Math.hypot(x1 - x0, z1 - z0), 0.001);
201
+ pushWallVertex(x0, 0, z0, 0, 0); // v0 — floor level
202
+ pushWallVertex(x1, 0, z1, segmentLength, 0); // v1floor level
203
+ pushWallVertex(x1, depth, z1, segmentLength, depth); // v2 — ground level
204
+ pushWallVertex(x0, depth, z0, 0, depth); // v3 — ground level
169
205
  indices.push(vBase, vBase + 1, vBase + 2);
170
206
  indices.push(vBase, vBase + 2, vBase + 3);
171
207
  }
172
208
  const geo = new THREE.BufferGeometry();
173
209
  geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
210
+ geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
174
211
  geo.setIndex(indices);
175
212
  geo.computeVertexNormals();
176
213
  return geo;
@@ -0,0 +1,6 @@
1
+ import type { AnyNode, AnyNodeId, CeilingNode, SlabNode } from '../../schema';
2
+ export declare function syncAutoStairOpenings(nodes: Record<string, AnyNode>): {
3
+ id: AnyNodeId;
4
+ data: Partial<SlabNode | CeilingNode>;
5
+ }[];
6
+ //# sourceMappingURL=stair-opening-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stair-opening-sync.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-opening-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAa,QAAQ,EAA+B,MAAM,cAAc,CAAA;AAulBrH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;QAIvC,SAAS;UAAQ,OAAO,CAAC,QAAQ,GAAG,WAAW,CAAC;IA6F5E"}