@pascal-app/editor 1.0.0-beta.5 → 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.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- 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 +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- 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 +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- 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/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- 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/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- 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/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Keyboard } from 'lucide-react'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
3
2
|
import { Button } from './../../../../../components/ui/primitives/button'
|
|
4
3
|
import {
|
|
5
4
|
Dialog,
|
|
@@ -9,7 +8,10 @@ import {
|
|
|
9
8
|
DialogTitle,
|
|
10
9
|
DialogTrigger,
|
|
11
10
|
} from './../../../../../components/ui/primitives/dialog'
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ShortcutToken,
|
|
13
|
+
shortcutDisplayValue,
|
|
14
|
+
} from './../../../../../components/ui/primitives/shortcut-token'
|
|
13
15
|
|
|
14
16
|
type Shortcut = {
|
|
15
17
|
keys: string[]
|
|
@@ -22,14 +24,6 @@ type ShortcutCategory = {
|
|
|
22
24
|
shortcuts: Shortcut[]
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const KEY_DISPLAY_MAP: Record<string, string> = {
|
|
26
|
-
'Arrow Up': '↑',
|
|
27
|
-
'Arrow Down': '↓',
|
|
28
|
-
Esc: '⎋',
|
|
29
|
-
Shift: '⇧',
|
|
30
|
-
Space: '␣',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
34
28
|
{
|
|
35
29
|
title: 'Editor Navigation',
|
|
@@ -60,10 +54,12 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
60
54
|
{
|
|
61
55
|
keys: ['Esc'],
|
|
62
56
|
action: 'Cancel the active tool and return to Select mode',
|
|
57
|
+
note: 'Mid-draw it cancels only the chain in progress and keeps the tool armed; press it again to leave the tool.',
|
|
63
58
|
},
|
|
64
59
|
{ keys: ['Delete / Backspace'], action: 'Delete selected objects' },
|
|
65
60
|
{ keys: ['Cmd/Ctrl', 'Z'], action: 'Undo' },
|
|
66
61
|
{ keys: ['Cmd/Ctrl', 'Shift', 'Z'], action: 'Redo' },
|
|
62
|
+
{ keys: ['Cmd/Ctrl', 'S'], action: 'Save' },
|
|
67
63
|
],
|
|
68
64
|
},
|
|
69
65
|
{
|
|
@@ -147,6 +143,19 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
147
143
|
{
|
|
148
144
|
title: 'Drawing Tools',
|
|
149
145
|
shortcuts: [
|
|
146
|
+
// Shift and Ctrl each mean one thing held and another tapped, and only
|
|
147
|
+
// the hold was documented — which read as the taps not existing. Both
|
|
148
|
+
// taps are listed first because they are the ones nobody discovers.
|
|
149
|
+
{
|
|
150
|
+
keys: ['Shift'],
|
|
151
|
+
action: 'Cycle the snapping mode',
|
|
152
|
+
note: 'Tap and release without pressing anything else, while a drawing or move gesture is available.',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
keys: ['Cmd/Ctrl'],
|
|
156
|
+
action: 'Cycle the grid step: 0.5 m → 0.25 m → 0.1 m → 0.05 m',
|
|
157
|
+
note: 'Tap and release on its own. Use it when the default half-metre grid is too coarse — placing a window, for instance.',
|
|
158
|
+
},
|
|
150
159
|
{
|
|
151
160
|
keys: ['Shift'],
|
|
152
161
|
action: 'Bypass guided snapping and angle constraints',
|
|
@@ -199,25 +208,13 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
|
|
|
199
208
|
},
|
|
200
209
|
]
|
|
201
210
|
|
|
202
|
-
function getDisplayKey(key: string, isMac: boolean): string {
|
|
203
|
-
if (key === 'Cmd/Ctrl') return isMac ? '⌘' : 'Ctrl'
|
|
204
|
-
if (key === 'Delete / Backspace') return isMac ? '⌫' : 'Backspace'
|
|
205
|
-
return KEY_DISPLAY_MAP[key] ?? key
|
|
206
|
-
}
|
|
207
|
-
|
|
208
211
|
function ShortcutKeys({ keys }: { keys: string[] }) {
|
|
209
|
-
const [isMac, setIsMac] = useState(true)
|
|
210
|
-
|
|
211
|
-
useEffect(() => {
|
|
212
|
-
setIsMac(navigator.platform.toUpperCase().indexOf('MAC') >= 0)
|
|
213
|
-
}, [])
|
|
214
|
-
|
|
215
212
|
return (
|
|
216
213
|
<div className="flex flex-wrap items-center gap-1">
|
|
217
214
|
{keys.map((key, index) => (
|
|
218
215
|
<div className="flex items-center gap-1" key={`${key}-${index}`}>
|
|
219
216
|
{index > 0 ? <span className="text-[10px] text-muted-foreground">+</span> : null}
|
|
220
|
-
<ShortcutToken displayValue={
|
|
217
|
+
<ShortcutToken displayValue={shortcutDisplayValue(key)} value={key} />
|
|
221
218
|
</div>
|
|
222
219
|
))}
|
|
223
220
|
</div>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { PrintLevelBundleReport } from '../../../../../lib/level-print-export'
|
|
3
|
+
import type { ModelExport, ModelExportOptions } from '../../../../../lib/model-export'
|
|
4
|
+
import type { PrintExportReport } from '../../../../../lib/print-export'
|
|
5
|
+
import { preparePrintExport } from './print-export-button'
|
|
6
|
+
|
|
7
|
+
const report: PrintExportReport = {
|
|
8
|
+
kind: 'print-export-report',
|
|
9
|
+
version: 2,
|
|
10
|
+
format: '3mf',
|
|
11
|
+
scale: 100,
|
|
12
|
+
units: 'millimeter',
|
|
13
|
+
orientation: 'z-up',
|
|
14
|
+
status: 'pass',
|
|
15
|
+
bounds: {
|
|
16
|
+
min: { x: -25, y: -15, z: 0 },
|
|
17
|
+
max: { x: 25, y: 15, z: 20 },
|
|
18
|
+
width: 50,
|
|
19
|
+
depth: 30,
|
|
20
|
+
height: 20,
|
|
21
|
+
},
|
|
22
|
+
triangleCount: 12,
|
|
23
|
+
invalidTriangleCount: 0,
|
|
24
|
+
degenerateTriangleCount: 0,
|
|
25
|
+
boundaryEdgeCount: 0,
|
|
26
|
+
nonManifoldEdgeCount: 0,
|
|
27
|
+
connectedComponentCount: 1,
|
|
28
|
+
solidComponentCount: 1,
|
|
29
|
+
invertedWinding: false,
|
|
30
|
+
volumeMm3: 30_000,
|
|
31
|
+
diagnostics: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('simple 3D print export', () => {
|
|
35
|
+
test('uses one fixed safe print profile', async () => {
|
|
36
|
+
const calls: { format?: string; options?: ModelExportOptions }[] = []
|
|
37
|
+
const artifact = { blob: new Blob(['3mf']), filename: 'house.3mf', metadata: report }
|
|
38
|
+
const modelExport: ModelExport = async (format, options) => {
|
|
39
|
+
calls.push({ format, options })
|
|
40
|
+
return artifact
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const prepared = await preparePrintExport(modelExport, true, 'print-3mf')
|
|
44
|
+
|
|
45
|
+
expect(calls).toEqual([
|
|
46
|
+
{
|
|
47
|
+
format: 'print-3mf',
|
|
48
|
+
options: {
|
|
49
|
+
onlyVisible: true,
|
|
50
|
+
download: false,
|
|
51
|
+
printScale: 100,
|
|
52
|
+
printScope: 'levels',
|
|
53
|
+
printContent: 'structure',
|
|
54
|
+
printBase: 'none',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
])
|
|
58
|
+
expect(prepared).toEqual({ artifact, report })
|
|
59
|
+
})
|
|
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
|
+
|
|
88
|
+
test('blocks the download when preflight finds invalid geometry', async () => {
|
|
89
|
+
const blockedReport: PrintExportReport = {
|
|
90
|
+
...report,
|
|
91
|
+
status: 'blocked',
|
|
92
|
+
diagnostics: [
|
|
93
|
+
{
|
|
94
|
+
severity: 'error',
|
|
95
|
+
code: 'open_boundary',
|
|
96
|
+
message: 'One wall has an open edge.',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
}
|
|
100
|
+
const modelExport: ModelExport = async () => ({
|
|
101
|
+
blob: new Blob(['3mf']),
|
|
102
|
+
filename: 'house.3mf',
|
|
103
|
+
metadata: blockedReport,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await expect(preparePrintExport(modelExport, true, 'print-3mf')).rejects.toThrow(
|
|
107
|
+
'One wall has an open edge.',
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('shows a per-level preflight error when the bundle has no top-level error', async () => {
|
|
112
|
+
const blockedPartReport: PrintExportReport = {
|
|
113
|
+
...report,
|
|
114
|
+
status: 'blocked',
|
|
115
|
+
diagnostics: [
|
|
116
|
+
{
|
|
117
|
+
severity: 'error',
|
|
118
|
+
code: 'open_boundary',
|
|
119
|
+
message: 'The upper level has an open edge.',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
const blockedBundleReport: PrintLevelBundleReport = {
|
|
124
|
+
kind: 'print-level-export-report',
|
|
125
|
+
version: 2,
|
|
126
|
+
format: '3mf',
|
|
127
|
+
scale: 100,
|
|
128
|
+
units: 'millimeter',
|
|
129
|
+
orientation: 'z-up',
|
|
130
|
+
status: 'blocked',
|
|
131
|
+
partCount: 1,
|
|
132
|
+
parts: [
|
|
133
|
+
{
|
|
134
|
+
kind: 'level',
|
|
135
|
+
levelId: 'upper-level',
|
|
136
|
+
label: 'Upper level',
|
|
137
|
+
objectName: 'Upper level',
|
|
138
|
+
filename: null,
|
|
139
|
+
sourceBaseMeters: 3,
|
|
140
|
+
report: blockedPartReport,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
excludedNodeIds: [],
|
|
144
|
+
diagnostics: [],
|
|
145
|
+
}
|
|
146
|
+
const modelExport: ModelExport = async () => ({
|
|
147
|
+
blob: new Blob(['3mf']),
|
|
148
|
+
filename: 'house.zip',
|
|
149
|
+
metadata: blockedBundleReport,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await expect(preparePrintExport(modelExport, true, 'print-3mf')).rejects.toThrow(
|
|
153
|
+
'The upper level has an open edge.',
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('rejects an exporter response without print metadata', async () => {
|
|
158
|
+
const modelExport: ModelExport = async () => ({
|
|
159
|
+
blob: new Blob(['3mf']),
|
|
160
|
+
filename: 'house.3mf',
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
await expect(preparePrintExport(modelExport, false, 'print-3mf')).rejects.toThrow(
|
|
164
|
+
'did not return a valid file',
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AlertTriangle, Printer } from 'lucide-react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { Button } from '../../../../../components/ui/primitives/button'
|
|
4
|
+
import {
|
|
5
|
+
isPrintLevelBundleReport,
|
|
6
|
+
type PrintLevelBundleReport,
|
|
7
|
+
} from '../../../../../lib/level-print-export'
|
|
8
|
+
import type {
|
|
9
|
+
ModelExport,
|
|
10
|
+
ModelExportArtifact,
|
|
11
|
+
ModelExportFormat,
|
|
12
|
+
} from '../../../../../lib/model-export'
|
|
13
|
+
import {
|
|
14
|
+
isPrintExportReport,
|
|
15
|
+
type PrintExportReport,
|
|
16
|
+
} from '../../../../../lib/print-export'
|
|
17
|
+
import useEditor from '../../../../../store/use-editor'
|
|
18
|
+
|
|
19
|
+
type PreparedPrintExport = {
|
|
20
|
+
artifact: ModelExportArtifact
|
|
21
|
+
report: PrintExportReport | PrintLevelBundleReport
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PrintModelExportFormat = Extract<ModelExportFormat, 'print-3mf' | 'print-stl'>
|
|
25
|
+
|
|
26
|
+
function downloadArtifact(artifact: ModelExportArtifact) {
|
|
27
|
+
const url = URL.createObjectURL(artifact.blob)
|
|
28
|
+
const link = document.createElement('a')
|
|
29
|
+
link.href = url
|
|
30
|
+
link.download = artifact.filename
|
|
31
|
+
link.click()
|
|
32
|
+
URL.revokeObjectURL(url)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function firstBlockingMessage(report: PrintExportReport | PrintLevelBundleReport) {
|
|
36
|
+
const bundleDiagnostic = report.diagnostics.find((item) => item.severity === 'error')
|
|
37
|
+
if (bundleDiagnostic || !isPrintLevelBundleReport(report)) return bundleDiagnostic?.message
|
|
38
|
+
|
|
39
|
+
for (const part of report.parts) {
|
|
40
|
+
const partDiagnostic = part.report.diagnostics.find((item) => item.severity === 'error')
|
|
41
|
+
if (partDiagnostic) return partDiagnostic.message
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function preparePrintExport(
|
|
46
|
+
modelExport: ModelExport,
|
|
47
|
+
onlyVisible: boolean,
|
|
48
|
+
format: PrintModelExportFormat,
|
|
49
|
+
): Promise<PreparedPrintExport> {
|
|
50
|
+
const artifact = await modelExport(format, {
|
|
51
|
+
onlyVisible,
|
|
52
|
+
download: false,
|
|
53
|
+
printScale: 100,
|
|
54
|
+
printScope: 'levels',
|
|
55
|
+
printContent: 'structure',
|
|
56
|
+
printBase: 'none',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
!artifact ||
|
|
61
|
+
(!isPrintExportReport(artifact.metadata) && !isPrintLevelBundleReport(artifact.metadata))
|
|
62
|
+
) {
|
|
63
|
+
throw new Error('The 3D print exporter did not return a valid file.')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (artifact.metadata.status === 'blocked') {
|
|
67
|
+
throw new Error(
|
|
68
|
+
firstBlockingMessage(artifact.metadata) ??
|
|
69
|
+
'This project cannot be exported as printable parts.',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { artifact, report: artifact.metadata }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function PrintExportButton({ onlyVisible }: { onlyVisible: boolean }) {
|
|
77
|
+
const modelExport = useEditor((state) => state.modelExport)
|
|
78
|
+
const [exportingFormat, setExportingFormat] = useState<PrintModelExportFormat | null>(null)
|
|
79
|
+
const [error, setError] = useState<string | null>(null)
|
|
80
|
+
|
|
81
|
+
const handleExport = async (format: PrintModelExportFormat) => {
|
|
82
|
+
if (!modelExport) return
|
|
83
|
+
|
|
84
|
+
setExportingFormat(format)
|
|
85
|
+
setError(null)
|
|
86
|
+
try {
|
|
87
|
+
const prepared = await preparePrintExport(modelExport, onlyVisible, format)
|
|
88
|
+
downloadArtifact(prepared.artifact)
|
|
89
|
+
} catch (reason) {
|
|
90
|
+
setError(reason instanceof Error ? reason.message : '3D print export failed.')
|
|
91
|
+
} finally {
|
|
92
|
+
setExportingFormat(null)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isExporting = exportingFormat !== null
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
<Button
|
|
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'}
|
|
112
|
+
className="w-full justify-start gap-2"
|
|
113
|
+
disabled={isExporting || !modelExport}
|
|
114
|
+
onClick={() => void handleExport('print-stl')}
|
|
115
|
+
variant="outline"
|
|
116
|
+
>
|
|
117
|
+
<Printer className="size-4" />
|
|
118
|
+
Export 3D print STL
|
|
119
|
+
</Button>
|
|
120
|
+
{error && (
|
|
121
|
+
<div className="flex gap-2 text-destructive text-xs">
|
|
122
|
+
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
|
|
123
|
+
<span>{error}</span>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -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)
|
|
@@ -2,10 +2,11 @@ import { type AnyNodeId, type DormerNode, useScene } from '@pascal-app/core'
|
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import Image from 'next/image'
|
|
4
4
|
import { memo, useCallback, useState } from 'react'
|
|
5
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
5
6
|
import { SnapTargetIcon } from '../../../snap-target-badge'
|
|
6
7
|
import useEditor from './../../../../../store/use-editor'
|
|
7
8
|
import { InlineRenameInput } from './inline-rename-input'
|
|
8
|
-
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
|
9
|
+
import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
|
|
9
10
|
import { TreeNodeActions } from './tree-node-actions'
|
|
10
11
|
|
|
11
12
|
interface DormerTreeNodeProps {
|
|
@@ -26,8 +27,12 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
26
27
|
isLast,
|
|
27
28
|
}: DormerTreeNodeProps) {
|
|
28
29
|
const [isEditing, setIsEditing] = useState(false)
|
|
30
|
+
const [expanded, setExpanded] = useState(true)
|
|
29
31
|
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
|
30
32
|
const node = useScene((s) => s.nodes[nodeId] as DormerNode | undefined)
|
|
33
|
+
const children = useScene(
|
|
34
|
+
useShallow((s) => (s.nodes[nodeId] as DormerNode | undefined)?.children ?? []),
|
|
35
|
+
)
|
|
31
36
|
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
|
32
37
|
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
|
33
38
|
const setSelection = useViewer((state) => state.setSelection)
|
|
@@ -55,8 +60,8 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
55
60
|
<TreeNodeWrapper
|
|
56
61
|
actions={<TreeNodeActions nodeId={nodeId} />}
|
|
57
62
|
depth={depth}
|
|
58
|
-
expanded={
|
|
59
|
-
hasChildren={
|
|
63
|
+
expanded={expanded}
|
|
64
|
+
hasChildren={children.length > 0}
|
|
60
65
|
icon={
|
|
61
66
|
<SnapTargetIcon target="roof">
|
|
62
67
|
<Image
|
|
@@ -86,7 +91,16 @@ export const DormerTreeNode = memo(function DormerTreeNode({
|
|
|
86
91
|
onDoubleClick={() => focusTreeNode(nodeId)}
|
|
87
92
|
onMouseEnter={() => setHoveredId(nodeId)}
|
|
88
93
|
onMouseLeave={() => setHoveredId(null)}
|
|
89
|
-
onToggle={() =>
|
|
90
|
-
|
|
94
|
+
onToggle={() => setExpanded((value) => !value)}
|
|
95
|
+
>
|
|
96
|
+
{children.map((childId, index) => (
|
|
97
|
+
<TreeNode
|
|
98
|
+
depth={depth + 1}
|
|
99
|
+
isLast={index === children.length - 1}
|
|
100
|
+
key={childId}
|
|
101
|
+
nodeId={childId as AnyNodeId}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</TreeNodeWrapper>
|
|
91
105
|
)
|
|
92
106
|
})
|