@pascal-app/editor 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -227,12 +227,12 @@ export function KeyboardShortcutsDialog() {
|
|
|
227
227
|
<DialogTrigger asChild>
|
|
228
228
|
<Button className="w-full justify-start gap-2" variant="outline">
|
|
229
229
|
<Keyboard className="size-4" />
|
|
230
|
-
Keyboard
|
|
230
|
+
Keyboard shortcuts
|
|
231
231
|
</Button>
|
|
232
232
|
</DialogTrigger>
|
|
233
233
|
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden p-0 sm:max-w-3xl">
|
|
234
234
|
<DialogHeader className="shrink-0 border-b px-6 py-4">
|
|
235
|
-
<DialogTitle>Keyboard
|
|
235
|
+
<DialogTitle>Keyboard shortcuts</DialogTitle>
|
|
236
236
|
<DialogDescription>
|
|
237
237
|
Shortcuts are context-aware. Guided constraints are enabled by default; hold Shift
|
|
238
238
|
during an active gesture to build freely.
|
|
@@ -40,7 +40,7 @@ describe('simple 3D print export', () => {
|
|
|
40
40
|
return artifact
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const prepared = await preparePrintExport(modelExport, true)
|
|
43
|
+
const prepared = await preparePrintExport(modelExport, true, 'print-3mf')
|
|
44
44
|
|
|
45
45
|
expect(calls).toEqual([
|
|
46
46
|
{
|
|
@@ -58,6 +58,33 @@ describe('simple 3D print export', () => {
|
|
|
58
58
|
expect(prepared).toEqual({ artifact, report })
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
+
test('uses the same safe profile for printable STL files', async () => {
|
|
62
|
+
const calls: { format?: string; options?: ModelExportOptions }[] = []
|
|
63
|
+
const stlReport: PrintExportReport = { ...report, format: 'stl' }
|
|
64
|
+
const artifact = { blob: new Blob(['stl']), filename: 'house.zip', metadata: stlReport }
|
|
65
|
+
const modelExport: ModelExport = async (format, options) => {
|
|
66
|
+
calls.push({ format, options })
|
|
67
|
+
return artifact
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const prepared = await preparePrintExport(modelExport, false, 'print-stl')
|
|
71
|
+
|
|
72
|
+
expect(calls).toEqual([
|
|
73
|
+
{
|
|
74
|
+
format: 'print-stl',
|
|
75
|
+
options: {
|
|
76
|
+
onlyVisible: false,
|
|
77
|
+
download: false,
|
|
78
|
+
printScale: 100,
|
|
79
|
+
printScope: 'levels',
|
|
80
|
+
printContent: 'structure',
|
|
81
|
+
printBase: 'none',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
])
|
|
85
|
+
expect(prepared).toEqual({ artifact, report: stlReport })
|
|
86
|
+
})
|
|
87
|
+
|
|
61
88
|
test('blocks the download when preflight finds invalid geometry', async () => {
|
|
62
89
|
const blockedReport: PrintExportReport = {
|
|
63
90
|
...report,
|
|
@@ -76,7 +103,7 @@ describe('simple 3D print export', () => {
|
|
|
76
103
|
metadata: blockedReport,
|
|
77
104
|
})
|
|
78
105
|
|
|
79
|
-
await expect(preparePrintExport(modelExport, true)).rejects.toThrow(
|
|
106
|
+
await expect(preparePrintExport(modelExport, true, 'print-3mf')).rejects.toThrow(
|
|
80
107
|
'One wall has an open edge.',
|
|
81
108
|
)
|
|
82
109
|
})
|
|
@@ -122,7 +149,7 @@ describe('simple 3D print export', () => {
|
|
|
122
149
|
metadata: blockedBundleReport,
|
|
123
150
|
})
|
|
124
151
|
|
|
125
|
-
await expect(preparePrintExport(modelExport, true)).rejects.toThrow(
|
|
152
|
+
await expect(preparePrintExport(modelExport, true, 'print-3mf')).rejects.toThrow(
|
|
126
153
|
'The upper level has an open edge.',
|
|
127
154
|
)
|
|
128
155
|
})
|
|
@@ -133,7 +160,7 @@ describe('simple 3D print export', () => {
|
|
|
133
160
|
filename: 'house.3mf',
|
|
134
161
|
})
|
|
135
162
|
|
|
136
|
-
await expect(preparePrintExport(modelExport, false)).rejects.toThrow(
|
|
163
|
+
await expect(preparePrintExport(modelExport, false, 'print-3mf')).rejects.toThrow(
|
|
137
164
|
'did not return a valid file',
|
|
138
165
|
)
|
|
139
166
|
})
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
isPrintLevelBundleReport,
|
|
6
6
|
type PrintLevelBundleReport,
|
|
7
7
|
} from '../../../../../lib/level-print-export'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ModelExport,
|
|
10
|
+
ModelExportArtifact,
|
|
11
|
+
ModelExportFormat,
|
|
12
|
+
} from '../../../../../lib/model-export'
|
|
9
13
|
import {
|
|
10
14
|
isPrintExportReport,
|
|
11
15
|
type PrintExportReport,
|
|
@@ -17,6 +21,8 @@ type PreparedPrintExport = {
|
|
|
17
21
|
report: PrintExportReport | PrintLevelBundleReport
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
type PrintModelExportFormat = Extract<ModelExportFormat, 'print-3mf' | 'print-stl'>
|
|
25
|
+
|
|
20
26
|
function downloadArtifact(artifact: ModelExportArtifact) {
|
|
21
27
|
const url = URL.createObjectURL(artifact.blob)
|
|
22
28
|
const link = document.createElement('a')
|
|
@@ -39,8 +45,9 @@ function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport
|
|
|
39
45
|
export async function preparePrintExport(
|
|
40
46
|
modelExport: ModelExport,
|
|
41
47
|
onlyVisible: boolean,
|
|
48
|
+
format: PrintModelExportFormat,
|
|
42
49
|
): Promise<PreparedPrintExport> {
|
|
43
|
-
const artifact = await modelExport(
|
|
50
|
+
const artifact = await modelExport(format, {
|
|
44
51
|
onlyVisible,
|
|
45
52
|
download: false,
|
|
46
53
|
printScale: 100,
|
|
@@ -68,38 +75,50 @@ export async function preparePrintExport(
|
|
|
68
75
|
|
|
69
76
|
export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) {
|
|
70
77
|
const modelExport = useEditor((state) => state.modelExport)
|
|
71
|
-
const [
|
|
78
|
+
const [exportingFormat, setExportingFormat] = useState<PrintModelExportFormat | null>(null)
|
|
72
79
|
const [error, setError] = useState<string | null>(null)
|
|
73
80
|
|
|
74
|
-
const handleExport = async () => {
|
|
81
|
+
const handleExport = async (format: PrintModelExportFormat) => {
|
|
75
82
|
if (!modelExport) return
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
setExportingFormat(format)
|
|
78
85
|
setError(null)
|
|
79
86
|
try {
|
|
80
|
-
const prepared = await preparePrintExport(modelExport, onlyVisible)
|
|
87
|
+
const prepared = await preparePrintExport(modelExport, onlyVisible, format)
|
|
81
88
|
downloadArtifact(prepared.artifact)
|
|
82
89
|
} catch (reason) {
|
|
83
90
|
setError(reason instanceof Error ? reason.message : '3D print export failed.')
|
|
84
91
|
} finally {
|
|
85
|
-
|
|
92
|
+
setExportingFormat(null)
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
const isExporting = exportingFormat !== null
|
|
97
|
+
|
|
89
98
|
return (
|
|
90
99
|
<>
|
|
91
100
|
<Button
|
|
92
|
-
aria-busy={
|
|
101
|
+
aria-busy={exportingFormat === 'print-3mf'}
|
|
102
|
+
className="w-full justify-start gap-2"
|
|
103
|
+
disabled={isExporting || !modelExport}
|
|
104
|
+
onClick={() => void handleExport('print-3mf')}
|
|
105
|
+
variant="outline"
|
|
106
|
+
>
|
|
107
|
+
<Printer className="size-4" />
|
|
108
|
+
Export 3D print 3MF
|
|
109
|
+
</Button>
|
|
110
|
+
<Button
|
|
111
|
+
aria-busy={exportingFormat === 'print-stl'}
|
|
93
112
|
className="w-full justify-start gap-2"
|
|
94
113
|
disabled={isExporting || !modelExport}
|
|
95
|
-
onClick={handleExport}
|
|
114
|
+
onClick={() => void handleExport('print-stl')}
|
|
96
115
|
variant="outline"
|
|
97
116
|
>
|
|
98
117
|
<Printer className="size-4" />
|
|
99
|
-
Export 3D print
|
|
118
|
+
Export 3D print STL
|
|
100
119
|
</Button>
|
|
101
120
|
{error && (
|
|
102
|
-
<div className="flex gap-2 text-destructive text-xs">
|
|
121
|
+
<div className="col-span-full flex gap-2 text-destructive text-xs">
|
|
103
122
|
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
|
|
104
123
|
<span>{error}</span>
|
|
105
124
|
</div>
|
|
@@ -26,8 +26,15 @@ export const BuildingTreeNode = memo(function BuildingTreeNode({
|
|
|
26
26
|
const createNode = useScene((state) => state.createNode)
|
|
27
27
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
28
28
|
const name = useScene((s) => s.nodes[nodeId]?.name)
|
|
29
|
+
// Levels and elevators first, units after, so the units read as one group.
|
|
29
30
|
const children = useScene(
|
|
30
|
-
useShallow((s) =>
|
|
31
|
+
useShallow((s) => {
|
|
32
|
+
const ids = (s.nodes[nodeId] as BuildingNode | undefined)?.children ?? []
|
|
33
|
+
return [
|
|
34
|
+
...ids.filter((id) => s.nodes[id]?.type !== 'unit'),
|
|
35
|
+
...ids.filter((id) => s.nodes[id]?.type === 'unit'),
|
|
36
|
+
]
|
|
37
|
+
}),
|
|
31
38
|
)
|
|
32
39
|
const isSelected = useViewer((state) => state.selection.buildingId === nodeId)
|
|
33
40
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
Copy,
|
|
20
20
|
Eye,
|
|
21
21
|
EyeOff,
|
|
22
|
+
Group,
|
|
22
23
|
Loader2,
|
|
23
24
|
MoreHorizontal,
|
|
24
25
|
Pencil,
|
|
@@ -50,15 +51,17 @@ import {
|
|
|
50
51
|
metersToLinearUnit,
|
|
51
52
|
squareMetersToAreaUnit,
|
|
52
53
|
} from './../../../../../lib/measurements'
|
|
53
|
-
import { createLocalGuideImage } from './../../../../../lib/local-guide-image'
|
|
54
|
+
import { createLocalGuideImage, createLocalScan } from './../../../../../lib/local-guide-image'
|
|
54
55
|
import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
|
|
56
|
+
import { createUnitInBuilding, toggleZoneMembership } from './../../../../../lib/units'
|
|
55
57
|
import { cn } from './../../../../../lib/utils'
|
|
56
58
|
import useEditor from './../../../../../store/use-editor'
|
|
57
59
|
import { useUploadStore } from '../../../../../store/use-upload'
|
|
58
60
|
import { MetricControl } from '../../../controls/metric-control'
|
|
59
61
|
import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
|
|
60
62
|
import { InlineRenameInput } from './inline-rename-input'
|
|
61
|
-
import {
|
|
63
|
+
import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
|
|
64
|
+
import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
62
65
|
import { TreeNodeDragProvider } from './tree-node-drag'
|
|
63
66
|
|
|
64
67
|
// ============================================================================
|
|
@@ -497,6 +500,7 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
497
500
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
498
501
|
const setSelection = useViewer((s) => s.setSelection)
|
|
499
502
|
const setShowGuides = useViewer((s) => s.setShowGuides)
|
|
503
|
+
const setShowScans = useViewer((s) => s.setShowScans)
|
|
500
504
|
const references = useScene(
|
|
501
505
|
useShallow((s) =>
|
|
502
506
|
Object.values(s.nodes).filter(
|
|
@@ -566,6 +570,23 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
566
570
|
return
|
|
567
571
|
}
|
|
568
572
|
|
|
573
|
+
if (!onUploadAsset) {
|
|
574
|
+
useUploadStore.getState().startUpload(levelId, 'scan', file.name)
|
|
575
|
+
useUploadStore.getState().setStatus(levelId, 'uploading')
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const { scan, url } = await createLocalScan({ createNode, file, levelId })
|
|
579
|
+
setShowScans(true)
|
|
580
|
+
setSelectedReferenceId(scan.id)
|
|
581
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
582
|
+
useUploadStore.getState().setResult(levelId, url)
|
|
583
|
+
window.setTimeout(() => useUploadStore.getState().clearUpload(levelId), 600)
|
|
584
|
+
} catch {
|
|
585
|
+
useUploadStore.getState().setError(levelId, 'Could not add that scan.')
|
|
586
|
+
}
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
569
590
|
if (!projectId) {
|
|
570
591
|
useUploadStore.getState().startUpload(levelId, 'scan', file.name)
|
|
571
592
|
useUploadStore.getState().setError(levelId, 'No active project. Please open a project first.')
|
|
@@ -573,7 +594,7 @@ const LevelReferences = memo(function LevelReferences({
|
|
|
573
594
|
}
|
|
574
595
|
|
|
575
596
|
clearUpload(levelId)
|
|
576
|
-
onUploadAsset
|
|
597
|
+
onUploadAsset(projectId, levelId, file, type)
|
|
577
598
|
}
|
|
578
599
|
|
|
579
600
|
const handleDelete = async (nodeId: string, e: React.MouseEvent) => {
|
|
@@ -949,7 +970,7 @@ const LevelItem = memo(function LevelItem({
|
|
|
949
970
|
precision={2}
|
|
950
971
|
step={0.05}
|
|
951
972
|
unit="m"
|
|
952
|
-
value={
|
|
973
|
+
value={(level.baseElevation ?? 0)}
|
|
953
974
|
/>
|
|
954
975
|
</div>
|
|
955
976
|
<LevelReferences
|
|
@@ -1060,6 +1081,74 @@ const LevelsSection = memo(function LevelsSection({
|
|
|
1060
1081
|
)
|
|
1061
1082
|
})
|
|
1062
1083
|
|
|
1084
|
+
const UnitsSection = memo(function UnitsSection({
|
|
1085
|
+
buildingId,
|
|
1086
|
+
}: {
|
|
1087
|
+
buildingId: BuildingNode['id']
|
|
1088
|
+
}) {
|
|
1089
|
+
const unitIds = useScene(
|
|
1090
|
+
useShallow((s) => {
|
|
1091
|
+
const building = s.nodes[buildingId] as BuildingNode | undefined
|
|
1092
|
+
return (building?.children ?? []).filter((id) => s.nodes[id]?.type === 'unit')
|
|
1093
|
+
}),
|
|
1094
|
+
)
|
|
1095
|
+
const hasUnits = unitIds.length > 0
|
|
1096
|
+
const [expanded, setExpanded] = useState(hasUnits)
|
|
1097
|
+
|
|
1098
|
+
useEffect(() => {
|
|
1099
|
+
if (hasUnits) setExpanded(true)
|
|
1100
|
+
}, [hasUnits])
|
|
1101
|
+
|
|
1102
|
+
const handleNewUnit = (event: React.MouseEvent) => {
|
|
1103
|
+
event.stopPropagation()
|
|
1104
|
+
createUnitInBuilding(buildingId)
|
|
1105
|
+
setExpanded(true)
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return (
|
|
1109
|
+
<div className="subtle-scrollbar max-h-72 shrink-0 overflow-y-auto overflow-x-hidden">
|
|
1110
|
+
<TreeNodeWrapper
|
|
1111
|
+
actions={
|
|
1112
|
+
<button
|
|
1113
|
+
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10"
|
|
1114
|
+
onClick={handleNewUnit}
|
|
1115
|
+
title="New unit"
|
|
1116
|
+
type="button"
|
|
1117
|
+
>
|
|
1118
|
+
<Plus className="h-3 w-3" />
|
|
1119
|
+
</button>
|
|
1120
|
+
}
|
|
1121
|
+
depth={1}
|
|
1122
|
+
expanded={expanded}
|
|
1123
|
+
hasChildren
|
|
1124
|
+
icon={<Group className="h-3.5 w-3.5" />}
|
|
1125
|
+
label={
|
|
1126
|
+
<span className="flex items-center gap-1.5">
|
|
1127
|
+
Units
|
|
1128
|
+
{hasUnits && <span className="text-muted-foreground text-xs">{unitIds.length}</span>}
|
|
1129
|
+
</span>
|
|
1130
|
+
}
|
|
1131
|
+
onClick={() => setExpanded((value) => !value)}
|
|
1132
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
1133
|
+
>
|
|
1134
|
+
{unitIds.map((unitId) => (
|
|
1135
|
+
<TreeNode depth={2} key={unitId} nodeId={unitId} />
|
|
1136
|
+
))}
|
|
1137
|
+
<TreeNodeWrapper
|
|
1138
|
+
depth={2}
|
|
1139
|
+
expanded={false}
|
|
1140
|
+
hasChildren={false}
|
|
1141
|
+
icon={<Plus className="h-3.5 w-3.5" />}
|
|
1142
|
+
isLast
|
|
1143
|
+
label="New unit"
|
|
1144
|
+
onClick={handleNewUnit}
|
|
1145
|
+
onToggle={() => {}}
|
|
1146
|
+
/>
|
|
1147
|
+
</TreeNodeWrapper>
|
|
1148
|
+
</div>
|
|
1149
|
+
)
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1063
1152
|
const LayerToggle = memo(function LayerToggle() {
|
|
1064
1153
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
1065
1154
|
const setStructureLayer = useEditor((state) => state.setStructureLayer)
|
|
@@ -1195,6 +1284,11 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1195
1284
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
1196
1285
|
const deleteNode = useScene((state) => state.deleteNode)
|
|
1197
1286
|
const updateNode = useScene((state) => state.updateNode)
|
|
1287
|
+
const focusedUnitId = useViewer((state) => state.focusedUnitId)
|
|
1288
|
+
const focusedUnit = useScene((s) => {
|
|
1289
|
+
const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
|
|
1290
|
+
return unit?.type === 'unit' ? unit : null
|
|
1291
|
+
})
|
|
1198
1292
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
1199
1293
|
const hoveredId = useViewer((state) => state.hoveredId)
|
|
1200
1294
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -1268,6 +1362,13 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
|
|
|
1268
1362
|
style={{ left: 8, width: 4 }}
|
|
1269
1363
|
/>
|
|
1270
1364
|
|
|
1365
|
+
{focusedUnit && (
|
|
1366
|
+
<ZoneMembershipCheckbox
|
|
1367
|
+
checked={focusedUnit.members.includes(zone.id)}
|
|
1368
|
+
onToggle={() => toggleZoneMembership(focusedUnit.id, zone.id)}
|
|
1369
|
+
unitName={focusedUnit.name || 'Unit'}
|
|
1370
|
+
/>
|
|
1371
|
+
)}
|
|
1271
1372
|
<span className={cn('mr-2', !isSelected && 'opacity-40')}>
|
|
1272
1373
|
<ColorDot color={zone.color} onChange={handleColorChange} />
|
|
1273
1374
|
</span>
|
|
@@ -1611,6 +1712,7 @@ const BuildingItem = memo(function BuildingItem({
|
|
|
1611
1712
|
onUploadAsset={onUploadAsset}
|
|
1612
1713
|
projectId={projectId}
|
|
1613
1714
|
/>
|
|
1715
|
+
<UnitsSection buildingId={building.id} />
|
|
1614
1716
|
<LayerToggle />
|
|
1615
1717
|
</div>
|
|
1616
1718
|
<div className="subtle-scrollbar relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
|
|
@@ -70,12 +70,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
70
70
|
const handleClick = useCallback(
|
|
71
71
|
(e: React.MouseEvent) => {
|
|
72
72
|
e.stopPropagation()
|
|
73
|
-
handleTreeSelection(
|
|
74
|
-
e,
|
|
75
|
-
nodeId,
|
|
76
|
-
useViewer.getState().selection.selectedIds,
|
|
77
|
-
setSelection,
|
|
78
|
-
)
|
|
73
|
+
handleTreeSelection(e, nodeId, useViewer.getState().selection.selectedIds, setSelection)
|
|
79
74
|
routeTreeSelectionToNode(node)
|
|
80
75
|
},
|
|
81
76
|
[node, nodeId, setSelection],
|
|
@@ -134,7 +129,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
|
|
|
134
129
|
depth={depth + 1}
|
|
135
130
|
isLast={index === children.length - 1}
|
|
136
131
|
key={childId}
|
|
137
|
-
nodeId={childId}
|
|
132
|
+
nodeId={childId as AnyNodeId}
|
|
138
133
|
/>
|
|
139
134
|
))}
|
|
140
135
|
</TreeNodeWrapper>
|
|
@@ -24,7 +24,7 @@ interface ShelfTreeNodeProps {
|
|
|
24
24
|
/**
|
|
25
25
|
* Sidebar tree entry for shelf. Mirrors `item-tree-node`'s shape so the
|
|
26
26
|
* shelf's hosted items list as collapsible children — same pattern items
|
|
27
|
-
* use for their nested items. The shelf has its own `children:
|
|
27
|
+
* use for their nested items. The shelf has its own `children: string[]`
|
|
28
28
|
* field on the schema; items reparent into it via `def.surfaces` + the
|
|
29
29
|
* placement coordinator's shelf strategy.
|
|
30
30
|
*/
|
|
@@ -69,12 +69,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
69
69
|
const handleClick = useCallback(
|
|
70
70
|
(e: React.MouseEvent) => {
|
|
71
71
|
e.stopPropagation()
|
|
72
|
-
handleTreeSelection(
|
|
73
|
-
e,
|
|
74
|
-
nodeId,
|
|
75
|
-
useViewer.getState().selection.selectedIds,
|
|
76
|
-
setSelection,
|
|
77
|
-
)
|
|
72
|
+
handleTreeSelection(e, nodeId, useViewer.getState().selection.selectedIds, setSelection)
|
|
78
73
|
routeTreeSelectionToNode(node)
|
|
79
74
|
},
|
|
80
75
|
[node, nodeId, setSelection],
|
|
@@ -124,7 +119,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
|
|
|
124
119
|
depth={depth + 1}
|
|
125
120
|
isLast={index === children.length - 1}
|
|
126
121
|
key={childId}
|
|
127
|
-
nodeId={childId}
|
|
122
|
+
nodeId={childId as AnyNodeId}
|
|
128
123
|
/>
|
|
129
124
|
))}
|
|
130
125
|
</TreeNodeWrapper>
|
|
@@ -107,6 +107,7 @@ import { SlabTreeNode } from './slab-tree-node'
|
|
|
107
107
|
import { SolarPanelTreeNode } from './solar-panel-tree-node'
|
|
108
108
|
import { SpawnTreeNode } from './spawn-tree-node'
|
|
109
109
|
import { StairTreeNode } from './stair-tree-node'
|
|
110
|
+
import { UnitTreeNode } from './unit-tree-node'
|
|
110
111
|
import { WallTreeNode } from './wall-tree-node'
|
|
111
112
|
import { WindowTreeNode } from './window-tree-node'
|
|
112
113
|
import { ZoneTreeNode } from './zone-tree-node'
|
|
@@ -175,6 +176,7 @@ const treeNodeByType: Record<string, TreeNodeComponent> = {
|
|
|
175
176
|
roof: RoofTreeNode,
|
|
176
177
|
scan: RegistryTreeNode,
|
|
177
178
|
stair: StairTreeNode,
|
|
179
|
+
unit: UnitTreeNode,
|
|
178
180
|
door: DoorTreeNode,
|
|
179
181
|
window: WindowTreeNode,
|
|
180
182
|
zone: ZoneTreeNode as React.ComponentType<{
|
|
@@ -207,6 +209,8 @@ export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: Tr
|
|
|
207
209
|
interface TreeNodeWrapperProps {
|
|
208
210
|
nodeId?: string
|
|
209
211
|
icon: React.ReactNode
|
|
212
|
+
/** Keep the icon's own color when unselected (color dots are the identity). */
|
|
213
|
+
keepIconColor?: boolean
|
|
210
214
|
label: React.ReactNode
|
|
211
215
|
depth: number
|
|
212
216
|
hasChildren: boolean
|
|
@@ -232,6 +236,7 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
232
236
|
{
|
|
233
237
|
nodeId,
|
|
234
238
|
icon,
|
|
239
|
+
keepIconColor,
|
|
235
240
|
label,
|
|
236
241
|
depth,
|
|
237
242
|
hasChildren,
|
|
@@ -326,7 +331,7 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
|
|
|
326
331
|
<span
|
|
327
332
|
className={cn(
|
|
328
333
|
'flex h-5 w-5 shrink-0 items-center justify-center transition-all duration-200',
|
|
329
|
-
!isSelected && 'opacity-60 grayscale',
|
|
334
|
+
!isSelected && !keepIconColor && 'opacity-60 grayscale',
|
|
330
335
|
)}
|
|
331
336
|
>
|
|
332
337
|
{icon}
|