@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.
- package/dist/events/bus.d.ts +42 -0
- package/dist/events/bus.d.ts.map +1 -0
- package/dist/events/bus.js +13 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts +18 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -0
- package/dist/hooks/scene-registry/scene-registry.js +35 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +90 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +466 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +4 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +115 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +23 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/spatial-grid.js +115 -0
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts +16 -0
- package/dist/hooks/spatial-grid/use-spatial-query.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/use-spatial-query.js +14 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts +47 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.d.ts.map +1 -0
- package/dist/hooks/spatial-grid/wall-spatial-grid.js +113 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/lib/asset-storage.d.ts +11 -0
- package/dist/lib/asset-storage.d.ts.map +1 -0
- package/dist/lib/asset-storage.js +48 -0
- package/dist/lib/space-detection.d.ts +34 -0
- package/dist/lib/space-detection.d.ts.map +1 -0
- package/dist/lib/space-detection.js +499 -0
- package/dist/schema/base.d.ts +30 -0
- package/dist/schema/base.d.ts.map +1 -0
- package/dist/schema/base.js +25 -0
- package/dist/schema/camera.d.ts +13 -0
- package/dist/schema/camera.d.ts.map +1 -0
- package/dist/schema/camera.js +9 -0
- package/dist/schema/index.d.ts +17 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +18 -0
- package/dist/schema/nodes/building.d.ts +25 -0
- package/dist/schema/nodes/building.d.ts.map +1 -0
- package/dist/schema/nodes/building.js +16 -0
- package/dist/schema/nodes/ceiling.d.ts +25 -0
- package/dist/schema/nodes/ceiling.d.ts.map +1 -0
- package/dist/schema/nodes/ceiling.js +16 -0
- package/dist/schema/nodes/guide.d.ts +27 -0
- package/dist/schema/nodes/guide.d.ts.map +1 -0
- package/dist/schema/nodes/guide.js +11 -0
- package/dist/schema/nodes/item.d.ts +65 -0
- package/dist/schema/nodes/item.d.ts.map +1 -0
- package/dist/schema/nodes/item.js +38 -0
- package/dist/schema/nodes/level.d.ts +24 -0
- package/dist/schema/nodes/level.d.ts.map +1 -0
- package/dist/schema/nodes/level.js +21 -0
- package/dist/schema/nodes/roof.d.ts +28 -0
- package/dist/schema/nodes/roof.d.ts.map +1 -0
- package/dist/schema/nodes/roof.js +28 -0
- package/dist/schema/nodes/scan.d.ts +27 -0
- package/dist/schema/nodes/scan.d.ts.map +1 -0
- package/dist/schema/nodes/scan.js +11 -0
- package/dist/schema/nodes/site.d.ts +90 -0
- package/dist/schema/nodes/site.d.ts.map +1 -0
- package/dist/schema/nodes/site.js +39 -0
- package/dist/schema/nodes/slab.d.ts +24 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -0
- package/dist/schema/nodes/slab.js +15 -0
- package/dist/schema/nodes/wall.d.ts +37 -0
- package/dist/schema/nodes/wall.d.ts.map +1 -0
- package/dist/schema/nodes/wall.js +30 -0
- package/dist/schema/nodes/zone.d.ts +24 -0
- package/dist/schema/nodes/zone.d.ts.map +1 -0
- package/dist/schema/nodes/zone.js +22 -0
- package/dist/schema/types.d.ts +339 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +25 -0
- package/dist/store/actions/node-actions.d.ts +12 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -0
- package/dist/store/actions/node-actions.js +121 -0
- package/dist/store/use-scene.d.ts +31 -0
- package/dist/store/use-scene.d.ts.map +1 -0
- package/dist/store/use-scene.js +127 -0
- package/dist/systems/ceiling/ceiling-system.d.ts +8 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -0
- package/dist/systems/ceiling/ceiling-system.js +65 -0
- package/dist/systems/item/item-system.d.ts +2 -0
- package/dist/systems/item/item-system.d.ts.map +1 -0
- package/dist/systems/item/item-system.js +43 -0
- package/dist/systems/roof/roof-system.d.ts +8 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-system.js +254 -0
- package/dist/systems/slab/slab-system.d.ts +8 -0
- package/dist/systems/slab/slab-system.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.js +117 -0
- package/dist/systems/wall/wall-mitering.d.ts +32 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -0
- package/dist/systems/wall/wall-mitering.js +214 -0
- package/dist/systems/wall/wall-system.d.ts +12 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -0
- package/dist/systems/wall/wall-system.js +286 -0
- package/dist/utils/types.d.ts +6 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +7 -0
- 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 @@
|
|
|
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"}
|
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
|
+
}
|