@pascal-app/core 0.5.0 → 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 (99) hide show
  1. package/dist/events/bus.d.ts +39 -4
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +1 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +1 -0
  7. package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
  8. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
  10. package/dist/index.d.ts +6 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/lib/polygon-geometry.d.ts +3 -0
  14. package/dist/lib/polygon-geometry.d.ts.map +1 -0
  15. package/dist/lib/polygon-geometry.js +90 -0
  16. package/dist/lib/space-detection.d.ts +10 -17
  17. package/dist/lib/space-detection.d.ts.map +1 -1
  18. package/dist/lib/space-detection.js +666 -453
  19. package/dist/material-library.d.ts +18 -0
  20. package/dist/material-library.d.ts.map +1 -0
  21. package/dist/material-library.js +603 -0
  22. package/dist/schema/index.d.ts +10 -4
  23. package/dist/schema/index.d.ts.map +1 -1
  24. package/dist/schema/index.js +6 -4
  25. package/dist/schema/material.d.ts +109 -0
  26. package/dist/schema/material.d.ts.map +1 -1
  27. package/dist/schema/material.js +52 -0
  28. package/dist/schema/nodes/ceiling.d.ts +10 -0
  29. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  30. package/dist/schema/nodes/ceiling.js +6 -0
  31. package/dist/schema/nodes/door.d.ts +1 -0
  32. package/dist/schema/nodes/door.d.ts.map +1 -1
  33. package/dist/schema/nodes/fence.d.ts +85 -0
  34. package/dist/schema/nodes/fence.d.ts.map +1 -0
  35. package/dist/schema/nodes/fence.js +34 -0
  36. package/dist/schema/nodes/item.d.ts +2 -2
  37. package/dist/schema/nodes/level.d.ts +1 -1
  38. package/dist/schema/nodes/level.d.ts.map +1 -1
  39. package/dist/schema/nodes/level.js +2 -0
  40. package/dist/schema/nodes/roof-segment.d.ts +2 -0
  41. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  42. package/dist/schema/nodes/roof-segment.js +1 -0
  43. package/dist/schema/nodes/roof.d.ts +108 -0
  44. package/dist/schema/nodes/roof.d.ts.map +1 -1
  45. package/dist/schema/nodes/roof.js +58 -2
  46. package/dist/schema/nodes/site.d.ts +1 -1
  47. package/dist/schema/nodes/slab.d.ts +10 -0
  48. package/dist/schema/nodes/slab.d.ts.map +1 -1
  49. package/dist/schema/nodes/slab.js +7 -0
  50. package/dist/schema/nodes/stair-segment.d.ts +2 -0
  51. package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
  52. package/dist/schema/nodes/stair-segment.js +1 -0
  53. package/dist/schema/nodes/stair.d.ts +164 -0
  54. package/dist/schema/nodes/stair.d.ts.map +1 -1
  55. package/dist/schema/nodes/stair.js +106 -5
  56. package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
  57. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
  58. package/dist/schema/nodes/surface-hole-metadata.js +5 -0
  59. package/dist/schema/nodes/wall.d.ts +87 -1
  60. package/dist/schema/nodes/wall.d.ts.map +1 -1
  61. package/dist/schema/nodes/wall.js +45 -4
  62. package/dist/schema/nodes/window.d.ts +1 -0
  63. package/dist/schema/nodes/window.d.ts.map +1 -1
  64. package/dist/schema/types.d.ts +406 -4
  65. package/dist/schema/types.d.ts.map +1 -1
  66. package/dist/schema/types.js +2 -0
  67. package/dist/store/actions/node-actions.d.ts +1 -1
  68. package/dist/store/actions/node-actions.d.ts.map +1 -1
  69. package/dist/store/actions/node-actions.js +175 -0
  70. package/dist/store/history-control.d.ts +14 -0
  71. package/dist/store/history-control.d.ts.map +1 -0
  72. package/dist/store/history-control.js +22 -0
  73. package/dist/store/use-scene.d.ts.map +1 -1
  74. package/dist/store/use-scene.js +249 -3
  75. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  76. package/dist/systems/ceiling/ceiling-system.js +7 -0
  77. package/dist/systems/fence/fence-system.d.ts +2 -0
  78. package/dist/systems/fence/fence-system.d.ts.map +1 -0
  79. package/dist/systems/fence/fence-system.js +187 -0
  80. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  81. package/dist/systems/roof/roof-system.js +31 -1
  82. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  83. package/dist/systems/slab/slab-system.js +45 -8
  84. package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
  85. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
  86. package/dist/systems/stair/stair-opening-sync.js +515 -0
  87. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  88. package/dist/systems/stair/stair-system.js +432 -10
  89. package/dist/systems/wall/wall-curve.d.ts +43 -0
  90. package/dist/systems/wall/wall-curve.d.ts.map +1 -0
  91. package/dist/systems/wall/wall-curve.js +176 -0
  92. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  93. package/dist/systems/wall/wall-footprint.js +16 -2
  94. package/dist/systems/wall/wall-mitering.d.ts +7 -0
  95. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  96. package/dist/systems/wall/wall-mitering.js +76 -3
  97. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  98. package/dist/systems/wall/wall-system.js +202 -2
  99. package/package.json +3 -3
