@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.
Files changed (71) hide show
  1. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +42 -8
  2. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -0
  6. package/dist/lib/node-visibility.d.ts +12 -0
  7. package/dist/lib/node-visibility.d.ts.map +1 -0
  8. package/dist/lib/node-visibility.js +12 -0
  9. package/dist/procedural-items/query.d.ts +1 -0
  10. package/dist/procedural-items/query.d.ts.map +1 -1
  11. package/dist/procedural-items/query.js +67 -23
  12. package/dist/registry/handles.d.ts +28 -1
  13. package/dist/registry/handles.d.ts.map +1 -1
  14. package/dist/registry/index.d.ts +1 -1
  15. package/dist/registry/index.d.ts.map +1 -1
  16. package/dist/registry/subtree.d.ts.map +1 -1
  17. package/dist/registry/subtree.js +6 -0
  18. package/dist/registry/types.d.ts +12 -2
  19. package/dist/registry/types.d.ts.map +1 -1
  20. package/dist/schema/index.d.ts +1 -0
  21. package/dist/schema/index.d.ts.map +1 -1
  22. package/dist/schema/index.js +1 -0
  23. package/dist/schema/nodes/block.d.ts +2 -2
  24. package/dist/schema/nodes/block.d.ts.map +1 -1
  25. package/dist/schema/nodes/block.js +21 -8
  26. package/dist/schema/nodes/cabinet.d.ts +2 -2
  27. package/dist/schema/nodes/curtain-wall.d.ts +129 -0
  28. package/dist/schema/nodes/curtain-wall.d.ts.map +1 -0
  29. package/dist/schema/nodes/curtain-wall.js +49 -0
  30. package/dist/schema/nodes/door.d.ts +2 -2
  31. package/dist/schema/nodes/wall.d.ts +73 -0
  32. package/dist/schema/nodes/wall.d.ts.map +1 -1
  33. package/dist/schema/nodes/wall.js +3 -0
  34. package/dist/schema/types.d.ts +76 -3
  35. package/dist/schema/types.d.ts.map +1 -1
  36. package/dist/services/__fixtures__/hosting.d.ts +3 -0
  37. package/dist/services/__fixtures__/hosting.d.ts.map +1 -0
  38. package/dist/services/__fixtures__/hosting.js +9 -0
  39. package/dist/services/__fixtures__/shelf-surface-rows.d.ts +4 -0
  40. package/dist/services/__fixtures__/shelf-surface-rows.d.ts.map +1 -0
  41. package/dist/services/__fixtures__/shelf-surface-rows.js +18 -0
  42. package/dist/services/hosting.d.ts +3 -0
  43. package/dist/services/hosting.d.ts.map +1 -1
  44. package/dist/services/hosting.js +24 -7
  45. package/dist/services/index.d.ts +3 -3
  46. package/dist/services/index.d.ts.map +1 -1
  47. package/dist/services/index.js +3 -3
  48. package/dist/services/snap.d.ts +3 -0
  49. package/dist/services/snap.d.ts.map +1 -1
  50. package/dist/services/snap.js +11 -0
  51. package/dist/services/surface-hosting.d.ts +6 -2
  52. package/dist/services/surface-hosting.d.ts.map +1 -1
  53. package/dist/services/surface-hosting.js +47 -10
  54. package/dist/store/actions/node-actions.d.ts.map +1 -1
  55. package/dist/store/actions/node-actions.js +3 -96
  56. package/dist/store/use-scene.d.ts.map +1 -1
  57. package/dist/store/use-scene.js +13 -1
  58. package/dist/systems/stair/stair-opening-preview.d.ts +42 -8
  59. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  60. package/dist/systems/wall/wall-merge.d.ts +38 -0
  61. package/dist/systems/wall/wall-merge.d.ts.map +1 -0
  62. package/dist/systems/wall/wall-merge.js +240 -0
  63. package/dist/systems/wall/wall-operations.d.ts +28 -0
  64. package/dist/systems/wall/wall-operations.d.ts.map +1 -0
  65. package/dist/systems/wall/wall-operations.js +145 -0
  66. package/dist/systems/wall/wall-topology.d.ts +7 -0
  67. package/dist/systems/wall/wall-topology.d.ts.map +1 -1
  68. package/dist/systems/wall/wall-topology.js +53 -5
  69. package/dist/validation/validate-build-json.d.ts.map +1 -1
  70. package/dist/validation/validate-build-json.js +12 -0
  71. 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;AA8GpC,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,CAiGrB"}
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
- function wallSegmentsCoverSegment(start, end, walls) {
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 false;
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
- const parameterTolerance = WALL_INTERSECTION_EPSILON / length;
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":"AACA,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,CA2XzE"}
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.1",
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",