@pascal-app/core 0.1.3

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 (103) hide show
  1. package/dist/events/bus.d.ts +42 -0
  2. package/dist/events/bus.d.ts.map +1 -0
  3. package/dist/events/bus.js +13 -0
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/scene-registry.js +35 -0
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +90 -0
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +466 -0
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +4 -0
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +115 -0
  13. package/dist/hooks/spatial-grid/spatial-grid.d.ts +23 -0
  14. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/spatial-grid.js +115 -0
  16. package/dist/hooks/spatial-grid/use-spatial-query.d.ts +16 -0
  17. package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/use-spatial-query.js +14 -0
  19. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +47 -0
  20. package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/wall-spatial-grid.js +113 -0
  22. package/dist/index.d.ts +17 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +22 -0
  25. package/dist/lib/asset-storage.d.ts +11 -0
  26. package/dist/lib/asset-storage.d.ts.map +1 -0
  27. package/dist/lib/asset-storage.js +48 -0
  28. package/dist/lib/space-detection.d.ts +34 -0
  29. package/dist/lib/space-detection.d.ts.map +1 -0
  30. package/dist/lib/space-detection.js +499 -0
  31. package/dist/schema/base.d.ts +30 -0
  32. package/dist/schema/base.d.ts.map +1 -0
  33. package/dist/schema/base.js +25 -0
  34. package/dist/schema/camera.d.ts +13 -0
  35. package/dist/schema/camera.d.ts.map +1 -0
  36. package/dist/schema/camera.js +9 -0
  37. package/dist/schema/index.d.ts +17 -0
  38. package/dist/schema/index.d.ts.map +1 -0
  39. package/dist/schema/index.js +18 -0
  40. package/dist/schema/nodes/building.d.ts +25 -0
  41. package/dist/schema/nodes/building.d.ts.map +1 -0
  42. package/dist/schema/nodes/building.js +16 -0
  43. package/dist/schema/nodes/ceiling.d.ts +25 -0
  44. package/dist/schema/nodes/ceiling.d.ts.map +1 -0
  45. package/dist/schema/nodes/ceiling.js +16 -0
  46. package/dist/schema/nodes/guide.d.ts +27 -0
  47. package/dist/schema/nodes/guide.d.ts.map +1 -0
  48. package/dist/schema/nodes/guide.js +11 -0
  49. package/dist/schema/nodes/item.d.ts +65 -0
  50. package/dist/schema/nodes/item.d.ts.map +1 -0
  51. package/dist/schema/nodes/item.js +38 -0
  52. package/dist/schema/nodes/level.d.ts +24 -0
  53. package/dist/schema/nodes/level.d.ts.map +1 -0
  54. package/dist/schema/nodes/level.js +21 -0
  55. package/dist/schema/nodes/roof.d.ts +28 -0
  56. package/dist/schema/nodes/roof.d.ts.map +1 -0
  57. package/dist/schema/nodes/roof.js +28 -0
  58. package/dist/schema/nodes/scan.d.ts +27 -0
  59. package/dist/schema/nodes/scan.d.ts.map +1 -0
  60. package/dist/schema/nodes/scan.js +11 -0
  61. package/dist/schema/nodes/site.d.ts +90 -0
  62. package/dist/schema/nodes/site.d.ts.map +1 -0
  63. package/dist/schema/nodes/site.js +39 -0
  64. package/dist/schema/nodes/slab.d.ts +24 -0
  65. package/dist/schema/nodes/slab.d.ts.map +1 -0
  66. package/dist/schema/nodes/slab.js +15 -0
  67. package/dist/schema/nodes/wall.d.ts +37 -0
  68. package/dist/schema/nodes/wall.d.ts.map +1 -0
  69. package/dist/schema/nodes/wall.js +30 -0
  70. package/dist/schema/nodes/zone.d.ts +24 -0
  71. package/dist/schema/nodes/zone.d.ts.map +1 -0
  72. package/dist/schema/nodes/zone.js +22 -0
  73. package/dist/schema/types.d.ts +339 -0
  74. package/dist/schema/types.d.ts.map +1 -0
  75. package/dist/schema/types.js +25 -0
  76. package/dist/store/actions/node-actions.d.ts +12 -0
  77. package/dist/store/actions/node-actions.d.ts.map +1 -0
  78. package/dist/store/actions/node-actions.js +121 -0
  79. package/dist/store/use-scene.d.ts +31 -0
  80. package/dist/store/use-scene.d.ts.map +1 -0
  81. package/dist/store/use-scene.js +127 -0
  82. package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
  83. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
  84. package/dist/systems/ceiling/ceiling-system.js +65 -0
  85. package/dist/systems/item/item-system.d.ts +2 -0
  86. package/dist/systems/item/item-system.d.ts.map +1 -0
  87. package/dist/systems/item/item-system.js +43 -0
  88. package/dist/systems/roof/roof-system.d.ts +8 -0
  89. package/dist/systems/roof/roof-system.d.ts.map +1 -0
  90. package/dist/systems/roof/roof-system.js +254 -0
  91. package/dist/systems/slab/slab-system.d.ts +8 -0
  92. package/dist/systems/slab/slab-system.d.ts.map +1 -0
  93. package/dist/systems/slab/slab-system.js +117 -0
  94. package/dist/systems/wall/wall-mitering.d.ts +32 -0
  95. package/dist/systems/wall/wall-mitering.d.ts.map +1 -0
  96. package/dist/systems/wall/wall-mitering.js +214 -0
  97. package/dist/systems/wall/wall-system.d.ts +12 -0
  98. package/dist/systems/wall/wall-system.d.ts.map +1 -0
  99. package/dist/systems/wall/wall-system.js +286 -0
  100. package/dist/utils/types.d.ts +6 -0
  101. package/dist/utils/types.d.ts.map +1 -0
  102. package/dist/utils/types.js +7 -0
  103. package/package.json +58 -0