@@ -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"}
@@ -6,10 +6,202 @@ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
6
6
  import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
7
7
  import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
8
8
  import useScene from '../../store/use-scene';
9
+ import { getWallCurveFrameAt, getWallSurfacePolygon, isCurvedWall } from './wall-curve';
9
10
  import { DEFAULT_WALL_HEIGHT, getWallPlanFootprint, getWallThickness } from './wall-footprint';
10
- import { calculateLevelMiters, getAdjacentWallIds, } from './wall-mitering';
11
+ import { calculateLevelMiters, getAdjacentWallIds, getWallMiterBoundaryPoints, pointToKey, } from './wall-mitering';
11
12
  // Reusable CSG evaluator for better performance
12
13
  const csgEvaluator = new Evaluator();
14
+ const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
15
+ const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
16
+ const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
17
+ function ensureUv2Attribute(geometry) {
18
+ const uv = geometry.getAttribute('uv');
19
+ if (!uv)
20
+ return;
21
+ geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
22
+ }
23
+ function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
24
+ if (!boundaryPoints || !isCurvedWall(wall)) {
25
+ return boundaryPoints;
26
+ }
27
+ const insetDistance = Math.min(CURVED_WALL_3D_ENDPOINT_INSET, Math.max((wall.thickness ?? 0.1) * 0.01, 0.0005));
28
+ if (insetDistance <= 0) {
29
+ return boundaryPoints;
30
+ }
31
+ const next = { ...boundaryPoints };
32
+ const startJunction = miterData.junctions.get(pointToKey({ x: wall.start[0], y: wall.start[1] }));
33
+ const endJunction = miterData.junctions.get(pointToKey({ x: wall.end[0], y: wall.end[1] }));
34
+ if (startJunction && startJunction.connectedWalls.length > 1) {
35
+ const frame = getWallCurveFrameAt(wall, 0);
36
+ next.startLeft = {
37
+ x: next.startLeft.x + frame.tangent.x * insetDistance,
38
+ y: next.startLeft.y + frame.tangent.y * insetDistance,
39
+ };
40
+ next.startRight = {
41
+ x: next.startRight.x + frame.tangent.x * insetDistance,
42
+ y: next.startRight.y + frame.tangent.y * insetDistance,
43
+ };
44
+ }
45
+ if (endJunction && endJunction.connectedWalls.length > 1) {
46
+ const frame = getWallCurveFrameAt(wall, 1);
47
+ next.endLeft = {
48
+ x: next.endLeft.x - frame.tangent.x * insetDistance,
49
+ y: next.endLeft.y - frame.tangent.y * insetDistance,
50
+ };
51
+ next.endRight = {
52
+ x: next.endRight.x - frame.tangent.x * insetDistance,
53
+ y: next.endRight.y - frame.tangent.y * insetDistance,
54
+ };
55
+ }
56
+ return next;
57
+ }
58
+ function addTaggedWallBoundaryEdge(edges, points, startIndex, endIndex, tag) {
59
+ const start = points[startIndex];
60
+ const end = points[endIndex];
61
+ if (!(start && end))
62
+ return;
63
+ if (Math.hypot(end.x - start.x, end.z - start.z) < 1e-6)
64
+ return;
65
+ edges.push({
66
+ start: new THREE.Vector2(start.x, start.z),
67
+ end: new THREE.Vector2(end.x, end.z),
68
+ tag,
69
+ });
70
+ }
71
+ function buildTaggedWallBoundaryEdges(wall, localPoints, miterData) {
72
+ if (localPoints.length < 2)
73
+ return [];
74
+ const edges = [];
75
+ if (isCurvedWall(wall)) {
76
+ const sidePointCount = Math.floor(localPoints.length / 2);
77
+ if (sidePointCount < 2)
78
+ return edges;
79
+ for (let index = 0; index < sidePointCount - 1; index += 1) {
80
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'back');
81
+ }
82
+ addTaggedWallBoundaryEdge(edges, localPoints, sidePointCount - 1, sidePointCount, 'base');
83
+ for (let index = sidePointCount; index < localPoints.length - 1; index += 1) {
84
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'front');
85
+ }
86
+ addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
87
+ return edges;
88
+ }
89
+ const startKey = pointToKey({ x: wall.start[0], y: wall.start[1] });
90
+ const startJunction = miterData.junctionData.get(startKey)?.get(wall.id);
91
+ const startLeftIndex = startJunction ? localPoints.length - 2 : localPoints.length - 1;
92
+ const endLeftIndex = startJunction ? localPoints.length - 3 : localPoints.length - 2;
93
+ addTaggedWallBoundaryEdge(edges, localPoints, 0, 1, 'back');
94
+ for (let index = 1; index < endLeftIndex; index += 1) {
95
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
96
+ }
97
+ addTaggedWallBoundaryEdge(edges, localPoints, endLeftIndex, startLeftIndex, 'front');
98
+ for (let index = startLeftIndex; index < localPoints.length - 1; index += 1) {
99
+ addTaggedWallBoundaryEdge(edges, localPoints, index, index + 1, 'base');
100
+ }
101
+ addTaggedWallBoundaryEdge(edges, localPoints, localPoints.length - 1, 0, 'base');
102
+ return edges;
103
+ }
104
+ function distanceToWallBoundaryEdge(point, edge) {
105
+ const edgeDx = edge.end.x - edge.start.x;
106
+ const edgeDz = edge.end.y - edge.start.y;
107
+ const pointDx = point.x - edge.start.x;
108
+ const pointDz = point.y - edge.start.y;
109
+ const edgeLengthSq = edgeDx * edgeDx + edgeDz * edgeDz;
110
+ if (edgeLengthSq < 1e-12) {
111
+ return point.distanceTo(edge.start);
112
+ }
113
+ const t = THREE.MathUtils.clamp((pointDx * edgeDx + pointDz * edgeDz) / edgeLengthSq, 0, 1);
114
+ const closestX = edge.start.x + edgeDx * t;
115
+ const closestZ = edge.start.y + edgeDz * t;
116
+ return Math.hypot(point.x - closestX, point.y - closestZ);
117
+ }
118
+ function getWallFaceMaterialIndex(wall, face) {
119
+ const semantic = face === 'front' ? wall.frontSide : wall.backSide;
120
+ const fallback = face === 'front' ? 1 : 2;
121
+ if (semantic === 'interior')
122
+ return 1;
123
+ if (semantic === 'exterior')
124
+ return 2;
125
+ return fallback;
126
+ }
127
+ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
128
+ const position = geometry.getAttribute('position');
129
+ if (!position)
130
+ return;
131
+ const index = geometry.getIndex();
132
+ const triangleCount = index ? Math.floor(index.count / 3) : Math.floor(position.count / 3);
133
+ if (triangleCount === 0) {
134
+ geometry.clearGroups();
135
+ return;
136
+ }
137
+ const triangleMaterials = new Array(triangleCount).fill(0);
138
+ const a = new THREE.Vector3();
139
+ const b = new THREE.Vector3();
140
+ const c = new THREE.Vector3();
141
+ const ab = new THREE.Vector3();
142
+ const ac = new THREE.Vector3();
143
+ const normal = new THREE.Vector3();
144
+ const centroid = new THREE.Vector3();
145
+ const projectedCentroid = new THREE.Vector2();
146
+ const maxBoundaryDistance = Math.max(getWallThickness(wall) * 0.02, WALL_FACE_EDGE_DISTANCE_EPSILON);
147
+ for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
148
+ const baseIndex = triangleIndex * 3;
149
+ const ia = index ? index.getX(baseIndex) : baseIndex;
150
+ const ib = index ? index.getX(baseIndex + 1) : baseIndex + 1;
151
+ const ic = index ? index.getX(baseIndex + 2) : baseIndex + 2;
152
+ a.fromBufferAttribute(position, ia);
153
+ b.fromBufferAttribute(position, ib);
154
+ c.fromBufferAttribute(position, ic);
155
+ ab.subVectors(b, a);
156
+ ac.subVectors(c, a);
157
+ normal.crossVectors(ab, ac);
158
+ if (normal.lengthSq() < 1e-12) {
159
+ triangleMaterials[triangleIndex] = 0;
160
+ continue;
161
+ }
162
+ normal.normalize();
163
+ if (Math.abs(normal.y) >= WALL_FACE_NORMAL_Y_EPSILON) {
164
+ triangleMaterials[triangleIndex] = 0;
165
+ continue;
166
+ }
167
+ centroid
168
+ .copy(a)
169
+ .add(b)
170
+ .add(c)
171
+ .multiplyScalar(1 / 3);
172
+ projectedCentroid.set(centroid.x, centroid.z);
173
+ let nearestTag = null;
174
+ let nearestDistance = Number.POSITIVE_INFINITY;
175
+ for (const edge of boundaryEdges) {
176
+ const distance = distanceToWallBoundaryEdge(projectedCentroid, edge);
177
+ if (distance < nearestDistance) {
178
+ nearestDistance = distance;
179
+ nearestTag = edge.tag;
180
+ }
181
+ }
182
+ if (!nearestTag || nearestDistance > maxBoundaryDistance) {
183
+ triangleMaterials[triangleIndex] = 0;
184
+ continue;
185
+ }
186
+ if (nearestTag === 'base') {
187
+ triangleMaterials[triangleIndex] = 0;
188
+ continue;
189
+ }
190
+ triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
191
+ }
192
+ geometry.clearGroups();
193
+ let currentMaterial = triangleMaterials[0] ?? 0;
194
+ let groupStart = 0;
195
+ for (let triangleIndex = 1; triangleIndex < triangleCount; triangleIndex += 1) {
196
+ const materialIndex = triangleMaterials[triangleIndex] ?? 0;
197
+ if (materialIndex === currentMaterial)
198
+ continue;
199
+ geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
200
+ groupStart = triangleIndex;
201
+ currentMaterial = materialIndex;
202
+ }
203
+ geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
204
+ }
13
205
  // ============================================================================
