@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
@@ -1,13 +1,19 @@
1
1
  import { useFrame } from '@react-three/fiber';
2
+ import { useEffect, useRef } from 'react';
2
3
  import * as THREE from 'three';
3
4
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
5
  import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
5
6
  import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
6
7
  import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
7
8
  import useScene from '../../store/use-scene';
9
+ import { syncAutoStairOpenings } from './stair-opening-sync';
8
10
  const pendingStairUpdates = new Set();
9
11
  const MAX_STAIRS_PER_FRAME = 2;
10
12
  const MAX_SEGMENTS_PER_FRAME = 4;
13
+ const STAIR_TREAD_MATERIAL_INDEX = 0;
14
+ const STAIR_SIDE_MATERIAL_INDEX = 1;
15
+ const _uvPosition = new THREE.Vector3();
16
+ const _uvNormal = new THREE.Vector3();
11
17
  // ============================================================================
12
18
  // STAIR SYSTEM
13
19
  // ============================================================================
@@ -15,6 +21,26 @@ export const StairSystem = () => {
15
21
  const dirtyNodes = useScene((state) => state.dirtyNodes);
16
22
  const clearDirty = useScene((state) => state.clearDirty);
17
23
  const rootNodeIds = useScene((state) => state.rootNodeIds);
24
+ const syncingAutoOpeningsRef = useRef(false);
25
+ useEffect(() => {
26
+ const applyUpdates = (updates) => {
27
+ if (updates.length === 0)
28
+ return;
29
+ syncingAutoOpeningsRef.current = true;
30
+ useScene.getState().updateNodes(updates);
31
+ queueMicrotask(() => {
32
+ syncingAutoOpeningsRef.current = false;
33
+ });
34
+ };
35
+ applyUpdates(syncAutoStairOpenings(useScene.getState().nodes));
36
+ return useScene.subscribe((state, prevState) => {
37
+ if (syncingAutoOpeningsRef.current)
38
+ return;
39
+ if (state.nodes === prevState.nodes)
40
+ return;
41
+ applyUpdates(syncAutoStairOpenings(state.nodes));
42
+ });
43
+ }, []);
18
44
  useFrame(() => {
19
45
  if (rootNodeIds.length === 0) {
20
46
  pendingStairUpdates.clear();
@@ -159,7 +185,7 @@ function generateStairSegmentGeometry(segment, absoluteHeight) {
159
185
  }
160
186
  }
161
187
  shape.lineTo(0, 0);
162
- const geometry = new THREE.ExtrudeGeometry(shape, {
188
+ const extrudedGeometry = new THREE.ExtrudeGeometry(shape, {
163
189
  steps: 1,
164
190
  depth: width,
165
191
  bevelEnabled: false,
@@ -169,13 +195,21 @@ function generateStairSegmentGeometry(segment, absoluteHeight) {
169
195
  const matrix = new THREE.Matrix4();
170
196
  matrix.makeRotationY(-Math.PI / 2);
171
197
  matrix.setPosition(width / 2, 0, 0);
172
- geometry.applyMatrix4(matrix);
198
+ extrudedGeometry.applyMatrix4(matrix);
199
+ extrudedGeometry.computeVertexNormals();
200
+ const geometry = extrudedGeometry.toNonIndexed() ?? extrudedGeometry;
201
+ if (geometry !== extrudedGeometry) {
202
+ extrudedGeometry.dispose();
203
+ }
204
+ applyStairSegmentUvs(geometry);
205
+ ensureUv2Attribute(geometry);
173
206
  return geometry;
174
207
  }
175
208
  function updateStairSegmentGeometry(node, mesh) {
176
209
  // Compute absolute height from parent chain
177
210
  const absoluteHeight = computeAbsoluteHeight(node);
178
211
  const newGeometry = generateStairSegmentGeometry(node, absoluteHeight);
212
+ applyStraightStairMaterialGroups(newGeometry);
179
213
  mesh.geometry.dispose();
180
214
  mesh.geometry = newGeometry;
181
215
  // NOTE: position/rotation are NOT set here — they're set by syncSegmentMeshTransforms
@@ -274,12 +308,93 @@ function updateMergedStairGeometry(stairNode, group, nodes) {
274
308
  geometries.push(geo);
275
309
  }
276
310
  const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry();
311
+ applyStraightStairMaterialGroups(merged);
277
312
  replaceMeshGeometry(mergedMesh, merged);
278
313
  // Dispose individual geometries
279
314
  for (const geo of geometries) {
280
315
  geo.dispose();
281
316
  }
282
317
  }
318
+ function applyStraightStairMaterialGroups(geometry) {
319
+ const position = geometry.getAttribute('position');
320
+ if (!position || position.count < 3) {
321
+ geometry.clearGroups();
322
+ return;
323
+ }
324
+ const index = geometry.getIndex();
325
+ const triangleCount = index ? index.count / 3 : position.count / 3;
326
+ if (!Number.isFinite(triangleCount) || triangleCount <= 0) {
327
+ geometry.clearGroups();
328
+ return;
329
+ }
330
+ const triangleMaterials = new Array(triangleCount);
331
+ const v0 = new THREE.Vector3();
332
+ const v1 = new THREE.Vector3();
333
+ const v2 = new THREE.Vector3();
334
+ const edge1 = new THREE.Vector3();
335
+ const edge2 = new THREE.Vector3();
336
+ const normal = new THREE.Vector3();
337
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex++) {
338
+ const vertexOffset = triangleIndex * 3;
339
+ const a = index ? index.getX(vertexOffset) : vertexOffset;
340
+ const b = index ? index.getX(vertexOffset + 1) : vertexOffset + 1;
341
+ const c = index ? index.getX(vertexOffset + 2) : vertexOffset + 2;
342
+ v0.fromBufferAttribute(position, a);
343
+ v1.fromBufferAttribute(position, b);
344
+ v2.fromBufferAttribute(position, c);
345
+ edge1.subVectors(v1, v0);
346
+ edge2.subVectors(v2, v0);
347
+ normal.crossVectors(edge1, edge2);
348
+ triangleMaterials[triangleIndex] =
349
+ normal.lengthSq() > 0 && normal.normalize().y > 0.75
350
+ ? STAIR_TREAD_MATERIAL_INDEX
351
+ : STAIR_SIDE_MATERIAL_INDEX;
352
+ }
353
+ geometry.clearGroups();
354
+ let currentMaterial = triangleMaterials[0];
355
+ let groupStart = 0;
356
+ for (let triangleIndex = 1; triangleIndex < triangleMaterials.length; triangleIndex++) {
357
+ const materialIndex = triangleMaterials[triangleIndex];
358
+ if (materialIndex === currentMaterial)
359
+ continue;
360
+ geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
361
+ groupStart = triangleIndex;
362
+ currentMaterial = materialIndex;
363
+ }
364
+ geometry.addGroup(groupStart * 3, (triangleMaterials.length - groupStart) * 3, currentMaterial ?? STAIR_SIDE_MATERIAL_INDEX);
365
+ }
366
+ function applyStairSegmentUvs(geometry) {
367
+ const position = geometry.getAttribute('position');
368
+ const normal = geometry.getAttribute('normal');
369
+ if (!position || !normal || position.count === 0) {
370
+ geometry.deleteAttribute('uv');
371
+ return;
372
+ }
373
+ const uv = [];
374
+ for (let index = 0; index < position.count; index++) {
375
+ _uvPosition.fromBufferAttribute(position, index);
376
+ _uvNormal.fromBufferAttribute(normal, index).normalize();
377
+ const absX = Math.abs(_uvNormal.x);
378
+ const absY = Math.abs(_uvNormal.y);
379
+ const absZ = Math.abs(_uvNormal.z);
380
+ if (absY >= absX && absY >= absZ) {
381
+ uv.push(_uvPosition.x, _uvPosition.z);
382
+ }
383
+ else if (absX >= absZ) {
384
+ uv.push(_uvPosition.z, _uvPosition.y);
385
+ }
386
+ else {
387
+ uv.push(_uvPosition.x, _uvPosition.y);
388
+ }
389
+ }
390
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
391
+ }
392
+ function ensureUv2Attribute(geometry) {
393
+ const uv = geometry.getAttribute('uv');
394
+ if (!uv)
395
+ return;
396
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
397
+ }
283
398
  /**
284
399
  * Computes world-relative transforms for each segment by chaining
285
400
  * based on attachmentSide. This mirrors the prototype's StairSystem logic.
@@ -334,6 +449,8 @@ function rotateXZ(x, z, angle) {
334
449
  function createEmptyGeometry() {
335
450
  const geometry = new THREE.BufferGeometry();
336
451
  geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
452
+ geometry.addGroup(0, 0, STAIR_TREAD_MATERIAL_INDEX);
453
+ geometry.addGroup(0, 0, STAIR_SIDE_MATERIAL_INDEX);
337
454
  return geometry;
338
455
  }
339
456
  function replaceMeshGeometry(mesh, geometry) {
@@ -0,0 +1,43 @@
1
+ import type { Point2D } from './wall-mitering';
2
+ import type { FenceNode, WallNode } from '../../schema';
3
+ type WallCurveLike = Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset'>;
4
+ type CurveFrame = {
5
+ point: Point2D;
6
+ tangent: Point2D;
7
+ normal: Point2D;
8
+ };
9
+ type WallSurfaceMiterOverrides = {
10
+ startLeft?: Point2D;
11
+ startRight?: Point2D;
12
+ endLeft?: Point2D;
13
+ endRight?: Point2D;
14
+ };
15
+ export declare function getWallStartPoint(wall: WallCurveLike): Point2D;
16
+ export declare function getWallEndPoint(wall: WallCurveLike): Point2D;
17
+ export declare function getWallChordLength(wall: WallCurveLike): number;
18
+ export declare function getMaxWallCurveOffset(wall: WallCurveLike): number;
19
+ export declare function getWallStraightSnapOffset(wall: WallCurveLike): number;
20
+ export declare function normalizeWallCurveOffset(wall: WallCurveLike, offset: number): number;
21
+ export declare function getClampedWallCurveOffset(wall: WallCurveLike): number;
22
+ export declare function isCurvedWall(wall: WallCurveLike): boolean;
23
+ export declare function getWallChordFrame(wall: WallCurveLike): {
24
+ start: Point2D;
25
+ end: Point2D;
26
+ midpoint: Point2D;
27
+ tangent: {
28
+ x: number;
29
+ y: number;
30
+ };
31
+ normal: {
32
+ x: number;
33
+ y: number;
34
+ };
35
+ length: number;
36
+ };
37
+ export declare function getWallCurveFrameAt(wall: WallCurveLike, t: number): CurveFrame;
38
+ export declare function getWallMidpointHandlePoint(wall: WallCurveLike): Point2D;
39
+ export declare function sampleWallCenterline(wall: WallCurveLike, segments?: number): Point2D[];
40
+ export declare function getWallCurveLength(wall: WallCurveLike, segments?: number): number;
41
+ export declare function getWallSurfacePolygon(wall: Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset' | 'thickness'>, segments?: number, miterOverrides?: WallSurfaceMiterOverrides): Point2D[];
42
+ export {};
43
+ //# sourceMappingURL=wall-curve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wall-curve.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-curve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKvD,KAAK,aAAa,GAAG,IAAI,CAAC,QAAQ,GAAG,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAC,CAAA;AAEhF,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,KAAK,yBAAyB,GAAG;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAcD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE5D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,UAErD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,UAExD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,aAAa,UAE5D;AAWD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,UAG3E;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,aAAa,UAI5D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,WAE/C;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa;;;;;;;;;;;;;EA6BpD;AA+BD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAwC9E;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,WAE7D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAA0B,aAG3F;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAA0B,UASzF;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAC,EAC/E,QAAQ,SAA0B,EAClC,cAAc,CAAC,EAAE,yBAAyB,aA2B3C"}
@@ -0,0 +1,176 @@
1
+ const CURVE_EPSILON = 1e-6;
2
+ const DEFAULT_SAMPLE_SEGMENTS = 24;
3
+ function clamp01(value) {
4
+ return Math.max(0, Math.min(1, value));
5
+ }
6
+ function lerp(a, b, t) {
7
+ return a + (b - a) * t;
8
+ }
9
+ function distance(a, b) {
10
+ return Math.hypot(b.x - a.x, b.y - a.y);
11
+ }
12
+ export function getWallStartPoint(wall) {
13
+ return { x: wall.start[0], y: wall.start[1] };
14
+ }
15
+ export function getWallEndPoint(wall) {
16
+ return { x: wall.end[0], y: wall.end[1] };
17
+ }
18
+ export function getWallChordLength(wall) {
19
+ return distance(getWallStartPoint(wall), getWallEndPoint(wall));
20
+ }
21
+ export function getMaxWallCurveOffset(wall) {
22
+ return getWallChordLength(wall) / 2;
23
+ }
24
+ export function getWallStraightSnapOffset(wall) {
25
+ return Math.min(0.03, Math.max(0.005, getWallChordLength(wall) * 0.005));
26
+ }
27
+ function clampCurveOffset(wall, offset) {
28
+ const maxOffset = getMaxWallCurveOffset(wall);
29
+ if (!Number.isFinite(maxOffset) || maxOffset < CURVE_EPSILON) {
30
+ return 0;
31
+ }
32
+ return Math.max(-maxOffset, Math.min(maxOffset, offset));
33
+ }
34
+ export function normalizeWallCurveOffset(wall, offset) {
35
+ const clamped = clampCurveOffset(wall, offset);
36
+ return Math.abs(clamped) <= getWallStraightSnapOffset(wall) ? 0 : clamped;
37
+ }
38
+ export function getClampedWallCurveOffset(wall) {
39
+ const value = wall.curveOffset ?? 0;
40
+ const normalized = normalizeWallCurveOffset(wall, value);
41
+ return Math.abs(normalized) > CURVE_EPSILON ? normalized : 0;
42
+ }
43
+ export function isCurvedWall(wall) {
44
+ return Math.abs(getClampedWallCurveOffset(wall)) > CURVE_EPSILON;
45
+ }
46
+ export function getWallChordFrame(wall) {
47
+ const start = getWallStartPoint(wall);
48
+ const end = getWallEndPoint(wall);
49
+ const dx = end.x - start.x;
50
+ const dy = end.y - start.y;
51
+ const length = Math.hypot(dx, dy);
52
+ if (length < CURVE_EPSILON) {
53
+ return {
54
+ start,
55
+ end,
56
+ midpoint: start,
57
+ tangent: { x: 1, y: 0 },
58
+ normal: { x: 0, y: 1 },
59
+ length: 0,
60
+ };
61
+ }
62
+ return {
63
+ start,
64
+ end,
65
+ midpoint: {
66
+ x: (start.x + end.x) / 2,
67
+ y: (start.y + end.y) / 2,
68
+ },
69
+ tangent: { x: dx / length, y: dy / length },
70
+ normal: { x: -dy / length, y: dx / length },
71
+ length,
72
+ };
73
+ }
74
+ function getWallArcData(wall) {
75
+ const chord = getWallChordFrame(wall);
76
+ const sagitta = getClampedWallCurveOffset(wall);
77
+ if (Math.abs(sagitta) <= CURVE_EPSILON || chord.length < CURVE_EPSILON) {
78
+ return null;
79
+ }
80
+ const absSagitta = Math.abs(sagitta);
81
+ const radius = chord.length * chord.length / (8 * absSagitta) + absSagitta / 2;
82
+ const centerOffset = radius - absSagitta;
83
+ const direction = Math.sign(sagitta) || 1;
84
+ const center = {
85
+ x: chord.midpoint.x + chord.normal.x * centerOffset * direction,
86
+ y: chord.midpoint.y + chord.normal.y * centerOffset * direction,
87
+ };
88
+ const startAngle = Math.atan2(chord.start.y - center.y, chord.start.x - center.x);
89
+ const endAngle = Math.atan2(chord.end.y - center.y, chord.end.x - center.x);
90
+ let delta = endAngle - startAngle;
91
+ if (direction > 0) {
92
+ while (delta <= 0)
93
+ delta += Math.PI * 2;
94
+ }
95
+ else {
96
+ while (delta >= 0)
97
+ delta -= Math.PI * 2;
98
+ }
99
+ return { center, radius, startAngle, delta, direction };
100
+ }
101
+ export function getWallCurveFrameAt(wall, t) {
102
+ const chord = getWallChordFrame(wall);
103
+ if (!isCurvedWall(wall) || chord.length < CURVE_EPSILON) {
104
+ return {
105
+ point: {
106
+ x: lerp(chord.start.x, chord.end.x, clamp01(t)),
107
+ y: lerp(chord.start.y, chord.end.y, clamp01(t)),
108
+ },
109
+ tangent: chord.tangent,
110
+ normal: chord.normal,
111
+ };
112
+ }
113
+ const arc = getWallArcData(wall);
114
+ if (!arc) {
115
+ return {
116
+ point: chord.midpoint,
117
+ tangent: chord.tangent,
118
+ normal: chord.normal,
119
+ };
120
+ }
121
+ const angle = arc.startAngle + arc.delta * clamp01(t);
122
+ const point = {
123
+ x: arc.center.x + Math.cos(angle) * arc.radius,
124
+ y: arc.center.y + Math.sin(angle) * arc.radius,
125
+ };
126
+ const tangent = arc.direction > 0
127
+ ? { x: -Math.sin(angle), y: Math.cos(angle) }
128
+ : { x: Math.sin(angle), y: -Math.cos(angle) };
129
+ return {
130
+ point,
131
+ tangent,
132
+ normal: {
133
+ x: -tangent.y,
134
+ y: tangent.x,
135
+ },
136
+ };
137
+ }
138
+ export function getWallMidpointHandlePoint(wall) {
139
+ return getWallCurveFrameAt(wall, 0.5).point;
140
+ }
141
+ export function sampleWallCenterline(wall, segments = DEFAULT_SAMPLE_SEGMENTS) {
142
+ const count = Math.max(1, segments);
143
+ return Array.from({ length: count + 1 }, (_, index) => getWallCurveFrameAt(wall, index / count).point);
144
+ }
145
+ export function getWallCurveLength(wall, segments = DEFAULT_SAMPLE_SEGMENTS) {
146
+ const points = sampleWallCenterline(wall, segments);
147
+ let totalLength = 0;
148
+ for (let index = 1; index < points.length; index += 1) {
149
+ totalLength += distance(points[index - 1], points[index]);
150
+ }
151
+ return totalLength;
152
+ }
153
+ export function getWallSurfacePolygon(wall, segments = DEFAULT_SAMPLE_SEGMENTS, miterOverrides) {
154
+ const halfThickness = (wall.thickness ?? 0.1) / 2;
155
+ const count = Math.max(1, segments);
156
+ const left = [];
157
+ const right = [];
158
+ for (let index = 0; index <= count; index += 1) {
159
+ const frame = getWallCurveFrameAt(wall, index / count);
160
+ left.push({
161
+ x: frame.point.x + frame.normal.x * halfThickness,
162
+ y: frame.point.y + frame.normal.y * halfThickness,
163
+ });
164
+ right.push({
165
+ x: frame.point.x - frame.normal.x * halfThickness,
166
+ y: frame.point.y - frame.normal.y * halfThickness,
167
+ });
168
+ }
169
+ if (left.length > 0 && right.length > 0) {
170
+ left[0] = miterOverrides?.startLeft ?? left[0];
171
+ right[0] = miterOverrides?.startRight ?? right[0];
172
+ left[left.length - 1] = miterOverrides?.endLeft ?? left[left.length - 1];
173
+ right[right.length - 1] = miterOverrides?.endRight ?? right[right.length - 1];
174
+ }
175
+ return [...right, ...left.reverse()];
176
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"wall-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-footprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,KAAK,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAE9E,eAAO,MAAM,sBAAsB,MAAM,CAAA;AACzC,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,EAAE,CAkD5F"}
1
+ {"version":3,"file":"wall-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-footprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,EAEL,KAAK,OAAO,EAEZ,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AAExB,eAAO,MAAM,sBAAsB,MAAM,CAAA;AACzC,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAGtC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,GAAG,OAAO,EAAE,CA6D5F"}
@@ -1,6 +1,8 @@
1
- import { pointToKey } from './wall-mitering';
1
+ import { getWallSurfacePolygon, isCurvedWall } from './wall-curve';
2
+ import { getWallMiterBoundaryPoints, pointToKey, } from './wall-mitering';
2
3
  export const DEFAULT_WALL_THICKNESS = 0.1;
3
4
  export const DEFAULT_WALL_HEIGHT = 2.5;
5
+ const CURVED_WALL_SURFACE_SEGMENTS = 24;
4
6
  export function getWallThickness(wallNode) {
5
7
  return wallNode.thickness ?? DEFAULT_WALL_THICKNESS;
6
8
  }
@@ -20,6 +22,19 @@ export function getWallPlanFootprint(wallNode, miterData) {
20
22
  const keyEnd = pointToKey(wallEnd);
21
23
  const startJunction = junctionData.get(keyStart)?.get(wallNode.id);
22
24
  const endJunction = junctionData.get(keyEnd)?.get(wallNode.id);
25
+ if (isCurvedWall(wallNode)) {
26
+ const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
27
+ if (!boundaryPoints) {
28
+ return [];
29
+ }
30
+ const { startLeft, startRight, endLeft, endRight } = boundaryPoints;
31
+ return getWallSurfacePolygon(wallNode, CURVED_WALL_SURFACE_SEGMENTS, {
32
+ endLeft,
33
+ endRight,
34
+ startLeft,
35
+ startRight,
36
+ });
37
+ }
23
38
  const pStartLeft = startJunction?.left || {
24
39
  x: wallStart.x + nUnit.x * halfT,
25
40
  y: wallStart.y + nUnit.y * halfT,
@@ -28,7 +43,6 @@ export function getWallPlanFootprint(wallNode, miterData) {
28
43
  x: wallStart.x - nUnit.x * halfT,
29
44
  y: wallStart.y - nUnit.y * halfT,
30
45
  };
31
- // Junction offsets are stored relative to the outgoing direction.
32
46
  const pEndLeft = endJunction?.right || {
33
47
  x: wallEnd.x + nUnit.x * halfT,
34
48
  y: wallEnd.y + nUnit.y * halfT,
@@ -3,6 +3,12 @@ export interface Point2D {
3
3
  x: number;
4
4
  y: number;
5
5
  }
6
+ export interface WallMiterBoundaryPoints {
7
+ startLeft: Point2D;
8
+ startRight: Point2D;
9
+ endLeft: Point2D;
10
+ endRight: Point2D;
11
+ }
6
12
  type WallIntersections = Map<string, {
7
13
  left?: Point2D;
8
14
  right?: Point2D;
@@ -24,6 +30,7 @@ export interface WallMiterData {
24
30
  * Calculates miter data for all walls on a level
25
31
  */
26
32
  export declare function calculateLevelMiters(walls: WallNode[]): WallMiterData;
33
+ export declare function getWallMiterBoundaryPoints(wall: WallNode, miterData: WallMiterData): WallMiterBoundaryPoints | null;
27
34
  /**
28
35
  * Gets wall IDs that share junctions with the given walls
29
36
  */
@@ -1 +1 @@
1
- {"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAM5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAQlD,iBAAS,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,SAAY,GAAG,MAAM,CAG7D;AA8CD,UAAU,QAAQ;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAA;KAAE,CAAC,CAAA;CACpF;AAkKD,MAAM,WAAW,aAAa;IAE5B,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,aAAa,CAWrE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CA8C/F;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
1
+ {"version":3,"file":"wall-mitering.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-mitering.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAO5C,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,OAAO,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB;AASD,KAAK,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAGzE,KAAK,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;AAQlD,iBAAS,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,SAAY,GAAG,MAAM,CAG7D;AA8CD,UAAU,QAAQ;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,cAAc,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAA;KAAE,CAAC,CAAA;CACpF;AA+ND,MAAM,WAAW,aAAa;IAE5B,YAAY,EAAE,YAAY,CAAA;IAE1B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,aAAa,CAWrE;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,QAAQ,EACd,SAAS,EAAE,aAAa,GACvB,uBAAuB,GAAG,IAAI,CA0BhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CA8C/F;AAGD,OAAO,EAAE,UAAU,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { getWallCurveFrameAt, isCurvedWall } from './wall-curve';
1
2
  // ============================================================================
2
3
  // UTILITY FUNCTIONS
3
4
  // ============================================================================
@@ -79,6 +80,54 @@ function findJunctions(walls) {
79
80
  }
80
81
  return actualJunctions;
81
82
  }
83
+ function getWallDirectionFromJunction(wall, endType) {
84
+ if (endType === 'passthrough') {
85
+ return {
86
+ x: wall.end[0] - wall.start[0],
87
+ y: wall.end[1] - wall.start[1],
88
+ };
89
+ }
90
+ if (isCurvedWall(wall)) {
91
+ const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1);
92
+ return endType === 'start'
93
+ ? frame.tangent
94
+ : { x: -frame.tangent.x, y: -frame.tangent.y };
95
+ }
96
+ return endType === 'start'
97
+ ? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
98
+ : { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] };
99
+ }
100
+ function getWallBoundaryFrame(wall, endType) {
101
+ if (isCurvedWall(wall)) {
102
+ const frame = getWallCurveFrameAt(wall, endType === 'start' ? 0 : 1);
103
+ return {
104
+ point: frame.point,
105
+ tangent: endType === 'start'
106
+ ? frame.tangent
107
+ : { x: -frame.tangent.x, y: -frame.tangent.y },
108
+ normal: frame.normal,
109
+ };
110
+ }
111
+ const point = endType === 'start'
112
+ ? { x: wall.start[0], y: wall.start[1] }
113
+ : { x: wall.end[0], y: wall.end[1] };
114
+ const vector = endType === 'start'
115
+ ? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
116
+ : { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] };
117
+ const length = Math.hypot(vector.x, vector.y);
118
+ if (length < 1e-9) {
119
+ return {
120
+ point,
121
+ tangent: { x: 1, y: 0 },
122
+ normal: { x: 0, y: 1 },
123
+ };
124
+ }
125
+ return {
126
+ point,
127
+ tangent: { x: vector.x / length, y: vector.y / length },
128
+ normal: { x: -vector.y / length, y: vector.x / length },
129
+ };
130
+ }
82
131
  function calculateJunctionIntersections(junction, getThickness) {
83
132
  const { meetingPoint, connectedWalls } = junction;
84
133
  const processedWalls = [];
@@ -104,9 +153,7 @@ function calculateJunctionIntersections(junction, getThickness) {
104
153
  }
105
154
  else {
106
155
  // Normal wall endpoint (start or end)
107
- const v = endType === 'start'
108
- ? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
109
- : { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] };
156
+ const v = getWallDirectionFromJunction(wall, endType);
110
157
  const L = Math.sqrt(v.x * v.x + v.y * v.y);
111
158
  if (L < 1e-9)
112
159
  continue;
@@ -169,6 +216,32 @@ export function calculateLevelMiters(walls) {
169
216
  }
170
217
  return { junctionData, junctions };
171
218
  }
219
+ export function getWallMiterBoundaryPoints(wall, miterData) {
220
+ const thickness = wall.thickness ?? 0.1;
221
+ const halfThickness = thickness / 2;
222
+ const startFrame = getWallBoundaryFrame(wall, 'start');
223
+ const endFrame = getWallBoundaryFrame(wall, 'end');
224
+ const startJunction = miterData.junctionData.get(pointToKey(startFrame.point))?.get(wall.id);
225
+ const endJunction = miterData.junctionData.get(pointToKey(endFrame.point))?.get(wall.id);
226
+ return {
227
+ startLeft: startJunction?.left ?? {
228
+ x: startFrame.point.x + startFrame.normal.x * halfThickness,
229
+ y: startFrame.point.y + startFrame.normal.y * halfThickness,
230
+ },
231
+ startRight: startJunction?.right ?? {
232
+ x: startFrame.point.x - startFrame.normal.x * halfThickness,
233
+ y: startFrame.point.y - startFrame.normal.y * halfThickness,
234
+ },
235
+ endLeft: endJunction?.right ?? {
236
+ x: endFrame.point.x + endFrame.normal.x * halfThickness,
237
+ y: endFrame.point.y + endFrame.normal.y * halfThickness,
238
+ },
239
+ endRight: endJunction?.left ?? {
240
+ x: endFrame.point.x - endFrame.normal.x * halfThickness,
241
+ y: endFrame.point.y - endFrame.normal.y * halfThickness,
242
+ },
243
+ };
244
+ }
172
245
  /**
173
246
  * Gets wall IDs that share junctions with the given walls
174
247
  */
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAGhE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AAUxB,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA+FlB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAM9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAIhE,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AAsRxB,eAAO,MAAM,UAAU,YAuDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA2GlB"}