@pascal-app/nodes 1.0.0 → 1.0.1

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 (154) hide show
  1. package/dist/box-vent/panel.js +4 -4
  2. package/dist/cabinet/appliance-layout.d.ts +26 -0
  3. package/dist/cabinet/appliance-layout.d.ts.map +1 -0
  4. package/dist/cabinet/appliance-layout.js +52 -0
  5. package/dist/cabinet/countertop-layout.d.ts +173 -0
  6. package/dist/cabinet/countertop-layout.d.ts.map +1 -0
  7. package/dist/cabinet/countertop-layout.js +80 -0
  8. package/dist/cabinet/definition.d.ts.map +1 -1
  9. package/dist/cabinet/definition.js +15 -8
  10. package/dist/cabinet/geometry/cooktop.d.ts.map +1 -1
  11. package/dist/cabinet/geometry/cooktop.js +2 -2
  12. package/dist/cabinet/geometry/run.d.ts.map +1 -1
  13. package/dist/cabinet/geometry/run.js +10 -71
  14. package/dist/cabinet/geometry/sink.d.ts +2 -12
  15. package/dist/cabinet/geometry/sink.d.ts.map +1 -1
  16. package/dist/cabinet/geometry/sink.js +8 -39
  17. package/dist/cabinet/hosted-resize.d.ts +8 -0
  18. package/dist/cabinet/hosted-resize.d.ts.map +1 -0
  19. package/dist/cabinet/hosted-resize.js +158 -0
  20. package/dist/cabinet/run-layout.d.ts +8 -1
  21. package/dist/cabinet/run-layout.d.ts.map +1 -1
  22. package/dist/cabinet/run-layout.js +22 -14
  23. package/dist/cabinet/run-ops.d.ts +3 -3
  24. package/dist/cabinet/run-ops.js +3 -3
  25. package/dist/cabinet/run-panel.d.ts +1 -1
  26. package/dist/cabinet/run-panel.d.ts.map +1 -1
  27. package/dist/cabinet/run-panel.js +16 -7
  28. package/dist/cabinet/surfaces.d.ts +4 -0
  29. package/dist/cabinet/surfaces.d.ts.map +1 -0
  30. package/dist/cabinet/surfaces.js +101 -0
  31. package/dist/cabinet/tree-structure.d.ts.map +1 -1
  32. package/dist/cabinet/tree-structure.js +10 -9
  33. package/dist/ceiling/definition.js +1 -1
  34. package/dist/ceiling/floorplan-affordances.d.ts +4 -4
  35. package/dist/ceiling/panel.js +1 -1
  36. package/dist/chimney/panel.js +9 -9
  37. package/dist/cupola/panel.js +1 -1
  38. package/dist/door/panel.js +12 -12
  39. package/dist/dormer/panel-position-section.js +2 -2
  40. package/dist/dormer/panel.js +1 -1
  41. package/dist/duct-segment/parametrics.d.ts.map +1 -1
  42. package/dist/duct-segment/parametrics.js +5 -1
  43. package/dist/duct-segment/tool.d.ts +1 -0
  44. package/dist/duct-segment/tool.d.ts.map +1 -1
  45. package/dist/duct-segment/tool.js +10 -8
  46. package/dist/eyebrow-vent/panel.js +1 -1
  47. package/dist/fence/inspector-editors.js +1 -1
  48. package/dist/imported-mesh/definition.d.ts +5 -0
  49. package/dist/imported-mesh/definition.d.ts.map +1 -0
  50. package/dist/imported-mesh/definition.js +38 -0
  51. package/dist/imported-mesh/floorplan.d.ts +4 -0
  52. package/dist/imported-mesh/floorplan.d.ts.map +1 -0
  53. package/dist/imported-mesh/floorplan.js +34 -0
  54. package/dist/imported-mesh/geometry.d.ts +5 -0
  55. package/dist/imported-mesh/geometry.d.ts.map +1 -0
  56. package/dist/imported-mesh/geometry.js +34 -0
  57. package/dist/imported-mesh/index.d.ts +4 -0
  58. package/dist/imported-mesh/index.d.ts.map +1 -0
  59. package/dist/imported-mesh/index.js +3 -0
  60. package/dist/imported-mesh/schema.d.ts +2 -0
  61. package/dist/imported-mesh/schema.d.ts.map +1 -0
  62. package/dist/imported-mesh/schema.js +1 -0
  63. package/dist/index.d.ts +2 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +8 -0
  66. package/dist/item/floorplan-move.d.ts +9 -1
  67. package/dist/item/floorplan-move.d.ts.map +1 -1
  68. package/dist/item/floorplan-move.js +55 -2
  69. package/dist/item/floorplan.d.ts.map +1 -1
  70. package/dist/item/floorplan.js +11 -0
  71. package/dist/item/move-tool.js +1 -1
  72. package/dist/item/panel.js +1 -1
  73. package/dist/pipe-segment/parametrics.d.ts.map +1 -1
  74. package/dist/pipe-segment/parametrics.js +5 -1
  75. package/dist/procedural-item/definition.d.ts +4 -0
  76. package/dist/procedural-item/definition.d.ts.map +1 -0
  77. package/dist/procedural-item/definition.js +254 -0
  78. package/dist/procedural-item/geometry.d.ts +31 -0
  79. package/dist/procedural-item/geometry.d.ts.map +1 -0
  80. package/dist/procedural-item/geometry.js +88 -0
  81. package/dist/procedural-item/index.d.ts +3 -0
  82. package/dist/procedural-item/index.d.ts.map +1 -0
  83. package/dist/procedural-item/index.js +2 -0
  84. package/dist/procedural-item/move-session.d.ts +15 -0
  85. package/dist/procedural-item/move-session.d.ts.map +1 -0
  86. package/dist/procedural-item/move-session.js +197 -0
  87. package/dist/procedural-item/move-tool.d.ts +5 -0
  88. package/dist/procedural-item/move-tool.d.ts.map +1 -0
  89. package/dist/procedural-item/move-tool.js +193 -0
  90. package/dist/procedural-item/renderer.d.ts +5 -0
  91. package/dist/procedural-item/renderer.d.ts.map +1 -0
  92. package/dist/procedural-item/renderer.js +66 -0
  93. package/dist/ridge-vent/panel.js +4 -4
  94. package/dist/roof/panel.d.ts.map +1 -1
  95. package/dist/roof/panel.js +7 -5
  96. package/dist/roof-segment/panel.d.ts.map +1 -1
  97. package/dist/roof-segment/panel.js +5 -5
  98. package/dist/shared/automatic-run-end-cap.d.ts +1 -0
  99. package/dist/shared/automatic-run-end-cap.d.ts.map +1 -1
  100. package/dist/shared/automatic-run-end-cap.js +10 -0
  101. package/dist/shared/distribution-run-tool.d.ts +10 -0
  102. package/dist/shared/distribution-run-tool.d.ts.map +1 -1
  103. package/dist/shared/distribution-run-tool.js +56 -11
  104. package/dist/shared/panel-fields.d.ts +15 -0
  105. package/dist/shared/panel-fields.d.ts.map +1 -0
  106. package/dist/shared/panel-fields.js +31 -0
  107. package/dist/shared/resting-surface-plan.d.ts +3 -0
  108. package/dist/shared/resting-surface-plan.d.ts.map +1 -0
  109. package/dist/shared/resting-surface-plan.js +28 -0
  110. package/dist/shelf/definition.d.ts.map +1 -1
  111. package/dist/shelf/definition.js +2 -1
  112. package/dist/shelf/floorplan-affordances.d.ts.map +1 -1
  113. package/dist/shelf/floorplan-affordances.js +5 -3
  114. package/dist/shelf/floorplan-move.d.ts.map +1 -1
  115. package/dist/shelf/floorplan-move.js +3 -2
  116. package/dist/shelf/geometry.d.ts.map +1 -1
  117. package/dist/shelf/geometry.js +2 -10
  118. package/dist/skylight/panel.js +6 -6
  119. package/dist/slab/panel.js +1 -1
  120. package/dist/solar-panel/panel.js +2 -2
  121. package/dist/spawn/panel.js +1 -1
  122. package/dist/stair/panel.d.ts.map +1 -1
  123. package/dist/stair/panel.js +9 -7
  124. package/dist/stair-segment/panel.js +4 -4
  125. package/dist/turbine-vent/panel.js +1 -1
  126. package/dist/unit/definition.d.ts +10 -0
  127. package/dist/unit/definition.d.ts.map +1 -0
  128. package/dist/unit/definition.js +38 -0
  129. package/dist/unit/index.d.ts +2 -0
  130. package/dist/unit/index.d.ts.map +1 -0
  131. package/dist/unit/index.js +1 -0
  132. package/dist/unit/parametrics.d.ts +3 -0
  133. package/dist/unit/parametrics.d.ts.map +1 -0
  134. package/dist/unit/parametrics.js +4 -0
  135. package/dist/unit/schema.d.ts +2 -0
  136. package/dist/unit/schema.d.ts.map +1 -0
  137. package/dist/unit/schema.js +1 -0
  138. package/dist/unit/unit-panel.d.ts +5 -0
  139. package/dist/unit/unit-panel.d.ts.map +1 -0
  140. package/dist/unit/unit-panel.js +61 -0
  141. package/dist/window/panel.js +6 -6
  142. package/dist/zone/definition.d.ts.map +1 -1
  143. package/dist/zone/definition.js +7 -1
  144. package/dist/zone/floorplan.d.ts +1 -3
  145. package/dist/zone/floorplan.d.ts.map +1 -1
  146. package/dist/zone/floorplan.js +43 -28
  147. package/dist/zone/quantities-panel.d.ts.map +1 -1
  148. package/dist/zone/quantities-panel.js +33 -29
  149. package/dist/zone/renderer.d.ts.map +1 -1
  150. package/dist/zone/renderer.js +20 -6
  151. package/dist/zone/unit-membership.d.ts +12 -0
  152. package/dist/zone/unit-membership.d.ts.map +1 -0
  153. package/dist/zone/unit-membership.js +22 -0
  154. package/package.json +14 -8
