@pascal-app/editor 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/package.json +6 -6
  2. package/src/components/editor/custom-camera-controls.tsx +2 -1
  3. package/src/components/editor/editor-layout-v2.tsx +4 -3
  4. package/src/components/editor/first-person/build-collider-world.ts +5 -7
  5. package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
  6. package/src/components/editor/first-person-controls.tsx +11 -11
  7. package/src/components/editor/floating-action-menu.tsx +0 -0
  8. package/src/components/editor/floorplan-panel.tsx +44 -37
  9. package/src/components/editor/index.tsx +68 -53
  10. package/src/components/editor/selection-manager.tsx +2 -2
  11. package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
  12. package/src/components/editor/thumbnail-generator.tsx +18 -61
  13. package/src/components/editor/use-floorplan-background-placement.ts +3 -3
  14. package/src/components/editor/wall-measurement-label.tsx +0 -0
  15. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  16. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
  17. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
  18. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
  19. package/src/components/systems/roof/roof-edit-system.tsx +1 -1
  20. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  21. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  22. package/src/components/systems/zone/zone-system.tsx +0 -0
  23. package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
  24. package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
  25. package/src/components/tools/fence/fence-tool.tsx +2 -2
  26. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
  27. package/src/components/tools/fence/move-fence-tool.tsx +13 -9
  28. package/src/components/tools/item/move-tool.tsx +3 -6
  29. package/src/components/tools/item/placement-math.ts +2 -4
  30. package/src/components/tools/item/placement-strategies.ts +11 -10
  31. package/src/components/tools/item/use-draft-node.ts +0 -1
  32. package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
  33. package/src/components/tools/roof/move-roof-tool.tsx +7 -2
  34. package/src/components/tools/select/box-select-tool.tsx +12 -17
  35. package/src/components/tools/shared/segment-angle.ts +1 -1
  36. package/src/components/tools/tool-manager.tsx +12 -12
  37. package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
  38. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
  39. package/src/components/tools/wall/move-wall-tool.tsx +6 -4
  40. package/src/components/tools/wall/wall-drafting.ts +0 -0
  41. package/src/components/tools/wall/wall-tool.tsx +3 -3
  42. package/src/components/tools/zone/zone-tool.tsx +20 -5
  43. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  44. package/src/components/ui/action-menu/control-modes.tsx +7 -1
  45. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  46. package/src/components/ui/action-menu/index.tsx +35 -86
  47. package/src/components/ui/action-menu/view-toggles.tsx +19 -31
  48. package/src/components/ui/command-palette/editor-commands.tsx +6 -4
  49. package/src/components/ui/command-palette/index.tsx +4 -255
  50. package/src/components/ui/controls/material-picker.tsx +8 -5
  51. package/src/components/ui/floating-level-selector.tsx +1 -1
  52. package/src/components/ui/helpers/helper-manager.tsx +5 -0
  53. package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
  54. package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
  55. package/src/components/ui/level-duplicate-dialog.tsx +3 -5
  56. package/src/components/ui/panels/ceiling-panel.tsx +2 -3
  57. package/src/components/ui/panels/column-panel.tsx +62 -18
  58. package/src/components/ui/panels/door-panel.tsx +272 -265
  59. package/src/components/ui/panels/fence-panel.tsx +0 -5
  60. package/src/components/ui/panels/paint-panel.tsx +66 -41
  61. package/src/components/ui/panels/panel-manager.tsx +3 -32
  62. package/src/components/ui/panels/reference-panel.tsx +28 -13
  63. package/src/components/ui/panels/roof-panel.tsx +52 -2
  64. package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
  65. package/src/components/ui/panels/slab-panel.tsx +0 -0
  66. package/src/components/ui/panels/spawn-panel.tsx +10 -4
  67. package/src/components/ui/panels/stair-panel.tsx +66 -14
  68. package/src/components/ui/panels/wall-panel.tsx +97 -1
  69. package/src/components/ui/panels/window-panel.tsx +13 -5
  70. package/src/components/ui/primitives/number-input.tsx +1 -1
  71. package/src/components/ui/primitives/sidebar.tsx +0 -0
  72. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  73. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  74. package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
  75. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
  76. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
  77. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  78. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
  79. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
  80. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
  81. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  82. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
  83. package/src/components/ui/slider.tsx +1 -1
  84. package/src/components/viewer-overlay.tsx +0 -0
  85. package/src/components/viewer-zone-system.tsx +0 -0
  86. package/src/hooks/use-auto-save.ts +14 -0
  87. package/src/hooks/use-keyboard.ts +10 -0
  88. package/src/index.tsx +8 -1
  89. package/src/lib/level-duplication.test.ts +0 -2
  90. package/src/lib/level-duplication.ts +1 -1
  91. package/src/lib/material-paint.ts +1 -1
  92. package/src/lib/roof-duplication.ts +1 -1
  93. package/src/lib/scene-bounds.ts +1 -1
  94. package/src/lib/scene.ts +0 -0
  95. package/src/lib/sfx-bus.ts +2 -0
  96. package/src/lib/sfx-player.ts +5 -5
  97. package/src/lib/stair-duplication.ts +2 -2
  98. package/src/store/use-editor.tsx +27 -59
  99. package/tsconfig.json +2 -1
  100. package/src/components/feedback-dialog.tsx +0 -265
  101. package/src/components/pascal-radio.tsx +0 -280
  102. package/src/components/preview-button.tsx +0 -16
  103. package/src/components/ui/viewer-toolbar.tsx +0 -436
