@pascal-app/core 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/capture/index.d.ts +3 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/schema.d.ts +417 -0
- package/dist/capture/schema.d.ts.map +1 -0
- package/dist/capture/schema.js +348 -0
- package/dist/capture/source.d.ts +60 -0
- package/dist/capture/source.d.ts.map +1 -0
- package/dist/capture/source.js +254 -0
- package/dist/events/bus.d.ts +55 -7
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
- package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
- package/dist/events/hidden-wall-pointer-hold.js +37 -0
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
- package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
- package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/lib/asset-storage.d.ts.map +1 -1
- package/dist/lib/asset-storage.js +4 -1
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +306 -0
- package/dist/lib/roof-overlap.d.ts +32 -0
- package/dist/lib/roof-overlap.d.ts.map +1 -0
- package/dist/lib/roof-overlap.js +72 -0
- package/dist/lib/slab-polygon.d.ts +35 -2
- package/dist/lib/slab-polygon.d.ts.map +1 -1
- package/dist/lib/slab-polygon.js +133 -27
- package/dist/lib/space-detection.d.ts +11 -0
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +42 -14
- package/dist/registry/handles.d.ts +24 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -4
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -2
- package/dist/registry/registry.d.ts +13 -6
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +37 -4
- package/dist/registry/scene-api.d.ts +16 -0
- package/dist/registry/scene-api.d.ts.map +1 -1
- package/dist/registry/scene-api.js +49 -1
- package/dist/registry/types.d.ts +226 -13
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
- package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
- package/dist/schema/__bench__/node-parsers.bench.js +314 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
- package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
- package/dist/schema/__fixtures__/node-fixtures.js +125 -0
- package/dist/schema/base.d.ts +1 -1
- package/dist/schema/base.d.ts.map +1 -1
- package/dist/schema/base.js +8 -1
- package/dist/schema/compiled-node-parsers.d.ts +40 -0
- package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
- package/dist/schema/compiled-node-parsers.js +95 -0
- package/dist/schema/index.d.ts +19 -15
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +17 -12
- package/dist/schema/material.d.ts +2 -1
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +1 -0
- package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
- package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
- package/dist/schema/nodes/automatic-downspout.js +237 -0
- package/dist/schema/nodes/block.d.ts +107 -0
- package/dist/schema/nodes/block.d.ts.map +1 -0
- package/dist/schema/nodes/block.js +207 -0
- package/dist/schema/nodes/box-vent.d.ts +8 -2
- package/dist/schema/nodes/box-vent.d.ts.map +1 -1
- package/dist/schema/nodes/box-vent.js +2 -0
- package/dist/schema/nodes/building.d.ts +1 -1
- package/dist/schema/nodes/cabinet.d.ts +25 -3
- package/dist/schema/nodes/cabinet.d.ts.map +1 -1
- package/dist/schema/nodes/cabinet.js +26 -5
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/chimney.d.ts +2 -2
- package/dist/schema/nodes/column.d.ts +6 -6
- package/dist/schema/nodes/construction-dimension.d.ts +4 -4
- package/dist/schema/nodes/cupola.d.ts +9 -1
- package/dist/schema/nodes/cupola.d.ts.map +1 -1
- package/dist/schema/nodes/cupola.js +2 -0
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/dormer.d.ts +44 -1
- package/dist/schema/nodes/dormer.d.ts.map +1 -1
- package/dist/schema/nodes/dormer.js +131 -7
- package/dist/schema/nodes/downspout.d.ts +11 -1
- package/dist/schema/nodes/downspout.d.ts.map +1 -1
- package/dist/schema/nodes/downspout.js +24 -0
- package/dist/schema/nodes/duct-fitting.d.ts +18 -1
- package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/duct-fitting.js +25 -4
- package/dist/schema/nodes/duct-segment.d.ts +23 -1
- package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
- package/dist/schema/nodes/duct-segment.js +20 -0
- package/dist/schema/nodes/duct-terminal.d.ts +1 -1
- package/dist/schema/nodes/elevator.d.ts +1 -1
- package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
- package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
- package/dist/schema/nodes/eyebrow-vent.js +2 -0
- package/dist/schema/nodes/fence.d.ts +1 -1
- package/dist/schema/nodes/guide.d.ts +1 -1
- package/dist/schema/nodes/gutter.d.ts +36 -2
- package/dist/schema/nodes/gutter.d.ts.map +1 -1
- package/dist/schema/nodes/gutter.js +308 -1
- package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
- package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
- package/dist/schema/nodes/hanger-overrides.js +6 -0
- package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
- package/dist/schema/nodes/item.d.ts +4 -3
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +5 -0
- package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
- package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
- package/dist/schema/nodes/lean-to-extension.js +114 -0
- package/dist/schema/nodes/level.d.ts +2 -2
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/lineset.d.ts +1 -1
- package/dist/schema/nodes/liquid-line.d.ts +1 -1
- package/dist/schema/nodes/measurement.d.ts +1 -1
- package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
- package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-fitting.js +6 -2
- package/dist/schema/nodes/pipe-segment.d.ts +23 -1
- package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
- package/dist/schema/nodes/pipe-segment.js +20 -0
- package/dist/schema/nodes/pipe-trap.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts +1 -1
- package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
- package/dist/schema/nodes/ridge-vent.js +4 -1
- package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
- package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-shape.js +64 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
- package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment-walls.js +1 -1
- package/dist/schema/nodes/roof-segment.d.ts +41 -1
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +79 -2
- package/dist/schema/nodes/roof.d.ts +22 -1
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +14 -0
- package/dist/schema/nodes/scan.d.ts +19 -2
- package/dist/schema/nodes/scan.d.ts.map +1 -1
- package/dist/schema/nodes/scan.js +11 -1
- package/dist/schema/nodes/shelf.d.ts +2 -2
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/skylight.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/solar-panel.d.ts +1 -1
- package/dist/schema/nodes/spawn.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +1 -1
- package/dist/schema/nodes/structural-grid.d.ts +1 -1
- package/dist/schema/nodes/turbine-vent.d.ts +7 -1
- package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
- package/dist/schema/nodes/turbine-vent.js +2 -0
- package/dist/schema/nodes/wall.d.ts +4 -4
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +7 -1
- package/dist/schema/nodes/window.d.ts +10 -3
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/nodes/window.js +4 -0
- package/dist/schema/nodes/zone.d.ts +2 -2
- package/dist/schema/nodes/zone.js +1 -1
- package/dist/schema/types.d.ts +484 -109
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +27 -1
- package/dist/services/hosting.d.ts +2 -2
- package/dist/services/hosting.d.ts.map +1 -1
- package/dist/services/hosting.js +4 -4
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/system-graph.d.ts +11 -0
- package/dist/services/system-graph.d.ts.map +1 -1
- package/dist/services/system-graph.js +35 -5
- package/dist/store/actions/node-actions.d.ts +7 -0
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +429 -17
- package/dist/store/history-invalidation.d.ts +3 -0
- package/dist/store/history-invalidation.d.ts.map +1 -0
- package/dist/store/history-invalidation.js +72 -0
- package/dist/store/scene-hydration.d.ts +5 -0
- package/dist/store/scene-hydration.d.ts.map +1 -0
- package/dist/store/scene-hydration.js +57 -0
- package/dist/store/use-scene.d.ts +3 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +332 -64
- package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
- package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation-system.js +94 -0
- package/dist/systems/roof/roof-elevation.d.ts +4 -0
- package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
- package/dist/systems/roof/roof-elevation.js +95 -0
- package/dist/systems/roof/roof-footprint.d.ts +17 -0
- package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
- package/dist/systems/roof/roof-footprint.js +83 -0
- package/dist/systems/stair/stair-flight.d.ts +17 -0
- package/dist/systems/stair/stair-flight.d.ts.map +1 -0
- package/dist/systems/stair/stair-flight.js +27 -0
- package/dist/systems/stair/stair-opening-preview.d.ts +1555 -1328
- package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.d.ts +1 -0
- package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-system.js +93 -96
- package/dist/systems/stair/stair-rise.d.ts.map +1 -1
- package/dist/systems/stair/stair-rise.js +20 -21
- package/dist/systems/wall/wall-curve.d.ts +1 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +73 -0
- package/dist/systems/wall/wall-footprint.d.ts +1 -0
- package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
- package/dist/systems/wall/wall-footprint.js +1 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +26 -0
- package/dist/validation/validate-build-json.d.ts +3 -0
- package/dist/validation/validate-build-json.d.ts.map +1 -1
- package/dist/validation/validate-build-json.js +49 -2
- package/package.json +10 -3
- package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
- package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
- package/dist/systems/elevator/elevator-runtime-system.js +0 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nodeRegistry } from '../../registry/registry';
|
|
2
|
-
import { AnyNode as AnyNodeSchema, createDefaultRidgeVentsForSegment, getEffectiveWallSurfaceMaterial, getWallSurfaceMaterialSignature, isAutoRidgeVentEnabled, isDefaultRidgeVentNode, } from '../../schema';
|
|
2
|
+
import { AnyNode as AnyNodeSchema, createDefaultGuttersForSegment, createDefaultRidgeVentsForSegment, DownspoutNode as DownspoutNodeSchema, defaultDownspoutMetadata, generateId, getDefaultGutterSide, getEffectiveWallSurfaceMaterial, getWallSurfaceMaterialSignature, isAutoGutterEnabled, isAutoRidgeVentEnabled, isDefaultDownspoutNode, isDefaultGutterNode, isDefaultRidgeVentNode, parseNode, planAutomaticDownspouts, resolveAutomaticDownspoutLength, } from '../../schema';
|
|
3
|
+
import { constrainWallCurveOffsetToAvoidIntersections } from '../../systems/wall/wall-curve';
|
|
3
4
|
import { addActiveSceneCommitNodeIds, runWithSceneCommitNodeIds } from '../history-control';
|
|
4
5
|
const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
|
|
5
6
|
'roofType',
|
|
@@ -15,6 +16,18 @@ const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
|
|
|
15
16
|
'dutchWaistLengthRatio',
|
|
16
17
|
'dutchGabletRake',
|
|
17
18
|
]);
|
|
19
|
+
const DEFAULT_GUTTER_REFRESH_FIELDS = new Set([
|
|
20
|
+
'metadata',
|
|
21
|
+
'position',
|
|
22
|
+
'rotation',
|
|
23
|
+
'roofType',
|
|
24
|
+
'width',
|
|
25
|
+
'depth',
|
|
26
|
+
'wallHeight',
|
|
27
|
+
'pitch',
|
|
28
|
+
'overhang',
|
|
29
|
+
'trim',
|
|
30
|
+
]);
|
|
18
31
|
const NUMBER_FORMAT_BOUNDS = {
|
|
19
32
|
safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
|
|
20
33
|
int32: [-2147483648, 2147483647],
|
|
@@ -319,21 +332,42 @@ function formatNumericValue(value) {
|
|
|
319
332
|
return '-Infinity';
|
|
320
333
|
return String(value);
|
|
321
334
|
}
|
|
322
|
-
function numericSanitizeIssuesToMessage(issues) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
335
|
+
export function numericSanitizeIssuesToMessage(issues) {
|
|
336
|
+
if (!Array.isArray(issues))
|
|
337
|
+
return '';
|
|
338
|
+
try {
|
|
339
|
+
return issues
|
|
340
|
+
.map((issue) => {
|
|
341
|
+
const path = Array.isArray(issue?.path)
|
|
342
|
+
? issue.path.map(String).join('.') || '<root>'
|
|
343
|
+
: '<unknown>';
|
|
344
|
+
const to = issue?.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`;
|
|
345
|
+
return `${path}: ${formatNumericValue(issue?.from)} ${issue?.action ?? 'sanitized'}${to}`;
|
|
346
|
+
})
|
|
347
|
+
.join('; ');
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return '<diagnostic unavailable>';
|
|
351
|
+
}
|
|
330
352
|
}
|
|
331
353
|
function warnSanitizedNodeMutation(mutation, nodeId, issues) {
|
|
332
|
-
|
|
354
|
+
let message = '<diagnostic unavailable>';
|
|
355
|
+
try {
|
|
356
|
+
message = numericSanitizeIssuesToMessage(issues);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Reporting must never interrupt a node mutation.
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, message);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// A broken diagnostic sink must not interrupt a node mutation either.
|
|
366
|
+
}
|
|
333
367
|
}
|
|
334
368
|
function parseCreatedNode(node, parentId) {
|
|
335
369
|
const candidate = { ...node, parentId };
|
|
336
|
-
const parsed =
|
|
370
|
+
const parsed = parseNode(candidate);
|
|
337
371
|
if (parsed.success)
|
|
338
372
|
return parsed.data;
|
|
339
373
|
const schema = getNodeSchemaForType(candidate.type);
|
|
@@ -359,7 +393,7 @@ function mergeNodeUpdate(currentNode, patch) {
|
|
|
359
393
|
}
|
|
360
394
|
function parseUpdatedNode(currentNode, data) {
|
|
361
395
|
const candidate = mergeNodeUpdate(currentNode, data);
|
|
362
|
-
const parsed =
|
|
396
|
+
const parsed = parseNode(candidate);
|
|
363
397
|
if (parsed.success)
|
|
364
398
|
return parsed.data;
|
|
365
399
|
const schema = getNodeSchemaForType(candidate.type);
|
|
@@ -398,6 +432,306 @@ function refreshDefaultRidgeVentsForSegment(nextNodes, segment) {
|
|
|
398
432
|
};
|
|
399
433
|
return nextVents.map((vent) => vent.id);
|
|
400
434
|
}
|
|
435
|
+
function shouldRefreshDefaultGutters(data) {
|
|
436
|
+
return Object.keys(data).some((key) => DEFAULT_GUTTER_REFRESH_FIELDS.has(key));
|
|
437
|
+
}
|
|
438
|
+
function getLeanToGutterExclusions(nodes, segmentId) {
|
|
439
|
+
return Object.values(nodes).flatMap((node) => {
|
|
440
|
+
if (node.type !== 'lean-to-extension' ||
|
|
441
|
+
node.connectionMode !== 'auto' ||
|
|
442
|
+
node.hostRoofSegmentId !== segmentId ||
|
|
443
|
+
!node.hostRoofEdge) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
const range = node.hostRoofEdgeRange ?? [0, 1];
|
|
447
|
+
return [{ side: node.hostRoofEdge, from: range[0], to: range[1] }];
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function addLeanToHostRoofId(node, nodes, roofIds) {
|
|
451
|
+
if (node?.type !== 'lean-to-extension' || !node.hostRoofSegmentId)
|
|
452
|
+
return;
|
|
453
|
+
const segment = nodes[node.hostRoofSegmentId];
|
|
454
|
+
const roofId = segment?.type === 'roof-segment'
|
|
455
|
+
? segment.parentId
|
|
456
|
+
: node.hostRoofId;
|
|
457
|
+
if (roofId && nodes[roofId]?.type === 'roof')
|
|
458
|
+
roofIds.add(roofId);
|
|
459
|
+
}
|
|
460
|
+
// When an eave carries several default gutter runs on the same side (e.g. a run
|
|
461
|
+
// split by a lean-to exclusion), reuse the existing node whose plan position is
|
|
462
|
+
// closest to the desired run rather than an arbitrary queue order — otherwise
|
|
463
|
+
// the runs swap positions and their downspouts follow the wrong segment.
|
|
464
|
+
function takeNearestGutterId(candidateIds, desired, nodes) {
|
|
465
|
+
if (candidateIds.length === 0)
|
|
466
|
+
return undefined;
|
|
467
|
+
let bestIndex = 0;
|
|
468
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
469
|
+
for (let i = 0; i < candidateIds.length; i++) {
|
|
470
|
+
const node = nodes[candidateIds[i]];
|
|
471
|
+
const position = node && 'position' in node ? node.position : undefined;
|
|
472
|
+
const dx = (position?.[0] ?? 0) - desired.position[0];
|
|
473
|
+
const dz = (position?.[2] ?? 0) - desired.position[2];
|
|
474
|
+
const distance = dx * dx + dz * dz;
|
|
475
|
+
if (distance < bestDistance) {
|
|
476
|
+
bestDistance = distance;
|
|
477
|
+
bestIndex = i;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return candidateIds.splice(bestIndex, 1)[0];
|
|
481
|
+
}
|
|
482
|
+
function refreshDefaultGuttersForSegment(nextNodes, segment, roofSegments) {
|
|
483
|
+
const childIds = Array.isArray(segment.children) ? segment.children : [];
|
|
484
|
+
const existingIds = childIds.filter((childId) => isDefaultGutterNode(nextNodes[childId], segment.id));
|
|
485
|
+
if (!isAutoGutterEnabled(segment, nextNodes) && existingIds.length === 0) {
|
|
486
|
+
return { dirtyIds: [], deletedIds: [] };
|
|
487
|
+
}
|
|
488
|
+
const existingBySide = new Map();
|
|
489
|
+
for (const id of existingIds) {
|
|
490
|
+
const side = getDefaultGutterSide(nextNodes[id], segment.id);
|
|
491
|
+
if (!side)
|
|
492
|
+
continue;
|
|
493
|
+
const ids = existingBySide.get(side) ?? [];
|
|
494
|
+
ids.push(id);
|
|
495
|
+
existingBySide.set(side, ids);
|
|
496
|
+
}
|
|
497
|
+
const desiredGutters = isAutoGutterEnabled(segment, nextNodes)
|
|
498
|
+
? createDefaultGuttersForSegment(segment, roofSegments, getLeanToGutterExclusions(nextNodes, segment.id))
|
|
499
|
+
: [];
|
|
500
|
+
const desiredChildIds = [];
|
|
501
|
+
const dirtyIds = [];
|
|
502
|
+
for (const desired of desiredGutters) {
|
|
503
|
+
const side = getDefaultGutterSide(desired, segment.id);
|
|
504
|
+
if (!side)
|
|
505
|
+
continue;
|
|
506
|
+
const matchingIds = existingBySide.get(side);
|
|
507
|
+
const existingId = matchingIds
|
|
508
|
+
? takeNearestGutterId(matchingIds, desired, nextNodes)
|
|
509
|
+
: undefined;
|
|
510
|
+
if (existingId) {
|
|
511
|
+
const existing = nextNodes[existingId];
|
|
512
|
+
nextNodes[existingId] = {
|
|
513
|
+
...existing,
|
|
514
|
+
parentId: segment.id,
|
|
515
|
+
roofSegmentId: segment.id,
|
|
516
|
+
position: desired.position,
|
|
517
|
+
rotation: desired.rotation,
|
|
518
|
+
length: desired.length,
|
|
519
|
+
};
|
|
520
|
+
desiredChildIds.push(existingId);
|
|
521
|
+
dirtyIds.push(existingId);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const desiredId = desired.id;
|
|
525
|
+
nextNodes[desiredId] = { ...desired, parentId: segment.id };
|
|
526
|
+
desiredChildIds.push(desiredId);
|
|
527
|
+
dirtyIds.push(desiredId);
|
|
528
|
+
}
|
|
529
|
+
const retainedIdSet = new Set(desiredChildIds);
|
|
530
|
+
const deletedIds = existingIds.filter((id) => !retainedIdSet.has(id));
|
|
531
|
+
const deletedGutterIds = new Set(deletedIds);
|
|
532
|
+
for (const [nodeId, node] of Object.entries(nextNodes)) {
|
|
533
|
+
if (node.type === 'downspout' &&
|
|
534
|
+
node.gutterId &&
|
|
535
|
+
deletedGutterIds.has(node.gutterId)) {
|
|
536
|
+
deletedIds.push(nodeId);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const deletedIdSet = new Set(deletedIds);
|
|
540
|
+
for (const id of deletedIds)
|
|
541
|
+
delete nextNodes[id];
|
|
542
|
+
nextNodes[segment.id] = {
|
|
543
|
+
...segment,
|
|
544
|
+
children: [
|
|
545
|
+
...childIds.filter((childId) => !deletedIdSet.has(childId) && !existingIds.includes(childId)),
|
|
546
|
+
...desiredChildIds,
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
return { dirtyIds, deletedIds };
|
|
550
|
+
}
|
|
551
|
+
function getRoofSegments(nextNodes, segment) {
|
|
552
|
+
const roof = segment.parentId ? nextNodes[segment.parentId] : undefined;
|
|
553
|
+
if (!(roof && roof.type === 'roof'))
|
|
554
|
+
return [segment];
|
|
555
|
+
return (roof.children ?? [])
|
|
556
|
+
.map((childId) => nextNodes[childId])
|
|
557
|
+
.filter((node) => node?.type === 'roof-segment');
|
|
558
|
+
}
|
|
559
|
+
function refreshDefaultDownspoutsForRoof(nextNodes, roofSegments) {
|
|
560
|
+
const gutters = roofSegments.flatMap((segment) => {
|
|
561
|
+
const current = nextNodes[segment.id];
|
|
562
|
+
if (current?.type !== 'roof-segment')
|
|
563
|
+
return [];
|
|
564
|
+
return (current.children ?? [])
|
|
565
|
+
.map((childId) => nextNodes[childId])
|
|
566
|
+
.filter((node) => node?.type === 'gutter' && isDefaultGutterNode(node, current.id));
|
|
567
|
+
});
|
|
568
|
+
const gutterById = new Map(gutters.map((gutter) => [gutter.id, gutter]));
|
|
569
|
+
const downspouts = Object.values(nextNodes).filter((node) => node?.type === 'downspout' && Boolean(node.gutterId && gutterById.has(node.gutterId)));
|
|
570
|
+
const generated = downspouts.filter((downspout) => {
|
|
571
|
+
if (!isDefaultDownspoutNode(downspout))
|
|
572
|
+
return false;
|
|
573
|
+
const gutter = downspout.gutterId ? gutterById.get(downspout.gutterId) : undefined;
|
|
574
|
+
return gutter?.outlets.some((outlet) => outlet.id === downspout.outletId && outlet.generatedBy === 'default-downspout');
|
|
575
|
+
});
|
|
576
|
+
const generatedIds = new Set(generated.map((downspout) => downspout.id));
|
|
577
|
+
const manual = downspouts.filter((downspout) => !generatedIds.has(downspout.id));
|
|
578
|
+
const placements = planAutomaticDownspouts({
|
|
579
|
+
segments: roofSegments,
|
|
580
|
+
gutters,
|
|
581
|
+
downspouts: manual,
|
|
582
|
+
});
|
|
583
|
+
const segmentById = new Map(roofSegments.map((segment) => [segment.id, segment]));
|
|
584
|
+
const availableByGutter = new Map();
|
|
585
|
+
for (const downspout of generated) {
|
|
586
|
+
if (!downspout.gutterId)
|
|
587
|
+
continue;
|
|
588
|
+
const available = availableByGutter.get(downspout.gutterId) ?? [];
|
|
589
|
+
available.push(downspout);
|
|
590
|
+
availableByGutter.set(downspout.gutterId, available);
|
|
591
|
+
}
|
|
592
|
+
const retainedIds = new Set();
|
|
593
|
+
const retainedOutletIds = new Set();
|
|
594
|
+
const dirtyIds = new Set();
|
|
595
|
+
const deletedIds = [];
|
|
596
|
+
const outletsByGutter = new Map(gutters.map((gutter) => [gutter.id, [...(gutter.outlets ?? [])]]));
|
|
597
|
+
for (const placement of placements) {
|
|
598
|
+
const gutter = gutterById.get(placement.gutterId);
|
|
599
|
+
if (!gutter?.roofSegmentId)
|
|
600
|
+
continue;
|
|
601
|
+
const segment = segmentById.get(gutter.roofSegmentId);
|
|
602
|
+
if (!segment)
|
|
603
|
+
continue;
|
|
604
|
+
const outlets = outletsByGutter.get(gutter.id) ?? [];
|
|
605
|
+
const available = availableByGutter.get(gutter.id) ?? [];
|
|
606
|
+
let bestIndex = -1;
|
|
607
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
608
|
+
for (let index = 0; index < available.length; index++) {
|
|
609
|
+
const candidate = available[index];
|
|
610
|
+
const outlet = outlets.find((entry) => entry.id === candidate.outletId);
|
|
611
|
+
const distance = outlet ? Math.abs(outlet.offset - placement.offset) : 0;
|
|
612
|
+
if (distance < bestDistance) {
|
|
613
|
+
bestDistance = distance;
|
|
614
|
+
bestIndex = index;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const existing = bestIndex >= 0 ? available.splice(bestIndex, 1)[0] : undefined;
|
|
618
|
+
const outletId = existing?.outletId ?? generateId('outlet');
|
|
619
|
+
const outletIndex = outlets.findIndex((outlet) => outlet.id === outletId);
|
|
620
|
+
const outlet = {
|
|
621
|
+
id: outletId,
|
|
622
|
+
offset: placement.offset,
|
|
623
|
+
diameter: 0.07,
|
|
624
|
+
generatedBy: 'default-downspout',
|
|
625
|
+
};
|
|
626
|
+
if (outletIndex >= 0)
|
|
627
|
+
outlets[outletIndex] = { ...outlets[outletIndex], ...outlet };
|
|
628
|
+
else
|
|
629
|
+
outlets.push(outlet);
|
|
630
|
+
outletsByGutter.set(gutter.id, outlets);
|
|
631
|
+
retainedOutletIds.add(outletId);
|
|
632
|
+
const length = resolveAutomaticDownspoutLength(nextNodes, segment, gutter, placement.offset);
|
|
633
|
+
const downspout = existing
|
|
634
|
+
? {
|
|
635
|
+
...existing,
|
|
636
|
+
parentId: segment.id,
|
|
637
|
+
gutterId: gutter.id,
|
|
638
|
+
outletId,
|
|
639
|
+
length: existing.lengthMode === 'manual' ? existing.length : length,
|
|
640
|
+
lengthMode: existing.lengthMode === 'manual' ? 'manual' : 'to-ground',
|
|
641
|
+
}
|
|
642
|
+
: DownspoutNodeSchema.parse({
|
|
643
|
+
name: 'Downspout',
|
|
644
|
+
parentId: segment.id,
|
|
645
|
+
gutterId: gutter.id,
|
|
646
|
+
outletId,
|
|
647
|
+
length,
|
|
648
|
+
lengthMode: 'to-ground',
|
|
649
|
+
diameter: outlet.diameter,
|
|
650
|
+
metadata: defaultDownspoutMetadata(),
|
|
651
|
+
});
|
|
652
|
+
nextNodes[downspout.id] = downspout;
|
|
653
|
+
retainedIds.add(downspout.id);
|
|
654
|
+
dirtyIds.add(downspout.id);
|
|
655
|
+
const currentSegment = nextNodes[segment.id];
|
|
656
|
+
if (currentSegment?.type === 'roof-segment') {
|
|
657
|
+
nextNodes[segment.id] = {
|
|
658
|
+
...currentSegment,
|
|
659
|
+
children: Array.from(new Set([...(currentSegment.children ?? []), downspout.id])),
|
|
660
|
+
};
|
|
661
|
+
dirtyIds.add(segment.id);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
for (const [gutterId, outlets] of outletsByGutter) {
|
|
665
|
+
const gutter = nextNodes[gutterId];
|
|
666
|
+
if (gutter?.type !== 'gutter')
|
|
667
|
+
continue;
|
|
668
|
+
nextNodes[gutterId] = {
|
|
669
|
+
...gutter,
|
|
670
|
+
outlets: outlets.filter((outlet) => outlet.generatedBy !== 'default-downspout' || retainedOutletIds.has(outlet.id)),
|
|
671
|
+
};
|
|
672
|
+
dirtyIds.add(gutterId);
|
|
673
|
+
}
|
|
674
|
+
for (const downspout of generated) {
|
|
675
|
+
const downspoutId = downspout.id;
|
|
676
|
+
if (retainedIds.has(downspoutId))
|
|
677
|
+
continue;
|
|
678
|
+
const gutter = downspout.gutterId ? nextNodes[downspout.gutterId] : undefined;
|
|
679
|
+
if (gutter?.type === 'gutter' && downspout.outletId) {
|
|
680
|
+
nextNodes[gutter.id] = {
|
|
681
|
+
...gutter,
|
|
682
|
+
outlets: (gutter.outlets ?? []).filter((outlet) => outlet.id !== downspout.outletId),
|
|
683
|
+
};
|
|
684
|
+
dirtyIds.add(gutter.id);
|
|
685
|
+
}
|
|
686
|
+
const parent = downspout.parentId ? nextNodes[downspout.parentId] : undefined;
|
|
687
|
+
if (parent?.type === 'roof-segment') {
|
|
688
|
+
nextNodes[parent.id] = {
|
|
689
|
+
...parent,
|
|
690
|
+
children: (parent.children ?? []).filter((childId) => childId !== downspout.id),
|
|
691
|
+
};
|
|
692
|
+
dirtyIds.add(parent.id);
|
|
693
|
+
}
|
|
694
|
+
delete nextNodes[downspoutId];
|
|
695
|
+
deletedIds.push(downspoutId);
|
|
696
|
+
}
|
|
697
|
+
return { dirtyIds: [...dirtyIds], deletedIds };
|
|
698
|
+
}
|
|
699
|
+
function refreshDefaultGuttersForRoof(nextNodes, segment) {
|
|
700
|
+
const roofSegments = getRoofSegments(nextNodes, segment);
|
|
701
|
+
const dirtyIds = [];
|
|
702
|
+
const deletedIds = [];
|
|
703
|
+
for (const roofSegment of roofSegments) {
|
|
704
|
+
const current = nextNodes[roofSegment.id];
|
|
705
|
+
if (current?.type !== 'roof-segment')
|
|
706
|
+
continue;
|
|
707
|
+
const result = refreshDefaultGuttersForSegment(nextNodes, current, roofSegments);
|
|
708
|
+
dirtyIds.push(...result.dirtyIds);
|
|
709
|
+
deletedIds.push(...result.deletedIds);
|
|
710
|
+
}
|
|
711
|
+
const downspoutResult = refreshDefaultDownspoutsForRoof(nextNodes, roofSegments);
|
|
712
|
+
dirtyIds.push(...downspoutResult.dirtyIds);
|
|
713
|
+
deletedIds.push(...downspoutResult.deletedIds);
|
|
714
|
+
return { dirtyIds, deletedIds };
|
|
715
|
+
}
|
|
716
|
+
function collectDefaultGutterRefresh(result, dirtyIds, deletedIds) {
|
|
717
|
+
for (const id of result.dirtyIds)
|
|
718
|
+
dirtyIds.add(id);
|
|
719
|
+
for (const id of result.deletedIds)
|
|
720
|
+
deletedIds.add(id);
|
|
721
|
+
}
|
|
722
|
+
function refreshDefaultGuttersForRoofIds(nextNodes, roofIds, dirtyIds, deletedIds) {
|
|
723
|
+
for (const roofId of new Set(roofIds)) {
|
|
724
|
+
const roof = nextNodes[roofId];
|
|
725
|
+
if (roof?.type !== 'roof')
|
|
726
|
+
continue;
|
|
727
|
+
const segment = (roof.children ?? [])
|
|
728
|
+
.map((childId) => nextNodes[childId])
|
|
729
|
+
.find((child) => child?.type === 'roof-segment');
|
|
730
|
+
if (!segment)
|
|
731
|
+
continue;
|
|
732
|
+
collectDefaultGutterRefresh(refreshDefaultGuttersForRoof(nextNodes, segment), dirtyIds, deletedIds);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
401
735
|
// Track pending RAF for updateNodesAction to prevent multiple queued callbacks
|
|
402
736
|
let pendingRafId = null;
|
|
403
737
|
let pendingUpdates = new Set();
|
|
@@ -551,6 +885,8 @@ function buildWallMergePlans(nodes, idsToDelete) {
|
|
|
551
885
|
const createNodesActionImpl = (set, get, ops) => {
|
|
552
886
|
if (get().readOnly)
|
|
553
887
|
return;
|
|
888
|
+
const extraNodesToMarkDirty = new Set();
|
|
889
|
+
const extraNodesToClearDirty = new Set();
|
|
554
890
|
set((state) => {
|
|
555
891
|
const nextNodes = { ...state.nodes };
|
|
556
892
|
const nextRootIds = [...state.rootNodeIds];
|
|
@@ -585,6 +921,16 @@ const createNodesActionImpl = (set, get, ops) => {
|
|
|
585
921
|
}
|
|
586
922
|
}
|
|
587
923
|
}
|
|
924
|
+
const refreshedRoofIds = new Set();
|
|
925
|
+
for (const { node } of ops) {
|
|
926
|
+
const created = nextNodes[node.id];
|
|
927
|
+
if (created?.type === 'roof-segment' && created.parentId) {
|
|
928
|
+
refreshedRoofIds.add(created.parentId);
|
|
929
|
+
}
|
|
930
|
+
addLeanToHostRoofId(created, nextNodes, refreshedRoofIds);
|
|
931
|
+
}
|
|
932
|
+
refreshDefaultGuttersForRoofIds(nextNodes, refreshedRoofIds, extraNodesToMarkDirty, extraNodesToClearDirty);
|
|
933
|
+
addActiveSceneCommitNodeIds([...extraNodesToMarkDirty, ...extraNodesToClearDirty]);
|
|
588
934
|
return { nodes: nextNodes, rootNodeIds: nextRootIds };
|
|
589
935
|
});
|
|
590
936
|
// 4. System Sync
|
|
@@ -595,6 +941,10 @@ const createNodesActionImpl = (set, get, ops) => {
|
|
|
595
941
|
else if (node.parentId)
|
|
596
942
|
get().markDirty(node.parentId);
|
|
597
943
|
});
|
|
944
|
+
for (const id of extraNodesToMarkDirty)
|
|
945
|
+
get().markDirty(id);
|
|
946
|
+
for (const id of extraNodesToClearDirty)
|
|
947
|
+
get().clearDirty(id);
|
|
598
948
|
};
|
|
599
949
|
const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
600
950
|
if (get().readOnly)
|
|
@@ -603,17 +953,21 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
|
603
953
|
const updateOps = changes.update ?? [];
|
|
604
954
|
const deleteOps = changes.delete ?? [];
|
|
605
955
|
const nodesToMarkDirty = new Set();
|
|
956
|
+
const nodesToClearDirty = new Set();
|
|
606
957
|
const parentsToMarkDirty = new Set();
|
|
607
958
|
set((state) => {
|
|
608
959
|
const nextNodes = { ...state.nodes };
|
|
609
960
|
const nextCollections = { ...state.collections };
|
|
610
961
|
const nextRootIds = [...state.rootNodeIds];
|
|
611
962
|
let resolvedRootIds = nextRootIds;
|
|
963
|
+
const roofsToRefresh = new Set();
|
|
612
964
|
for (const { id, data } of updateOps) {
|
|
613
965
|
const currentNode = nextNodes[id];
|
|
614
966
|
if (!currentNode)
|
|
615
967
|
continue;
|
|
968
|
+
addLeanToHostRoofId(currentNode, nextNodes, roofsToRefresh);
|
|
616
969
|
const updatedNode = parseUpdatedNode(currentNode, data);
|
|
970
|
+
addLeanToHostRoofId(updatedNode, nextNodes, roofsToRefresh);
|
|
617
971
|
if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
|
|
618
972
|
const oldParentId = currentNode.parentId;
|
|
619
973
|
if (oldParentId && nextNodes[oldParentId]) {
|
|
@@ -640,6 +994,11 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
|
640
994
|
nodesToMarkDirty.add(ventId);
|
|
641
995
|
}
|
|
642
996
|
}
|
|
997
|
+
const currentSegment = nextNodes[id];
|
|
998
|
+
if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
|
|
999
|
+
if (currentSegment.parentId)
|
|
1000
|
+
roofsToRefresh.add(currentSegment.parentId);
|
|
1001
|
+
}
|
|
643
1002
|
nodesToMarkDirty.add(id);
|
|
644
1003
|
}
|
|
645
1004
|
for (const { node, parentId } of createOps) {
|
|
@@ -647,6 +1006,10 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
|
647
1006
|
const newNode = parseCreatedNode(node, effectiveParentId);
|
|
648
1007
|
nextNodes[newNode.id] = newNode;
|
|
649
1008
|
nodesToMarkDirty.add(newNode.id);
|
|
1009
|
+
if (newNode.type === 'roof-segment' && effectiveParentId) {
|
|
1010
|
+
roofsToRefresh.add(effectiveParentId);
|
|
1011
|
+
}
|
|
1012
|
+
addLeanToHostRoofId(newNode, nextNodes, roofsToRefresh);
|
|
650
1013
|
if (effectiveParentId && nextNodes[effectiveParentId]) {
|
|
651
1014
|
const parent = nextNodes[effectiveParentId];
|
|
652
1015
|
if ('children' in parent && Array.isArray(parent.children)) {
|
|
@@ -676,6 +1039,13 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
|
676
1039
|
for (const id of deleteOps) {
|
|
677
1040
|
collectDelete(id);
|
|
678
1041
|
}
|
|
1042
|
+
for (const id of allIdsToDelete) {
|
|
1043
|
+
const node = nextNodes[id];
|
|
1044
|
+
addLeanToHostRoofId(node, nextNodes, roofsToRefresh);
|
|
1045
|
+
if (node?.type === 'roof-segment' && node.parentId) {
|
|
1046
|
+
roofsToRefresh.add(node.parentId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
679
1049
|
for (const id of allIdsToDelete) {
|
|
680
1050
|
const node = nextNodes[id];
|
|
681
1051
|
if (!node)
|
|
@@ -705,12 +1075,21 @@ const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
|
705
1075
|
}
|
|
706
1076
|
delete nextNodes[id];
|
|
707
1077
|
}
|
|
708
|
-
|
|
1078
|
+
refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, nodesToMarkDirty, nodesToClearDirty);
|
|
1079
|
+
addActiveSceneCommitNodeIds([
|
|
1080
|
+
...allIdsToDelete,
|
|
1081
|
+
...nodesToMarkDirty,
|
|
1082
|
+
...nodesToClearDirty,
|
|
1083
|
+
...parentsToMarkDirty,
|
|
1084
|
+
]);
|
|
709
1085
|
return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections };
|
|
710
1086
|
});
|
|
711
1087
|
for (const id of nodesToMarkDirty) {
|
|
712
1088
|
get().markDirty(id);
|
|
713
1089
|
}
|
|
1090
|
+
for (const id of nodesToClearDirty) {
|
|
1091
|
+
get().clearDirty(id);
|
|
1092
|
+
}
|
|
714
1093
|
for (const id of parentsToMarkDirty) {
|
|
715
1094
|
get().markDirty(id);
|
|
716
1095
|
const parent = get().nodes[id];
|
|
@@ -726,13 +1105,24 @@ const updateNodesActionImpl = (set, get, updates) => {
|
|
|
726
1105
|
return;
|
|
727
1106
|
const parentsToUpdate = new Set();
|
|
728
1107
|
const extraNodesToUpdate = new Set();
|
|
1108
|
+
const extraNodesToDelete = new Set();
|
|
1109
|
+
const roofsToRefresh = new Set();
|
|
729
1110
|
set((state) => {
|
|
730
1111
|
const nextNodes = { ...state.nodes };
|
|
731
1112
|
for (const { id, data } of updates) {
|
|
732
1113
|
const currentNode = nextNodes[id];
|
|
733
1114
|
if (!currentNode)
|
|
734
1115
|
continue;
|
|
735
|
-
|
|
1116
|
+
addLeanToHostRoofId(currentNode, nextNodes, roofsToRefresh);
|
|
1117
|
+
const curveOffset = currentNode.type === 'wall' ? data.curveOffset : undefined;
|
|
1118
|
+
const constrainedData = currentNode.type === 'wall' && typeof curveOffset === 'number'
|
|
1119
|
+
? {
|
|
1120
|
+
...data,
|
|
1121
|
+
curveOffset: constrainWallCurveOffsetToAvoidIntersections(currentNode, curveOffset, Object.values(nextNodes).filter((node) => node.type === 'wall' && node.parentId === currentNode.parentId)),
|
|
1122
|
+
}
|
|
1123
|
+
: data;
|
|
1124
|
+
const updatedNode = parseUpdatedNode(currentNode, constrainedData);
|
|
1125
|
+
addLeanToHostRoofId(updatedNode, nextNodes, roofsToRefresh);
|
|
736
1126
|
// Handle Reparenting Logic
|
|
737
1127
|
if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
|
|
738
1128
|
// 1. Remove from old parent
|
|
@@ -774,11 +1164,18 @@ const updateNodesActionImpl = (set, get, updates) => {
|
|
|
774
1164
|
extraNodesToUpdate.add(ventId);
|
|
775
1165
|
}
|
|
776
1166
|
}
|
|
1167
|
+
const currentSegment = nextNodes[id];
|
|
1168
|
+
if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
|
|
1169
|
+
if (currentSegment.parentId)
|
|
1170
|
+
roofsToRefresh.add(currentSegment.parentId);
|
|
1171
|
+
}
|
|
777
1172
|
}
|
|
1173
|
+
refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, extraNodesToUpdate, extraNodesToDelete);
|
|
778
1174
|
addActiveSceneCommitNodeIds([
|
|
779
1175
|
...updates.map(({ id }) => id),
|
|
780
1176
|
...parentsToUpdate,
|
|
781
1177
|
...extraNodesToUpdate,
|
|
1178
|
+
...extraNodesToDelete,
|
|
782
1179
|
]);
|
|
783
1180
|
return { nodes: nextNodes };
|
|
784
1181
|
});
|
|
@@ -797,6 +1194,9 @@ const updateNodesActionImpl = (set, get, updates) => {
|
|
|
797
1194
|
for (const id of extraNodesToUpdate) {
|
|
798
1195
|
pendingUpdates.add(id);
|
|
799
1196
|
}
|
|
1197
|
+
for (const id of extraNodesToDelete) {
|
|
1198
|
+
get().clearDirty(id);
|
|
1199
|
+
}
|
|
800
1200
|
if (pendingRafId !== null) {
|
|
801
1201
|
cancelAnimationFrame(pendingRafId);
|
|
802
1202
|
}
|
|
@@ -815,6 +1215,7 @@ const deleteNodesActionImpl = (set, get, ids) => {
|
|
|
815
1215
|
const nodesToMarkDirty = new Set();
|
|
816
1216
|
const deletedIds = new Set();
|
|
817
1217
|
const mergePlans = buildWallMergePlans(get().nodes, ids);
|
|
1218
|
+
const requestedDeleteIds = new Set(ids);
|
|
818
1219
|
set((state) => {
|
|
819
1220
|
const nextNodes = { ...state.nodes };
|
|
820
1221
|
const nextCollections = { ...state.collections };
|
|
@@ -828,7 +1229,9 @@ const deleteNodesActionImpl = (set, get, ids) => {
|
|
|
828
1229
|
allIds.add(id);
|
|
829
1230
|
const node = nextNodes[id];
|
|
830
1231
|
const cascadeDeletes = node
|
|
831
|
-
? nodeRegistry
|
|
1232
|
+
? nodeRegistry
|
|
1233
|
+
.get(node.type)
|
|
1234
|
+
?.parametrics?.onDeleteCascade?.(node, nextNodes, allIds, requestedDeleteIds)
|
|
832
1235
|
: null;
|
|
833
1236
|
if (cascadeDeletes) {
|
|
834
1237
|
for (const companionId of cascadeDeletes)
|
|
@@ -844,6 +1247,14 @@ const deleteNodesActionImpl = (set, get, ids) => {
|
|
|
844
1247
|
for (const plan of mergePlans) {
|
|
845
1248
|
allIds.add(plan.secondaryWallId);
|
|
846
1249
|
}
|
|
1250
|
+
const affectedRoofIds = new Set();
|
|
1251
|
+
for (const id of allIds) {
|
|
1252
|
+
const node = nextNodes[id];
|
|
1253
|
+
addLeanToHostRoofId(node, nextNodes, affectedRoofIds);
|
|
1254
|
+
if (node?.type === 'roof-segment' && node.parentId) {
|
|
1255
|
+
affectedRoofIds.add(node.parentId);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
847
1258
|
for (const id of allIds)
|
|
848
1259
|
deletedIds.add(id);
|
|
849
1260
|
// Let each deleted kind undo what it imposed on its neighbours (e.g. an
|
|
@@ -857,7 +1268,7 @@ const deleteNodesActionImpl = (set, get, ids) => {
|
|
|
857
1268
|
const onDelete = nodeRegistry.get(node.type)?.parametrics?.onDelete;
|
|
858
1269
|
if (!onDelete)
|
|
859
1270
|
continue;
|
|
860
|
-
for (const { id: targetId, data } of onDelete(node, nextNodes)) {
|
|
1271
|
+
for (const { id: targetId, data } of onDelete(node, nextNodes, allIds, requestedDeleteIds)) {
|
|
861
1272
|
if (allIds.has(targetId))
|
|
862
1273
|
continue;
|
|
863
1274
|
const target = nextNodes[targetId];
|
|
@@ -947,7 +1358,8 @@ const deleteNodesActionImpl = (set, get, ids) => {
|
|
|
947
1358
|
// 4. Delete the node itself
|
|
948
1359
|
delete nextNodes[id];
|
|
949
1360
|
}
|
|
950
|
-
|
|
1361
|
+
refreshDefaultGuttersForRoofIds(nextNodes, affectedRoofIds, nodesToMarkDirty, deletedIds);
|
|
1362
|
+
addActiveSceneCommitNodeIds([...deletedIds, ...parentsToMarkDirty, ...nodesToMarkDirty]);
|
|
951
1363
|
return { nodes: nextNodes, rootNodeIds: nextRootIds, collections: nextCollections };
|
|
952
1364
|
});
|
|
953
1365
|
// Deleted ids must leave the dirty set: every consumer skips missing
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"history-invalidation.d.ts","sourceRoot":"","sources":["../../src/store/history-invalidation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAY,MAAM,WAAW,CAAA;AAG7D,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,GAAG,CAAC,SAAS,CAAC,CAyEhB"}
|