@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.
Files changed (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -0
  3. package/dist/capture/index.d.ts +3 -0
  4. package/dist/capture/index.d.ts.map +1 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/schema.d.ts +417 -0
  7. package/dist/capture/schema.d.ts.map +1 -0
  8. package/dist/capture/schema.js +348 -0
  9. package/dist/capture/source.d.ts +60 -0
  10. package/dist/capture/source.d.ts.map +1 -0
  11. package/dist/capture/source.js +254 -0
  12. package/dist/events/bus.d.ts +55 -7
  13. package/dist/events/bus.d.ts.map +1 -1
  14. package/dist/events/hidden-wall-pointer-hold.d.ts +28 -0
  15. package/dist/events/hidden-wall-pointer-hold.d.ts.map +1 -0
  16. package/dist/events/hidden-wall-pointer-hold.js +37 -0
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +2785 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  19. package/dist/hooks/spatial-grid/spatial-grid-sync.js +132 -19
  20. package/dist/hooks/spatial-grid/support-host-patch.d.ts +45 -1
  21. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -1
  22. package/dist/hooks/spatial-grid/support-host-patch.js +171 -6
  23. package/dist/index.d.ts +14 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +16 -5
  26. package/dist/lib/asset-storage.d.ts.map +1 -1
  27. package/dist/lib/asset-storage.js +4 -1
  28. package/dist/lib/polygon-union.d.ts +4 -0
  29. package/dist/lib/polygon-union.d.ts.map +1 -0
  30. package/dist/lib/polygon-union.js +306 -0
  31. package/dist/lib/roof-overlap.d.ts +32 -0
  32. package/dist/lib/roof-overlap.d.ts.map +1 -0
  33. package/dist/lib/roof-overlap.js +72 -0
  34. package/dist/lib/slab-polygon.d.ts +35 -2
  35. package/dist/lib/slab-polygon.d.ts.map +1 -1
  36. package/dist/lib/slab-polygon.js +133 -27
  37. package/dist/lib/space-detection.d.ts +11 -0
  38. package/dist/lib/space-detection.d.ts.map +1 -1
  39. package/dist/lib/space-detection.js +42 -14
  40. package/dist/registry/handles.d.ts +24 -3
  41. package/dist/registry/handles.d.ts.map +1 -1
  42. package/dist/registry/index.d.ts +3 -4
  43. package/dist/registry/index.d.ts.map +1 -1
  44. package/dist/registry/index.js +1 -2
  45. package/dist/registry/registry.d.ts +13 -6
  46. package/dist/registry/registry.d.ts.map +1 -1
  47. package/dist/registry/registry.js +37 -4
  48. package/dist/registry/scene-api.d.ts +16 -0
  49. package/dist/registry/scene-api.d.ts.map +1 -1
  50. package/dist/registry/scene-api.js +49 -1
  51. package/dist/registry/types.d.ts +226 -13
  52. package/dist/registry/types.d.ts.map +1 -1
  53. package/dist/schema/__bench__/node-parsers.bench.d.ts +24 -0
  54. package/dist/schema/__bench__/node-parsers.bench.d.ts.map +1 -0
  55. package/dist/schema/__bench__/node-parsers.bench.js +314 -0
  56. package/dist/schema/__fixtures__/node-fixtures.d.ts +28 -0
  57. package/dist/schema/__fixtures__/node-fixtures.d.ts.map +1 -0
  58. package/dist/schema/__fixtures__/node-fixtures.js +125 -0
  59. package/dist/schema/base.d.ts +1 -1
  60. package/dist/schema/base.d.ts.map +1 -1
  61. package/dist/schema/base.js +8 -1
  62. package/dist/schema/compiled-node-parsers.d.ts +40 -0
  63. package/dist/schema/compiled-node-parsers.d.ts.map +1 -0
  64. package/dist/schema/compiled-node-parsers.js +95 -0
  65. package/dist/schema/index.d.ts +19 -15
  66. package/dist/schema/index.d.ts.map +1 -1
  67. package/dist/schema/index.js +17 -12
  68. package/dist/schema/material.d.ts +2 -1
  69. package/dist/schema/material.d.ts.map +1 -1
  70. package/dist/schema/material.js +1 -0
  71. package/dist/schema/nodes/automatic-downspout.d.ts +17 -0
  72. package/dist/schema/nodes/automatic-downspout.d.ts.map +1 -0
  73. package/dist/schema/nodes/automatic-downspout.js +237 -0
  74. package/dist/schema/nodes/block.d.ts +107 -0
  75. package/dist/schema/nodes/block.d.ts.map +1 -0
  76. package/dist/schema/nodes/block.js +207 -0
  77. package/dist/schema/nodes/box-vent.d.ts +8 -2
  78. package/dist/schema/nodes/box-vent.d.ts.map +1 -1
  79. package/dist/schema/nodes/box-vent.js +2 -0
  80. package/dist/schema/nodes/building.d.ts +1 -1
  81. package/dist/schema/nodes/cabinet.d.ts +25 -3
  82. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  83. package/dist/schema/nodes/cabinet.js +26 -5
  84. package/dist/schema/nodes/ceiling.d.ts +1 -1
  85. package/dist/schema/nodes/chimney.d.ts +2 -2
  86. package/dist/schema/nodes/column.d.ts +6 -6
  87. package/dist/schema/nodes/construction-dimension.d.ts +4 -4
  88. package/dist/schema/nodes/cupola.d.ts +9 -1
  89. package/dist/schema/nodes/cupola.d.ts.map +1 -1
  90. package/dist/schema/nodes/cupola.js +2 -0
  91. package/dist/schema/nodes/door.d.ts +1 -1
  92. package/dist/schema/nodes/dormer.d.ts +44 -1
  93. package/dist/schema/nodes/dormer.d.ts.map +1 -1
  94. package/dist/schema/nodes/dormer.js +131 -7
  95. package/dist/schema/nodes/downspout.d.ts +11 -1
  96. package/dist/schema/nodes/downspout.d.ts.map +1 -1
  97. package/dist/schema/nodes/downspout.js +24 -0
  98. package/dist/schema/nodes/duct-fitting.d.ts +18 -1
  99. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -1
  100. package/dist/schema/nodes/duct-fitting.js +25 -4
  101. package/dist/schema/nodes/duct-segment.d.ts +23 -1
  102. package/dist/schema/nodes/duct-segment.d.ts.map +1 -1
  103. package/dist/schema/nodes/duct-segment.js +20 -0
  104. package/dist/schema/nodes/duct-terminal.d.ts +1 -1
  105. package/dist/schema/nodes/elevator.d.ts +1 -1
  106. package/dist/schema/nodes/eyebrow-vent.d.ts +8 -2
  107. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -1
  108. package/dist/schema/nodes/eyebrow-vent.js +2 -0
  109. package/dist/schema/nodes/fence.d.ts +1 -1
  110. package/dist/schema/nodes/guide.d.ts +1 -1
  111. package/dist/schema/nodes/gutter.d.ts +36 -2
  112. package/dist/schema/nodes/gutter.d.ts.map +1 -1
  113. package/dist/schema/nodes/gutter.js +308 -1
  114. package/dist/schema/nodes/hanger-overrides.d.ts +7 -0
  115. package/dist/schema/nodes/hanger-overrides.d.ts.map +1 -0
  116. package/dist/schema/nodes/hanger-overrides.js +6 -0
  117. package/dist/schema/nodes/hvac-equipment.d.ts +1 -1
  118. package/dist/schema/nodes/item.d.ts +4 -3
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +5 -0
  121. package/dist/schema/nodes/lean-to-extension.d.ts +212 -0
  122. package/dist/schema/nodes/lean-to-extension.d.ts.map +1 -0
  123. package/dist/schema/nodes/lean-to-extension.js +114 -0
  124. package/dist/schema/nodes/level.d.ts +2 -2
  125. package/dist/schema/nodes/level.d.ts.map +1 -1
  126. package/dist/schema/nodes/lineset.d.ts +1 -1
  127. package/dist/schema/nodes/liquid-line.d.ts +1 -1
  128. package/dist/schema/nodes/measurement.d.ts +1 -1
  129. package/dist/schema/nodes/pipe-fitting.d.ts +9 -1
  130. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -1
  131. package/dist/schema/nodes/pipe-fitting.js +6 -2
  132. package/dist/schema/nodes/pipe-segment.d.ts +23 -1
  133. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -1
  134. package/dist/schema/nodes/pipe-segment.js +20 -0
  135. package/dist/schema/nodes/pipe-trap.d.ts +1 -1
  136. package/dist/schema/nodes/ridge-vent.d.ts +1 -1
  137. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  138. package/dist/schema/nodes/ridge-vent.js +4 -1
  139. package/dist/schema/nodes/roof-segment-shape.d.ts +4 -0
  140. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -1
  141. package/dist/schema/nodes/roof-segment-shape.js +64 -1
  142. package/dist/schema/nodes/roof-segment-walls.d.ts +3 -1
  143. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -1
  144. package/dist/schema/nodes/roof-segment-walls.js +1 -1
  145. package/dist/schema/nodes/roof-segment.d.ts +41 -1
  146. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  147. package/dist/schema/nodes/roof-segment.js +79 -2
  148. package/dist/schema/nodes/roof.d.ts +22 -1
  149. package/dist/schema/nodes/roof.d.ts.map +1 -1
  150. package/dist/schema/nodes/roof.js +14 -0
  151. package/dist/schema/nodes/scan.d.ts +19 -2
  152. package/dist/schema/nodes/scan.d.ts.map +1 -1
  153. package/dist/schema/nodes/scan.js +11 -1
  154. package/dist/schema/nodes/shelf.d.ts +2 -2
  155. package/dist/schema/nodes/site.d.ts +1 -1
  156. package/dist/schema/nodes/skylight.d.ts +1 -1
  157. package/dist/schema/nodes/slab.d.ts +1 -1
  158. package/dist/schema/nodes/solar-panel.d.ts +1 -1
  159. package/dist/schema/nodes/spawn.d.ts +1 -1
  160. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  161. package/dist/schema/nodes/stair.d.ts +1 -1
  162. package/dist/schema/nodes/structural-grid.d.ts +1 -1
  163. package/dist/schema/nodes/turbine-vent.d.ts +7 -1
  164. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -1
  165. package/dist/schema/nodes/turbine-vent.js +2 -0
  166. package/dist/schema/nodes/wall.d.ts +4 -4
  167. package/dist/schema/nodes/wall.d.ts.map +1 -1
  168. package/dist/schema/nodes/wall.js +7 -1
  169. package/dist/schema/nodes/window.d.ts +10 -3
  170. package/dist/schema/nodes/window.d.ts.map +1 -1
  171. package/dist/schema/nodes/window.js +4 -0
  172. package/dist/schema/nodes/zone.d.ts +2 -2
  173. package/dist/schema/nodes/zone.js +1 -1
  174. package/dist/schema/types.d.ts +484 -109
  175. package/dist/schema/types.d.ts.map +1 -1
  176. package/dist/schema/types.js +27 -1
  177. package/dist/services/hosting.d.ts +2 -2
  178. package/dist/services/hosting.d.ts.map +1 -1
  179. package/dist/services/hosting.js +4 -4
  180. package/dist/services/index.d.ts +1 -1
  181. package/dist/services/index.d.ts.map +1 -1
  182. package/dist/services/index.js +1 -1
  183. package/dist/services/system-graph.d.ts +11 -0
  184. package/dist/services/system-graph.d.ts.map +1 -1
  185. package/dist/services/system-graph.js +35 -5
  186. package/dist/store/actions/node-actions.d.ts +7 -0
  187. package/dist/store/actions/node-actions.d.ts.map +1 -1
  188. package/dist/store/actions/node-actions.js +429 -17
  189. package/dist/store/history-invalidation.d.ts +3 -0
  190. package/dist/store/history-invalidation.d.ts.map +1 -0
  191. package/dist/store/history-invalidation.js +72 -0
  192. package/dist/store/scene-hydration.d.ts +5 -0
  193. package/dist/store/scene-hydration.d.ts.map +1 -0
  194. package/dist/store/scene-hydration.js +57 -0
  195. package/dist/store/use-scene.d.ts +3 -0
  196. package/dist/store/use-scene.d.ts.map +1 -1
  197. package/dist/store/use-scene.js +332 -64
  198. package/dist/systems/roof/roof-elevation-system.d.ts +3 -0
  199. package/dist/systems/roof/roof-elevation-system.d.ts.map +1 -0
  200. package/dist/systems/roof/roof-elevation-system.js +94 -0
  201. package/dist/systems/roof/roof-elevation.d.ts +4 -0
  202. package/dist/systems/roof/roof-elevation.d.ts.map +1 -0
  203. package/dist/systems/roof/roof-elevation.js +95 -0
  204. package/dist/systems/roof/roof-footprint.d.ts +17 -0
  205. package/dist/systems/roof/roof-footprint.d.ts.map +1 -0
  206. package/dist/systems/roof/roof-footprint.js +83 -0
  207. package/dist/systems/stair/stair-flight.d.ts +17 -0
  208. package/dist/systems/stair/stair-flight.d.ts.map +1 -0
  209. package/dist/systems/stair/stair-flight.js +27 -0
  210. package/dist/systems/stair/stair-opening-preview.d.ts +1555 -1328
  211. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  212. package/dist/systems/stair/stair-opening-system.d.ts +1 -0
  213. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  214. package/dist/systems/stair/stair-opening-system.js +93 -96
  215. package/dist/systems/stair/stair-rise.d.ts.map +1 -1
  216. package/dist/systems/stair/stair-rise.js +20 -21
  217. package/dist/systems/wall/wall-curve.d.ts +1 -0
  218. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  219. package/dist/systems/wall/wall-curve.js +73 -0
  220. package/dist/systems/wall/wall-footprint.d.ts +1 -0
  221. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  222. package/dist/systems/wall/wall-footprint.js +1 -0
  223. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  224. package/dist/utils/clone-scene-graph.js +26 -0
  225. package/dist/validation/validate-build-json.d.ts +3 -0
  226. package/dist/validation/validate-build-json.d.ts.map +1 -1
  227. package/dist/validation/validate-build-json.js +49 -2
  228. package/package.json +10 -3
  229. package/dist/systems/elevator/elevator-runtime-system.d.ts +0 -2
  230. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +0 -1
  231. 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
- return issues
324
- .map((issue) => {
325
- const path = issue.path.map(String).join('.') || '<root>';
326
- const to = issue.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`;
327
- return `${path}: ${formatNumericValue(issue.from)} ${issue.action}${to}`;
328
- })
329
- .join('; ');
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
- console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, numericSanitizeIssuesToMessage(issues));
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 = AnyNodeSchema.safeParse(candidate);
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 = AnyNodeSchema.safeParse(candidate);
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
- addActiveSceneCommitNodeIds([...allIdsToDelete, ...nodesToMarkDirty, ...parentsToMarkDirty]);
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
- const updatedNode = parseUpdatedNode(currentNode, data);
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.get(node.type)?.parametrics?.onDeleteCascade?.(node, nextNodes, allIds)
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
- addActiveSceneCommitNodeIds([...allIds, ...parentsToMarkDirty, ...nodesToMarkDirty]);
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,3 @@
1
+ import type { AnyNode, AnyNodeId } from '../schema';
2
+ export declare function getHistoryDirtyNodeIds(before: Record<string, AnyNode>, after: Record<string, AnyNode>): Set<AnyNodeId>;
3
+ //# sourceMappingURL=history-invalidation.d.ts.map
@@ -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"}