@@ -12,8 +12,8 @@ import { useViewer } from '@pascal-app/viewer'
12
12
  import { BookMarked, Copy, DoorOpen, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
13
13
  import { useCallback, useRef } from 'react'
14
14
  import { usePresetsAdapter } from '../../../contexts/presets-context'
15
- import { cn } from '../../../lib/utils'
16
15
  import { sfxEmitter } from '../../../lib/sfx-bus'
16
+ import { cn } from '../../../lib/utils'
17
17
  import useEditor from '../../../store/use-editor'
18
18
  import { ActionButton, ActionGroup } from '../controls/action-button'
19
19
  import { MetricControl } from '../controls/metric-control'
@@ -148,7 +148,13 @@ export function DoorPanel() {
148
148
  const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
149
149
  if (liveNode?.type !== 'door') return
150
150
 
151
- if (!(previewRef.current && previewRef.current.id === selectedId && previewRef.current.key === key)) {
151
+ if (
152
+ !(
153
+ previewRef.current &&
154
+ previewRef.current.id === selectedId &&
155
+ previewRef.current.key === key
156
+ )
157
+ ) {
152
158
  previewRef.current = {
153
159
  id: selectedId as AnyNodeId,
154
160
  key,
@@ -333,7 +339,8 @@ export function DoorPanel() {
333
339
  const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
334
340
  const isOpening = node.openingKind === 'opening'
335
341
  const openingShape = node.openingShape ?? 'rectangle'
336
- const doorShape = openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
342
+ const doorShape =
343
+ openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
337
344
  const openingRadiusMode = node.openingRadiusMode ?? 'all'
338
345
  const openingTopRadii = node.openingTopRadii ?? [0.15, 0.15]
339
346
  const cornerRadius = node.cornerRadius ?? 0.15
@@ -578,7 +585,8 @@ export function DoorPanel() {
578
585
  isSelected
579
586
  ? 'border-orange-400/60 bg-orange-400/10 text-foreground'
580
587
  : 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
581
- !option.available && 'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground',
588
+ !option.available &&
589
+ 'cursor-not-allowed opacity-45 hover:bg-[#2C2C2E] hover:text-muted-foreground',
582
590
  )}
583
591
  disabled={!option.available}
584
592
  key={option.value}
@@ -962,312 +970,311 @@ export function DoorPanel() {
962
970
  />
963
971
  </PanelSection>
964
972
 
965
- {!isGarageDoor && (
966
- <PanelSection title="Content Padding">
967
- <SliderControl
968
- label="Horizontal"
969
- max={0.2}
970
- min={0}
971
- onChange={(v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] })}
972
- precision={3}
973
- step={0.005}
974
- unit="m"
975
- value={Math.round(node.contentPadding[0] * 1000) / 1000}
976
- />
977
- <SliderControl
978
- label="Vertical"
979
- max={0.2}
980
- min={0}
981
- onChange={(v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] })}
982
- precision={3}
983
- step={0.005}
984
- unit="m"
985
- value={Math.round(node.contentPadding[1] * 1000) / 1000}
986
- />
987
- </PanelSection>
988
- )}
989
-
990
- {isSwingDoor && (
991
- <PanelSection title="Swing">
992
- <div className="flex flex-col gap-2 px-1 pb-1">
993
- <div className="space-y-1">
994
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
995
- Hinges Side
996
- </span>
997
- <SegmentedControl
998
- onChange={(v) => handleUpdate({ hingesSide: v })}
999
- options={[
1000
- { label: 'Left', value: 'left' },
1001
- { label: 'Right', value: 'right' },
1002
- ]}
1003
- value={node.hingesSide}
1004
- />
1005
- </div>
1006
- <div className="space-y-1">
1007
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1008
- Direction
1009
- </span>
1010
- <SegmentedControl
1011
- onChange={(v) => handleUpdate({ swingDirection: v })}
1012
- options={[
1013
- { label: 'Inward', value: 'inward' },
1014
- { label: 'Outward', value: 'outward' },
1015
- ]}
1016
- value={node.swingDirection}
973
+ {!isGarageDoor && (
974
+ <PanelSection title="Content Padding">
975
+ <SliderControl
976
+ label="Horizontal"
977
+ max={0.2}
978
+ min={0}
979
+ onChange={(v) => handleUpdate({ contentPadding: [v, node.contentPadding[1]] })}
980
+ precision={3}
981
+ step={0.005}
982
+ unit="m"
983
+ value={Math.round(node.contentPadding[0] * 1000) / 1000}
1017
984
  />
1018
- </div>
1019
- </div>
1020
- </PanelSection>
1021
- )}
1022
-
1023
- {isSwingDoor && (
1024
- <PanelSection title="Threshold">
1025
- <ToggleControl
1026
- checked={node.threshold}
1027
- label="Enable Threshold"
1028
- onChange={(checked) => handleUpdate({ threshold: checked })}
1029
- />
1030
- {node.threshold && (
1031
- <div className="mt-1 flex flex-col gap-1">
1032
985
  <SliderControl
1033
- label="Height"
1034
- max={0.1}
1035
- min={0.005}
1036
- onChange={(v) => handleUpdate({ thresholdHeight: v })}
986
+ label="Vertical"
987
+ max={0.2}
988
+ min={0}
989
+ onChange={(v) => handleUpdate({ contentPadding: [node.contentPadding[0], v] })}
1037
990
  precision={3}
1038
991
  step={0.005}
1039
992
  unit="m"
1040
- value={Math.round(node.thresholdHeight * 1000) / 1000}
993
+ value={Math.round(node.contentPadding[1] * 1000) / 1000}
1041
994
  />
1042
- </div>
995
+ </PanelSection>
1043
996
  )}
1044
- </PanelSection>
1045
- )}
1046
997
 
1047
- {!isGarageDoor && (
1048
- <PanelSection title="Handle">
1049
998
  {isSwingDoor && (
1050
- <ToggleControl
1051
- checked={node.handle}
1052
- label="Enable Handle"
1053
- onChange={(checked) => handleUpdate({ handle: checked })}
1054
- />
1055
- )}
1056
- {(node.handle || !isSwingDoor) && (
1057
- <div className="mt-1 flex flex-col gap-1">
1058
- <SliderControl
1059
- label="Height"
1060
- max={node.height - 0.1}
1061
- min={0.5}
1062
- onChange={(v) => handleUpdate({ handleHeight: v })}
1063
- precision={2}
1064
- step={0.05}
1065
- unit="m"
1066
- value={Math.round(node.handleHeight * 100) / 100}
1067
- />
1068
- {supportsHandleSide && (
999
+ <PanelSection title="Swing">
1000
+ <div className="flex flex-col gap-2 px-1 pb-1">
1069
1001
  <div className="space-y-1">
1070
1002
  <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1071
- Handle Side
1003
+ Hinges Side
1072
1004
  </span>
1073
1005
  <SegmentedControl
1074
- onChange={(v) => handleUpdate({ handleSide: v })}
1006
+ onChange={(v) => handleUpdate({ hingesSide: v })}
1075
1007
  options={[
1076
1008
  { label: 'Left', value: 'left' },
1077
1009
  { label: 'Right', value: 'right' },
1078
1010
  ]}
1079
- value={node.handleSide}
1011
+ value={node.hingesSide}
1080
1012
  />
1081
1013
  </div>
1082
- )}
1083
- </div>
1014
+ <div className="space-y-1">
1015
+ <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1016
+ Direction
1017
+ </span>
1018
+ <SegmentedControl
1019
+ onChange={(v) => handleUpdate({ swingDirection: v })}
1020
+ options={[
1021
+ { label: 'Inward', value: 'inward' },
1022
+ { label: 'Outward', value: 'outward' },
1023
+ ]}
1024
+ value={node.swingDirection}
1025
+ />
1026
+ </div>
1027
+ </div>
1028
+ </PanelSection>
1084
1029
  )}
1085
- </PanelSection>
1086
- )}
1087
1030
 
1088
- {isSwingDoor && (
1089
- <PanelSection title="Hardware">
1090
- <ToggleControl
1091
- checked={node.doorCloser}
1092
- label="Door Closer"
1093
- onChange={(checked) => handleUpdate({ doorCloser: checked })}
1094
- />
1095
- <ToggleControl
1096
- checked={node.panicBar}
1097
- label="Panic Bar"
1098
- onChange={(checked) => handleUpdate({ panicBar: checked })}
1099
- />
1100
- {node.panicBar && (
1101
- <div className="mt-1 flex flex-col gap-1">
1102
- <SliderControl
1103
- label="Bar Height"
1104
- max={node.height - 0.1}
1105
- min={0.5}
1106
- onChange={(v) => handleUpdate({ panicBarHeight: v })}
1107
- precision={2}
1108
- step={0.05}
1109
- unit="m"
1110
- value={Math.round(node.panicBarHeight * 100) / 100}
1031
+ {isSwingDoor && (
1032
+ <PanelSection title="Threshold">
1033
+ <ToggleControl
1034
+ checked={node.threshold}
1035
+ label="Enable Threshold"
1036
+ onChange={(checked) => handleUpdate({ threshold: checked })}
1111
1037
  />
1112
- </div>
1113
- )}
1114
- </PanelSection>
1115
- )}
1116
-
1117
- {!isGarageDoor && (
1118
- <PanelSection title="Segments">
1119
- {node.segments.map((seg, i) => {
1120
- const numCols = seg.columnRatios.length
1121
- const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
1122
- const normCols = seg.columnRatios.map((r) => r / colSum)
1123
- return (
1124
- <div className="mb-2 flex flex-col gap-1" key={i}>
1125
- <div className="flex items-center justify-between pb-1">
1126
- <span className="font-medium text-white/80 text-xs">Segment {i + 1}</span>
1038
+ {node.threshold && (
1039
+ <div className="mt-1 flex flex-col gap-1">
1040
+ <SliderControl
1041
+ label="Height"
1042
+ max={0.1}
1043
+ min={0.005}
1044
+ onChange={(v) => handleUpdate({ thresholdHeight: v })}
1045
+ precision={3}
1046
+ step={0.005}
1047
+ unit="m"
1048
+ value={Math.round(node.thresholdHeight * 1000) / 1000}
1049
+ />
1127
1050
  </div>
1051
+ )}
1052
+ </PanelSection>
1053
+ )}
1128
1054
 
1129
- <SegmentedControl
1130
- onChange={(t) => {
1131
- const updated = node.segments.map((s, idx) =>
1132
- idx === i ? { ...s, type: t } : s,
1133
- )
1134
- handleUpdate({ segments: updated })
1135
- }}
1136
- options={[
1137
- { label: 'Panel', value: 'panel' },
1138
- { label: 'Glass', value: 'glass' },
1139
- { label: 'Empty', value: 'empty' },
1140
- ]}
1141
- value={seg.type}
1142
- />
1143
-
1144
- <SliderControl
1145
- label="Height"
1146
- max={95}
1147
- min={5}
1148
- onChange={(v) => setSegmentHeightRatio(i, v / 100)}
1149
- precision={1}
1150
- step={1}
1151
- unit="%"
1152
- value={Math.round(normHeights[i]! * 100 * 10) / 10}
1055
+ {!isGarageDoor && (
1056
+ <PanelSection title="Handle">
1057
+ {isSwingDoor && (
1058
+ <ToggleControl
1059
+ checked={node.handle}
1060
+ label="Enable Handle"
1061
+ onChange={(checked) => handleUpdate({ handle: checked })}
1153
1062
  />
1063
+ )}
1064
+ {(node.handle || !isSwingDoor) && (
1065
+ <div className="mt-1 flex flex-col gap-1">
1066
+ <SliderControl
1067
+ label="Height"
1068
+ max={node.height - 0.1}
1069
+ min={0.5}
1070
+ onChange={(v) => handleUpdate({ handleHeight: v })}
1071
+ precision={2}
1072
+ step={0.05}
1073
+ unit="m"
1074
+ value={Math.round(node.handleHeight * 100) / 100}
1075
+ />
1076
+ {supportsHandleSide && (
1077
+ <div className="space-y-1">
1078
+ <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
1079
+ Handle Side
1080
+ </span>
1081
+ <SegmentedControl
1082
+ onChange={(v) => handleUpdate({ handleSide: v })}
1083
+ options={[
1084
+ { label: 'Left', value: 'left' },
1085
+ { label: 'Right', value: 'right' },
1086
+ ]}
1087
+ value={node.handleSide}
1088
+ />
1089
+ </div>
1090
+ )}
1091
+ </div>
1092
+ )}
1093
+ </PanelSection>
1094
+ )}
1154
1095
 
1155
- <SliderControl
1156
- label="Columns"
1157
- max={8}
1158
- min={1}
1159
- onChange={(v) => {
1160
- const n = Math.max(1, Math.min(8, Math.round(v)))
1161
- const updated = node.segments.map((s, idx) =>
1162
- idx === i ? { ...s, columnRatios: Array(n).fill(1 / n) } : s,
1163
- )
1164
- handleUpdate({ segments: updated })
1165
- }}
1166
- precision={0}
1167
- step={1}
1168
- value={numCols}
1169
- />
1096
+ {isSwingDoor && (
1097
+ <PanelSection title="Hardware">
1098
+ <ToggleControl
1099
+ checked={node.doorCloser}
1100
+ label="Door Closer"
1101
+ onChange={(checked) => handleUpdate({ doorCloser: checked })}
1102
+ />
1103
+ <ToggleControl
1104
+ checked={node.panicBar}
1105
+ label="Panic Bar"
1106
+ onChange={(checked) => handleUpdate({ panicBar: checked })}
1107
+ />
1108
+ {node.panicBar && (
1109
+ <div className="mt-1 flex flex-col gap-1">
1110
+ <SliderControl
1111
+ label="Bar Height"
1112
+ max={node.height - 0.1}
1113
+ min={0.5}
1114
+ onChange={(v) => handleUpdate({ panicBarHeight: v })}
1115
+ precision={2}
1116
+ step={0.05}
1117
+ unit="m"
1118
+ value={Math.round(node.panicBarHeight * 100) / 100}
1119
+ />
1120
+ </div>
1121
+ )}
1122
+ </PanelSection>
1123
+ )}
1170
1124
 
1171
- {numCols > 1 && (
1172
- <div className="mt-1 border-border/50 border-t pt-1">
1173
- {normCols.map((ratio, ci) => (
1174
- <SliderControl
1175
- key={`c-${ci}`}
1176
- label={`C${ci + 1}`}
1177
- max={95}
1178
- min={5}
1179
- onChange={(v) => setSegmentColumnRatio(i, ci, v / 100)}
1180
- precision={1}
1181
- step={1}
1182
- unit="%"
1183
- value={Math.round(ratio * 100 * 10) / 10}
1184
- />
1185
- ))}
1186
- <SliderControl
1187
- label="Divider"
1188
- max={0.1}
1189
- min={0.005}
1190
- onChange={(v) => {
1125
+ {!isGarageDoor && (
1126
+ <PanelSection title="Segments">
1127
+ {node.segments.map((seg, i) => {
1128
+ const numCols = seg.columnRatios.length
1129
+ const colSum = seg.columnRatios.reduce((a, b) => a + b, 0)
1130
+ const normCols = seg.columnRatios.map((r) => r / colSum)
1131
+ return (
1132
+ <div className="mb-2 flex flex-col gap-1" key={i}>
1133
+ <div className="flex items-center justify-between pb-1">
1134
+ <span className="font-medium text-white/80 text-xs">Segment {i + 1}</span>
1135
+ </div>
1136
+
1137
+ <SegmentedControl
1138
+ onChange={(t) => {
1191
1139
  const updated = node.segments.map((s, idx) =>
1192
- idx === i ? { ...s, dividerThickness: v } : s,
1140
+ idx === i ? { ...s, type: t } : s,
1193
1141
  )
1194
1142
  handleUpdate({ segments: updated })
1195
1143
  }}
1196
- precision={3}
1197
- step={0.005}
1198
- unit="m"
1199
- value={Math.round(seg.dividerThickness * 1000) / 1000}
1144
+ options={[
1145
+ { label: 'Panel', value: 'panel' },
1146
+ { label: 'Glass', value: 'glass' },
1147
+ { label: 'Empty', value: 'empty' },
1148
+ ]}
1149
+ value={seg.type}
1200
1150
  />
1201
- </div>
1202
- )}
1203
1151
 
1204
- {seg.type === 'panel' && (
1205
- <div className="mt-1 border-border/50 border-t pt-1">
1206
1152
  <SliderControl
1207
- label="Inset"
1208
- max={0.1}
1209
- min={0}
1210
- onChange={(v) => {
1211
- const updated = node.segments.map((s, idx) =>
1212
- idx === i ? { ...s, panelInset: v } : s,
1213
- )
1214
- handleUpdate({ segments: updated })
1215
- }}
1216
- precision={3}
1217
- step={0.005}
1218
- unit="m"
1219
- value={Math.round(seg.panelInset * 1000) / 1000}
1153
+ label="Height"
1154
+ max={95}
1155
+ min={5}
1156
+ onChange={(v) => setSegmentHeightRatio(i, v / 100)}
1157
+ precision={1}
1158
+ step={1}
1159
+ unit="%"
1160
+ value={Math.round(normHeights[i]! * 100 * 10) / 10}
1220
1161
  />
1162
+
1221
1163
  <SliderControl
1222
- label="Depth"
1223
- max={0.1}
1224
- min={0}
1164
+ label="Columns"
1165
+ max={8}
1166
+ min={1}
1225
1167
  onChange={(v) => {
1168
+ const n = Math.max(1, Math.min(8, Math.round(v)))
1226
1169
  const updated = node.segments.map((s, idx) =>
1227
- idx === i ? { ...s, panelDepth: v } : s,
1170
+ idx === i ? { ...s, columnRatios: Array(n).fill(1 / n) } : s,
1228
1171
  )
1229
1172
  handleUpdate({ segments: updated })
1230
1173
  }}
1231
- precision={3}
1232
- step={0.005}
1233
- unit="m"
1234
- value={Math.round(seg.panelDepth * 1000) / 1000}
1174
+ precision={0}
1175
+ step={1}
1176
+ value={numCols}
1235
1177
  />
1178
+
1179
+ {numCols > 1 && (
1180
+ <div className="mt-1 border-border/50 border-t pt-1">
1181
+ {normCols.map((ratio, ci) => (
1182
+ <SliderControl
1183
+ key={`c-${ci}`}
1184
+ label={`C${ci + 1}`}
1185
+ max={95}
1186
+ min={5}
1187
+ onChange={(v) => setSegmentColumnRatio(i, ci, v / 100)}
1188
+ precision={1}
1189
+ step={1}
1190
+ unit="%"
1191
+ value={Math.round(ratio * 100 * 10) / 10}
1192
+ />
1193
+ ))}
1194
+ <SliderControl
1195
+ label="Divider"
1196
+ max={0.1}
1197
+ min={0.005}
1198
+ onChange={(v) => {
1199
+ const updated = node.segments.map((s, idx) =>
1200
+ idx === i ? { ...s, dividerThickness: v } : s,
1201
+ )
1202
+ handleUpdate({ segments: updated })
1203
+ }}
1204
+ precision={3}
1205
+ step={0.005}
1206
+ unit="m"
1207
+ value={Math.round(seg.dividerThickness * 1000) / 1000}
1208
+ />
1209
+ </div>
1210
+ )}
1211
+
1212
+ {seg.type === 'panel' && (
1213
+ <div className="mt-1 border-border/50 border-t pt-1">
1214
+ <SliderControl
1215
+ label="Inset"
1216
+ max={0.1}
1217
+ min={0}
1218
+ onChange={(v) => {
1219
+ const updated = node.segments.map((s, idx) =>
1220
+ idx === i ? { ...s, panelInset: v } : s,
1221
+ )
1222
+ handleUpdate({ segments: updated })
1223
+ }}
1224
+ precision={3}
1225
+ step={0.005}
1226
+ unit="m"
1227
+ value={Math.round(seg.panelInset * 1000) / 1000}
1228
+ />
1229
+ <SliderControl
1230
+ label="Depth"
1231
+ max={0.1}
1232
+ min={0}
1233
+ onChange={(v) => {
1234
+ const updated = node.segments.map((s, idx) =>
1235
+ idx === i ? { ...s, panelDepth: v } : s,
1236
+ )
1237
+ handleUpdate({ segments: updated })
1238
+ }}
1239
+ precision={3}
1240
+ step={0.005}
1241
+ unit="m"
1242
+ value={Math.round(seg.panelDepth * 1000) / 1000}
1243
+ />
1244
+ </div>
1245
+ )}
1236
1246
  </div>
1247
+ )
1248
+ })}
1249
+
1250
+ <div className="flex gap-1.5 px-1 pt-1">
1251
+ <ActionButton
1252
+ label="+ Add Segment"
1253
+ onClick={() => {
1254
+ const updated = [
1255
+ ...node.segments,
1256
+ {
1257
+ type: 'panel' as const,
1258
+ heightRatio: 1,
1259
+ columnRatios: [1],
1260
+ dividerThickness: 0.03,
1261
+ panelDepth: 0.01,
1262
+ panelInset: 0.04,
1263
+ },
1264
+ ]
1265
+ handleUpdate({ segments: updated })
1266
+ }}
1267
+ />
1268
+ {node.segments.length > 1 && (
1269
+ <ActionButton
1270
+ className="text-white/60 hover:text-white"
1271
+ label="- Remove"
1272
+ onClick={() => handleUpdate({ segments: node.segments.slice(0, -1) })}
1273
+ />
1237
1274
  )}
1238
1275
  </div>
1239
- )
1240
- })}
1241
-
1242
- <div className="flex gap-1.5 px-1 pt-1">
1243
- <ActionButton
1244
- label="+ Add Segment"
1245
- onClick={() => {
1246
- const updated = [
1247
- ...node.segments,
1248
- {
1249
- type: 'panel' as const,
1250
- heightRatio: 1,
1251
- columnRatios: [1],
1252
- dividerThickness: 0.03,
1253
- panelDepth: 0.01,
1254
- panelInset: 0.04,
1255
- },
1256
- ]
1257
- handleUpdate({ segments: updated })
1258
- }}
1259
- />
1260
- {node.segments.length > 1 && (
1261
- <ActionButton
1262
- className="text-white/60 hover:text-white"
1263
- label="- Remove"
1264
- onClick={() => handleUpdate({ segments: node.segments.slice(0, -1) })}
1265
- />
1266
- )}
1267
- </div>
1268
- </PanelSection>
1269
- )}
1270
-
1276
+ </PanelSection>
1277
+ )}
1271
1278
  </>
1272
1279
  )}
1273
1280
 
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
-
4
3
  import {
5
4
  type AnyNode,
6
5
  type AnyNodeId,
@@ -86,10 +85,6 @@ export function FencePanel() {
86
85
  setSelection({ selectedIds: [] })
87
86
  }, [setSelection])
88
87
 
89
-
90
-
91
-
92
-
93
88
  if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null
94
89
 
95
90
  const length = getWallCurveLength(node)