@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
@@ -102,17 +102,17 @@ export default function RidgeVentPanel() {
102
102
  ], value: node.style ?? 'standard' }), _jsx(SegmentedControl, { onChange: (v) => handleUpdate({ endCaps: v === 'yes' }), options: [
103
103
  { label: 'End Caps', value: 'yes' },
104
104
  { label: 'Open', value: 'no' },
105
- ], value: (node.endCaps ?? true) ? 'yes' : 'no' })] }), _jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Length", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ length: v }), onCommit: (v) => handleUpdate({ length: v }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: Math.round(node.length * 100) / 100 }), _jsx(SliderControl, { label: "Width", max: 0.6, min: 0.1, onChange: (v) => handleUpdate({ width: v }), onCommit: (v) => handleUpdate({ width: v }), precision: 2, restoreOnCommit: false, step: 0.01, unit: "m", value: Math.round(node.width * 100) / 100 }), _jsx(SliderControl, { label: "Height", max: 0.2, min: 0.03, onChange: (v) => handleUpdate({ height: v }), onCommit: (v) => handleUpdate({ height: v }), precision: 3, restoreOnCommit: false, step: 0.005, unit: "m", value: Math.round(node.height * 1000) / 1000 })] }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: "X", max: halfW, min: -halfW, onChange: (v) => handleUpdate({
105
+ ], value: (node.endCaps ?? true) ? 'yes' : 'no' })] }), _jsxs(PanelSection, { title: "Dimensions", children: [_jsx(SliderControl, { label: "Length", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ length: v }), onCommit: (v) => handleUpdate({ length: v }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.length }), _jsx(SliderControl, { label: "Width", max: 0.6, min: 0.1, onChange: (v) => handleUpdate({ width: v }), onCommit: (v) => handleUpdate({ width: v }), precision: 2, restoreOnCommit: false, step: 0.01, unit: "m", value: node.width }), _jsx(SliderControl, { label: "Height", max: 0.2, min: 0.03, onChange: (v) => handleUpdate({ height: v }), onCommit: (v) => handleUpdate({ height: v }), precision: 3, restoreOnCommit: false, step: 0.005, unit: "m", value: node.height })] }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: "X", max: halfW, min: -halfW, onChange: (v) => handleUpdate({
106
106
  position: [v, node.position[1] ?? 0, node.position[2] ?? 0],
107
107
  }), onCommit: (v) => handleUpdate({
108
108
  position: [v, node.position[1] ?? 0, node.position[2] ?? 0],
109
- }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: Math.round((node.position[0] ?? 0) * 100) / 100 }), _jsx(SliderControl, { label: "Y", onChange: (v) => handleUpdate({
109
+ }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.position[0] ?? 0 }), _jsx(SliderControl, { label: "Y", onChange: (v) => handleUpdate({
110
110
  position: [node.position[0] ?? 0, v, node.position[2] ?? 0],
111
111
  }), onCommit: (v) => handleUpdate({
112
112
  position: [node.position[0] ?? 0, v, node.position[2] ?? 0],
113
- }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: Math.round((node.position[1] ?? 0) * 100) / 100 }), _jsx(SliderControl, { label: "Z", max: halfD, min: -halfD, onChange: (v) => handleUpdate({
113
+ }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.position[1] ?? 0 }), _jsx(SliderControl, { label: "Z", max: halfD, min: -halfD, onChange: (v) => handleUpdate({
114
114
  position: [node.position[0] ?? 0, node.position[1] ?? 0, v],
115
115
  }), onCommit: (v) => handleUpdate({
116
116
  position: [node.position[0] ?? 0, node.position[1] ?? 0, v],
117
- }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: Math.round((node.position[2] ?? 0) * 100) / 100 }), _jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (deg) => handleUpdate({ rotation: (deg * Math.PI) / 180 }), onCommit: (deg) => handleUpdate({ rotation: (deg * Math.PI) / 180 }), precision: 0, restoreOnCommit: false, step: 1, unit: "\u00B0", value: Math.round(((node.rotation ?? 0) * 180) / Math.PI) })] }), _jsx(PanelSection, { title: "Actions", children: _jsxs(ActionGroup, { children: [_jsx(ActionButton, { icon: _jsx(Move, { className: "h-3.5 w-3.5" }), label: "Move", onClick: handleMove }), _jsx(ActionButton, { icon: _jsx(Copy, { className: "h-3.5 w-3.5" }), label: "Duplicate", onClick: handleDuplicate }), _jsx(ActionButton, { className: "hover:bg-red-500/20", icon: _jsx(Trash2, { className: "h-3.5 w-3.5 text-red-400" }), label: "Delete", onClick: handleDelete })] }) })] }));
117
+ }), precision: 2, restoreOnCommit: false, step: 0.05, unit: "m", value: node.position[2] ?? 0 }), _jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (deg) => handleUpdate({ rotation: (deg * Math.PI) / 180 }), onCommit: (deg) => handleUpdate({ rotation: (deg * Math.PI) / 180 }), precision: 0, restoreOnCommit: false, step: 1, unit: "\u00B0", value: Math.round(((node.rotation ?? 0) * 180) / Math.PI) })] }), _jsx(PanelSection, { title: "Actions", children: _jsxs(ActionGroup, { children: [_jsx(ActionButton, { icon: _jsx(Move, { className: "h-3.5 w-3.5" }), label: "Move", onClick: handleMove }), _jsx(ActionButton, { icon: _jsx(Copy, { className: "h-3.5 w-3.5" }), label: "Duplicate", onClick: handleDuplicate }), _jsx(ActionButton, { className: "hover:bg-red-500/20", icon: _jsx(Trash2, { className: "h-3.5 w-3.5 text-red-400" }), label: "Delete", onClick: handleDelete })] }) })] }));
118
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/roof/panel.tsx"],"names":[],"mappings":"AAiCA,MAAM,CAAC,OAAO,UAAU,SAAS,uCA0gBhC"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/roof/panel.tsx"],"names":[],"mappings":"AAkCA,MAAM,CAAC,OAAO,UAAU,SAAS,uCA4gBhC"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useScene, } from '@pascal-app/core';
4
- import { ActionButton, ActionGroup, duplicateRoofSubtree, PanelSection, PanelWrapper, SegmentedControl, SliderControl, triggerSFX, useEditor, } from '@pascal-app/editor';
4
+ import { ActionButton, ActionGroup, duplicateRoofSubtree, formatLinearMeasurement, PanelSection, PanelWrapper, SegmentedControl, SliderControl, triggerSFX, useEditor, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
6
  import { Copy, Move, Plus, Trash2 } from 'lucide-react';
7
7
  import { useCallback, useState } from 'react';
@@ -9,6 +9,8 @@ import { useShallow } from 'zustand/react/shallow';
9
9
  export default function RoofPanel() {
10
10
  const [ventType, setVentType] = useState('box-vent');
11
11
  const selectedId = useViewer((s) => s.selection.selectedIds[0]);
12
+ const unit = useViewer((s) => s.unit);
13
+ const metricNotation = useViewer((s) => s.metricNotation);
12
14
  const setSelection = useViewer((s) => s.setSelection);
13
15
  const updateNode = useScene((s) => s.updateNode);
14
16
  const setMovingNode = useEditor((s) => s.setMovingNode);
@@ -169,10 +171,10 @@ export default function RoofPanel() {
169
171
  const pos = [...node.position];
170
172
  pos[0] = v;
171
173
  handleUpdate({ position: pos });
172
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[0] * 100) / 100 }), node.support?.kind !== 'roof' && (_jsx(SegmentedControl, { onChange: (mode) => handleUpdate({ support: { kind: mode === 'walls' ? 'walls' : 'level' } }), options: [
174
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[0] }), node.support?.kind !== 'roof' && (_jsx(SegmentedControl, { onChange: (mode) => handleUpdate({ support: { kind: mode === 'walls' ? 'walls' : 'level' } }), options: [
173
175
  { label: 'Follows walls', value: 'walls' },
174
176
  { label: 'Custom', value: 'custom' },
175
- ], value: node.support?.kind === 'walls' ? 'walls' : 'custom' })), node.support?.kind === 'walls' ? (_jsxs("div", { className: "px-1 text-[11px] text-muted-foreground", children: ["Currently ", Math.round(node.position[1] * 100) / 100, " m"] })) : (_jsx(SliderControl, { label: "Y", onChange: (v) => {
177
+ ], value: node.support?.kind === 'walls' ? 'walls' : 'custom' })), node.support?.kind === 'walls' ? (_jsxs("div", { className: "px-1 text-[11px] text-muted-foreground", children: ["Currently ", formatLinearMeasurement(node.position[1], unit, metricNotation)] })) : (_jsx(SliderControl, { label: "Y", onChange: (v) => {
176
178
  const pos = [...node.position];
177
179
  pos[1] = v;
178
180
  const current = useScene.getState().nodes[node.id];
@@ -184,11 +186,11 @@ export default function RoofPanel() {
184
186
  ? { support: { kind: 'level' } }
185
187
  : {}),
186
188
  });
187
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[1] * 100) / 100 })), _jsx(SliderControl, { label: "Z", onChange: (v) => {
189
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[1] })), _jsx(SliderControl, { label: "Z", onChange: (v) => {
188
190
  const pos = [...node.position];
189
191
  pos[2] = v;
190
192
  handleUpdate({ position: pos });
191
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[2] * 100) / 100 }), _jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (degrees) => {
193
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[2] }), _jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (degrees) => {
192
194
  handleUpdate({ rotation: (degrees * Math.PI) / 180 });
193
195
  }, precision: 0, step: 1, unit: "\u00B0", value: Math.round((node.rotation * 180) / Math.PI) }), _jsxs("div", { className: "flex gap-1.5 px-1 pt-2 pb-1", children: [_jsx(ActionButton, { label: "-45\u00B0", onClick: () => {
194
196
  triggerSFX('sfx:item-rotate');
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/roof-segment/panel.tsx"],"names":[],"mappings":"AAkGA,MAAM,CAAC,OAAO,UAAU,gBAAgB,uCA4oBvC"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../src/roof-segment/panel.tsx"],"names":[],"mappings":"AAkGA,MAAM,CAAC,OAAO,UAAU,gBAAgB,uCAsoBvC"}
@@ -249,21 +249,21 @@ export default function RoofSegmentPanel() {
249
249
  const showTrimPlanes = shouldShowTrimPlanes(node.metadata);
250
250
  const managedLeanToRoofSegment = isManagedLeanToRoofSegment(node.metadata);
251
251
  const conicalCoverage = getConicalRoofCoverage(node);
252
- return (_jsxs(PanelWrapper, { icon: "/icons/roof.webp", onBack: handleBack, onClose: handleClose, title: node.name || 'Roof Segment', width: 300, children: [_jsxs(PanelSection, { title: "Roof Type", children: [_jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS, value: node.roofType, disabled: managedLeanToRoofSegment }), _jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS_2, value: node.roofType, disabled: managedLeanToRoofSegment }), _jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS_3, value: node.roofType, disabled: managedLeanToRoofSegment })] }), node.roofType !== 'conical' && (_jsxs(PanelSection, { title: "Trim", children: [_jsxs(ActionGroup, { children: [_jsx(ActionButton, { icon: showTrimPlanes ? (_jsx(Check, { className: "h-3.5 w-3.5" })) : (_jsx(Pencil, { className: "h-3.5 w-3.5" })), label: showTrimPlanes ? 'Done editing' : 'Edit footprint', onClick: () => (showTrimPlanes ? handleBack() : handleTrimEditing(true)) }), _jsx(ActionButton, { className: "disabled:cursor-not-allowed disabled:opacity-40", disabled: !hasSegmentTrim(node), icon: _jsx(RotateCcw, { className: "h-3.5 w-3.5" }), label: "Reset", onClick: handleResetTrim })] }), node.roofType !== 'shed' && node.roofType !== 'flat' && (_jsx(ToggleControl, { checked: autoRidgeVentEnabled, label: "Auto ridge vent", onChange: handleAutoRidgeVentToggle }))] })), node.roofType !== 'conical' && (_jsx(PanelSection, { title: "Drainage", children: _jsx(ToggleControl, { checked: autoGutterEnabled, label: "Auto gutters", onChange: handleAutoGutterToggle }) })), _jsx(PanelSection, { title: "Footprint", children: node.roofType === 'conical' ? (_jsx(SliderControl, { label: "Diameter", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ width: v, depth: v }), precision: 2, step: 0.5, unit: "m", value: Math.round(node.width * 100) / 100 })) : (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Width", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ width: v }), precision: 2, step: 0.5, unit: "m", value: Math.round(node.width * 100) / 100 }), _jsx(SliderControl, { label: "Depth", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ depth: v }), precision: 2, step: 0.5, unit: "m", value: Math.round(node.depth * 100) / 100 })] })) }), node.roofType === 'conical' && (_jsxs(PanelSection, { title: "Conical Shape", children: [_jsx(ToggleControl, { checked: !conicalCoverage.fullCircle, label: "Clipped version", onChange: (checked) => handleUpdate({ conicalFullCircle: !checked }) }), !conicalCoverage.fullCircle && (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Start Angle", max: 180, min: -180, onChange: (degrees) => handleUpdate({ conicalStartAngle: (degrees * Math.PI) / 180 }), precision: 0, step: 1, unit: "\u00B0", value: Math.round((conicalCoverage.startAngle * 180) / Math.PI) }), _jsx(SliderControl, { label: "Arc", max: 345, min: 15, onChange: (degrees) => handleUpdate({
252
+ return (_jsxs(PanelWrapper, { icon: "/icons/roof.webp", onBack: handleBack, onClose: handleClose, title: node.name || 'Roof Segment', width: 300, children: [_jsxs(PanelSection, { title: "Roof Type", children: [_jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS, value: node.roofType, disabled: managedLeanToRoofSegment }), _jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS_2, value: node.roofType, disabled: managedLeanToRoofSegment }), _jsx(SegmentedControl, { onChange: (v) => handleRoofTypeChange(v), options: ROOF_TYPE_OPTIONS_3, value: node.roofType, disabled: managedLeanToRoofSegment })] }), node.roofType !== 'conical' && (_jsxs(PanelSection, { title: "Trim", children: [_jsxs(ActionGroup, { children: [_jsx(ActionButton, { icon: showTrimPlanes ? (_jsx(Check, { className: "h-3.5 w-3.5" })) : (_jsx(Pencil, { className: "h-3.5 w-3.5" })), label: showTrimPlanes ? 'Done editing' : 'Edit footprint', onClick: () => (showTrimPlanes ? handleBack() : handleTrimEditing(true)) }), _jsx(ActionButton, { className: "disabled:cursor-not-allowed disabled:opacity-40", disabled: !hasSegmentTrim(node), icon: _jsx(RotateCcw, { className: "h-3.5 w-3.5" }), label: "Reset", onClick: handleResetTrim })] }), node.roofType !== 'shed' && node.roofType !== 'flat' && (_jsx(ToggleControl, { checked: autoRidgeVentEnabled, label: "Auto ridge vent", onChange: handleAutoRidgeVentToggle }))] })), node.roofType !== 'conical' && (_jsx(PanelSection, { title: "Drainage", children: _jsx(ToggleControl, { checked: autoGutterEnabled, label: "Auto gutters", onChange: handleAutoGutterToggle }) })), _jsx(PanelSection, { title: "Footprint", children: node.roofType === 'conical' ? (_jsx(SliderControl, { label: "Diameter", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ width: v, depth: v }), precision: 2, step: 0.5, unit: "m", value: node.width })) : (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Width", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ width: v }), precision: 2, step: 0.5, unit: "m", value: node.width }), _jsx(SliderControl, { label: "Depth", max: 1000, min: 0.5, onChange: (v) => handleUpdate({ depth: v }), precision: 2, step: 0.5, unit: "m", value: node.depth })] })) }), node.roofType === 'conical' && (_jsxs(PanelSection, { title: "Conical Shape", children: [_jsx(ToggleControl, { checked: !conicalCoverage.fullCircle, label: "Clipped version", onChange: (checked) => handleUpdate({ conicalFullCircle: !checked }) }), !conicalCoverage.fullCircle && (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Start Angle", max: 180, min: -180, onChange: (degrees) => handleUpdate({ conicalStartAngle: (degrees * Math.PI) / 180 }), precision: 0, step: 1, unit: "\u00B0", value: Math.round((conicalCoverage.startAngle * 180) / Math.PI) }), _jsx(SliderControl, { label: "Arc", max: 345, min: 15, onChange: (degrees) => handleUpdate({
253
253
  conicalSweepAngle: (Math.sign(conicalCoverage.sweepAngle) * degrees * Math.PI) / 180,
254
- }), precision: 0, step: 1, unit: "\u00B0", value: Math.round((Math.abs(conicalCoverage.sweepAngle) * 180) / Math.PI) })] }))] })), _jsx(PanelSection, { title: "Wall Height", children: _jsx(SliderControl, { label: "Wall", max: 1000, min: 0, onChange: (v) => handleUpdate({ wallHeight: v }), precision: 2, step: 0.1, unit: "m", value: Math.round(node.wallHeight * 100) / 100 }) }), _jsxs(PanelSection, { title: "Pitch", children: [_jsx(SliderControl, { label: "Angle", max: 60, min: 0, onChange: (v) => handleUpdate({ pitch: v }), precision: 0, step: 1, unit: "\u00B0", value: Math.round(node.pitch) }), _jsx("div", { className: "flex gap-1.5 px-1 pt-2 pb-1", children: PITCH_PRESETS.map((preset) => (_jsx(ActionButton, { label: preset.label, onClick: () => handleUpdate({ pitch: preset.deg }) }, preset.label))) })] }), node.roofType === 'gambrel' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Kink Depth", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ gambrelLowerWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.gambrelLowerWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Kink Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ gambrelLowerHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.gambrelLowerHeightRatio * 100) / 100 })] })), node.roofType === 'mansard' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Waist Width", max: 0.45, min: 0.05, onChange: (v) => handleUpdate({ mansardSteepWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.mansardSteepWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ mansardSteepHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.mansardSteepHeightRatio * 100) / 100 })] })), node.roofType === 'dutch' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Waist Width", max: 0.45, min: 0.05, onChange: (v) => handleUpdate({ dutchHipWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.dutchHipWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ dutchHipHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.dutchHipHeightRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Length", max: 1, min: 0.1, onChange: (v) => handleUpdate({ dutchWaistLengthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round((node.dutchWaistLengthRatio ?? ROOF_SHAPE_DEFAULTS.dutchWaistLengthRatio) * 100) / 100 }), _jsx(SliderControl, { label: "Top Rake Thick.", max: 0.5, min: 0.01, onChange: (v) => handleUpdate({ dutchTopRakeThickness: v }), precision: 2, step: 0.01, unit: "m", value: Math.round((node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness) * 100) / 100 }), _jsx(SliderControl, { label: "Top Rake Length", max: 3, min: 0, onChange: (v) => handleUpdate({ dutchGabletRake: v }), precision: 2, step: 0.01, unit: "m", value: Math.round((node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake) * 100) / 100 })] })), _jsxs(PanelSection, { title: "Structure", children: [_jsx(SliderControl, { label: "Wall Thick.", max: 1, min: 0.05, onChange: (v) => handleUpdate({ wallThickness: v }), precision: 2, step: 0.05, unit: "m", value: Math.round(node.wallThickness * 100) / 100 }), _jsx(SliderControl, { label: "Deck Thick.", max: 0.3, min: 0.04, onChange: (v) => handleUpdate({ deckThickness: v }), precision: 2, step: 0.01, unit: "m", value: Math.round(node.deckThickness * 100) / 100 }), _jsx(SliderControl, { label: "Overhang", max: 1, min: 0, onChange: (v) => handleUpdate({ overhang: v }), precision: 2, step: 0.05, unit: "m", value: Math.round(node.overhang * 100) / 100 }), _jsx(SliderControl, { label: "Shingle Thick.", max: 0.3, min: 0.02, onChange: (v) => handleUpdate({ shingleThickness: v }), precision: 2, step: 0.01, unit: "m", value: Math.round(node.shingleThickness * 100) / 100 })] }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: "X", onChange: (v) => {
254
+ }), precision: 0, step: 1, unit: "\u00B0", value: Math.round((Math.abs(conicalCoverage.sweepAngle) * 180) / Math.PI) })] }))] })), _jsx(PanelSection, { title: "Wall Height", children: _jsx(SliderControl, { label: "Wall", max: 1000, min: 0, onChange: (v) => handleUpdate({ wallHeight: v }), precision: 2, step: 0.1, unit: "m", value: node.wallHeight }) }), _jsxs(PanelSection, { title: "Pitch", children: [_jsx(SliderControl, { label: "Angle", max: 60, min: 0, onChange: (v) => handleUpdate({ pitch: v }), precision: 0, step: 1, unit: "\u00B0", value: Math.round(node.pitch) }), _jsx("div", { className: "flex gap-1.5 px-1 pt-2 pb-1", children: PITCH_PRESETS.map((preset) => (_jsx(ActionButton, { label: preset.label, onClick: () => handleUpdate({ pitch: preset.deg }) }, preset.label))) })] }), node.roofType === 'gambrel' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Kink Depth", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ gambrelLowerWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.gambrelLowerWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Kink Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ gambrelLowerHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.gambrelLowerHeightRatio * 100) / 100 })] })), node.roofType === 'mansard' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Waist Width", max: 0.45, min: 0.05, onChange: (v) => handleUpdate({ mansardSteepWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.mansardSteepWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ mansardSteepHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.mansardSteepHeightRatio * 100) / 100 })] })), node.roofType === 'dutch' && (_jsxs(PanelSection, { title: "Shape", children: [_jsx(SliderControl, { label: "Waist Width", max: 0.45, min: 0.05, onChange: (v) => handleUpdate({ dutchHipWidthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.dutchHipWidthRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Height", max: 0.9, min: 0.1, onChange: (v) => handleUpdate({ dutchHipHeightRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round(node.dutchHipHeightRatio * 100) / 100 }), _jsx(SliderControl, { label: "Waist Length", max: 1, min: 0.1, onChange: (v) => handleUpdate({ dutchWaistLengthRatio: v }), precision: 2, step: 0.01, unit: "", value: Math.round((node.dutchWaistLengthRatio ?? ROOF_SHAPE_DEFAULTS.dutchWaistLengthRatio) * 100) / 100 }), _jsx(SliderControl, { label: "Top Rake Thick.", max: 0.5, min: 0.01, onChange: (v) => handleUpdate({ dutchTopRakeThickness: v }), precision: 2, step: 0.01, unit: "m", value: node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness }), _jsx(SliderControl, { label: "Top Rake Length", max: 3, min: 0, onChange: (v) => handleUpdate({ dutchGabletRake: v }), precision: 2, step: 0.01, unit: "m", value: node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake })] })), _jsxs(PanelSection, { title: "Structure", children: [_jsx(SliderControl, { label: "Wall Thick.", max: 1, min: 0.05, onChange: (v) => handleUpdate({ wallThickness: v }), precision: 2, step: 0.05, unit: "m", value: node.wallThickness }), _jsx(SliderControl, { label: "Deck Thick.", max: 0.3, min: 0.04, onChange: (v) => handleUpdate({ deckThickness: v }), precision: 2, step: 0.01, unit: "m", value: node.deckThickness }), _jsx(SliderControl, { label: "Overhang", max: 1, min: 0, onChange: (v) => handleUpdate({ overhang: v }), precision: 2, step: 0.05, unit: "m", value: node.overhang }), _jsx(SliderControl, { label: "Shingle Thick.", max: 0.3, min: 0.02, onChange: (v) => handleUpdate({ shingleThickness: v }), precision: 2, step: 0.01, unit: "m", value: node.shingleThickness })] }), _jsxs(PanelSection, { title: "Position", children: [_jsx(SliderControl, { label: "X", onChange: (v) => {
255
255
  const pos = [...node.position];
256
256
  pos[0] = v;
257
257
  handleUpdate({ position: pos });
258
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[0] * 100) / 100 }), _jsx(SliderControl, { label: "Y", onChange: (v) => {
258
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[0] }), _jsx(SliderControl, { label: "Y", onChange: (v) => {
259
259
  const pos = [...node.position];
260
260
  pos[1] = v;
261
261
  handleUpdate({ position: pos });
262
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[1] * 100) / 100 }), _jsx(SliderControl, { label: "Z", onChange: (v) => {
262
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[1] }), _jsx(SliderControl, { label: "Z", onChange: (v) => {
263
263
  const pos = [...node.position];
264
264
  pos[2] = v;
265
265
  handleUpdate({ position: pos });
266
- }, precision: 2, step: 0.05, unit: "m", value: Math.round(node.position[2] * 100) / 100 }), (node.roofType !== 'conical' || !conicalCoverage.fullCircle) && (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (degrees) => {
266
+ }, precision: 2, step: 0.05, unit: "m", value: node.position[2] }), (node.roofType !== 'conical' || !conicalCoverage.fullCircle) && (_jsxs(_Fragment, { children: [_jsx(SliderControl, { label: "Rotation", max: 180, min: -180, onChange: (degrees) => {
267
267
  handleUpdate({ rotation: (degrees * Math.PI) / 180 });
268
268
  }, precision: 0, step: 1, unit: "\u00B0", value: Math.round((node.rotation * 180) / Math.PI) }), _jsxs("div", { className: "flex gap-1.5 px-1 pt-2 pb-1", children: [_jsx(ActionButton, { label: "-45\u00B0", onClick: () => {
269
269
  triggerSFX('sfx:item-rotate');
@@ -4,6 +4,7 @@ export declare function createDuctRunEndCap(duct: DuctSegmentNode, endpoint?: 's
4
4
  export declare function createPipeRunEndCap(pipe: PipeSegmentNode, endpoint?: 'start' | 'end'): PipeFittingNode | null;
5
5
  export declare function isRunEndCapPort(port: ScenePort, nodes: Readonly<Record<string, AnyNode>>): boolean;
6
6
  export declare function findMatedRunEndCapIds(source: ScenePort | null, nodes: Readonly<Record<string, AnyNode>>, fittingKind: 'duct-fitting' | 'pipe-fitting'): AnyNodeId[];
7
+ export declare function findAutomaticRunEndCapIds(runId: AnyNodeId, nodes: Readonly<Record<string, AnyNode>>, fittingKind: 'duct-fitting' | 'pipe-fitting'): AnyNodeId[];
7
8
  export declare function planRunEndCapFollowUpdates(originalRun: DuctSegmentNode | PipeSegmentNode, nextRun: DuctSegmentNode | PipeSegmentNode, endpoint: 'start' | 'end', nodes: Readonly<Record<string, AnyNode>>): {
8
9
  id: AnyNodeId;
9
10
  data: Partial<AnyNode>;
@@ -1 +1 @@
1
- {"version":3,"file":"automatic-run-end-cap.d.ts","sourceRoot":"","sources":["../../src/shared/automatic-run-end-cap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,eAAe,EACf,KAAK,eAAe,EACpB,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAA;AAMzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAyCxC,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,EACrB,QAAQ,GAAE,OAAO,GAAG,KAAa,GAChC,eAAe,GAAG,IAAI,CAwCxB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,EACrB,QAAQ,GAAE,OAAO,GAAG,KAAa,GAChC,eAAe,GAAG,IAAI,CAgCxB;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAMT;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,GAAG,IAAI,EACxB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,cAAc,GAAG,cAAc,GAC3C,SAAS,EAAE,CAgBb;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,GAAG,eAAe,EAC9C,OAAO,EAAE,eAAe,GAAG,eAAe,EAC1C,QAAQ,EAAE,OAAO,GAAG,KAAK,EACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,CAwB7C"}
1
+ {"version":3,"file":"automatic-run-end-cap.d.ts","sourceRoot":"","sources":["../../src/shared/automatic-run-end-cap.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,eAAe,EACf,KAAK,eAAe,EACpB,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAA;AAMzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAyCxC,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,EACrB,QAAQ,GAAE,OAAO,GAAG,KAAa,GAChC,eAAe,GAAG,IAAI,CAwCxB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,eAAe,EACrB,QAAQ,GAAE,OAAO,GAAG,KAAa,GAChC,eAAe,GAAG,IAAI,CAgCxB;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAMT;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,GAAG,IAAI,EACxB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,cAAc,GAAG,cAAc,GAC3C,SAAS,EAAE,CAgBb;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,SAAS,EAChB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,cAAc,GAAG,cAAc,GAC3C,SAAS,EAAE,CAWb;AAED,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,GAAG,eAAe,EAC9C,OAAO,EAAE,eAAe,GAAG,eAAe,EAC1C,QAAQ,EAAE,OAAO,GAAG,KAAK,EACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,EAAE,CAwB7C"}
@@ -130,6 +130,16 @@ export function findMatedRunEndCapIds(source, nodes, fittingKind) {
130
130
  }
131
131
  return ids;
132
132
  }
133
+ export function findAutomaticRunEndCapIds(runId, nodes, fittingKind) {
134
+ return Object.values(nodes).flatMap((node) => {
135
+ if (!node ||
136
+ node.type !== fittingKind ||
137
+ node.fittingType !== 'end-cap' ||
138
+ node.metadata[END_CAP_OWNER_ID_KEY] !== runId)
139
+ return [];
140
+ return [node.id];
141
+ });
142
+ }
133
143
  export function planRunEndCapFollowUpdates(originalRun, nextRun, endpoint, nodes) {
134
144
  if (originalRun.type !== nextRun.type)
135
145
  return [];
@@ -29,6 +29,16 @@ export type CameraDirectionProjection = {
29
29
  point: RunPoint;
30
30
  direction: RunPoint;
31
31
  };
32
+ type RunWallSurfaceTarget = Extract<RunSurfaceTarget, {
33
+ kind: 'wall';
34
+ }>;
35
+ export declare function shouldLockRunWallSurface(target: RunSurfaceTarget | null | undefined, connection: RunConnection): target is RunWallSurfaceTarget;
36
+ export declare function isSameRunWallSurface(target: RunWallSurfaceTarget, hit: {
37
+ kind?: string;
38
+ hostId?: string;
39
+ face?: string;
40
+ side?: string;
41
+ } | undefined): boolean;
32
42
  /** Build a stable 2D drawing frame for a floor, wall, ceiling, or sloped face. */
33
43
  export declare function createRunSurfaceFrame(origin: readonly number[], surfaceNormal?: readonly number[]): RunSurfaceFrame;
34
44
  export declare function projectRunPointToSurface(point: readonly number[], frame: RunSurfaceFrame): RunPoint;
@@ -1 +1 @@
1
- {"version":3,"file":"distribution-run-tool.d.ts","sourceRoot":"","sources":["../../src/shared/distribution-run-tool.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"distribution-run-tool.d.ts","sourceRoot":"","sources":["../../src/shared/distribution-run-tool.tsx"],"names":[],"mappings":"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"}
@@ -12,6 +12,15 @@ import { resolveRunCursorPlane } from './run-cursor';
12
12
  import { RunDirectionFeedback, run3DDirectionCandidates, runHorizontalDirectionCandidates, } from './run-direction-feedback';
13
13
  import { findScreenPort } from './run-port-snap';
14
14
  import { chooseScreenDirection, screenDirectionScore } from './run-screen-direction';
15
+ export function shouldLockRunWallSurface(target, connection) {
16
+ return target?.kind === 'wall' && !connection.port && !connection.body;
17
+ }
18
+ export function isSameRunWallSurface(target, hit) {
19
+ return (hit?.kind === 'wall' &&
20
+ hit.hostId === target.hostId &&
21
+ hit.face === 'side' &&
22
+ hit.side === target.side);
23
+ }
15
24
  const UP = [0, 1, 0];
16
25
  const X_AXIS = [1, 0, 0];
17
26
  const Z_AXIS = [0, 0, 1];
@@ -347,6 +356,7 @@ export function useDistributionRunTool(config) {
347
356
  const refreshCursorRef = useRef(() => { });
348
357
  const lastClientYRef = useRef(null);
349
358
  const lastResolvedRef = useRef(null);
359
+ const lockedWallTargetRef = useRef(null);
350
360
  const forcedDirectionRef = useRef(null);
351
361
  const hoveredDirectionRef = useRef(null);
352
362
  const lengthInputRef = useRef('');
@@ -409,17 +419,29 @@ export function useDistributionRunTool(config) {
409
419
  const hit = surfacePointFromEvent(event, adapter.levelId);
410
420
  const currentStart = startRef.current;
411
421
  const previous = lastResolvedRef.current;
412
- // A wall is only an attachment candidate, not a constraint for the
413
- // whole run. Once the ray leaves the wall, continue on a horizontal
414
- // plane through the start point so the user can route freely at the
415
- // same elevation (or use angle lock for a deliberate diagonal).
416
- const working = !event.surfaceHit && previous?.surfaceTarget?.kind === 'wall' && currentStart
417
- ? createRunSurfaceFrame(currentStart, UP)
418
- : (previous?.frame ?? (currentStart ? createRunSurfaceFrame(currentStart) : null));
422
+ const lockedWallTarget = currentStart ? lockedWallTargetRef.current : null;
423
+ const lockedWallHit = lockedWallTarget && isSameRunWallSurface(lockedWallTarget, event.surfaceHit);
424
+ const working = lockedWallTarget
425
+ ? {
426
+ ...lockedWallTarget.frame,
427
+ origin: lockedWallTarget.frame.origin.map((value, index) => value +
428
+ lockedWallTarget.frame.normal[index] *
429
+ (adapter.surfaceClearance?.(lockedWallTarget) ?? 0)),
430
+ }
431
+ : !event.surfaceHit && previous?.surfaceTarget?.kind === 'wall' && currentStart
432
+ ? createRunSurfaceFrame(currentStart, UP)
433
+ : (previous?.frame ?? (currentStart ? createRunSurfaceFrame(currentStart) : null));
419
434
  const hasSurface = !!event.surfaceHit || !working || !event.localRay;
420
- const target = hasSurface ? hit.target : null;
435
+ const target = lockedWallTarget ?? (hasSurface ? hit.target : null);
436
+ const hitForCursor = lockedWallTarget
437
+ ? lockedWallHit
438
+ ? { point: hit.point, frame: lockedWallTarget.frame }
439
+ : null
440
+ : hasSurface
441
+ ? { point: hit.point, frame: hit.frame }
442
+ : null;
421
443
  const resolved = resolveRunCursorPlane({
422
- hit: hasSurface ? { point: hit.point, frame: hit.frame } : null,
444
+ hit: hitForCursor,
423
445
  working,
424
446
  ray: event.localRay,
425
447
  fallback: previous?.point ?? currentStart ?? hit.point,
@@ -592,7 +614,7 @@ export function useDistributionRunTool(config) {
592
614
  resolved = {
593
615
  ...resolved,
594
616
  frame: { ...lastResolvedRef.current.frame, origin: resolved.point },
595
- surfaceTarget: null,
617
+ surfaceTarget: lockedWallTargetRef.current,
596
618
  };
597
619
  lastResolvedRef.current = resolved;
598
620
  const currentStart = startRef.current;
@@ -654,6 +676,24 @@ export function useDistributionRunTool(config) {
654
676
  setValidationMessage('Fitting clearance is too small for this connection');
655
677
  return;
656
678
  }
679
+ const lockedWallTarget = lockedWallTargetRef.current;
680
+ if (lockedWallTarget) {
681
+ const offset = result.nextStart.map((value, index) => value - lockedWallTarget.frame.origin[index]);
682
+ const clearance = configRef.current.surfaceClearance?.(lockedWallTarget) ?? 0;
683
+ if (Math.abs(dotRun(offset, lockedWallTarget.frame.normal) - clearance) > 1e-4) {
684
+ lockedWallTargetRef.current = null;
685
+ lastResolvedRef.current = {
686
+ point: result.nextStart,
687
+ frame: createRunSurfaceFrame(result.nextStart),
688
+ surfaceTarget: null,
689
+ snapped: null,
690
+ directionMode: 'free',
691
+ port: null,
692
+ body: null,
693
+ };
694
+ clearPlacementSurface();
695
+ }
696
+ }
657
697
  triggerSFX('sfx:item-place');
658
698
  startRef.current = result.nextStart;
659
699
  setStart(result.nextStart);
@@ -706,11 +746,14 @@ export function useDistributionRunTool(config) {
706
746
  const resolved = applyTypedLength(resolvePoint(event));
707
747
  updateCursor(resolved);
708
748
  if (!currentStart) {
709
- triggerSFX('sfx:grid-snap');
710
749
  const connection = {
711
750
  port: resolved.port,
712
751
  body: resolved.port ? null : resolved.body,
713
752
  };
753
+ if (shouldLockRunWallSurface(resolved.surfaceTarget, connection)) {
754
+ lockedWallTargetRef.current = resolved.surfaceTarget;
755
+ }
756
+ triggerSFX('sfx:grid-snap');
714
757
  startConnectionRef.current = connection;
715
758
  configRef.current.inheritFromConnection?.(connection);
716
759
  startRef.current = resolved.point;
@@ -805,6 +848,7 @@ export function useDistributionRunTool(config) {
805
848
  setDirectionMode('free');
806
849
  };
807
850
  const onCancel = () => {
851
+ lockedWallTargetRef.current = null;
808
852
  clearDrawAlignment();
809
853
  if (!startRef.current)
810
854
  return;
@@ -857,6 +901,7 @@ export function useDistributionRunTool(config) {
857
901
  window.removeEventListener('keyup', onKeyUp);
858
902
  altAnchorRef.current = null;
859
903
  lastPointerRef.current = null;
904
+ lockedWallTargetRef.current = null;
860
905
  refreshCursorRef.current = () => { };
861
906
  clearPlacementSurface();
862
907
  clearDrawAlignment();
@@ -0,0 +1,15 @@
1
+ export declare function PanelTextField({ label, onCommit, value, }: {
2
+ label: string;
3
+ onCommit: (value: string) => void;
4
+ value: string;
5
+ }): import("react").JSX.Element;
6
+ export declare function PanelSelect({ label, onChange, options, value, }: {
7
+ label: string;
8
+ onChange: (value: string) => void;
9
+ options: ReadonlyArray<{
10
+ label: string;
11
+ value: string;
12
+ }>;
13
+ value: string;
14
+ }): import("react").JSX.Element;
15
+ //# sourceMappingURL=panel-fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-fields.d.ts","sourceRoot":"","sources":["../../src/shared/panel-fields.tsx"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,KAAK,EAAE,MAAM,CAAA;CACd,+BAoCA;AAED,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,KAAK,GACN,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,KAAK,EAAE,MAAM,CAAA;CACd,+BAiBA"}
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ export function PanelTextField({ label, onCommit, value, }) {
5
+ const [draft, setDraft] = useState(value);
6
+ const cancelRef = useRef(false);
7
+ useEffect(() => setDraft(value), [value]);
8
+ const commit = () => {
9
+ if (cancelRef.current) {
10
+ cancelRef.current = false;
11
+ setDraft(value);
12
+ return;
13
+ }
14
+ const next = draft.trim();
15
+ if (next !== value)
16
+ onCommit(next);
17
+ else
18
+ setDraft(value);
19
+ };
20
+ return (_jsxs("label", { className: "flex h-10 items-center gap-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm", children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: label }), _jsx("input", { className: "min-w-0 flex-1 bg-transparent text-right text-foreground outline-none selection:bg-primary/30", onBlur: commit, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => {
21
+ if (event.key === 'Enter')
22
+ event.currentTarget.blur();
23
+ if (event.key === 'Escape') {
24
+ cancelRef.current = true;
25
+ event.currentTarget.blur();
26
+ }
27
+ }, type: "text", value: draft })] }));
28
+ }
29
+ export function PanelSelect({ label, onChange, options, value, }) {
30
+ return (_jsxs("label", { className: "flex h-10 items-center justify-between gap-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("select", { className: "min-w-0 rounded-md border border-border/50 bg-[#232325] px-2 py-1 text-foreground text-xs outline-none", onChange: (event) => onChange(event.target.value), value: value, children: options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] }));
31
+ }
@@ -0,0 +1,3 @@
1
+ import { type AnyNode, type AnyNodeId } from '@pascal-app/core';
2
+ export declare function restingNodePlanFrame(node: AnyNode, resolve: (id: AnyNodeId) => AnyNode | undefined): import("@pascal-app/core/procedural-items").Frame;
3
+ //# sourceMappingURL=resting-surface-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resting-surface-plan.d.ts","sourceRoot":"","sources":["../../src/shared/resting-surface-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,SAAS,EAAuC,MAAM,kBAAkB,CAAA;AAGpG,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,GAAG,SAAS,qDAwBhD"}
@@ -0,0 +1,28 @@
1
+ import { getEffectiveNode, useLiveTransforms } from '@pascal-app/core';
2
+ import { nodeLevelFrame } from '@pascal-app/core/procedural-items';
3
+ export function restingNodePlanFrame(node, resolve) {
4
+ const nodes = { [node.id]: node };
5
+ let id = node.parentId;
6
+ const visited = new Set([node.id]);
7
+ while (id && !visited.has(id)) {
8
+ visited.add(id);
9
+ const parent = resolve(id);
10
+ if (!parent)
11
+ break;
12
+ const effective = getEffectiveNode(parent);
13
+ const live = useLiveTransforms.getState().get(parent.id);
14
+ nodes[id] =
15
+ live &&
16
+ (parent.type === 'shelf' || parent.type === 'cabinet' || parent.type === 'procedural-item')
17
+ ? {
18
+ ...effective,
19
+ position: live.position,
20
+ rotation: parent.type === 'cabinet' ? live.rotation : [0, live.rotation, 0],
21
+ }
22
+ : effective;
23
+ if (parent.type === 'level')
24
+ break;
25
+ id = parent.parentId;
26
+ }
27
+ return nodeLevelFrame(node.id, nodes);
28
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/shelf/definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,cAAc,EAA8B,MAAM,kBAAkB,CAAA;AAQpG,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AA4HpC,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,OAAO,SAAS,CAuJ5D,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/shelf/definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,cAAc,EAA8B,MAAM,kBAAkB,CAAA;AAQpG,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AA4HpC,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,OAAO,SAAS,CAwJ5D,CAAA"}
@@ -194,7 +194,7 @@ export const shelfDefinition = {
194
194
  // declared here so the placement coordinator's shelf strategy can
195
195
  // confirm parent-kind compatibility before reparenting.
196
196
  relations: {
197
- hosts: ['item'],
197
+ hosts: ['item', 'procedural-item'],
198
198
  cascadeDelete: 'descendants',
199
199
  },
200
200
  parametrics: shelfParametrics,
@@ -209,6 +209,7 @@ export const shelfDefinition = {
209
209
  // Boards/posts/back depend only on these fields — never on hosted
210
210
  // `children`. Lets <GeometrySystem> skip the dispose+rebuild (and the
211
211
  // pointer enter/leave churn it causes) when an item reparents onto a row.
212
+ geometryChildTypes: [],
212
213
  geometryKey: (n) => {
213
214
  const s = sanitizeShelfDimensions(n);
214
215
  return JSON.stringify([
@@ -1 +1 @@
1
- {"version":3,"file":"floorplan-affordances.d.ts","sourceRoot":"","sources":["../../src/shelf/floorplan-affordances.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EAGf,MAAM,kBAAkB,CAAA;AASzB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,OAAO,GAAG,OAAO,CAAA;IAGtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAAmB,CAAC,SAAS,CAmChE,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAAmB,CAAC,SAAS,CAiChE,CAAA"}
1
+ {"version":3,"file":"floorplan-affordances.d.ts","sourceRoot":"","sources":["../../src/shelf/floorplan-affordances.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EAGf,MAAM,kBAAkB,CAAA;AASzB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,OAAO,GAAG,OAAO,CAAA;IAGtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAAmB,CAAC,SAAS,CAoChE,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAAmB,CAAC,SAAS,CAkChE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { useLiveNodeOverrides, useScene, } from '@pascal-app/core';
1
+ import { cascadeDirty, createSceneApi, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { isAngleSnapActive } from '@pascal-app/editor';
3
3
  import { rotateAffordanceDelta } from '../shared/rotate-affordance';
4
4
  // Mirror the 3D handles in `shelf/definition.ts` so a drag can't push a
@@ -32,7 +32,8 @@ export const shelfResizeAffordance = {
32
32
  lastPatch = { depth: Math.max(MIN_SHELF_DEPTH, initialDepth + 2 * projDelta) };
33
33
  }
34
34
  useLiveNodeOverrides.getState().set(shelfId, lastPatch);
35
- useScene.getState().markDirty(shelfId);
35
+ for (const id of cascadeDirty(shelfId, { scene: createSceneApi(useScene) }))
36
+ useScene.getState().markDirty(id);
36
37
  },
37
38
  canCommit() {
38
39
  return true;
@@ -79,7 +80,8 @@ export const shelfRotateAffordance = {
79
80
  const newRotationY = initialRotationY - delta;
80
81
  lastRotation = [r[0], newRotationY, r[2]];
81
82
  useLiveNodeOverrides.getState().set(shelfId, { rotation: lastRotation });
82
- useScene.getState().markDirty(shelfId);
83
+ for (const id of cascadeDirty(shelfId, { scene: createSceneApi(useScene) }))
84
+ useScene.getState().markDirty(id);
83
85
  },
84
86
  canCommit() {
85
87
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"floorplan-move.d.ts","sourceRoot":"","sources":["../../src/shelf/floorplan-move.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,mBAAmB,EAGxB,KAAK,SAAS,EAGf,MAAM,kBAAkB,CAAA;AAYzB;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,mBAAmB,CAAC,SAAS,CAqEnE,CAAA"}
1
+ {"version":3,"file":"floorplan-move.d.ts","sourceRoot":"","sources":["../../src/shelf/floorplan-move.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,mBAAmB,EAGxB,KAAK,SAAS,EAGf,MAAM,kBAAkB,CAAA;AAYzB;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,EAAE,mBAAmB,CAAC,SAAS,CAsEnE,CAAA"}
@@ -1,4 +1,4 @@
1
- import { collectAlignmentAnchors, movingFootprintAnchors, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
1
+ import { cascadeDirty, collectAlignmentAnchors, createSceneApi, movingFootprintAnchors, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { applyFloorplanAlignment, getFloorStackPreviewPosition, getSegmentGridStep, isGridSnapActive, isMagneticSnapActive, triggerSFX, } from '@pascal-app/editor';
3
3
  import { createFloorplanCursorResolver } from '../shared/floorplan-cursor';
4
4
  /**
@@ -52,7 +52,8 @@ export const shelfFloorplanMoveTarget = ({ node, nodes }) => {
52
52
  });
53
53
  lastVisualPosition = visualPosition;
54
54
  useLiveNodeOverrides.getState().set(shelfId, { position: visualPosition });
55
- useScene.getState().markDirty(shelfId);
55
+ for (const id of cascadeDirty(shelfId, { scene: createSceneApi(useScene) }))
56
+ useScene.getState().markDirty(id);
56
57
  },
57
58
  canCommit() {
58
59
  const live = useScene.getState().nodes[shelfId];
@@ -1 +1 @@
1
- {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/shelf/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAA0B,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAe,KAAK,EAAuB,MAAM,OAAO,CAAA;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAuFzC,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,EAClB,GAAG,CAAC,EAAE,eAAe,EACrB,OAAO,GAAE,aAA0B,GAClC,KAAK,CAkCP;AAySD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,CAM3D"}
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/shelf/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAIL,KAAK,aAAa,EAEnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAe,KAAK,EAAuB,MAAM,OAAO,CAAA;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AA0EzC,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,SAAS,EAClB,GAAG,CAAC,EAAE,eAAe,EACrB,OAAO,GAAE,aAA0B,GAClC,KAAK,CAkCP;AAySD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,CAM3D"}
@@ -1,4 +1,4 @@
1
- import { getMaterialPresetByRef } from '@pascal-app/core';
1
+ import { SHELF_BOARD_INSET as BOARD_INSET, getMaterialPresetByRef, shelfBoardDimensions, } from '@pascal-app/core';
2
2
  import { applyMaterialPresetToMaterials, createDefaultMaterial, createMaterial, resolveMaterialRef, } from '@pascal-app/viewer';
3
3
  import { BoxGeometry, Group, Mesh } from 'three';
4
4
  import { sanitizeShelfDimensions } from './dimensions';
@@ -27,14 +27,6 @@ function stampShelfSlot(mesh, slotId) {
27
27
  mesh.userData.slotId = slotId;
28
28
  return mesh;
29
29
  }
30
- // A board's front/back faces land on the frame's outer faces (posts / back panel)
31
- // — coplanar surfaces the depth buffer can't separate, which flickers as z-fighting.
32
- // Recess 1mm so the board sits just inside: the meshes still overlap (no gap), but
33
- // no faces are coplanar. Depth is always recessed (boards reach into the back panel
34
- // / posts). Width is recessed only at call sites where boards span OVER posts
35
- // (open-rack / no-sides bookshelf); boards that ABUT side panels keep full width so
36
- // they meet the sides flush — abutting faces are back-to-back and never fight.
37
- const BOARD_INSET = 0.001;
38
30
  // Frame members that pass under the top board (dividers / back / corner posts) reach
39
31
  // y=unitHeight, coplanar with the top board's top face → z-fighting. Drop their top 1mm so
40
32
  // the board cleanly caps them; their bottom stays on the floor.
@@ -44,7 +36,7 @@ function cappedFrameY(unitHeight) {
44
36
  return { height, centerY: height / 2 };
45
37
  }
46
38
  function boardGeometry(width, thickness, depth, insetWidth = false) {
47
- return new BoxGeometry(insetWidth ? Math.max(width - 2 * BOARD_INSET, 0.001) : width, thickness, Math.max(depth - 2 * BOARD_INSET, 0.001));
39
+ return new BoxGeometry(...shelfBoardDimensions(width, thickness, depth, insetWidth));
48
40
  }
49
41
  export function buildShelfGeometry(rawNode, ctx, shading = 'rendered') {
50
42
  const node = sanitizeShelfDimensions(rawNode);