@pascal-app/core 1.0.1 → 1.0.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/hooks/spatial-grid/spatial-grid-sync.d.ts +42 -8
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/lib/node-visibility.d.ts +12 -0
- package/dist/lib/node-visibility.d.ts.map +1 -0
- package/dist/lib/node-visibility.js +12 -0
- package/dist/procedural-items/query.d.ts +1 -0
- package/dist/procedural-items/query.d.ts.map +1 -1
- package/dist/procedural-items/query.js +67 -23
- package/dist/registry/handles.d.ts +28 -1
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/subtree.d.ts.map +1 -1
- package/dist/registry/subtree.js +6 -0
- package/dist/registry/types.d.ts +12 -2
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +1 -0
- package/dist/schema/nodes/block.d.ts +2 -2
- package/dist/schema/nodes/block.d.ts.map +1 -1
- package/dist/schema/nodes/block.js +21 -8
- package/dist/schema/nodes/cabinet.d.ts +2 -2
- package/dist/schema/nodes/curtain-wall.d.ts +129 -0
- package/dist/schema/nodes/curtain-wall.d.ts.map +1 -0
- package/dist/schema/nodes/curtain-wall.js +49 -0
- package/dist/schema/nodes/door.d.ts +2 -2
- package/dist/schema/nodes/wall.d.ts +73 -0
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +3 -0
- package/dist/schema/types.d.ts +76 -3
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/services/__fixtures__/hosting.d.ts +3 -0
- package/dist/services/__fixtures__/hosting.d.ts.map +1 -0
- package/dist/services/__fixtures__/hosting.js +9 -0
- package/dist/services/__fixtures__/shelf-surface-rows.d.ts +4 -0
- package/dist/services/__fixtures__/shelf-surface-rows.d.ts.map +1 -0
- package/dist/services/__fixtures__/shelf-surface-rows.js +18 -0
- package/dist/services/hosting.d.ts +3 -0
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +24 -7
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -3
- package/dist/services/snap.d.ts +3 -0
- package/dist/services/snap.d.ts.map +1 -1
- package/dist/services/snap.js +11 -0
- package/dist/services/surface-hosting.d.ts +6 -2
- package/dist/services/surface-hosting.d.ts.map +1 -1
- package/dist/services/surface-hosting.js +47 -10
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +3 -96
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +13 -1
- package/dist/systems/stair/stair-opening-preview.d.ts +42 -8
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/wall/wall-merge.d.ts +38 -0
- package/dist/systems/wall/wall-merge.d.ts.map +1 -0
- package/dist/systems/wall/wall-merge.js +240 -0
- package/dist/systems/wall/wall-operations.d.ts +28 -0
- package/dist/systems/wall/wall-operations.d.ts.map +1 -0
- package/dist/systems/wall/wall-operations.js +145 -0
- package/dist/systems/wall/wall-topology.d.ts +7 -0
- package/dist/systems/wall/wall-topology.d.ts.map +1 -1
- package/dist/systems/wall/wall-topology.js +53 -5
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +12 -0
- package/package.json +2 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { getWallEffectiveHeightForNodes } from '../../hooks/spatial-grid/spatial-grid-manager.js';
|
|
2
|
+
import { getEffectiveWallSurfaceMaterial, getWallSurfaceMaterialSignature, } from '../../schema/index.js';
|
|
3
|
+
function pointsEqual(a, b, tolerance = 1e-6) {
|
|
4
|
+
const dx = a[0] - b[0];
|
|
5
|
+
const dz = a[1] - b[1];
|
|
6
|
+
return dx * dx + dz * dz <= tolerance * tolerance;
|
|
7
|
+
}
|
|
8
|
+
function wallLength(wall) {
|
|
9
|
+
return Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1]);
|
|
10
|
+
}
|
|
11
|
+
export function getWallEndpointAtPoint(wall, point) {
|
|
12
|
+
if (pointsEqual(wall.start, point))
|
|
13
|
+
return 'start';
|
|
14
|
+
if (pointsEqual(wall.end, point))
|
|
15
|
+
return 'end';
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function getWallFreeEndpoint(wall, sharedPoint) {
|
|
19
|
+
return pointsEqual(wall.start, sharedPoint) ? wall.end : wall.start;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The first thing two walls disagree on (for the merge's explanation), or null.
|
|
23
|
+
* The delete heal is strict: room sides must match, and a wall following the
|
|
24
|
+
* storey never joins one with an explicit height. An explicit merge compares
|
|
25
|
+
* what is visible instead — `heightOf` resolves each wall's actual height — and
|
|
26
|
+
* leaves the sides to room detection, which reclassifies the merged wall.
|
|
27
|
+
*/
|
|
28
|
+
export function wallStyleMismatch(a, b, options) {
|
|
29
|
+
if ((a.parentId ?? null) !== (b.parentId ?? null))
|
|
30
|
+
return 'floor';
|
|
31
|
+
if (Math.abs((a.curveOffset ?? 0) - (b.curveOffset ?? 0)) > 1e-6)
|
|
32
|
+
return 'curve';
|
|
33
|
+
if (Math.abs((a.thickness ?? 0.2) - (b.thickness ?? 0.2)) > 1e-6)
|
|
34
|
+
return 'thickness';
|
|
35
|
+
const { heightOf } = options;
|
|
36
|
+
if (heightOf
|
|
37
|
+
? Math.abs(heightOf(a) - heightOf(b)) > 1e-6
|
|
38
|
+
: (a.height == null) !== (b.height == null) ||
|
|
39
|
+
Math.abs((a.height ?? 0) - (b.height ?? 0)) > 1e-6)
|
|
40
|
+
return 'height';
|
|
41
|
+
for (const side of ['interior', 'exterior']) {
|
|
42
|
+
if (getWallSurfaceMaterialSignature(getEffectiveWallSurfaceMaterial(a, side)) !==
|
|
43
|
+
getWallSurfaceMaterialSignature(getEffectiveWallSurfaceMaterial(b, side)))
|
|
44
|
+
return `${side} finish`;
|
|
45
|
+
}
|
|
46
|
+
if (options.sides && (a.frontSide !== b.frontSide || a.backSide !== b.backSide))
|
|
47
|
+
return 'room sides';
|
|
48
|
+
if (a.visible !== b.visible)
|
|
49
|
+
return 'visibility';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
export function areWallStylesCompatible(a, b) {
|
|
53
|
+
return wallStyleMismatch(a, b, { sides: true }) === null;
|
|
54
|
+
}
|
|
55
|
+
export function areWallsCollinearAcrossPoint(a, b, sharedPoint) {
|
|
56
|
+
const freeA = getWallFreeEndpoint(a, sharedPoint);
|
|
57
|
+
const freeB = getWallFreeEndpoint(b, sharedPoint);
|
|
58
|
+
const ax = freeA[0] - sharedPoint[0];
|
|
59
|
+
const az = freeA[1] - sharedPoint[1];
|
|
60
|
+
const bx = freeB[0] - sharedPoint[0];
|
|
61
|
+
const bz = freeB[1] - sharedPoint[1];
|
|
62
|
+
const lenA = Math.hypot(ax, az);
|
|
63
|
+
const lenB = Math.hypot(bx, bz);
|
|
64
|
+
if (lenA < 1e-6 || lenB < 1e-6)
|
|
65
|
+
return false;
|
|
66
|
+
const cross = (ax * bz - az * bx) / (lenA * lenB);
|
|
67
|
+
const dot = (ax * bx + az * bz) / (lenA * lenB);
|
|
68
|
+
return Math.abs(cross) <= 1e-4 && dot < -0.999;
|
|
69
|
+
}
|
|
70
|
+
export function resolveMergedWallEndpoints(primary, secondary, sharedPoint) {
|
|
71
|
+
const primaryEndpoint = getWallEndpointAtPoint(primary, sharedPoint);
|
|
72
|
+
const secondaryEndpoint = getWallEndpointAtPoint(secondary, sharedPoint);
|
|
73
|
+
if (primaryEndpoint === 'end' && secondaryEndpoint === 'start') {
|
|
74
|
+
return { start: primary.start, end: secondary.end };
|
|
75
|
+
}
|
|
76
|
+
if (primaryEndpoint === 'start' && secondaryEndpoint === 'end') {
|
|
77
|
+
return { start: secondary.start, end: primary.end };
|
|
78
|
+
}
|
|
79
|
+
// The kept wall never turns around: its sides, finishes and hosted faces stay
|
|
80
|
+
// where they are. Meeting start-to-start or end-to-end, the absorbed wall is
|
|
81
|
+
// the one read backwards.
|
|
82
|
+
if (primaryEndpoint === 'start' && secondaryEndpoint === 'start') {
|
|
83
|
+
return { start: secondary.end, end: primary.end };
|
|
84
|
+
}
|
|
85
|
+
return { start: primary.start, end: secondary.start };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Hosted nodes live in the wall's local frame (+X along it, +Z its front, yaw
|
|
89
|
+
* 0 front / π back). A wall read backwards turns that frame by 180° about Y,
|
|
90
|
+
* so the face and the yaw swap and the caller mirrors depth. Hinges, handles
|
|
91
|
+
* and swing hang off the node's own yaw (the plan symbol flips them from it),
|
|
92
|
+
* so they stay as stored.
|
|
93
|
+
*/
|
|
94
|
+
function reversedWallChildPatch(child) {
|
|
95
|
+
const patch = {};
|
|
96
|
+
if ('side' in child && (child.side === 'front' || child.side === 'back')) {
|
|
97
|
+
patch.side = child.side === 'front' ? 'back' : 'front';
|
|
98
|
+
}
|
|
99
|
+
if ('rotation' in child && Array.isArray(child.rotation)) {
|
|
100
|
+
const yaw = child.rotation[1] + Math.PI;
|
|
101
|
+
patch.rotation = [
|
|
102
|
+
child.rotation[0],
|
|
103
|
+
Math.atan2(Math.sin(yaw), Math.cos(yaw)),
|
|
104
|
+
child.rotation[2],
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
return patch;
|
|
108
|
+
}
|
|
109
|
+
export function buildMergedWallAttachmentUpdates(primary, secondary, mergedWallId, mergedStart, mergedEnd, nodes) {
|
|
110
|
+
const mergedLength = Math.max(Math.hypot(mergedEnd[0] - mergedStart[0], mergedEnd[1] - mergedStart[1]), 1e-6);
|
|
111
|
+
const tangentX = (mergedEnd[0] - mergedStart[0]) / mergedLength;
|
|
112
|
+
const tangentZ = (mergedEnd[1] - mergedStart[1]) / mergedLength;
|
|
113
|
+
const updates = [];
|
|
114
|
+
const wallChildren = [...(primary.children ?? []), ...(secondary.children ?? [])];
|
|
115
|
+
for (const childId of wallChildren) {
|
|
116
|
+
const child = nodes[childId];
|
|
117
|
+
if (!child)
|
|
118
|
+
continue;
|
|
119
|
+
// Every child moves to the kept wall: deleting the absorbed wall would
|
|
120
|
+
// otherwise take a child left on it along. Only positioned ones re-place.
|
|
121
|
+
const rehost = { parentId: mergedWallId, wallId: mergedWallId };
|
|
122
|
+
if (!('position' in child && Array.isArray(child.position))) {
|
|
123
|
+
updates.push({ id: childId, data: rehost });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const sourceWall = child.parentId === secondary.id ? secondary : primary;
|
|
127
|
+
const sourceLength = Math.max(wallLength(sourceWall), 1e-6);
|
|
128
|
+
const reversed = (sourceWall.end[0] - sourceWall.start[0]) * tangentX +
|
|
129
|
+
(sourceWall.end[1] - sourceWall.start[1]) * tangentZ <
|
|
130
|
+
0;
|
|
131
|
+
const mirrored = reversed ? reversedWallChildPatch(child) : {};
|
|
132
|
+
const localX = typeof child.position[0] === 'number' ? child.position[0] : 0;
|
|
133
|
+
const worldX = sourceWall.start[0] + ((sourceWall.end[0] - sourceWall.start[0]) * localX) / sourceLength;
|
|
134
|
+
const worldZ = sourceWall.start[1] + ((sourceWall.end[1] - sourceWall.start[1]) * localX) / sourceLength;
|
|
135
|
+
const nextLocalX = Math.max(0, Math.min(mergedLength, (worldX - mergedStart[0]) * tangentX + (worldZ - mergedStart[1]) * tangentZ));
|
|
136
|
+
updates.push({
|
|
137
|
+
id: childId,
|
|
138
|
+
data: {
|
|
139
|
+
...rehost,
|
|
140
|
+
...mirrored,
|
|
141
|
+
position: [
|
|
142
|
+
nextLocalX,
|
|
143
|
+
child.position[1],
|
|
144
|
+
reversed ? -child.position[2] : child.position[2],
|
|
145
|
+
],
|
|
146
|
+
...('wallT' in child ? { wallT: nextLocalX / mergedLength } : {}),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return updates;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Merges a straight run of adjoining walls into one — the inverse of a split.
|
|
154
|
+
* Every joint must hold exactly these walls (a T or a cross stays split), and
|
|
155
|
+
* neighbours must continue in line and look alike (`wallStyleMismatch`: same
|
|
156
|
+
* thickness, visible height and finish). The wall with the most attachments
|
|
157
|
+
* keeps its id and height mode; openings and wall items keep their world
|
|
158
|
+
* position on it.
|
|
159
|
+
*/
|
|
160
|
+
export function planWallMerge(nodes, wallIds) {
|
|
161
|
+
const walls = [...new Set(wallIds)]
|
|
162
|
+
.map((id) => nodes[id])
|
|
163
|
+
.filter((node) => node?.type === 'wall');
|
|
164
|
+
if (walls.length < 2 || walls.length !== new Set(wallIds).size)
|
|
165
|
+
throw Error('Select two or more walls to merge.');
|
|
166
|
+
if (walls.some((wall) => (wall.curveOffset ?? 0) !== 0))
|
|
167
|
+
throw Error('Only straight walls can be merged.');
|
|
168
|
+
const levelId = walls[0].parentId ?? null;
|
|
169
|
+
if (walls.some((wall) => (wall.parentId ?? null) !== levelId))
|
|
170
|
+
throw Error('Merge walls on the same floor.');
|
|
171
|
+
const virtual = { ...nodes };
|
|
172
|
+
const [primary, ...rest] = [...walls].sort((a, b) => (b.children?.length ?? 0) - (a.children?.length ?? 0) || a.id.localeCompare(b.id));
|
|
173
|
+
let merged = primary;
|
|
174
|
+
let remaining = rest;
|
|
175
|
+
while (remaining.length > 0) {
|
|
176
|
+
let next;
|
|
177
|
+
let joint;
|
|
178
|
+
for (const wall of remaining) {
|
|
179
|
+
joint = [merged.start, merged.end].find((end) => getWallEndpointAtPoint(wall, end) !== null);
|
|
180
|
+
if (joint) {
|
|
181
|
+
next = wall;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!(next && joint))
|
|
186
|
+
throw Error('Merge walls that touch end to end.');
|
|
187
|
+
const atJoint = Object.values(virtual).filter((node) => node?.type === 'wall' &&
|
|
188
|
+
(node.parentId ?? null) === levelId &&
|
|
189
|
+
getWallEndpointAtPoint(node, joint) !== null);
|
|
190
|
+
if (atJoint.length !== 2)
|
|
191
|
+
throw Error('Another wall meets this joint, so merging would disconnect it.');
|
|
192
|
+
if (!areWallsCollinearAcrossPoint(merged, next, joint))
|
|
193
|
+
throw Error('Merge walls that continue in a straight line.');
|
|
194
|
+
const mismatch = wallStyleMismatch(merged, next, {
|
|
195
|
+
sides: false,
|
|
196
|
+
heightOf: (wall) => getWallEffectiveHeightForNodes(wall, virtual),
|
|
197
|
+
});
|
|
198
|
+
if (mismatch)
|
|
199
|
+
throw Error(`These walls have a different ${mismatch}.`);
|
|
200
|
+
const { start, end } = resolveMergedWallEndpoints(merged, next, joint);
|
|
201
|
+
for (const update of buildMergedWallAttachmentUpdates(merged, next, merged.id, start, end, virtual)) {
|
|
202
|
+
virtual[update.id] = { ...virtual[update.id], ...update.data };
|
|
203
|
+
}
|
|
204
|
+
merged = {
|
|
205
|
+
...merged,
|
|
206
|
+
start,
|
|
207
|
+
end,
|
|
208
|
+
// Ids without a node are dropped rather than carried onto the kept wall.
|
|
209
|
+
children: [
|
|
210
|
+
...new Set([
|
|
211
|
+
...(merged.children ?? []),
|
|
212
|
+
...(next.children ?? []).filter((id) => virtual[id]),
|
|
213
|
+
]),
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
virtual[merged.id] = merged;
|
|
217
|
+
delete virtual[next.id];
|
|
218
|
+
for (const node of Object.values(virtual)) {
|
|
219
|
+
if (node?.type !== 'zone' || !node.boundaryWallIds?.includes(next.id))
|
|
220
|
+
continue;
|
|
221
|
+
virtual[node.id] = {
|
|
222
|
+
...node,
|
|
223
|
+
boundaryWallIds: [
|
|
224
|
+
...new Set(node.boundaryWallIds.map((id) => (id === next.id ? merged.id : id))),
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
remaining = remaining.filter((wall) => wall !== next);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
changes: {
|
|
232
|
+
create: [],
|
|
233
|
+
update: Object.values(virtual)
|
|
234
|
+
.filter((node) => nodes[node.id] && node !== nodes[node.id])
|
|
235
|
+
.map((node) => ({ id: node.id, data: node })),
|
|
236
|
+
delete: walls.filter((wall) => !virtual[wall.id]).map((wall) => wall.id),
|
|
237
|
+
},
|
|
238
|
+
wallId: merged.id,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnyNode, AnyNodeId, WallNode } from '../../schema/index.js';
|
|
2
|
+
import type { WallPlanPoint } from './wall-move.js';
|
|
3
|
+
import { type WallTopologyChanges } from './wall-topology.js';
|
|
4
|
+
export declare function wallRectangleCorners(a: WallPlanPoint, b: WallPlanPoint): WallPlanPoint[];
|
|
5
|
+
/** Plan against a scratch graph so four sides commit atomically, including junction splits. */
|
|
6
|
+
export declare function planWallRectangle(nodes: Record<AnyNodeId, AnyNode>, args: {
|
|
7
|
+
levelId: AnyNodeId;
|
|
8
|
+
start: WallPlanPoint;
|
|
9
|
+
end: WallPlanPoint;
|
|
10
|
+
wallDefaults?: Partial<WallNode>;
|
|
11
|
+
}): {
|
|
12
|
+
changes: WallTopologyChanges;
|
|
13
|
+
walls: WallNode[];
|
|
14
|
+
};
|
|
15
|
+
/** Explicit split keeps the first wall's identity and reuses the junction planner's host migration. */
|
|
16
|
+
export declare function planWallDivision(nodes: Record<AnyNodeId, AnyNode>, wallId: WallNode['id'], distance: number): {
|
|
17
|
+
changes: WallTopologyChanges;
|
|
18
|
+
point: WallPlanPoint;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Several cuts as one edit. Cutting farthest first keeps the first wall's
|
|
22
|
+
* identity at every step, so the nearer distances stay valid on it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function planWallDivisions(nodes: Record<AnyNodeId, AnyNode>, wallId: WallNode['id'], distances: readonly number[]): {
|
|
25
|
+
changes: WallTopologyChanges;
|
|
26
|
+
points: WallPlanPoint[];
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=wall-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-operations.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEhE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAA;AAExB,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,aAAa,EAAE,CAiBxF;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE;IACJ,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,EAAE,aAAa,CAAA;IACpB,GAAG,EAAE,aAAa,CAAA;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;CACjC,GACA;IAAE,OAAO,EAAE,mBAAmB,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,CAyCrD;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,EACtB,QAAQ,EAAE,MAAM;;WAsD+B,aAAa;EAC7D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,EACtB,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B;IAAE,OAAO,EAAE,mBAAmB,CAAC;IAAC,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,CAyB3D"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getWallCurveFrameAt, getWallCurveLength } from './wall-curve.js';
|
|
2
|
+
import { planWallInsertion, planWallSplitAtPoint, uncoveredWallSegments, } from './wall-topology.js';
|
|
3
|
+
export function wallRectangleCorners(a, b) {
|
|
4
|
+
if (![...a, ...b].every(Number.isFinite) ||
|
|
5
|
+
Math.abs(a[0] - b[0]) < 0.05 ||
|
|
6
|
+
Math.abs(a[1] - b[1]) < 0.05)
|
|
7
|
+
return [];
|
|
8
|
+
const x = Math.min(a[0], b[0]);
|
|
9
|
+
const z = Math.min(a[1], b[1]);
|
|
10
|
+
const right = Math.max(a[0], b[0]);
|
|
11
|
+
const top = Math.max(a[1], b[1]);
|
|
12
|
+
return [
|
|
13
|
+
[x, z],
|
|
14
|
+
[right, z],
|
|
15
|
+
[right, top],
|
|
16
|
+
[x, top],
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
/** Plan against a scratch graph so four sides commit atomically, including junction splits. */
|
|
20
|
+
export function planWallRectangle(nodes, args) {
|
|
21
|
+
if (nodes[args.levelId]?.type !== 'level')
|
|
22
|
+
throw Error('Select an editable floor.');
|
|
23
|
+
const corners = wallRectangleCorners(args.start, args.end);
|
|
24
|
+
if (!corners.length)
|
|
25
|
+
throw Error('Both rectangle dimensions must be at least 5 cm.');
|
|
26
|
+
const virtual = { ...nodes };
|
|
27
|
+
const added = new Set();
|
|
28
|
+
for (let i = 0; i < 4; i++) {
|
|
29
|
+
// A side that runs along existing walls only adds what they don't cover,
|
|
30
|
+
// so snapping to a wall and dragging past its end reuses it rather than
|
|
31
|
+
// doubling it.
|
|
32
|
+
const levelWalls = Object.values(virtual).filter((node) => node.type === 'wall' && node.parentId === args.levelId);
|
|
33
|
+
for (const [start, end] of uncoveredWallSegments(corners[i], corners[(i + 1) % 4], levelWalls)) {
|
|
34
|
+
const result = planWallInsertion(virtual, { ...args, start, end, joinRadius: 0 });
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
throw Error('The rectangle cannot be drawn here.');
|
|
37
|
+
for (const id of result.plan.changes.delete)
|
|
38
|
+
delete virtual[id];
|
|
39
|
+
for (const { node, parentId } of result.plan.changes.create) {
|
|
40
|
+
virtual[node.id] = { ...node, ...(parentId ? { parentId } : {}) };
|
|
41
|
+
}
|
|
42
|
+
for (const wall of result.plan.insertedWalls)
|
|
43
|
+
added.add(wall.id);
|
|
44
|
+
for (const { id, data } of result.plan.changes.update) {
|
|
45
|
+
if (virtual[id])
|
|
46
|
+
virtual[id] = { ...virtual[id], ...data };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const created = Object.values(virtual).filter((n) => !nodes[n.id]);
|
|
51
|
+
return {
|
|
52
|
+
changes: {
|
|
53
|
+
create: created.map((node) => ({ node, parentId: node.parentId })),
|
|
54
|
+
update: Object.values(virtual)
|
|
55
|
+
.filter((n) => nodes[n.id] && n !== nodes[n.id])
|
|
56
|
+
.map((n) => ({ id: n.id, data: n })),
|
|
57
|
+
delete: Object.keys(nodes).filter((id) => !virtual[id]),
|
|
58
|
+
},
|
|
59
|
+
walls: created.filter((n) => n.type === 'wall' && added.has(n.id)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Explicit split keeps the first wall's identity and reuses the junction planner's host migration. */
|
|
63
|
+
export function planWallDivision(nodes, wallId, distance) {
|
|
64
|
+
const wall = nodes[wallId];
|
|
65
|
+
if (wall?.type !== 'wall' || !wall.parentId)
|
|
66
|
+
throw Error('Select a wall.');
|
|
67
|
+
const length = getWallCurveLength(wall);
|
|
68
|
+
if (!Number.isFinite(distance) || distance < 0.05 || distance > length - 0.05)
|
|
69
|
+
throw Error('Keep at least 5 cm on each side of the split.');
|
|
70
|
+
const point = getWallCurveFrameAt(wall, distance / length).point;
|
|
71
|
+
const result = planWallSplitAtPoint(nodes, {
|
|
72
|
+
levelId: wall.parentId,
|
|
73
|
+
point: [point.x, point.y],
|
|
74
|
+
radius: 0.001,
|
|
75
|
+
ignoreWallIds: Object.values(nodes)
|
|
76
|
+
.filter((n) => n.type === 'wall' && n.id !== wallId)
|
|
77
|
+
.map((n) => n.id),
|
|
78
|
+
});
|
|
79
|
+
if (!result.ok || result.plan.changes.create.length !== 2)
|
|
80
|
+
throw Error('Move the split away from doors, windows and mounted objects.');
|
|
81
|
+
const [first, second] = result.plan.changes.create;
|
|
82
|
+
const firstId = first.node.id;
|
|
83
|
+
const changes = {
|
|
84
|
+
create: [second],
|
|
85
|
+
delete: [],
|
|
86
|
+
update: [
|
|
87
|
+
{
|
|
88
|
+
id: wallId,
|
|
89
|
+
data: { ...first.node, id: wallId, parentId: wall.parentId },
|
|
90
|
+
},
|
|
91
|
+
...result.plan.changes.update.map((op) => ({
|
|
92
|
+
...op,
|
|
93
|
+
data: {
|
|
94
|
+
...op.data,
|
|
95
|
+
...(op.data.parentId === firstId ? { parentId: wallId } : {}),
|
|
96
|
+
...('wallId' in op.data && op.data.wallId === firstId ? { wallId } : {}),
|
|
97
|
+
},
|
|
98
|
+
})),
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
for (const node of Object.values(nodes)) {
|
|
102
|
+
if (node.type === 'zone' &&
|
|
103
|
+
node.parentId === wall.parentId &&
|
|
104
|
+
node.boundaryWallIds?.includes(wallId)) {
|
|
105
|
+
changes.update.push({
|
|
106
|
+
id: node.id,
|
|
107
|
+
data: {
|
|
108
|
+
boundaryWallIds: node.boundaryWallIds.flatMap((id) => id === wallId ? [wallId, second.node.id] : [id]),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { changes, point: [point.x, point.y] };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Several cuts as one edit. Cutting farthest first keeps the first wall's
|
|
117
|
+
* identity at every step, so the nearer distances stay valid on it.
|
|
118
|
+
*/
|
|
119
|
+
export function planWallDivisions(nodes, wallId, distances) {
|
|
120
|
+
const virtual = { ...nodes };
|
|
121
|
+
const points = [];
|
|
122
|
+
for (const distance of [...distances].sort((a, b) => b - a)) {
|
|
123
|
+
const plan = planWallDivision(virtual, wallId, distance);
|
|
124
|
+
for (const { node, parentId } of plan.changes.create) {
|
|
125
|
+
virtual[node.id] = { ...node, ...(parentId ? { parentId } : {}) };
|
|
126
|
+
}
|
|
127
|
+
for (const { id, data } of plan.changes.update) {
|
|
128
|
+
if (virtual[id])
|
|
129
|
+
virtual[id] = { ...virtual[id], ...data };
|
|
130
|
+
}
|
|
131
|
+
points.push(plan.point);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
changes: {
|
|
135
|
+
create: Object.values(virtual)
|
|
136
|
+
.filter((n) => !nodes[n.id])
|
|
137
|
+
.map((node) => ({ node, parentId: node.parentId })),
|
|
138
|
+
update: Object.values(virtual)
|
|
139
|
+
.filter((n) => nodes[n.id] && n !== nodes[n.id])
|
|
140
|
+
.map((n) => ({ id: n.id, data: n })),
|
|
141
|
+
delete: [],
|
|
142
|
+
},
|
|
143
|
+
points: points.reverse(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -37,6 +37,13 @@ export type WallPointSplitResult = {
|
|
|
37
37
|
ok: false;
|
|
38
38
|
reason: 'no-host';
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* The parts of `start→end` that no straight wall already runs along, in order.
|
|
42
|
+
* A side drawn along an existing wall and past its end yields the overhang
|
|
43
|
+
* only, so the existing wall is reused instead of doubled. Gaps too short to
|
|
44
|
+
* be a wall are treated as covered.
|
|
45
|
+
*/
|
|
46
|
+
export declare function uncoveredWallSegments(start: WallPlanPoint, end: WallPlanPoint, walls: WallNode[]): Array<[WallPlanPoint, WallPlanPoint]>;
|
|
40
47
|
export declare function planWallSplitAtPoint(nodes: Record<AnyNodeId, AnyNode>, args: {
|
|
41
48
|
levelId: AnyNodeId | null;
|
|
42
49
|
point: WallPlanPoint;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-topology.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-topology.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAId,KAAK,QAAQ,EAGd,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACtD,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC,CAAA;IACxD,MAAM,EAAE,SAAS,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,mBAAmB,CAAA;IAC5B,aAAa,EAAE,QAAQ,EAAE,CAAA;IACzB,cAAc,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC9B,aAAa,EAAE,aAAa,CAAA;IAC5B,WAAW,EAAE,aAAa,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,uBAAuB,GAAG,mBAAmB,CAAA;CACtD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAAG,qBAAqB,CAAA;AAE/F,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,mBAAmB,CAAA;IAC5B,KAAK,EAAE,aAAa,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"wall-topology.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-topology.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAId,KAAK,QAAQ,EAGd,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACtD,MAAM,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC,CAAA;IACxD,MAAM,EAAE,SAAS,EAAE,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,mBAAmB,CAAA;IAC5B,aAAa,EAAE,QAAQ,EAAE,CAAA;IACzB,cAAc,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC9B,aAAa,EAAE,aAAa,CAAA;IAC5B,WAAW,EAAE,aAAa,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,uBAAuB,GAAG,mBAAmB,CAAA;CACtD,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAA;CAAE,GAAG,qBAAqB,CAAA;AAE/F,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,mBAAmB,CAAA;IAC5B,KAAK,EAAE,aAAa,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAA;AAkEpC;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,QAAQ,EAAE,GAChB,KAAK,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CAiBvC;AAwDD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE;IACJ,OAAO,EAAE,SAAS,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,aAAa,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC,GACA,oBAAoB,CAqCtB;AA4OD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE;IACJ,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,EAAE,aAAa,CAAA;IACpB,GAAG,EAAE,aAAa,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;CACjC,GACA,mBAAmB,CAoHrB"}
|
|
@@ -13,12 +13,13 @@ function distanceSquared(a, b) {
|
|
|
13
13
|
function isSegmentLongEnough(start, end) {
|
|
14
14
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
/** Where straight walls run along `start→end`, as sorted parameter intervals of it. */
|
|
17
|
+
function collinearWallIntervals(start, end, walls) {
|
|
17
18
|
const dx = end[0] - start[0];
|
|
18
19
|
const dz = end[1] - start[1];
|
|
19
20
|
const lengthSquared = dx * dx + dz * dz;
|
|
20
21
|
if (lengthSquared <= WALL_INTERSECTION_EPSILON * WALL_INTERSECTION_EPSILON)
|
|
21
|
-
return
|
|
22
|
+
return null;
|
|
22
23
|
const length = Math.sqrt(lengthSquared);
|
|
23
24
|
const intervals = [];
|
|
24
25
|
for (const wall of walls) {
|
|
@@ -37,9 +38,15 @@ function wallSegmentsCoverSegment(start, end, walls) {
|
|
|
37
38
|
intervals.push([intervalStart, intervalEnd]);
|
|
38
39
|
}
|
|
39
40
|
intervals.sort((left, right) => left[0] - right[0]);
|
|
40
|
-
|
|
41
|
+
return { intervals, length };
|
|
42
|
+
}
|
|
43
|
+
function wallSegmentsCoverSegment(start, end, walls) {
|
|
44
|
+
const collinear = collinearWallIntervals(start, end, walls);
|
|
45
|
+
if (!collinear)
|
|
46
|
+
return false;
|
|
47
|
+
const parameterTolerance = WALL_INTERSECTION_EPSILON / collinear.length;
|
|
41
48
|
let coveredUntil = 0;
|
|
42
|
-
for (const [intervalStart, intervalEnd] of intervals) {
|
|
49
|
+
for (const [intervalStart, intervalEnd] of collinear.intervals) {
|
|
43
50
|
if (intervalStart > coveredUntil + parameterTolerance)
|
|
44
51
|
return false;
|
|
45
52
|
coveredUntil = Math.max(coveredUntil, intervalEnd);
|
|
@@ -48,6 +55,32 @@ function wallSegmentsCoverSegment(start, end, walls) {
|
|
|
48
55
|
}
|
|
49
56
|
return false;
|
|
50
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The parts of `start→end` that no straight wall already runs along, in order.
|
|
60
|
+
* A side drawn along an existing wall and past its end yields the overhang
|
|
61
|
+
* only, so the existing wall is reused instead of doubled. Gaps too short to
|
|
62
|
+
* be a wall are treated as covered.
|
|
63
|
+
*/
|
|
64
|
+
export function uncoveredWallSegments(start, end, walls) {
|
|
65
|
+
const collinear = collinearWallIntervals(start, end, walls);
|
|
66
|
+
if (!collinear)
|
|
67
|
+
return [];
|
|
68
|
+
const minimumGap = WALL_MIN_LENGTH / collinear.length;
|
|
69
|
+
const at = (t) => [
|
|
70
|
+
start[0] + (end[0] - start[0]) * t,
|
|
71
|
+
start[1] + (end[1] - start[1]) * t,
|
|
72
|
+
];
|
|
73
|
+
const segments = [];
|
|
74
|
+
let coveredUntil = 0;
|
|
75
|
+
for (const [intervalStart, intervalEnd] of collinear.intervals) {
|
|
76
|
+
if (intervalStart > coveredUntil + minimumGap)
|
|
77
|
+
segments.push([at(coveredUntil), at(intervalStart)]);
|
|
78
|
+
coveredUntil = Math.max(coveredUntil, intervalEnd);
|
|
79
|
+
}
|
|
80
|
+
if (coveredUntil < 1 - minimumGap)
|
|
81
|
+
segments.push([at(coveredUntil), end]);
|
|
82
|
+
return segments;
|
|
83
|
+
}
|
|
51
84
|
function projectPointOntoWallCenterline(point, wall) {
|
|
52
85
|
if (isCurvedWall(wall)) {
|
|
53
86
|
const arc = getWallArcData(wall);
|
|
@@ -396,13 +429,28 @@ export function planWallInsertion(nodes, args) {
|
|
|
396
429
|
return split ? [[wallId, split]] : [];
|
|
397
430
|
});
|
|
398
431
|
const replacementWalls = splitPlans.flatMap(([, split]) => split.create);
|
|
432
|
+
// Rooms name the walls that enclose them; a split wall's replacements take
|
|
433
|
+
// its place, as they do for an explicit division or a merge.
|
|
434
|
+
const replacements = new Map(splitPlans.map(([wallId, split]) => [wallId, split.create.map((wall) => wall.id)]));
|
|
435
|
+
const zoneUpdates = Object.values(nodes).flatMap((node) => node.type === 'zone' &&
|
|
436
|
+
node.parentId === args.levelId &&
|
|
437
|
+
node.boundaryWallIds.some((id) => replacements.has(id))
|
|
438
|
+
? [
|
|
439
|
+
{
|
|
440
|
+
id: node.id,
|
|
441
|
+
data: {
|
|
442
|
+
boundaryWallIds: node.boundaryWallIds.flatMap((id) => replacements.get(id) ?? [id]),
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
]
|
|
446
|
+
: []);
|
|
399
447
|
const plan = {
|
|
400
448
|
changes: {
|
|
401
449
|
create: [...replacementWalls, ...insertedWalls].map((node) => ({
|
|
402
450
|
node,
|
|
403
451
|
parentId: args.levelId,
|
|
404
452
|
})),
|
|
405
|
-
update: splitPlans.flatMap(([, split]) => split.update),
|
|
453
|
+
update: [...splitPlans.flatMap(([, split]) => split.update), ...zoneUpdates],
|
|
406
454
|
delete: splitPlans.map(([wallId]) => wallId),
|
|
407
455
|
},
|
|
408
456
|
insertedWalls,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate-build-json.d.ts","sourceRoot":"","sources":["../../src/validation/validate-build-json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAW,KAAK,WAAW,EAAc,MAAM,iBAAiB,CAAA;AAGvE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,kBAAkB,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACzC,yEAAyE;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,KAAK,EAAE,UAAU,CAAA;IACjB,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,YAAY,EAAE,WAAW,EAAE,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAwCD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAsYzE"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HIDDEN_SITE_NOTE } from '../lib/node-visibility.js';
|
|
1
2
|
import { nodeRegistry } from '../registry/index.js';
|
|
2
3
|
import { SceneMaterial } from '../schema/scene-material.js';
|
|
3
4
|
import { AnyNode, nodeKindOf } from '../schema/types.js';
|
|
@@ -202,6 +203,17 @@ export function validateBuildJson(input) {
|
|
|
202
203
|
message: '"rootNodeIds" is empty — no entry point into the scene.',
|
|
203
204
|
});
|
|
204
205
|
}
|
|
206
|
+
// Hand-authored files hide the Site expecting the parcel to disappear; the
|
|
207
|
+
// flag is accepted but reaches nothing beneath it, so say so at import.
|
|
208
|
+
for (const [key, value] of Object.entries(nodes)) {
|
|
209
|
+
if (!isPlainObject(value) || value.type !== 'site' || value.visible !== false)
|
|
210
|
+
continue;
|
|
211
|
+
warnings.push({
|
|
212
|
+
severity: 'warning',
|
|
213
|
+
code: 'site_hidden',
|
|
214
|
+
message: `Site "${typeof value.id === 'string' ? value.id : key}" is hidden. ${HIDDEN_SITE_NOTE}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
205
217
|
// Ids of nodes whose type falls outside the static schema union — plugin
|
|
206
218
|
// kinds (`trees:tree`) or genuinely unknown types. The scene store accepts
|
|
207
219
|
// them on load (they already round-trip through the DB fine) and they're
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Core library for Pascal 3D building editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
],
|
|
119
119
|
"repository": {
|
|
120
120
|
"type": "git",
|
|
121
|
-
"url": "https://github.com/pascalorg/editor.git",
|
|
121
|
+
"url": "git+https://github.com/pascalorg/editor.git",
|
|
122
122
|
"directory": "packages/core"
|
|
123
123
|
},
|
|
124
124
|
"license": "MIT",
|