@pascal-app/core 1.0.0-beta.4 → 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-manager.d.ts +12 -0
- package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid-manager.js +39 -1
- 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 +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -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/room-topology-index.d.ts +50 -0
- package/dist/lib/room-topology-index.d.ts.map +1 -0
- package/dist/lib/room-topology-index.js +237 -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 +40 -3
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +890 -246
- package/dist/registry/handles.d.ts +24 -3
- package/dist/registry/handles.d.ts.map +1 -1
- package/dist/registry/index.d.ts +3 -3
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/registry/registry.d.ts +29 -6
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +111 -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 +261 -13
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/use-registry-version.d.ts +11 -0
- package/dist/registry/use-registry-version.d.ts.map +1 -0
- package/dist/registry/use-registry-version.js +15 -0
- 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 +20 -15
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +19 -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 +5 -2
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +9 -0
- 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 +485 -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 +2 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -2
- package/dist/services/storey.d.ts +3 -10
- package/dist/services/storey.d.ts.map +1 -1
- package/dist/services/storey.js +45 -15
- 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 +15 -4
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +458 -19
- package/dist/store/history-control.d.ts +5 -0
- package/dist/store/history-control.d.ts.map +1 -1
- package/dist/store/history-control.js +68 -6
- 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 +407 -227
- package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-service.js +16 -11
- 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/slab/slab-support.d.ts +0 -31
- package/dist/systems/slab/slab-support.d.ts.map +1 -1
- package/dist/systems/slab/slab-support.js +26 -4
- 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 +1556 -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.js +21 -20
- 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/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +88 -9
- package/dist/systems/wall/wall-topology.d.ts +53 -0
- package/dist/systems/wall/wall-topology.d.ts.map +1 -0
- package/dist/systems/wall/wall-topology.js +414 -0
- package/dist/utils/clone-scene-graph.d.ts +2 -0
- package/dist/utils/clone-scene-graph.d.ts.map +1 -1
- package/dist/utils/clone-scene-graph.js +39 -2
- package/dist/utils/scene-migrations.d.ts +1 -0
- package/dist/utils/scene-migrations.d.ts.map +1 -1
- package/dist/utils/scene-migrations.js +1 -0
- package/dist/utils/vertical-scene-migration.d.ts +13 -0
- package/dist/utils/vertical-scene-migration.d.ts.map +1 -0
- package/dist/utils/vertical-scene-migration.js +179 -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,7 @@
|
|
|
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';
|
|
4
|
+
import { addActiveSceneCommitNodeIds, runWithSceneCommitNodeIds } from '../history-control';
|
|
3
5
|
const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
|
|
4
6
|
'roofType',
|
|
5
7
|
'width',
|
|
@@ -14,6 +16,18 @@ const DEFAULT_RIDGE_VENT_REFRESH_FIELDS = new Set([
|
|
|
14
16
|
'dutchWaistLengthRatio',
|
|
15
17
|
'dutchGabletRake',
|
|
16
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
|
+
]);
|
|
17
31
|
const NUMBER_FORMAT_BOUNDS = {
|
|
18
32
|
safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],
|
|
19
33
|
int32: [-2147483648, 2147483647],
|
|
@@ -318,21 +332,42 @@ function formatNumericValue(value) {
|
|
|
318
332
|
return '-Infinity';
|
|
319
333
|
return String(value);
|
|
320
334
|
}
|
|
321
|
-
function numericSanitizeIssuesToMessage(issues) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
}
|
|
329
352
|
}
|
|
330
353
|
function warnSanitizedNodeMutation(mutation, nodeId, issues) {
|
|
331
|
-
|
|
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
|
+
}
|
|
332
367
|
}
|
|
333
368
|
function parseCreatedNode(node, parentId) {
|
|
334
369
|
const candidate = { ...node, parentId };
|
|
335
|
-
const parsed =
|
|
370
|
+
const parsed = parseNode(candidate);
|
|
336
371
|
if (parsed.success)
|
|
337
372
|
return parsed.data;
|
|
338
373
|
const schema = getNodeSchemaForType(candidate.type);
|
|
@@ -358,7 +393,7 @@ function mergeNodeUpdate(currentNode, patch) {
|
|
|
358
393
|
}
|
|
359
394
|
function parseUpdatedNode(currentNode, data) {
|
|
360
395
|
const candidate = mergeNodeUpdate(currentNode, data);
|
|
361
|
-
const parsed =
|
|
396
|
+
const parsed = parseNode(candidate);
|
|
362
397
|
if (parsed.success)
|
|
363
398
|
return parsed.data;
|
|
364
399
|
const schema = getNodeSchemaForType(candidate.type);
|
|
@@ -397,6 +432,306 @@ function refreshDefaultRidgeVentsForSegment(nextNodes, segment) {
|
|
|
397
432
|
};
|
|
398
433
|
return nextVents.map((vent) => vent.id);
|
|
399
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
|
+
}
|
|
400
735
|
// Track pending RAF for updateNodesAction to prevent multiple queued callbacks
|
|
401
736
|
let pendingRafId = null;
|
|
402
737
|
let pendingUpdates = new Set();
|
|
@@ -547,9 +882,11 @@ function buildWallMergePlans(nodes, idsToDelete) {
|
|
|
547
882
|
}
|
|
548
883
|
return mergePlans;
|
|
549
884
|
}
|
|
550
|
-
|
|
885
|
+
const createNodesActionImpl = (set, get, ops) => {
|
|
551
886
|
if (get().readOnly)
|
|
552
887
|
return;
|
|
888
|
+
const extraNodesToMarkDirty = new Set();
|
|
889
|
+
const extraNodesToClearDirty = new Set();
|
|
553
890
|
set((state) => {
|
|
554
891
|
const nextNodes = { ...state.nodes };
|
|
555
892
|
const nextRootIds = [...state.rootNodeIds];
|
|
@@ -584,6 +921,16 @@ export const createNodesAction = (set, get, ops) => {
|
|
|
584
921
|
}
|
|
585
922
|
}
|
|
586
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]);
|
|
587
934
|
return { nodes: nextNodes, rootNodeIds: nextRootIds };
|
|
588
935
|
});
|
|
589
936
|
// 4. System Sync
|
|
@@ -594,25 +941,33 @@ export const createNodesAction = (set, get, ops) => {
|
|
|
594
941
|
else if (node.parentId)
|
|
595
942
|
get().markDirty(node.parentId);
|
|
596
943
|
});
|
|
944
|
+
for (const id of extraNodesToMarkDirty)
|
|
945
|
+
get().markDirty(id);
|
|
946
|
+
for (const id of extraNodesToClearDirty)
|
|
947
|
+
get().clearDirty(id);
|
|
597
948
|
};
|
|
598
|
-
|
|
949
|
+
const applyNodeChangesActionImpl = (set, get, changes) => {
|
|
599
950
|
if (get().readOnly)
|
|
600
951
|
return;
|
|
601
952
|
const createOps = changes.create ?? [];
|
|
602
953
|
const updateOps = changes.update ?? [];
|
|
603
954
|
const deleteOps = changes.delete ?? [];
|
|
604
955
|
const nodesToMarkDirty = new Set();
|
|
956
|
+
const nodesToClearDirty = new Set();
|
|
605
957
|
const parentsToMarkDirty = new Set();
|
|
606
958
|
set((state) => {
|
|
607
959
|
const nextNodes = { ...state.nodes };
|
|
608
960
|
const nextCollections = { ...state.collections };
|
|
609
961
|
const nextRootIds = [...state.rootNodeIds];
|
|
610
962
|
let resolvedRootIds = nextRootIds;
|
|
963
|
+
const roofsToRefresh = new Set();
|
|
611
964
|
for (const { id, data } of updateOps) {
|
|
612
965
|
const currentNode = nextNodes[id];
|
|
613
966
|
if (!currentNode)
|
|
614
967
|
continue;
|
|
968
|
+
addLeanToHostRoofId(currentNode, nextNodes, roofsToRefresh);
|
|
615
969
|
const updatedNode = parseUpdatedNode(currentNode, data);
|
|
970
|
+
addLeanToHostRoofId(updatedNode, nextNodes, roofsToRefresh);
|
|
616
971
|
if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
|
|
617
972
|
const oldParentId = currentNode.parentId;
|
|
618
973
|
if (oldParentId && nextNodes[oldParentId]) {
|
|
@@ -639,6 +994,11 @@ export const applyNodeChangesAction = (set, get, changes) => {
|
|
|
639
994
|
nodesToMarkDirty.add(ventId);
|
|
640
995
|
}
|
|
641
996
|
}
|
|
997
|
+
const currentSegment = nextNodes[id];
|
|
998
|
+
if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
|
|
999
|
+
if (currentSegment.parentId)
|
|
1000
|
+
roofsToRefresh.add(currentSegment.parentId);
|
|
1001
|
+
}
|
|
642
1002
|
nodesToMarkDirty.add(id);
|
|
643
1003
|
}
|
|
644
1004
|
for (const { node, parentId } of createOps) {
|
|
@@ -646,6 +1006,10 @@ export const applyNodeChangesAction = (set, get, changes) => {
|
|
|
646
1006
|
const newNode = parseCreatedNode(node, effectiveParentId);
|
|
647
1007
|
nextNodes[newNode.id] = newNode;
|
|
648
1008
|
nodesToMarkDirty.add(newNode.id);
|
|
1009
|
+
if (newNode.type === 'roof-segment' && effectiveParentId) {
|
|
1010
|
+
roofsToRefresh.add(effectiveParentId);
|
|
1011
|
+
}
|
|
1012
|
+
addLeanToHostRoofId(newNode, nextNodes, roofsToRefresh);
|
|
649
1013
|
if (effectiveParentId && nextNodes[effectiveParentId]) {
|
|
650
1014
|
const parent = nextNodes[effectiveParentId];
|
|
651
1015
|
if ('children' in parent && Array.isArray(parent.children)) {
|
|
@@ -675,6 +1039,13 @@ export const applyNodeChangesAction = (set, get, changes) => {
|
|
|
675
1039
|
for (const id of deleteOps) {
|
|
676
1040
|
collectDelete(id);
|
|
677
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
|
+
}
|
|
678
1049
|
for (const id of allIdsToDelete) {
|
|
679
1050
|
const node = nextNodes[id];
|
|
680
1051
|
if (!node)
|
|
@@ -704,11 +1075,21 @@ export const applyNodeChangesAction = (set, get, changes) => {
|
|
|
704
1075
|
}
|
|
705
1076
|
delete nextNodes[id];
|
|
706
1077
|
}
|
|
1078
|
+
refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, nodesToMarkDirty, nodesToClearDirty);
|
|
1079
|
+
addActiveSceneCommitNodeIds([
|
|
1080
|
+
...allIdsToDelete,
|
|
1081
|
+
...nodesToMarkDirty,
|
|
1082
|
+
...nodesToClearDirty,
|
|
1083
|
+
...parentsToMarkDirty,
|
|
1084
|
+
]);
|
|
707
1085
|
return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections };
|
|
708
1086
|
});
|
|
709
1087
|
for (const id of nodesToMarkDirty) {
|
|
710
1088
|
get().markDirty(id);
|
|
711
1089
|
}
|
|
1090
|
+
for (const id of nodesToClearDirty) {
|
|
1091
|
+
get().clearDirty(id);
|
|
1092
|
+
}
|
|
712
1093
|
for (const id of parentsToMarkDirty) {
|
|
713
1094
|
get().markDirty(id);
|
|
714
1095
|
const parent = get().nodes[id];
|
|
@@ -719,18 +1100,29 @@ export const applyNodeChangesAction = (set, get, changes) => {
|
|
|
719
1100
|
}
|
|
720
1101
|
}
|
|
721
1102
|
};
|
|
722
|
-
|
|
1103
|
+
const updateNodesActionImpl = (set, get, updates) => {
|
|
723
1104
|
if (get().readOnly)
|
|
724
1105
|
return;
|
|
725
1106
|
const parentsToUpdate = new Set();
|
|
726
1107
|
const extraNodesToUpdate = new Set();
|
|
1108
|
+
const extraNodesToDelete = new Set();
|
|
1109
|
+
const roofsToRefresh = new Set();
|
|
727
1110
|
set((state) => {
|
|
728
1111
|
const nextNodes = { ...state.nodes };
|
|
729
1112
|
for (const { id, data } of updates) {
|
|
730
1113
|
const currentNode = nextNodes[id];
|
|
731
1114
|
if (!currentNode)
|
|
732
1115
|
continue;
|
|
733
|
-
|
|
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);
|
|
734
1126
|
// Handle Reparenting Logic
|
|
735
1127
|
if (data.parentId !== undefined && data.parentId !== currentNode.parentId) {
|
|
736
1128
|
// 1. Remove from old parent
|
|
@@ -772,11 +1164,28 @@ export const updateNodesAction = (set, get, updates) => {
|
|
|
772
1164
|
extraNodesToUpdate.add(ventId);
|
|
773
1165
|
}
|
|
774
1166
|
}
|
|
1167
|
+
const currentSegment = nextNodes[id];
|
|
1168
|
+
if (currentSegment?.type === 'roof-segment' && shouldRefreshDefaultGutters(data)) {
|
|
1169
|
+
if (currentSegment.parentId)
|
|
1170
|
+
roofsToRefresh.add(currentSegment.parentId);
|
|
1171
|
+
}
|
|
775
1172
|
}
|
|
1173
|
+
refreshDefaultGuttersForRoofIds(nextNodes, roofsToRefresh, extraNodesToUpdate, extraNodesToDelete);
|
|
1174
|
+
addActiveSceneCommitNodeIds([
|
|
1175
|
+
...updates.map(({ id }) => id),
|
|
1176
|
+
...parentsToUpdate,
|
|
1177
|
+
...extraNodesToUpdate,
|
|
1178
|
+
...extraNodesToDelete,
|
|
1179
|
+
]);
|
|
776
1180
|
return { nodes: nextNodes };
|
|
777
1181
|
});
|
|
778
1182
|
// Batch dirty-marking into a single RAF to avoid redundant callbacks during rapid updates
|
|
779
1183
|
for (const u of updates) {
|
|
1184
|
+
// Visibility is applied by React before the deferred dirty callback. Mark
|
|
1185
|
+
// it now so render systems can release collective geometry in that same
|
|
1186
|
+
// frame, including when the host uses render-on-demand.
|
|
1187
|
+
if (u.data.visible !== undefined)
|
|
1188
|
+
get().markDirty(u.id);
|
|
780
1189
|
pendingUpdates.add(u.id);
|
|
781
1190
|
}
|
|
782
1191
|
for (const pId of parentsToUpdate) {
|
|
@@ -785,6 +1194,9 @@ export const updateNodesAction = (set, get, updates) => {
|
|
|
785
1194
|
for (const id of extraNodesToUpdate) {
|
|
786
1195
|
pendingUpdates.add(id);
|
|
787
1196
|
}
|
|
1197
|
+
for (const id of extraNodesToDelete) {
|
|
1198
|
+
get().clearDirty(id);
|
|
1199
|
+
}
|
|
788
1200
|
if (pendingRafId !== null) {
|
|
789
1201
|
cancelAnimationFrame(pendingRafId);
|
|
790
1202
|
}
|
|
@@ -796,13 +1208,14 @@ export const updateNodesAction = (set, get, updates) => {
|
|
|
796
1208
|
pendingRafId = null;
|
|
797
1209
|
});
|
|
798
1210
|
};
|
|
799
|
-
|
|
1211
|
+
const deleteNodesActionImpl = (set, get, ids) => {
|
|
800
1212
|
if (get().readOnly)
|
|
801
1213
|
return;
|
|
802
1214
|
const parentsToMarkDirty = new Set();
|
|
803
1215
|
const nodesToMarkDirty = new Set();
|
|
804
1216
|
const deletedIds = new Set();
|
|
805
1217
|
const mergePlans = buildWallMergePlans(get().nodes, ids);
|
|
1218
|
+
const requestedDeleteIds = new Set(ids);
|
|
806
1219
|
set((state) => {
|
|
807
1220
|
const nextNodes = { ...state.nodes };
|
|
808
1221
|
const nextCollections = { ...state.collections };
|
|
@@ -816,7 +1229,9 @@ export const deleteNodesAction = (set, get, ids) => {
|
|
|
816
1229
|
allIds.add(id);
|
|
817
1230
|
const node = nextNodes[id];
|
|
818
1231
|
const cascadeDeletes = node
|
|
819
|
-
? nodeRegistry
|
|
1232
|
+
? nodeRegistry
|
|
1233
|
+
.get(node.type)
|
|
1234
|
+
?.parametrics?.onDeleteCascade?.(node, nextNodes, allIds, requestedDeleteIds)
|
|
820
1235
|
: null;
|
|
821
1236
|
if (cascadeDeletes) {
|
|
822
1237
|
for (const companionId of cascadeDeletes)
|
|
@@ -832,6 +1247,14 @@ export const deleteNodesAction = (set, get, ids) => {
|
|
|
832
1247
|
for (const plan of mergePlans) {
|
|
833
1248
|
allIds.add(plan.secondaryWallId);
|
|
834
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
|
+
}
|
|
835
1258
|
for (const id of allIds)
|
|
836
1259
|
deletedIds.add(id);
|
|
837
1260
|
// Let each deleted kind undo what it imposed on its neighbours (e.g. an
|
|
@@ -845,7 +1268,7 @@ export const deleteNodesAction = (set, get, ids) => {
|
|
|
845
1268
|
const onDelete = nodeRegistry.get(node.type)?.parametrics?.onDelete;
|
|
846
1269
|
if (!onDelete)
|
|
847
1270
|
continue;
|
|
848
|
-
for (const { id: targetId, data } of onDelete(node, nextNodes)) {
|
|
1271
|
+
for (const { id: targetId, data } of onDelete(node, nextNodes, allIds, requestedDeleteIds)) {
|
|
849
1272
|
if (allIds.has(targetId))
|
|
850
1273
|
continue;
|
|
851
1274
|
const target = nextNodes[targetId];
|
|
@@ -935,6 +1358,8 @@ export const deleteNodesAction = (set, get, ids) => {
|
|
|
935
1358
|
// 4. Delete the node itself
|
|
936
1359
|
delete nextNodes[id];
|
|
937
1360
|
}
|
|
1361
|
+
refreshDefaultGuttersForRoofIds(nextNodes, affectedRoofIds, nodesToMarkDirty, deletedIds);
|
|
1362
|
+
addActiveSceneCommitNodeIds([...deletedIds, ...parentsToMarkDirty, ...nodesToMarkDirty]);
|
|
938
1363
|
return { nodes: nextNodes, rootNodeIds: nextRootIds, collections: nextCollections };
|
|
939
1364
|
});
|
|
940
1365
|
// Deleted ids must leave the dirty set: every consumer skips missing
|
|
@@ -957,3 +1382,17 @@ export const deleteNodesAction = (set, get, ids) => {
|
|
|
957
1382
|
get().markDirty(id);
|
|
958
1383
|
});
|
|
959
1384
|
};
|
|
1385
|
+
export const createNodesAction = (set, get, ops) => runWithSceneCommitNodeIds(ops.flatMap(({ node, parentId }) => {
|
|
1386
|
+
const effectiveParentId = parentId ?? node.parentId;
|
|
1387
|
+
return effectiveParentId ? [node.id, effectiveParentId] : [node.id];
|
|
1388
|
+
}), () => createNodesActionImpl(set, get, ops));
|
|
1389
|
+
export const applyNodeChangesAction = (set, get, changes) => runWithSceneCommitNodeIds([
|
|
1390
|
+
...(changes.create ?? []).flatMap(({ node, parentId }) => {
|
|
1391
|
+
const effectiveParentId = parentId ?? node.parentId;
|
|
1392
|
+
return effectiveParentId ? [node.id, effectiveParentId] : [node.id];
|
|
1393
|
+
}),
|
|
1394
|
+
...(changes.update ?? []).map(({ id }) => id),
|
|
1395
|
+
...(changes.delete ?? []),
|
|
1396
|
+
], () => applyNodeChangesActionImpl(set, get, changes));
|
|
1397
|
+
export const updateNodesAction = (set, get, updates) => runWithSceneCommitNodeIds(updates.map(({ id }) => id), () => updateNodesActionImpl(set, get, updates));
|
|
1398
|
+
export const deleteNodesAction = (set, get, ids) => runWithSceneCommitNodeIds(ids, () => deleteNodesActionImpl(set, get, ids));
|
|
@@ -13,6 +13,7 @@ export type SceneCommit = {
|
|
|
13
13
|
origin: SceneCommitOrigin;
|
|
14
14
|
before: SceneSnapshot;
|
|
15
15
|
current: SceneSnapshot;
|
|
16
|
+
changedNodeIds?: ReadonlySet<AnyNodeId>;
|
|
16
17
|
};
|
|
17
18
|
export type SceneCommitListener = (commit: SceneCommit) => void;
|
|
18
19
|
type TemporalStoreLike = {
|
|
@@ -33,11 +34,15 @@ type TemporalHistoryStoreLike<TPastState> = {
|
|
|
33
34
|
}): void;
|
|
34
35
|
};
|
|
35
36
|
};
|
|
37
|
+
export declare function activeSceneCommitNodeIds(): ReadonlySet<AnyNodeId> | undefined;
|
|
38
|
+
export declare function addActiveSceneCommitNodeIds(nodeIds: Iterable<AnyNodeId>): void;
|
|
39
|
+
export declare function runWithSceneCommitNodeIds<TResult>(nodeIds: Iterable<AnyNodeId>, run: () => TResult): TResult;
|
|
36
40
|
export declare function areSceneSnapshotsEqual(left: SceneSnapshot, right: SceneSnapshot): boolean;
|
|
37
41
|
export declare function subscribeSceneCommits(listener: SceneCommitListener): () => void;
|
|
38
42
|
export declare function notifySceneCommit(commit: SceneCommit): void;
|
|
39
43
|
export declare function pauseSceneHistory(sceneStore: TemporalStoreLike): void;
|
|
40
44
|
export declare function resumeSceneHistory(sceneStore: TemporalStoreLike): void;
|
|
45
|
+
export declare function acquireSceneHistoryPause(sceneStore: TemporalStoreLike): () => void;
|
|
41
46
|
export declare function getSceneHistoryPauseDepth(): number;
|
|
42
47
|
export declare function resetSceneHistoryPauseDepth(): void;
|
|
43
48
|
export declare function runAsSingleSceneHistoryStep<TPastState, TResult>(sceneStore: TemporalHistoryStoreLike<TPastState>, run: () => TResult): TResult;
|