@@ -1,16 +1,6 @@
1
1
  import { Group, type Material, Mesh } from 'three';
2
- import type { SinkLayout } from '../stack';
3
- export declare const FAUCET_SETBACK = 0.03;
4
- export type SinkBowlSpec = {
5
- centerX: number;
6
- width: number;
7
- depth: number;
8
- };
9
- /**
10
- * Bowl rects in module-local X/Z given the usable countertop footprint.
11
- * Shared by the 3D cut, the run-countertop cut, and the 2D floorplan symbol.
12
- */
13
- export declare function sinkBowls(layout: SinkLayout, usableWidth: number, usableDepth: number): SinkBowlSpec[];
2
+ import { type SinkBowlSpec } from '../appliance-layout';
3
+ export { FAUCET_SETBACK, type SinkBowlSpec, sinkBowls } from '../appliance-layout';
14
4
  /**
15
5
  * Subtract the sink bowl openings from a countertop mesh via three-bvh-csg.
16
6
  * `cutCenterX/Z` position the sink footprint in the countertop mesh's local
@@ -1 +1 @@
1
- {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../../../src/cabinet/geometry/sink.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,EACL,KAAK,QAAQ,EACb,IAAI,EAGL,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAQ1C,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC,MAAM,MAAM,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5E;;;GAGG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,YAAY,EAAE,CAuBhB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,IAAI,EAChB,KAAK,EAAE,YAAY,EAAE,EACrB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,mBAAmB,EAAE,MAAM,GAC1B,IAAI,CA8BN;AAkUD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,mBAAmB,EAAE,MAAM,EAC3B,KAAK,EAAE,MAAM,EACb,iBAAiB,EAAE,QAAQ,QAY5B"}
1
+ {"version":3,"file":"sink.d.ts","sourceRoot":"","sources":["../../../src/cabinet/geometry/sink.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,EACL,KAAK,QAAQ,EACb,IAAI,EAGL,MAAM,OAAO,CAAA;AACd,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAMlF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,IAAI,EAChB,KAAK,EAAE,YAAY,EAAE,EACrB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,mBAAmB,EAAE,MAAM,GAC1B,IAAI,CA6BN;AAqUD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,YAAY,EAAE,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,mBAAmB,EAAE,MAAM,EAC3B,KAAK,EAAE,MAAM,EACb,iBAAiB,EAAE,QAAQ,QAW5B"}
@@ -1,40 +1,9 @@
1
1
  import { Brush, csgEvaluator, csgGeometry, prepareBrushForCSG, SUBTRACTION, } from '@pascal-app/viewer';
2
2
  import { BoxGeometry, CylinderGeometry, Group, Mesh, SphereGeometry, TorusGeometry, } from 'three';
3
+ import { BASIN_WALL, FAUCET_BASE_RADIUS, sinkFaucetFootprint, sinkOpening, } from '../appliance-layout';
4
+ export { FAUCET_SETBACK, sinkBowls } from '../appliance-layout';
3
5
  import { createWorldScaleBoxGeometry, stampSlot } from './shared';
4
- const BASIN_WALL = 0.012;
5
6
  const BASIN_DEPTH = 0.19;
6
- const BASIN_CORNER_MARGIN = 0.06;
7
- // Centers the faucet base in the strip between the bowl's back edge and the
8
- // countertop's back edge (BASIN_CORNER_MARGIN wide).
9
- export const FAUCET_SETBACK = 0.03;
10
- /**
11
- * Bowl rects in module-local X/Z given the usable countertop footprint.
12
- * Shared by the 3D cut, the run-countertop cut, and the 2D floorplan symbol.
13
- */
14
- export function sinkBowls(layout, usableWidth, usableDepth) {
15
- const depth = Math.max(0.1, usableDepth - BASIN_CORNER_MARGIN * 2);
16
- const full = Math.max(0.15, usableWidth - BASIN_CORNER_MARGIN * 2);
17
- if (layout === 'single') {
18
- const width = Math.min(0.7, full);
19
- return [{ centerX: 0, width, depth }];
20
- }
21
- const divider = 0.03;
22
- if (layout === 'double') {
23
- const width = Math.min(0.42, (full - divider) / 2);
24
- return [
25
- { centerX: -(width + divider) / 2, width, depth },
26
- { centerX: (width + divider) / 2, width, depth },
27
- ];
28
- }
29
- // double-offset: 60/40 split
30
- const total = Math.min(0.86, full);
31
- const main = (total - divider) * 0.6;
32
- const side = (total - divider) * 0.4;
33
- return [
34
- { centerX: -(total / 2) + main / 2, width: main, depth },
35
- { centerX: total / 2 - side / 2, width: side, depth },
36
- ];
37
- }
38
7
  /**
39
8
  * Subtract the sink bowl openings from a countertop mesh via three-bvh-csg.
40
9
  * `cutCenterX/Z` position the sink footprint in the countertop mesh's local
@@ -49,7 +18,8 @@ export function cutSinkIntoCountertop(countertop, bowls, cutCenterX, cutCenterZ,
49
18
  for (const bowl of bowls) {
50
19
  // Rim reveal: the opening is slightly smaller than the basin shell so
51
20
  // the undermount lip tucks under the countertop.
52
- const cutter = new Brush(new BoxGeometry(bowl.width - BASIN_WALL, countertopThickness * 4, bowl.depth - BASIN_WALL));
21
+ const opening = sinkOpening(bowl);
22
+ const cutter = new Brush(new BoxGeometry(opening.width, countertopThickness * 4, opening.depth));
53
23
  cutter.position.set(cutCenterX + bowl.centerX, countertop.position.y, cutCenterZ);
54
24
  prepareBrushForCSG(cutter);
55
25
  const next = csgEvaluator.evaluate(result, cutter, SUBTRACTION);
@@ -174,7 +144,7 @@ function addFaucet(group, x, z, rimY, name, applianceMaterial) {
174
144
  const riserTopY = rimY + FAUCET_RISER_TOP;
175
145
  const reach = FAUCET_ARC_RADIUS * 2;
176
146
  // Round base flare where the body meets the countertop.
177
- const flare = stampSlot(new Mesh(new CylinderGeometry(FAUCET_BODY_RADIUS + 0.002, 0.032, 0.008, 28), applianceMaterial), 'appliance');
147
+ const flare = stampSlot(new Mesh(new CylinderGeometry(FAUCET_BODY_RADIUS + 0.002, FAUCET_BASE_RADIUS, 0.008, 28), applianceMaterial), 'appliance');
178
148
  flare.name = `${name}-faucet-escutcheon`;
179
149
  flare.position.set(x, rimY + 0.004, z);
180
150
  flare.castShadow = true;
@@ -254,9 +224,8 @@ export function addSinkCompartment(group, bowls, centerX, centerZ, rimY, counter
254
224
  for (const [bowlIndex, bowl] of bowls.entries()) {
255
225
  addBasinShell(group, bowl, centerX, centerZ, rimY, `${name}-${bowlIndex}`, applianceMaterial);
256
226
  }
257
- const bowlsMinX = Math.min(...bowls.map((bowl) => bowl.centerX - bowl.width / 2));
258
- const bowlsMaxX = Math.max(...bowls.map((bowl) => bowl.centerX + bowl.width / 2));
259
- const faucetX = centerX + (bowlsMinX + bowlsMaxX) / 2;
260
- const faucetZ = centerZ - bowls[0].depth / 2 - FAUCET_SETBACK;
227
+ const faucet = sinkFaucetFootprint(bowls);
228
+ const faucetX = centerX + faucet.x;
229
+ const faucetZ = centerZ + faucet.z;
261
230
  addFaucet(group, faucetX, faucetZ, rimY + countertopThickness, name, applianceMaterial);
262
231
  }
@@ -0,0 +1,8 @@
1
+ import { type AnyNode, type AnyNodeId, type HandleDescriptor, type SceneApi } from '@pascal-app/core';
2
+ type Nodes = Record<AnyNodeId, AnyNode>;
3
+ type Update = readonly [AnyNodeId, Partial<AnyNode>];
4
+ export declare function cabinetHostedChildUpdates(before: Nodes, after: Nodes): Update[] | null;
5
+ export declare function planCabinetHostedEdit(sceneApi: SceneApi, apply: (staged: SceneApi) => void): Update[] | null;
6
+ export declare function withCabinetHostedChildren<N extends AnyNode>(descriptor: HandleDescriptor<N>): HandleDescriptor<N>;
7
+ export {};
8
+ //# sourceMappingURL=hosted-resize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosted-resize.d.ts","sourceRoot":"","sources":["../../src/cabinet/hosted-resize.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAGd,KAAK,gBAAgB,EAErB,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AAGzB,KAAK,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;AACvC,KAAK,MAAM,GAAG,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAA;AASpD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,GAAG,IAAI,CAiFtF;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,GAChC,MAAM,EAAE,GAAG,IAAI,CA8BjB;AAED,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,OAAO,EACzD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC9B,gBAAgB,CAAC,CAAC,CAAC,CA8CrB"}
@@ -0,0 +1,158 @@
1
+ import { getScaledDimensions, getSurfaceProvider, nodeRegistry, surfaceRegionContainsFootprint, } from '@pascal-app/core';
2
+ import { boundsOf, boxCorners, frame, transformPoint } from '@pascal-app/core/procedural-items';
3
+ function surfaceScene(nodes) {
4
+ return {
5
+ nodes: () => nodes,
6
+ get: (id) => nodes[id],
7
+ };
8
+ }
9
+ export function cabinetHostedChildUpdates(before, after) {
10
+ const updates = [];
11
+ for (const host of Object.values(before)) {
12
+ if (host.type !== 'cabinet')
13
+ continue;
14
+ const children = host.children
15
+ .map((id) => before[id])
16
+ .filter((child) => child?.parentId === host.id &&
17
+ child.type !== 'cabinet' &&
18
+ child.type !== 'cabinet-module');
19
+ if (!children.length)
20
+ continue;
21
+ const nextHost = after[host.id];
22
+ if (nextHost?.type !== 'cabinet')
23
+ return null;
24
+ const provider = getSurfaceProvider(host);
25
+ const nextSurfaces = provider.surfaces?.(nextHost, { scene: surfaceScene(after) }) ?? [];
26
+ if (JSON.stringify(provider.surfaces?.(host, { scene: surfaceScene(before) })) ===
27
+ JSON.stringify(nextSurfaces))
28
+ continue;
29
+ for (const child of children) {
30
+ if (!child || !('position' in child) || !('rotation' in child))
31
+ continue;
32
+ const bounds = child.type === 'item'
33
+ ? { size: getScaledDimensions(child), center: undefined }
34
+ : nodeRegistry.get(child.type)?.capabilities.dragBounds?.(child, before);
35
+ if (!bounds)
36
+ continue;
37
+ const center = bounds.center ?? [0, bounds.size[1] / 2, 0];
38
+ const localBounds = {
39
+ min: center.map((v, i) => v - bounds.size[i] / 2),
40
+ max: center.map((v, i) => v + bounds.size[i] / 2),
41
+ };
42
+ const position = child.position;
43
+ const rotation = (typeof child.rotation === 'number' ? [0, child.rotation, 0] : child.rotation);
44
+ const projected = boundsOf(boxCorners(localBounds.min, localBounds.max).map((p) => transformPoint(frame(position, rotation), p)));
45
+ const hit = {
46
+ point: [
47
+ (projected.min[0] + projected.max[0]) / 2,
48
+ projected.min[1],
49
+ (projected.min[2] + projected.max[2]) / 2,
50
+ ],
51
+ normalWorldY: 1,
52
+ };
53
+ const previousSurface = provider.resolveHit(host, hit, { scene: surfaceScene(before) });
54
+ if (!previousSurface || Math.abs(previousSurface.position[1] - projected.min[1]) > 1e-5)
55
+ continue;
56
+ const nextSurface = nextSurfaces.find((surface) => surface.id === previousSurface.id);
57
+ if (!nextSurface)
58
+ return null;
59
+ const nextPosition = [
60
+ position[0],
61
+ position[1] + nextSurface.position[1] - previousSurface.position[1],
62
+ position[2],
63
+ ];
64
+ const surfaceLocal = [
65
+ nextPosition[0] - nextSurface.position[0],
66
+ nextPosition[1] - nextSurface.position[1],
67
+ nextPosition[2] - nextSurface.position[2],
68
+ ];
69
+ if (!surfaceRegionContainsFootprint(nextSurface.region, surfaceLocal, bounds.size, rotation, localBounds))
70
+ return null;
71
+ if (Math.abs(nextPosition[1] - position[1]) > 1e-8)
72
+ updates.push([child.id, { position: nextPosition }]);
73
+ }
74
+ }
75
+ return updates;
76
+ }
77
+ export function planCabinetHostedEdit(sceneApi, apply) {
78
+ const before = sceneApi.nodes();
79
+ const after = { ...before };
80
+ const updates = new Map();
81
+ let supported = true;
82
+ const staged = {
83
+ ...sceneApi,
84
+ ...surfaceScene(after),
85
+ update(id, data) {
86
+ if (!after[id])
87
+ return;
88
+ after[id] = { ...after[id], ...data };
89
+ updates.set(id, { ...updates.get(id), ...data });
90
+ },
91
+ upsert(node) {
92
+ supported = false;
93
+ return node.id;
94
+ },
95
+ delete() {
96
+ supported = false;
97
+ },
98
+ markDirty() { },
99
+ pauseHistory() { },
100
+ resumeHistory() { },
101
+ };
102
+ apply(staged);
103
+ if (!supported)
104
+ return null;
105
+ const children = cabinetHostedChildUpdates(before, after);
106
+ if (!children)
107
+ return null;
108
+ for (const [id, data] of children)
109
+ updates.set(id, data);
110
+ return [...updates];
111
+ }
112
+ export function withCabinetHostedChildren(descriptor) {
113
+ if (descriptor.kind !== 'linear-resize')
114
+ return descriptor;
115
+ const plan = (node, patch, sceneApi, modifiers) => planCabinetHostedEdit(sceneApi, (staged) => {
116
+ if (descriptor.commit)
117
+ descriptor.commit(node, patch, staged, modifiers);
118
+ else
119
+ staged.update(descriptor.overrideTarget?.(node, staged) ?? node.id, patch);
120
+ });
121
+ const hasHostedChildren = (scene) => Object.values(scene?.nodes() ?? {}).some((node) => node.type === 'cabinet' &&
122
+ node.children.some((id) => {
123
+ const child = scene.get(id);
124
+ return child && child.type !== 'cabinet' && child.type !== 'cabinet-module';
125
+ }));
126
+ return {
127
+ ...descriptor,
128
+ apply(node, value, sceneApi, modifiers) {
129
+ const patch = descriptor.apply(node, value, sceneApi, modifiers);
130
+ if (!hasHostedChildren(sceneApi) || plan(node, patch, sceneApi, modifiers))
131
+ return patch;
132
+ const target = sceneApi.get(descriptor.overrideTarget?.(node, sceneApi) ?? node.id) ?? node;
133
+ return Object.fromEntries(Object.keys(patch).map((key) => [key, target[key]]));
134
+ },
135
+ previewOverrides(node, value, sceneApi, modifiers) {
136
+ if (!hasHostedChildren(sceneApi))
137
+ return descriptor.previewOverrides?.(node, value, sceneApi, modifiers) ?? [];
138
+ const patch = descriptor.apply(node, value, sceneApi, modifiers);
139
+ return plan(node, patch, sceneApi, modifiers) ?? [];
140
+ },
141
+ commit(node, patch, sceneApi, modifiers) {
142
+ if (!hasHostedChildren(sceneApi)) {
143
+ if (descriptor.commit)
144
+ descriptor.commit(node, patch, sceneApi, modifiers);
145
+ else
146
+ sceneApi.update(descriptor.overrideTarget?.(node, sceneApi) ?? node.id, patch);
147
+ return;
148
+ }
149
+ const updates = plan(node, patch, sceneApi, modifiers);
150
+ if (!updates)
151
+ return;
152
+ for (const [id, data] of updates) {
153
+ sceneApi.update(id, data);
154
+ sceneApi.markDirty(id);
155
+ }
156
+ },
157
+ };
158
+ }
@@ -54,15 +54,22 @@ export type RunSpan = {
54
54
  topY: number;
55
55
  hasCountertop: boolean;
56
56
  };
57
+ type SpanModule = Pick<CabinetModuleNode, 'position' | 'width' | 'depth' | 'carcassHeight' | 'cabinetType'>;
57
58
  /**
58
59
  * Contiguous same-height module groups along the run — the units the
59
60
  * countertop, plinth, and appliance-gap logic operate on. A gap, a
60
61
  * base↔tall transition, a top-height change, or a depth-footprint change
61
62
  * starts a new span.
62
63
  */