14
206
  // WALL SYSTEM
15
207
  // ============================================================================
@@ -131,7 +323,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
131
323
  if (L < 1e-9) {
132
324
  return new THREE.BufferGeometry();
133
325
  }
134
- const polyPoints = getWallPlanFootprint(wallNode, miterData);
326
+ const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
327
+ const polyPoints = isCurvedWall(wallNode)
328
+ ? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
329
+ : getWallPlanFootprint(wallNode, miterData);
135
330
  if (polyPoints.length < 3) {
136
331
  return new THREE.BufferGeometry();
137
332
  }
@@ -150,6 +345,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
150
345
  };
151
346
  // Convert polygon to local coordinates
152
347
  const localPoints = polyPoints.map(worldToLocal);
348
+ const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
153
349
  // Build THREE.js shape
154
350
  // Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
155
351
  // The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
@@ -167,6 +363,8 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
167
363
  // Rotate so extrusion direction (Z) becomes height direction (Y)
168
364
  geometry.rotateX(-Math.PI / 2);
169
365
  geometry.computeVertexNormals();
366
+ assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
367
+ ensureUv2Attribute(geometry);
170
368
  // Apply CSG subtraction for cutouts (doors/windows)
171
369
  const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
172
370
  if (cutoutBrushes.length === 0) {
@@ -195,6 +393,8 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
195
393
  }
196
394
  const resultGeometry = resultBrush.geometry;
197
395
  resultGeometry.computeVertexNormals();
396
+ assignWallMaterialGroups(resultGeometry, wallNode, boundaryEdges);
397
+ ensureUv2Attribute(resultGeometry);
198
398
  return resultGeometry;
199
399
  }
200
400
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Core library for Pascal 3D building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "@react-three/drei": "^10",
31
31
  "@react-three/fiber": "^9",
32
32
  "react": "^18 || ^19",
33
- "three": "^0.182"
33
+ "three": "^0.184"
34
34
  },
35
35
  "dependencies": {
36
36
  "dedent": "^1.7.1",
@@ -47,7 +47,7 @@
47
47
  "@pascal/typescript-config": "*",
48
48
  "@types/react": "^19.2.2",
49
49
  "typescript": "5.9.3",
50
- "@types/three": "^0.183.0"
50
+ "@types/three": "^0.184.0"
51
51
  },
52
52
  "keywords": [
53
53
  "3d",