@@ -0,0 +1,32 @@
1
+ import type { WallNode } from '../../schema';
2
+ export interface Point2D {
3
+ x: number;
4
+ y: number;
5
+ }
6
+ type WallIntersections = Map<string, {
7
+ left?: Point2D;
8
+ right?: Point2D;
9
+ }>;
10
+ type JunctionData = Map<string, WallIntersections>;
11
+ declare function pointToKey(p: Point2D, tolerance?: number): string;
12
+ interface Junction {
13
+ meetingPoint: Point2D;
14
+ connectedWalls: Array<{
15
+ wall: WallNode;
16
+ endType: 'start' | 'end' | 'passthrough';
17
+ }>;
18
+ }
19
+ export interface WallMiterData {
20
+ junctionData: JunctionData;
21
+ junctions: Map<string, Junction>;
22
+ }
23
+ /**
24
+ * Calculates miter data for all walls on a level
25
+ */
26
+ export declare function calculateLevelMiters(walls: WallNode[]): WallMiterData;
27
+ /**
28
+ * Gets wall IDs that share junctions with the given walls
29
+ */
30
+ export declare function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<string>): Set<string>;
31
+ export { pointToKey };
32
+ //# sourceMappingURL=wall-mitering.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,214 @@
1
+ // ============================================================================
2
+ // UTILITY FUNCTIONS
3
+ // ============================================================================
4
+ const TOLERANCE = 0.001;
5
+ function pointToKey(p, tolerance = TOLERANCE) {
6
+ const snap = 1 / tolerance;
7
+ return `${Math.round(p.x * snap)},${Math.round(p.y * snap)}`;
8
+ }
9
+ function createLineFromPointAndVector(p, v) {
10
+ const a = -v.y;
11
+ const b = v.x;
12
+ const c = -(a * p.x + b * p.y);
13
+ return { a, b, c };
14
+ }
15
+ /**
16
+ * Checks if a point lies on a wall segment (not at its endpoints)
17
+ */
18
+ function pointOnWallSegment(point, wall, tolerance = TOLERANCE) {
19
+ const start = { x: wall.start[0], y: wall.start[1] };
20
+ const end = { x: wall.end[0], y: wall.end[1] };
21
+ // Check if point is at endpoints (those are handled separately)
22
+ if (pointToKey(point, tolerance) === pointToKey(start, tolerance))
23
+ return false;
24
+ if (pointToKey(point, tolerance) === pointToKey(end, tolerance))
25
+ return false;
26
+ // Vector from start to end
27
+ const v = { x: end.x - start.x, y: end.y - start.y };
28
+ const L = Math.sqrt(v.x * v.x + v.y * v.y);
29
+ if (L < 1e-9)
30
+ return false;
31
+ // Vector from start to point
32
+ const w = { x: point.x - start.x, y: point.y - start.y };
33
+ // Project point onto wall line (t is parametric position along segment)
34
+ const t = (v.x * w.x + v.y * w.y) / (L * L);
35
+ // Check if projection is within segment (not at endpoints)
36
+ if (t < tolerance || t > 1 - tolerance)
37
+ return false;
38
+ // Check distance from point to line
39
+ const projX = start.x + t * v.x;
40
+ const projY = start.y + t * v.y;
41
+ const dist = Math.sqrt((point.x - projX) ** 2 + (point.y - projY) ** 2);
42
+ return dist < tolerance;
43
+ }
44
+ function findJunctions(walls) {
45
+ const junctions = new Map();
46
+ // First pass: group walls by their endpoints
47
+ for (const wall of walls) {
48
+ const startPt = { x: wall.start[0], y: wall.start[1] };
49
+ const endPt = { x: wall.end[0], y: wall.end[1] };
50
+ const keyStart = pointToKey(startPt);
51
+ const keyEnd = pointToKey(endPt);
52
+ if (!junctions.has(keyStart)) {
53
+ junctions.set(keyStart, { meetingPoint: startPt, connectedWalls: [] });
54
+ }
55
+ junctions.get(keyStart).connectedWalls.push({ wall, endType: 'start' });
56
+ if (!junctions.has(keyEnd)) {
57
+ junctions.set(keyEnd, { meetingPoint: endPt, connectedWalls: [] });
58
+ }
59
+ junctions.get(keyEnd).connectedWalls.push({ wall, endType: 'end' });
60
+ }
61
+ // Second pass: detect T-junctions (walls passing through junction points)
62
+ for (const [_key, junction] of junctions.entries()) {
63
+ for (const wall of walls) {
64
+ // Skip if wall already in this junction
65
+ if (junction.connectedWalls.some((cw) => cw.wall.id === wall.id))
66
+ continue;
67
+ // Check if junction point lies on this wall's segment (not at endpoints)
68
+ if (pointOnWallSegment(junction.meetingPoint, wall)) {
69
+ junction.connectedWalls.push({ wall, endType: 'passthrough' });
70
+ }
71
+ }
72
+ }
73
+ // Filter to only junctions with 2+ walls
74
+ const actualJunctions = new Map();
75
+ for (const [key, junction] of junctions.entries()) {
76
+ if (junction.connectedWalls.length >= 2) {
77
+ actualJunctions.set(key, junction);
78
+ }
79
+ }
80
+ return actualJunctions;
81
+ }
82
+ function calculateJunctionIntersections(junction, getThickness) {
83
+ const { meetingPoint, connectedWalls } = junction;
84
+ const processedWalls = [];
85
+ for (const { wall, endType } of connectedWalls) {
86
+ const halfT = getThickness(wall) / 2;
87
+ if (endType === 'passthrough') {
88
+ // For passthrough walls (T-junctions), add both directions
89
+ // This allows walls meeting the middle of this wall to miter against it
90
+ const v1 = { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] };
91
+ const v2 = { x: -v1.x, y: -v1.y };
92
+ for (const v of [v1, v2]) {
93
+ const L = Math.sqrt(v.x * v.x + v.y * v.y);
94
+ if (L < 1e-9)
95
+ continue;
96
+ const nUnit = { x: -v.y / L, y: v.x / L };
97
+ const pA = { x: meetingPoint.x + nUnit.x * halfT, y: meetingPoint.y + nUnit.y * halfT };
98
+ const pB = { x: meetingPoint.x - nUnit.x * halfT, y: meetingPoint.y - nUnit.y * halfT };
99
+ const edgeA = createLineFromPointAndVector(pA, v);
100
+ const edgeB = createLineFromPointAndVector(pB, v);
101
+ const angle = Math.atan2(v.y, v.x);
102
+ processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB, isPassthrough: true });
103
+ }
104
+ }
105
+ else {
106
+ // 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] };
110
+ const L = Math.sqrt(v.x * v.x + v.y * v.y);
111
+ if (L < 1e-9)
112
+ continue;
113
+ const nUnit = { x: -v.y / L, y: v.x / L };
114
+ const pA = { x: meetingPoint.x + nUnit.x * halfT, y: meetingPoint.y + nUnit.y * halfT };
115
+ const pB = { x: meetingPoint.x - nUnit.x * halfT, y: meetingPoint.y - nUnit.y * halfT };
116
+ const edgeA = createLineFromPointAndVector(pA, v);
117
+ const edgeB = createLineFromPointAndVector(pB, v);
118
+ const angle = Math.atan2(v.y, v.x);
119
+ processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB, isPassthrough: false });
120
+ }
121
+ }
122
+ // Sort by outgoing angle
123
+ processedWalls.sort((a, b) => a.angle - b.angle);
124
+ const wallIntersections = new Map();
125
+ const n = processedWalls.length;
126
+ if (n < 2)
127
+ return wallIntersections;
128
+ // Calculate intersections between adjacent walls (exactly like demo)
129
+ for (let i = 0; i < n; i++) {
130
+ const wall1 = processedWalls[i];
131
+ const wall2 = processedWalls[(i + 1) % n];
132
+ // Intersect left edge of wall1 with right edge of wall2
133
+ const det = wall1.edgeA.a * wall2.edgeB.b - wall2.edgeB.a * wall1.edgeA.b;
134
+ // If lines are parallel (det ≈ 0), skip this intersection - walls will use defaults
135
+ if (Math.abs(det) < 1e-9) {
136
+ continue;
137
+ }
138
+ const p = {
139
+ x: (wall1.edgeA.b * wall2.edgeB.c - wall2.edgeB.b * wall1.edgeA.c) / det,
140
+ y: (wall2.edgeB.a * wall1.edgeA.c - wall1.edgeA.a * wall2.edgeB.c) / det,
141
+ };
142
+ // Only assign intersection to non-passthrough walls
143
+ // Passthrough walls don't receive junction data (their geometry doesn't change)
144
+ if (!wall1.isPassthrough) {
145
+ if (!wallIntersections.has(wall1.wallId)) {
146
+ wallIntersections.set(wall1.wallId, {});
147
+ }
148
+ wallIntersections.get(wall1.wallId).left = p;
149
+ }
150
+ if (!wall2.isPassthrough) {
151
+ if (!wallIntersections.has(wall2.wallId)) {
152
+ wallIntersections.set(wall2.wallId, {});
153
+ }
154
+ wallIntersections.get(wall2.wallId).right = p;
155
+ }
156
+ }
157
+ return wallIntersections;
158
+ }
159
+ /**
160
+ * Calculates miter data for all walls on a level
161
+ */
162
+ export function calculateLevelMiters(walls) {
163
+ const getThickness = (wall) => wall.thickness ?? 0.1;
164
+ const junctions = findJunctions(walls);
165
+ const junctionData = new Map();
166
+ for (const [key, junction] of junctions.entries()) {
167
+ const wallIntersections = calculateJunctionIntersections(junction, getThickness);
168
+ junctionData.set(key, wallIntersections);
169
+ }
170
+ return { junctionData, junctions };
171
+ }
172
+ /**
173
+ * Gets wall IDs that share junctions with the given walls
174
+ */
175
+ export function getAdjacentWallIds(allWalls, dirtyWallIds) {
176
+ const adjacent = new Set();
177
+ for (const dirtyId of dirtyWallIds) {
178
+ const dirtyWall = allWalls.find((w) => w.id === dirtyId);
179
+ if (!dirtyWall)
180
+ continue;
181
+ const dirtyStart = { x: dirtyWall.start[0], y: dirtyWall.start[1] };
182
+ const dirtyEnd = { x: dirtyWall.end[0], y: dirtyWall.end[1] };
183
+ for (const wall of allWalls) {
184
+ if (wall.id === dirtyId)
185
+ continue;
186
+ const wallStart = { x: wall.start[0], y: wall.start[1] };
187
+ const wallEnd = { x: wall.end[0], y: wall.end[1] };
188
+ // Check corner connections (endpoints meeting)
189
+ const startKey = pointToKey(wallStart);
190
+ const endKey = pointToKey(wallEnd);
191
+ const dirtyStartKey = pointToKey(dirtyStart);
192
+ const dirtyEndKey = pointToKey(dirtyEnd);
193
+ if (startKey === dirtyStartKey ||
194
+ startKey === dirtyEndKey ||
195
+ endKey === dirtyStartKey ||
196
+ endKey === dirtyEndKey) {
197
+ adjacent.add(wall.id);
198
+ continue;
199
+ }
200
+ // Check T-junction connections (dirty wall endpoint on other wall's segment)
201
+ if (pointOnWallSegment(dirtyStart, wall) || pointOnWallSegment(dirtyEnd, wall)) {
202
+ adjacent.add(wall.id);
203
+ continue;
204
+ }
205
+ // Check reverse T-junction (other wall endpoint on dirty wall's segment)
206
+ if (pointOnWallSegment(wallStart, dirtyWall) || pointOnWallSegment(wallEnd, dirtyWall)) {
207
+ adjacent.add(wall.id);
208
+ }
209
+ }
210
+ }
211
+ return adjacent;
212
+ }
213
+ // Re-export for backwards compatibility
214
+ export { pointToKey };
@@ -0,0 +1,12 @@
1
+ import * as THREE from 'three';
2
+ import type { AnyNode, WallNode } from '../../schema';
3
+ import { type WallMiterData } from './wall-mitering';
4
+ export declare const WallSystem: () => null;
5
+ /**
6
+ * Generates extruded wall geometry with mitering and cutouts
7
+ *
8
+ * Key insight from demo: polygon is built in WORLD coordinates first,
9
+ * then we transform to wall-local for the 3D mesh.
10
+ */
11
+ export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number): THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>;
12
+ //# sourceMappingURL=wall-system.d.ts.map
@@ -0,0 +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;AAK9B,OAAO,KAAK,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEhE,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAA;AASxB,eAAO,MAAM,UAAU,YAwDtB,CAAA;AA0DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,oFA0IlB"}
@@ -0,0 +1,286 @@
1
+ import { useFrame } from '@react-three/fiber';
2
+ import * as THREE from 'three';
3
+ import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
4
+ import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
5
+ import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
6
+ import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
7
+ import useScene from '../../store/use-scene';
8
+ import { calculateLevelMiters, getAdjacentWallIds, pointToKey, } from './wall-mitering';
9
+ // Reusable CSG evaluator for better performance
10
+ const csgEvaluator = new Evaluator();
11
+ // ============================================================================
12
+ // WALL SYSTEM
13
+ // ============================================================================
14
+ export const WallSystem = () => {
15
+ const dirtyNodes = useScene((state) => state.dirtyNodes);
16
+ const clearDirty = useScene((state) => state.clearDirty);
17
+ useFrame(() => {
18
+ if (dirtyNodes.size === 0)
19
+ return;
20
+ const nodes = useScene.getState().nodes;
21
+ // Collect dirty walls and their levels
22
+ const dirtyWallsByLevel = new Map();
23
+ dirtyNodes.forEach((id) => {
24
+ const node = nodes[id];
25
+ if (!node || node.type !== 'wall')
26
+ return;
27
+ console.log('wall front/back', node.frontSide, node.backSide);
28
+ const levelId = node.parentId;
29
+ if (!levelId)
30
+ return;
31
+ if (!dirtyWallsByLevel.has(levelId)) {
32
+ dirtyWallsByLevel.set(levelId, new Set());
33
+ }
34
+ dirtyWallsByLevel.get(levelId).add(id);
35
+ });
36
+ // Process each level that has dirty walls
37
+ for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
38
+ const levelWalls = getLevelWalls(levelId);
39
+ const miterData = calculateLevelMiters(levelWalls);
40
+ // Update dirty walls
41
+ for (const wallId of dirtyWallIds) {
42
+ const mesh = sceneRegistry.nodes.get(wallId);
43
+ if (mesh) {
44
+ updateWallGeometry(wallId, miterData);
45
+ clearDirty(wallId);
46
+ }
47
+ // If mesh not found, keep it dirty for next frame
48
+ }
49
+ // Update adjacent walls that share junctions
50
+ const adjacentWallIds = getAdjacentWallIds(levelWalls, dirtyWallIds);
51
+ for (const wallId of adjacentWallIds) {
52
+ if (!dirtyWallIds.has(wallId)) {
53
+ const mesh = sceneRegistry.nodes.get(wallId);
54
+ if (mesh) {
55
+ updateWallGeometry(wallId, miterData);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ });
61
+ return null;
62
+ };
63
+ /**
64
+ * Gets all walls that belong to a level
65
+ */
66
+ function getLevelWalls(levelId) {
67
+ const { nodes } = useScene.getState();
68
+ const level = nodes[levelId];
69
+ if (!level || level.type !== 'level')
70
+ return [];
71
+ const walls = [];
72
+ for (const childId of level.children) {
73
+ const child = nodes[childId];
74
+ if (child?.type === 'wall') {
75
+ walls.push(child);
76
+ }
77
+ }
78
+ return walls;
79
+ }
80
+ /**
81
+ * Updates the geometry for a single wall
82
+ */
83
+ function updateWallGeometry(wallId, miterData) {
84
+ const nodes = useScene.getState().nodes;
85
+ const node = nodes[wallId];
86
+ if (!node || node.type !== 'wall')
87
+ return;
88
+ const mesh = sceneRegistry.nodes.get(wallId);
89
+ if (!mesh)
90
+ return;
91
+ const levelId = resolveLevelId(node, nodes);
92
+ const slabElevation = spatialGridManager.getSlabElevationForWall(levelId, node.start, node.end);
93
+ const childrenIds = node.children || [];
94
+ const childrenNodes = childrenIds
95
+ .map((childId) => nodes[childId])
96
+ .filter((n) => n !== undefined);
97
+ const newGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation);
98
+ mesh.geometry.dispose();
99
+ mesh.geometry = newGeo;
100
+ // Update collision mesh
101
+ const collisionMesh = mesh.getObjectByName('collision-mesh');
102
+ if (collisionMesh) {
103
+ const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
104
+ collisionMesh.geometry.dispose();
105
+ collisionMesh.geometry = collisionGeo;
106
+ }
107
+ mesh.position.set(node.start[0], 0, node.start[1]);
108
+ const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
109
+ mesh.rotation.y = -angle;
110
+ }
111
+ /**
112
+ * Generates extruded wall geometry with mitering and cutouts
113
+ *
114
+ * Key insight from demo: polygon is built in WORLD coordinates first,
115
+ * then we transform to wall-local for the 3D mesh.
116
+ */
117
+ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0) {
118
+ const { junctionData } = miterData;
119
+ const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
120
+ const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
121
+ // Wall height is adjusted by slab elevation (positive reduces, negative increases)
122
+ const height = (wallNode.height ?? 2.5) - slabElevation;
123
+ const thickness = wallNode.thickness ?? 0.1;
124
+ const halfT = thickness / 2;
125
+ // Wall direction and normal (exactly like demo)
126
+ const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
127
+ const L = Math.sqrt(v.x * v.x + v.y * v.y);
128
+ if (L < 1e-9) {
129
+ return new THREE.BufferGeometry();
130
+ }
131
+ const nUnit = { x: -v.y / L, y: v.x / L };
132
+ // Get junction data for start and end (exactly like demo)
133
+ const keyStart = pointToKey(wallStart);
134
+ const keyEnd = pointToKey(wallEnd);
135
+ const startJunction = junctionData.get(keyStart)?.get(wallNode.id);
136
+ const endJunction = junctionData.get(keyEnd)?.get(wallNode.id);
137
+ // Calculate polygon corners in world coordinates (exactly like demo)
138
+ // p_start_L = left side at start
139
+ // p_start_R = right side at start
140
+ // p_end_L = left side at end
141
+ // p_end_R = right side at end
142
+ const p_start_L = startJunction?.left || {
143
+ x: wallStart.x + nUnit.x * halfT,
144
+ y: wallStart.y + nUnit.y * halfT,
145
+ };
146
+ const p_start_R = startJunction?.right || {
147
+ x: wallStart.x - nUnit.x * halfT,
148
+ y: wallStart.y - nUnit.y * halfT,
149
+ };
150
+ // At end, SWAP left/right from junction data (exactly like demo)
151
+ // This is because junction stores left/right relative to OUTGOING direction,
152
+ // which is reversed at the end of the wall
153
+ const p_end_L = endJunction?.right || {
154
+ x: wallEnd.x + nUnit.x * halfT,
155
+ y: wallEnd.y + nUnit.y * halfT,
156
+ };
157
+ const p_end_R = endJunction?.left || {
158
+ x: wallEnd.x - nUnit.x * halfT,
159
+ y: wallEnd.y - nUnit.y * halfT,
160
+ };
161
+ // Build polygon points (exactly like demo)
162
+ // Order: start-right -> end-right -> [end center] -> end-left -> start-left -> [start center]
163
+ const polyPoints = [p_start_R, p_end_R];
164
+ if (endJunction) {
165
+ polyPoints.push(wallEnd); // Add center vertex at junction
166
+ }
167
+ polyPoints.push(p_end_L, p_start_L);
168
+ if (startJunction) {
169
+ polyPoints.push(wallStart); // Add center vertex at junction
170
+ }
171
+ // Transform world coordinates to wall-local coordinates
172
+ // Wall-local: x along wall, z perpendicular (thickness direction)
173
+ const wallAngle = Math.atan2(v.y, v.x);
174
+ const cosA = Math.cos(-wallAngle);
175
+ const sinA = Math.sin(-wallAngle);
176
+ const worldToLocal = (worldPt) => {
177
+ const dx = worldPt.x - wallStart.x;
178
+ const dy = worldPt.y - wallStart.y;
179
+ return {
180
+ x: dx * cosA - dy * sinA,
181
+ z: dx * sinA + dy * cosA,
182
+ };
183
+ };
184
+ // Convert polygon to local coordinates
185
+ const localPoints = polyPoints.map(worldToLocal);
186
+ // Build THREE.js shape
187
+ // Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
188
+ // The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
189
+ const footprint = new THREE.Shape();
190
+ footprint.moveTo(localPoints[0].x, -localPoints[0].z);
191
+ for (let i = 1; i < localPoints.length; i++) {
192
+ footprint.lineTo(localPoints[i].x, -localPoints[i].z);
193
+ }
194
+ footprint.closePath();
195
+ // Extrude along Z by height
196
+ const geometry = new THREE.ExtrudeGeometry(footprint, {
197
+ depth: height,
198
+ bevelEnabled: false,
199
+ });
200
+ // Rotate so extrusion direction (Z) becomes height direction (Y)
201
+ geometry.rotateX(-Math.PI / 2);
202
+ // Translate by slab elevation (works for both positive and negative values)
203
+ if (slabElevation !== 0) {
204
+ geometry.translate(0, slabElevation, 0);
205
+ }
206
+ geometry.computeVertexNormals();
207
+ // Apply CSG subtraction for cutouts (doors/windows)
208
+ const cutoutBrushes = collectCutoutBrushes(wallNode, childrenNodes, thickness);
209
+ if (cutoutBrushes.length === 0) {
210
+ return geometry;
211
+ }
212
+ // Create wall brush from geometry
213
+ const wallBrush = new Brush(geometry);
214
+ wallBrush.updateMatrixWorld();
215
+ // Subtract each cutout from the wall
216
+ let resultBrush = wallBrush;
217
+ for (const cutoutBrush of cutoutBrushes) {
218
+ cutoutBrush.updateMatrixWorld();
219
+ const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
220
+ if (resultBrush !== wallBrush) {
221
+ resultBrush.geometry.dispose();
222
+ }
223
+ resultBrush = newResult;
224
+ }
225
+ // Clean up
226
+ wallBrush.geometry.dispose();
227
+ for (const brush of cutoutBrushes) {
228
+ brush.geometry.dispose();
229
+ }
230
+ const resultGeometry = resultBrush.geometry;
231
+ resultGeometry.computeVertexNormals();
232
+ return resultGeometry;
233
+ }
234
+ /**
235
+ * Collects cutout brushes from child items for CSG subtraction
236
+ * The cutout mesh is a plane, so we extrude it into a box that goes through the wall
237
+ */
238
+ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
239
+ const brushes = [];
240
+ const wallMesh = sceneRegistry.nodes.get(wallNode.id);
241
+ if (!wallMesh)
242
+ return brushes;
243
+ // Get wall's world matrix inverse to transform cutouts to wall-local space
244
+ wallMesh.updateMatrixWorld();
245
+ const wallMatrixInverse = wallMesh.matrixWorld.clone().invert();
246
+ for (const child of childrenNodes) {
247
+ if (child.type !== 'item')
248
+ continue;
249
+ const childMesh = sceneRegistry.nodes.get(child.id);
250
+ if (!childMesh)
251
+ continue;
252
+ const cutoutMesh = childMesh.getObjectByName('cutout');
253
+ if (!cutoutMesh)
254
+ continue;
255
+ // Get the cutout's bounding box in world space
256
+ cutoutMesh.updateMatrixWorld();
257
+ const positions = cutoutMesh.geometry?.attributes?.position;
258
+ if (!positions)
259
+ continue;
260
+ // Calculate bounds in wall-local space
261
+ const v3 = new THREE.Vector3();
262
+ let minX = Infinity, maxX = -Infinity;
263
+ let minY = Infinity, maxY = -Infinity;
264
+ for (let i = 0; i < positions.count; i++) {
265
+ v3.fromBufferAttribute(positions, i);
266
+ v3.applyMatrix4(cutoutMesh.matrixWorld);
267
+ v3.applyMatrix4(wallMatrixInverse);
268
+ minX = Math.min(minX, v3.x);
269
+ maxX = Math.max(maxX, v3.x);
270
+ minY = Math.min(minY, v3.y);
271
+ maxY = Math.max(maxY, v3.y);
272
+ }
273
+ if (!Number.isFinite(minX))
274
+ continue;
275
+ // Create a box geometry that extends through the wall thickness
276
+ const width = maxX - minX;
277
+ const height = maxY - minY;
278
+ const depth = wallThickness * 2; // Extend beyond wall to ensure clean cut
279
+ const boxGeo = new THREE.BoxGeometry(width, height, depth);
280
+ // Position box at the center of the cutout
281
+ boxGeo.translate(minX + width / 2, minY + height / 2, 0);
282
+ const brush = new Brush(boxGeo);
283
+ brushes.push(brush);
284
+ }
285
+ return brushes;
286
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Type guard to check if a value is a plain object (and not null or an array).
3
+ * Useful for narrowing down Zod's generic JSON types.
4
+ */
5
+ export declare const isObject: (val: unknown) => val is Record<string, any>;
6
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/utils/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,OAAO,KAAG,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAEhE,CAAA"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Type guard to check if a value is a plain object (and not null or an array).
3
+ * Useful for narrowing down Zod's generic JSON types.
4
+ */
5
+ export const isObject = (val) => {
6
+ return val !== null && typeof val === 'object' && !Array.isArray(val);
7
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@pascal-app/core",
3
+ "version": "0.1.3",
4
+ "description": "Core library for Pascal 3D building editor",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc --declaration --emitDeclarationOnly && tsc",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "peerDependencies": {
24
+ "@react-three/drei": "^10",
25
+ "@react-three/fiber": "^9",
26
+ "react": "^18 || ^19",
27
+ "three": "^0.182"
28
+ },
29
+ "dependencies": {
30
+ "dedent": "^1.7.1",
31
+ "idb-keyval": "^6.2.2",
32
+ "mitt": "^3.0.1",
33
+ "nanoid": "^5.1.6",
34
+ "zod": "^4.3.5",
35
+ "zundo": "^2.3.0",
36
+ "zustand": "^5"
37
+ },
38
+ "devDependencies": {
39
+ "@repo/typescript-config": "*",
40
+ "@types/react": "^19.2.2",
41
+ "typescript": "5.9.2",
42
+ "@types/three": "^0.182.0"
43
+ },
44
+ "keywords": [
45
+ "3d",
46
+ "building",
47
+ "editor",
48
+ "architecture",
49
+ "webgpu",
50
+ "three.js"
51
+ ],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/your-username/pascal-editor.git",
55
+ "directory": "packages/core"
56
+ },
57
+ "license": "MIT"
58
+ }