63
- export declare function getRunSpans(modules: readonly Pick<CabinetModuleNode, 'position' | 'width' | 'depth' | 'carcassHeight' | 'cabinetType'>[], opts?: {
64
+ export declare function getRunSpans(modules: readonly SpanModule[], opts?: {
64
65
  runTier?: CabinetNode['runTier'];
65
66
  }): RunSpan[];
67
+ export declare function getRunSpanGroups<T extends SpanModule>(modules: readonly T[], opts?: {
68
+ runTier?: CabinetNode['runTier'];
69
+ }): Array<{
70
+ span: RunSpan;
71
+ modules: T[];
72
+ }>;
66
73
  export declare function derivedCornerRole(metadata: unknown): {
67
74
  role: 'base-leg' | 'wall-leg' | 'bridge';
68
75
  side: 'left' | 'right';
@@ -1 +1 @@
1
- {"version":3,"file":"run-layout.d.ts","sourceRoot":"","sources":["../../src/cabinet/run-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,eAAe,EAEhB,MAAM,kBAAkB,CAAA;AAGzB;;;;;GAKG;AAEH,eAAO,MAAM,qBAAqB,SAAO,CAAA;AAMzC,KAAK,UAAU,GAAG,IAAI,CAAC,iBAAiB,EAAE,IAAI,GAAG,UAAU,GAAG,OAAO,CAAC,CAAA;AAEtE,KAAK,uBAAuB,GAAG;IAC7B,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC7B,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAClC,gBAAgB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAA;IACvD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/D,gBAAgB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/D,mBAAmB,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;CACnE,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,oBAAoB,CAAA;IAC1B,KAAK,EAAE,oBAAoB,CAAA;CAC5B,CAAA;AAED,KAAK,wBAAwB,GAAG;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAID,wBAAgB,cAAc,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAE/E;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,MAAM,CAExF;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,MAAM,CAExF;AAgGD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,EAChF,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,EACpD,OAAO,GAAE,wBAA6B,GACrC,kBAAkB,CAuBpB;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAE9D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAE9D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,UAAU,EACjD,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,OAAO,SAAwB,GAC9B,OAAO,CAUT;AAED,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,OAAO,CAAA;CACvB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,SAAS,IAAI,CACpB,iBAAiB,EACjB,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,eAAe,GAAG,aAAa,CACjE,EAAE,EACH,IAAI,GAAE;IACJ,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAA;CAC5B,GACL,OAAO,EAAE,CA+CX;AAMD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,GAChB;IAAE,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAAG,IAAI,CAa7E;AAuFD,MAAM,MAAM,WAAW,GAAG;IACxB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,GAAG,EAAE,eAAe,GAAG,SAAS,EAChC,KAAK,EAAE,SAAS,OAAO,EAAE,GACxB,WAAW,EAAE,CAwEf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAC1B,YAAY,EACZ,OAAO,EACP,IAAI,EACJ,KAAK,EACL,OAA+B,GAChC,EAAE;IACD,YAAY,EAAE,UAAU,GAAG,IAAI,CAAA;IAC/B,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,GAAG,IAAI,CAiChB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EACnD,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,UAAU,EAAE,iBAAiB,CAAC,IAAI,CAAC,EACnC,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,uBAA4B,GACpC,KAAK,CAAC;IAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAqLhE;AAED,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,UAAU,IAC3D;IACE,EAAE,EAAE,IAAI,CAAA;IACR,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAA;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACxE,GACD;IACE,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAAA;CAC9C,CAAA;AAEL;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,CAAC,SAAS,UAAU,EAAE,EACnE,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,GACjB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC,EAAE,CAAA;IACrB,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAClC,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;IAC/C,gBAAgB,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAA;CAChD,GAAG,8BAA8B,CAAC,CAAC,CAAC,CAsDpC;AAED,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,UAAU,IACnD;IACE,EAAE,EAAE,IAAI,CAAA;IACR,QAAQ,EAAE;QAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACjE,OAAO,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvE,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;IACnC,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAA;CAC3B,GACD;IACE,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,eAAe,GAAG,cAAc,GAAG,UAAU,CAAA;CACtD,CAAA;AAEL;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,UAAU,EAAE,EAC3D,OAAO,EACP,SAAS,EACT,eAAe,EACf,SAA8B,EAC9B,kBAAyB,EACzB,WAAW,EACX,YAAY,EACZ,mBAAmB,GACpB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC,EAAE,CAAA;IACrB,SAAS,EAAE;QAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAAC,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAClE,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;IACzD,mBAAmB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACvC,GAAG,sBAAsB,CAAC,CAAC,CAAC,CA0I5B;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,IAAI,CAKP;AAED,MAAM,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,UAAU,CAAC,CAAA;AAEhE,wEAAwE;AACxE,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GACvC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAS1B;AAED,yCAAyC;AACzC,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAM1B"}
1
+ {"version":3,"file":"run-layout.d.ts","sourceRoot":"","sources":["../../src/cabinet/run-layout.ts"],"names":[],"mappings":"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"}
@@ -141,8 +141,11 @@ export function moduleSideOpen(modules, moduleId, side, epsilon = RUN_ADJACENCY_
141
141
  * starts a new span.
142
142
  */
143
143
  export function getRunSpans(modules, opts = {}) {
144
+ return getRunSpanGroups(modules, opts).map((group) => group.span);
145
+ }
146
+ export function getRunSpanGroups(modules, opts = {}) {
144
147
  const sorted = [...modules].sort((a, b) => a.position[0] - b.position[0]);
145
- const spans = [];
148
+ const groups = [];
146
149
  const runTier = opts.runTier ?? 'base';
147
150
  for (const module of sorted) {
148
151
  const minX = module.position[0] - module.width / 2;
@@ -151,27 +154,32 @@ export function getRunSpans(modules, opts = {}) {
151
154
  const maxZ = module.position[2] + module.depth / 2;
152
155
  const topY = module.position[1] + module.carcassHeight;
153
156
  const hasCountertop = runTier === 'base' && (module.cabinetType ?? 'base') !== 'tall';
154
- const current = spans.at(-1);
157
+ const group = groups.at(-1);
158
+ const current = group?.span;
155
159
  if (!current ||
156
160
  minX - current.maxX > RUN_ADJACENCY_EPSILON ||
157
161
  current.hasCountertop !== hasCountertop ||
158
162
  Math.abs(current.topY - topY) > RUN_ADJACENCY_EPSILON ||
159
163
  Math.abs(current.minZ - minZ) > RUN_ADJACENCY_EPSILON ||
160
164
  Math.abs(current.maxZ - maxZ) > RUN_ADJACENCY_EPSILON) {
161
- spans.push({
162
- minX,
163
- maxX,
164
- centerX: module.position[0],
165
- centerZ: module.position[2],
166
- width: module.width,
167
- depth: module.depth,
168
- minZ,
169
- maxZ,
170
- topY,
171
- hasCountertop,
165
+ groups.push({
166
+ modules: [module],
167
+ span: {
168
+ minX,
169
+ maxX,
170
+ centerX: module.position[0],
171
+ centerZ: module.position[2],
172
+ width: module.width,
173
+ depth: module.depth,
174
+ minZ,
175
+ maxZ,
176
+ topY,
177
+ hasCountertop,
178
+ },
172
179
  });
173
180
  continue;
174
181
  }
182
+ group.modules.push(module);
175
183
  current.maxX = Math.max(current.maxX, maxX);
176
184
  current.minZ = Math.min(current.minZ, minZ);
177
185
  current.maxZ = Math.max(current.maxZ, maxZ);
@@ -181,7 +189,7 @@ export function getRunSpans(modules, opts = {}) {
181
189
  current.centerZ = (current.minZ + current.maxZ) / 2;
182
190
  current.topY = Math.max(current.topY, topY);
183
191
  }
184
- return spans;
192
+ return groups;
185
193
  }
186
194
  function angleDelta(a, b) {
187
195
  return Math.atan2(Math.sin(a - b), Math.cos(a - b));
@@ -48,8 +48,8 @@ export declare function cabinetCornerUnlinkPatchesOnDelete(node: CabinetNode | C
48
48
  * A cabinet run is a grouping container — once its last child is deleted
49
49
  * the empty run must go too, so no orphan group lingers in the scene graph
50
50
  * or the persisted data. Children may be modules or derived corner leg
51
- * runs (which position themselves relative to the run), so ANY survivor
52
- * keeps the run alive. `pendingDeleteIds` covers multi-select deletes:
51
+ * runs (which position themselves relative to the run). Hosted assets do not
52
+ * keep an empty run alive. `pendingDeleteIds` covers multi-select deletes:
53
53
  * siblings already part of the same gesture count as gone.
54
54
  */
55
55
  export declare function cabinetEmptyRunCascadeDeleteIds(node: CabinetEditableNode, nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>, pendingDeleteIds: ReadonlySet<AnyNodeId>): AnyNodeId[];
@@ -112,7 +112,7 @@ export declare function cabinetRunWidthEqualizationPlan(run: CabinetNode, nodes:
112
112
  withCountertop: boolean;
113
113
  id: `cabinet-module_${string}`;
114
114
  type: "cabinet-module";
115
- children: (`cabinet_${string}` | `cabinet-module_${string}`)[];
115
+ children: string[];
116
116
  cabinetType: "base" | "tall";
117
117
  moduleKind: "standard" | "corner-filler";
118
118
  topFinish: "trim" | "none" | "top-cabinet";
@@ -161,8 +161,8 @@ export function cabinetCornerUnlinkPatchesOnDelete(node, nodes) {
161
161
  * A cabinet run is a grouping container — once its last child is deleted
162
162
  * the empty run must go too, so no orphan group lingers in the scene graph
163
163
  * or the persisted data. Children may be modules or derived corner leg
164
- * runs (which position themselves relative to the run), so ANY survivor
165
- * keeps the run alive. `pendingDeleteIds` covers multi-select deletes:
164
+ * runs (which position themselves relative to the run). Hosted assets do not
165
+ * keep an empty run alive. `pendingDeleteIds` covers multi-select deletes:
166
166
  * siblings already part of the same gesture count as gone.
167
167
  */
168
168
  export function cabinetEmptyRunCascadeDeleteIds(node, nodes, pendingDeleteIds) {
@@ -173,7 +173,7 @@ export function cabinetEmptyRunCascadeDeleteIds(node, nodes, pendingDeleteIds) {
173
173
  const id = childId;
174
174
  if (id === node.id || pendingDeleteIds.has(id))
175
175
  return false;
176
- return nodes[id] != null;
176
+ return nodes[id]?.type === 'cabinet' || nodes[id]?.type === 'cabinet-module';
177
177
  });
178
178
  return hasSurvivingChild ? [] : [parent.id];
179
179
  }
@@ -9,7 +9,7 @@ export declare function reflowRunModules({ modules, parentRun, patch, scene, sel
9
9
  scene: ReturnType<typeof useScene.getState>;
10
10
  selected: CabinetModuleNodeType;
11
11
  }): boolean;
12
- export declare function updateCabinetRun({ modules, node, patch, }: {
12
+ export declare function updateCabinetRun(args: {
13
13
  modules: CabinetModuleNodeType[];
14
14
  node: CabinetNodeType;
15
15
  patch: Partial<CabinetNodeType>;
@@ -1 +1 @@
1
- {"version":3,"file":"run-panel.d.ts","sourceRoot":"","sources":["../../src/cabinet/run-panel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,iBAAiB,IAAI,qBAAqB,EAC1C,WAAW,IAAI,eAAe,EAC/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkC,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AA+D3E,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAoFzE,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,EAC3C,GAAG,EAAE,eAAe,QAIrB;AAuBD,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,KAAK,EACL,KAAK,EACL,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,SAAS,EAAE,eAAe,CAAA;IAC1B,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACrC,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC3C,QAAQ,EAAE,qBAAqB,CAAA;CAChC,GAAG,OAAO,CA8JV;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,IAAI,EACJ,KAAK,GACN,EAAE;IACD,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,IAAI,EAAE,eAAe,CAAA;IACrB,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;CAChC,QAgFA;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE;IACD,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,+BA8iBA"}
1
+ {"version":3,"file":"run-panel.d.ts","sourceRoot":"","sources":["../../src/cabinet/run-panel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAGV,iBAAiB,IAAI,qBAAqB,EAC1C,WAAW,IAAI,eAAe,EAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAgD,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAgEzF,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG,qBAAqB,CAAA;AAoFzE,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,EAC3C,GAAG,EAAE,eAAe,QAIrB;AAuBD,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,KAAK,EACL,KAAK,EACL,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,SAAS,EAAE,eAAe,CAAA;IAC1B,KAAK,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACrC,KAAK,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAA;IAC3C,QAAQ,EAAE,qBAAqB,CAAA;CAChC,GAAG,OAAO,CA8JV;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,IAAI,EAAE,eAAe,CAAA;IACrB,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;CAChC,QAOA;AA8FD,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE;IACD,IAAI,EAAE,eAAe,CAAA;IACrB,OAAO,EAAE,qBAAqB,EAAE,CAAA;IAChC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,+BA8iBA"}
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { createSceneApi, resolveLevelId, useScene } from '@pascal-app/core';
3
+ import { cascadeDirty, createSceneApi, resolveLevelId, useScene } from '@pascal-app/core';
4
4
  import { ActionButton, PanelSection, PanelWrapper, SegmentedControl, SliderControl, ToggleControl, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { Copy, Equal as EqualIcon, Plus, Trash } from 'lucide-react';
7
7
  import { useCallback, useMemo, useState } from 'react';
8
8
  import { useShallow } from 'zustand/react/shallow';
9
+ import { planCabinetHostedEdit } from './hosted-resize';
9
10
  import { metadataForSelectedWidth, metadataWithPresetWidthDebt, presetNominalWidth, presetWidthDebt, recordedPresetNominalWidth, } from './preset-width-debt';
10
11
  import { CABINET_DIMENSION_PROFILES, cabinetDimensionProfileById, cabinetDimensionProfileId, } from './profiles';
11
12
  import { MAX_CABINET_WIDTH } from './resize-limits';
@@ -227,16 +228,24 @@ export function reflowRunModules({ modules, parentRun, patch, scene, selected, }
227
228
  bumpRunLayoutRevisionViaStore(scene, parentRun);
228
229
  return true;
229
230
  }
230
- export function updateCabinetRun({ modules, node, patch, }) {
231
- const scene = useScene.getState();
231
+ export function updateCabinetRun(args) {
232
232
  const sceneApi = createSceneApi(useScene);
233
+ const updates = planCabinetHostedEdit(sceneApi, (staged) => writeCabinetRun(args, staged));
234
+ if (!updates)
235
+ return;
236
+ useScene.getState().updateNodes(updates.map(([id, data]) => ({ id, data })));
237
+ for (const id of cascadeDirty(args.node.id, { scene: sceneApi }))
238
+ useScene.getState().markDirty(id);
239
+ }
240
+ function writeCabinetRun({ modules, node, patch, }, sceneApi) {
241
+ const nodes = sceneApi.nodes();
233
242
  const nextPatch = { ...patch };
234
243
  if (typeof nextPatch.carcassHeight === 'number') {
235
244
  const minModuleHeight = Math.max(0.4, ...modules.map((module) => minCabinetCarcassHeightForStack(module)));
236
245
  nextPatch.carcassHeight = Math.max(nextPatch.carcassHeight, minModuleHeight);
237
246
  }
238
247
  const nextNode = { ...node, ...nextPatch };
239
- scene.updateNode(node.id, nextPatch);
248
+ sceneApi.update(node.id, nextPatch);
240
249
  const shouldSyncDepth = RUN_DEPTH_PATCH_KEY in nextPatch;
241
250
  const shouldSyncHeight = 'carcassHeight' in nextPatch;
242
251
  const shouldSyncPosition = Object.keys(nextPatch).some((key) => RUN_POSITION_PATCH_KEYS.has(key));
@@ -277,11 +286,11 @@ export function updateCabinetRun({ modules, node, patch, }) {
277
286
  if ('frontGap' in nextPatch)
278
287
  modulePatch.frontGap = nextNode.frontGap;
279
288
  }
280
- scene.updateNode(module.id, modulePatch);
289
+ sceneApi.update(module.id, modulePatch);
281
290
  if (shouldSyncModules) {
282
- const wallChild = wallChildOf(module, scene.nodes);
291
+ const wallChild = wallChildOf(module, nodes);
283
292
  if (wallChild) {
284
- scene.updateNode(wallChild.id, {
293
+ sceneApi.update(wallChild.id, {
285
294
  frontStyle: nextNode.frontStyle,
286
295
  frontOverlay: nextNode.frontOverlay,
287
296
  handleStyle: nextNode.handleStyle,
@@ -0,0 +1,4 @@
1
+ import { type CabinetNode, type DeclaredHostSurface, type GeometryContext, type SurfaceProvider } from '@pascal-app/core';
2
+ export declare function getCabinetSurfaces(node: CabinetNode, ctx?: GeometryContext): DeclaredHostSurface[];
3
+ export declare const cabinetSurfaceProvider: SurfaceProvider;
4
+ //# sourceMappingURL=surfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surfaces.d.ts","sourceRoot":"","sources":["../../src/cabinet/surfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAGpB,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAA;AA8BzB,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,GAAG,CAAC,EAAE,eAAe,GACpB,mBAAmB,EAAE,CA0CvB;AAgBD,eAAO,MAAM,sBAAsB,EAAE,eAkCpC,CAAA"}
@@ -0,0 +1,101 @@
1
+ import { pointInPolygon2D, } from '@pascal-app/core';
2
+ import { cooktopFootprint, sinkFaucetFootprint, sinkOpening } from './appliance-layout';
3
+ import { getCabinetCountertopLayout } from './countertop-layout';
4
+ import { isCooktopCompartmentType } from './stack';
5
+ function rectangle(x, z, width, depth) {
6
+ return [
7
+ [x - width / 2, z - depth / 2],
8
+ [x + width / 2, z - depth / 2],
9
+ [x + width / 2, z + depth / 2],
10
+ [x - width / 2, z + depth / 2],
11
+ ];
12
+ }
13
+ function slabSurface(id, label, slab) {
14
+ // Keep XZ anchored to the run, so changing either end or an overhang cannot move an attachment.
15
+ return {
16
+ id,
17
+ label,
18
+ position: [0, slab.position[1] + slab.size[1] / 2, 0],
19
+ normal: [0, 1, 0],
20
+ gridSnap: true,
21
+ region: {
22
+ kind: 'rect',
23
+ center: [slab.position[0], slab.position[2]],
24
+ size: [slab.size[0] / 2, slab.size[2] / 2],
25
+ },
26
+ };
27
+ }
28
+ export function getCabinetSurfaces(node, ctx) {
29
+ const surfaces = [];
30
+ for (const layout of getCabinetCountertopLayout(node, ctx)) {
31
+ // First member identity survives edits to other spans and changes to this span's far end.
32
+ // Removing/replacing that member or splitting/merging the span is a topology edit.
33
+ const anchorId = layout.modules[0].id;
34
+ if (layout.countertop) {
35
+ const surface = slabSurface(`countertop:${anchorId}`, 'Countertop', layout.countertop);
36
+ const holes = layout.sinkCuts.flatMap((cut) => cut.bowls.map((bowl) => {
37
+ const opening = sinkOpening(bowl);
38
+ return rectangle(cut.x + opening.centerX, cut.z, opening.width, opening.depth);
39
+ }));
40
+ for (const cut of layout.sinkCuts) {
41
+ const faucet = sinkFaucetFootprint(cut.bowls);
42
+ holes.push(Array.from({ length: 28 }, (_, index) => {
43
+ const angle = (index * Math.PI * 2) / 28;
44
+ return [
45
+ cut.x + faucet.x + Math.cos(angle) * faucet.radius,
46
+ cut.z + faucet.z + Math.sin(angle) * faucet.radius,
47
+ ];
48
+ }));
49
+ }
50
+ for (const module of layout.modules) {
51
+ if (!module.stack?.some((compartment) => isCooktopCompartmentType(compartment.type)))
52
+ continue;
53
+ const footprint = cooktopFootprint(module);
54
+ holes.push(rectangle(module.position[0], module.position[2], footprint.width, footprint.depth));
55
+ }
56
+ surface.region = { ...surface.region, ...(holes.length > 0 ? { holes } : {}) };
57
+ surfaces.push(surface);
58
+ }
59
+ if (layout.bar) {
60
+ surfaces.push(slabSurface(`bar:${layout.bar.edge}:${anchorId}`, 'Bar ledge', layout.bar.slab));
61
+ }
62
+ }
63
+ return surfaces;
64
+ }
65
+ function geometryContext(host, ctx) {
66
+ const nodes = ctx.scene.nodes();
67
+ return {
68
+ resolve: (id) => nodes[id],
69
+ children: host.children
70
+ .map((id) => ctx.scene.get(id))
71
+ .filter((node) => !!node),
72
+ parent: host.parentId ? (nodes[host.parentId] ?? null) : null,
73
+ siblings: Object.values(nodes).filter((node) => node.type === host.type && node.parentId === host.parentId),
74
+ };
75
+ }
76
+ export const cabinetSurfaceProvider = {
77
+ childFrame: 'host-local',
78
+ surfaces(host, ctx) {
79
+ return host.type === 'cabinet' ? getCabinetSurfaces(host, geometryContext(host, ctx)) : [];
80
+ },
81
+ resolveHit(host, hit, ctx) {
82
+ if (host.type !== 'cabinet' || !(hit.normalWorldY >= 0.75) || !hit.point.every(Number.isFinite))
83
+ return null;
84
+ const surfaces = getCabinetSurfaces(host, geometryContext(host, ctx));
85
+ let nearest = null;
86
+ for (const surface of surfaces) {
87
+ const region = surface.region;
88
+ const [x, z] = [hit.point[0], hit.point[2]];
89
+ const [cx, cz] = region.center;
90
+ const [width, depth] = region.size;
91
+ if (Math.abs(x - cx) > width ||
92
+ Math.abs(z - cz) > depth ||
93
+ region.holes?.some((hole) => pointInPolygon2D([x, z], hole.map(([hx, hz]) => [hx, hz]))))
94
+ continue;
95
+ if (!nearest ||
96
+ Math.abs(surface.position[1] - hit.point[1]) < Math.abs(nearest.position[1] - hit.point[1]))
97
+ nearest = surface;
98
+ }
99
+ return nearest;
100
+ },
101
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"tree-structure.d.ts","sourceRoot":"","sources":["../../src/cabinet/tree-structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EAGV,MAAM,kBAAkB,CAAA;AA2FzB;8DAC8D;AAC9D,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACpD,OAAO,CAET;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACnD,MAAM,CAWR;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACnD,SAAS,EAAE,CAuBb;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACpD,GAAG,SAAS,SAAS,EAAE,CAkCvB"}
1
+ {"version":3,"file":"tree-structure.d.ts","sourceRoot":"","sources":["../../src/cabinet/tree-structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EAGV,MAAM,kBAAkB,CAAA;AA0FzB;8DAC8D;AAC9D,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACpD,OAAO,CAET;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACnD,MAAM,CAWR;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GACnD,SAAS,EAAE,CA0Bb;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IAChD,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACjC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACpD,GAAG,SAAS,SAAS,EAAE,CAkCvB"}