@pascal-app/editor 0.9.1 → 0.9.2
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 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -2,28 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
4
|
import {
|
|
5
|
+
getCatalogMaterialById,
|
|
6
|
+
getLibraryMaterialIdFromRef,
|
|
7
|
+
getSceneMaterialIdFromRef,
|
|
5
8
|
initSpaceDetectionSync,
|
|
6
9
|
initSpatialGridSync,
|
|
7
10
|
spatialGridManager,
|
|
8
11
|
useScene,
|
|
9
12
|
} from '@pascal-app/core'
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type HoverStyles,
|
|
15
|
+
InteractiveSystem,
|
|
16
|
+
SceneEnvironment,
|
|
17
|
+
useViewer,
|
|
18
|
+
Viewer,
|
|
19
|
+
} from '@pascal-app/viewer'
|
|
11
20
|
import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
|
12
21
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
13
22
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
14
23
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
|
15
24
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
|
25
|
+
import { type ActivePaintMaterial, hasActivePaintMaterial } from '../../lib/material-paint'
|
|
16
26
|
import {
|
|
17
27
|
applySceneGraphToEditor,
|
|
18
28
|
loadSceneFromLocalStorage,
|
|
19
29
|
type SceneGraph,
|
|
20
30
|
writePersistedSelection,
|
|
21
31
|
} from '../../lib/scene'
|
|
22
|
-
import { initSFXBus } from '../../lib/sfx-bus'
|
|
32
|
+
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
23
33
|
import useEditor from '../../store/use-editor'
|
|
24
34
|
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
25
35
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
26
36
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
37
|
+
import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
|
|
27
38
|
import { StairEditSystem } from '../systems/stair/stair-edit-system'
|
|
28
39
|
import { ZoneLabelEditorSystem } from '../systems/zone/zone-label-editor-system'
|
|
29
40
|
import { ZoneSystem } from '../systems/zone/zone-system'
|
|
@@ -44,17 +55,22 @@ import type { ExtraPanel } from '../ui/sidebar/icon-rail'
|
|
|
44
55
|
import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/settings-panel'
|
|
45
56
|
import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
|
|
46
57
|
import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
58
|
+
import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
|
|
47
59
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
48
60
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
49
61
|
import { ExportManager } from './export-manager'
|
|
62
|
+
import { FenceTangentLines3D } from './fence-tangent-lines-3d'
|
|
50
63
|
import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
|
|
51
64
|
import { FloatingActionMenu } from './floating-action-menu'
|
|
52
65
|
import { FloatingBuildingActionMenu } from './floating-building-action-menu'
|
|
53
66
|
import { FloorplanPanel } from './floorplan-panel'
|
|
54
67
|
import { Grid } from './grid'
|
|
55
|
-
import {
|
|
68
|
+
import { GroupFloatingActionMenu } from './group-floating-action-menu'
|
|
56
69
|
import { GroupRotateHandle } from './group-rotate-handle'
|
|
70
|
+
import { GroupSelectionBox3D } from './group-selection-box-3d'
|
|
57
71
|
import { NodeArrowHandles } from './node-arrow-handles'
|
|
72
|
+
import { QuickMeasurementHud } from './quick-measurement-hud'
|
|
73
|
+
import { RiserDiagramPanel } from './riser-diagram-panel'
|
|
58
74
|
import { SelectionManager } from './selection-manager'
|
|
59
75
|
import { SiteEdgeLabels } from './site-edge-labels'
|
|
60
76
|
import { SlabHoleHighlights } from './slab-hole-highlights'
|
|
@@ -72,6 +88,8 @@ const PAINT_CURSOR_BADGE_COLOR = '#818cf8'
|
|
|
72
88
|
const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
73
89
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
74
90
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
91
|
+
const SCENE_READY_FALLBACK_MS = 8000
|
|
92
|
+
type PaintCursorBadgeState = 'empty' | 'ready' | 'blocked'
|
|
75
93
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
76
94
|
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
77
95
|
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
@@ -102,6 +120,7 @@ function initializeEditorRuntime(): () => void {
|
|
|
102
120
|
unsubscribeSpaceDetection?.()
|
|
103
121
|
|
|
104
122
|
spatialGridManager.clear()
|
|
123
|
+
disposeSFXBus()
|
|
105
124
|
|
|
106
125
|
const outliner = useViewer.getState().outliner
|
|
107
126
|
outliner.selectedObjects.length = 0
|
|
@@ -121,6 +140,12 @@ export interface EditorProps {
|
|
|
121
140
|
sidebarTabs?: (SidebarTab & { component: React.ComponentType })[]
|
|
122
141
|
viewerToolbarLeft?: ReactNode
|
|
123
142
|
viewerToolbarRight?: ReactNode
|
|
143
|
+
/**
|
|
144
|
+
* Full-bleed surface swapped in over the 3D canvas (v2) — e.g. the studio
|
|
145
|
+
* gallery. The canvas stays mounted underneath (no WebGL re-init) and the
|
|
146
|
+
* viewer toolbar stays on top so the host's stage switch remains reachable.
|
|
147
|
+
*/
|
|
148
|
+
stageOverlay?: ReactNode
|
|
124
149
|
/**
|
|
125
150
|
* Docked below the node inspector (v2). Hosts mount the "save as preset"
|
|
126
151
|
* affordance here so it reads as part of the inspector surface and shows
|
|
@@ -143,6 +168,10 @@ export interface EditorProps {
|
|
|
143
168
|
// Loading indicator (e.g. project fetching in community mode)
|
|
144
169
|
isLoading?: boolean
|
|
145
170
|
|
|
171
|
+
// Fires when the full-screen scene loader shows/hides — lets hosts measure
|
|
172
|
+
// open-to-interactive time without reaching into internal loader state.
|
|
173
|
+
onLoaderChange?: (visible: boolean) => void
|
|
174
|
+
|
|
146
175
|
// Thumbnail
|
|
147
176
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
148
177
|
|
|
@@ -526,14 +555,70 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } })
|
|
|
526
555
|
)
|
|
527
556
|
}
|
|
528
557
|
|
|
558
|
+
function getActivePaintMaterialSwatchColor(
|
|
559
|
+
material: ActivePaintMaterial | null,
|
|
560
|
+
sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
|
|
561
|
+
) {
|
|
562
|
+
const directColor = material?.material?.properties?.color
|
|
563
|
+
if (directColor) return directColor
|
|
564
|
+
|
|
565
|
+
const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
|
|
566
|
+
if (sceneMaterialId) {
|
|
567
|
+
const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
|
|
568
|
+
const sceneColor = sceneMaterial?.material.properties?.color
|
|
569
|
+
if (sceneColor) return sceneColor
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const catalogId =
|
|
573
|
+
getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
|
|
574
|
+
const catalogMaterial = getCatalogMaterialById(catalogId)
|
|
575
|
+
return (
|
|
576
|
+
catalogMaterial?.previewColor ??
|
|
577
|
+
catalogMaterial?.preset.mapProperties.color ??
|
|
578
|
+
PAINT_CURSOR_BADGE_COLOR
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function getActivePaintMaterialSwatchImageUrl(
|
|
583
|
+
material: ActivePaintMaterial | null,
|
|
584
|
+
sceneMaterials: ReturnType<typeof useScene.getState>['materials'],
|
|
585
|
+
) {
|
|
586
|
+
const directTextureUrl = material?.material?.texture?.url
|
|
587
|
+
if (directTextureUrl) return directTextureUrl
|
|
588
|
+
|
|
589
|
+
const sceneMaterialId = getSceneMaterialIdFromRef(material?.materialPreset)
|
|
590
|
+
if (sceneMaterialId) {
|
|
591
|
+
const sceneMaterial = sceneMaterials[sceneMaterialId as keyof typeof sceneMaterials]
|
|
592
|
+
const sceneTextureUrl = sceneMaterial?.material.texture?.url
|
|
593
|
+
if (sceneTextureUrl) return sceneTextureUrl
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const catalogId =
|
|
597
|
+
getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
|
|
598
|
+
const catalogMaterial = getCatalogMaterialById(catalogId)
|
|
599
|
+
return catalogMaterial?.previewThumbnailUrl ?? catalogMaterial?.preset.maps.albedoMap
|
|
600
|
+
}
|
|
601
|
+
|
|
529
602
|
function PaintCursorBadge({
|
|
530
603
|
position,
|
|
531
|
-
|
|
604
|
+
state,
|
|
605
|
+
swatchColor,
|
|
606
|
+
swatchImageUrl,
|
|
607
|
+
isEraser,
|
|
532
608
|
}: {
|
|
533
609
|
position: { x: number; y: number }
|
|
534
|
-
|
|
610
|
+
state: PaintCursorBadgeState
|
|
611
|
+
swatchColor: string
|
|
612
|
+
swatchImageUrl?: string
|
|
613
|
+
isEraser: boolean
|
|
535
614
|
}) {
|
|
536
|
-
const accentColor =
|
|
615
|
+
const accentColor =
|
|
616
|
+
state === 'ready'
|
|
617
|
+
? isEraser
|
|
618
|
+
? PAINT_CURSOR_BADGE_COLOR
|
|
619
|
+
: swatchColor
|
|
620
|
+
: PAINT_CURSOR_BADGE_DISABLED_COLOR
|
|
621
|
+
const iconOpacity = state === 'ready' ? 1 : state === 'blocked' ? 0.62 : 0.42
|
|
537
622
|
const lineHeight = 18
|
|
538
623
|
|
|
539
624
|
return (
|
|
@@ -566,8 +651,49 @@ function PaintCursorBadge({
|
|
|
566
651
|
alt=""
|
|
567
652
|
aria-hidden="true"
|
|
568
653
|
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]"
|
|
569
|
-
src="/icons/paint.
|
|
654
|
+
src="/icons/paint.webp"
|
|
655
|
+
style={{
|
|
656
|
+
filter: state === 'ready' ? undefined : 'grayscale(1)',
|
|
657
|
+
opacity: iconOpacity,
|
|
658
|
+
}}
|
|
570
659
|
/>
|
|
660
|
+
{state === 'ready' ? (
|
|
661
|
+
isEraser ? (
|
|
662
|
+
<span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/35 bg-zinc-950 text-white shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
|
|
663
|
+
<Icon
|
|
664
|
+
aria-hidden="true"
|
|
665
|
+
color="currentColor"
|
|
666
|
+
height={10}
|
|
667
|
+
icon="mdi:eraser-variant"
|
|
668
|
+
width={10}
|
|
669
|
+
/>
|
|
670
|
+
</span>
|
|
671
|
+
) : (
|
|
672
|
+
<span
|
|
673
|
+
className="-right-1 -bottom-1 absolute h-3.5 w-3.5 rounded-full border border-white/70 bg-cover bg-center shadow-[0_2px_6px_rgba(0,0,0,0.45)]"
|
|
674
|
+
style={{
|
|
675
|
+
backgroundColor: swatchColor,
|
|
676
|
+
backgroundImage: swatchImageUrl
|
|
677
|
+
? `url(${JSON.stringify(swatchImageUrl)})`
|
|
678
|
+
: undefined,
|
|
679
|
+
}}
|
|
680
|
+
/>
|
|
681
|
+
)
|
|
682
|
+
) : state === 'blocked' ? (
|
|
683
|
+
<span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/30 bg-zinc-950 text-rose-300 shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
|
|
684
|
+
<Icon
|
|
685
|
+
aria-hidden="true"
|
|
686
|
+
color="currentColor"
|
|
687
|
+
height={12}
|
|
688
|
+
icon="mdi:cancel"
|
|
689
|
+
width={12}
|
|
690
|
+
/>
|
|
691
|
+
</span>
|
|
692
|
+
) : (
|
|
693
|
+
<span className="-right-1 -bottom-1 absolute flex h-3.5 w-3.5 items-center justify-center rounded-full border border-white/30 bg-zinc-950 font-semibold text-[9px] text-slate-300 shadow-[0_2px_6px_rgba(0,0,0,0.45)]">
|
|
694
|
+
?
|
|
695
|
+
</span>
|
|
696
|
+
)}
|
|
571
697
|
</div>
|
|
572
698
|
</div>
|
|
573
699
|
)
|
|
@@ -598,24 +724,32 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
598
724
|
}) {
|
|
599
725
|
// Studio mode is a clean render/snapshot surface — no selection or editing
|
|
600
726
|
// affordances. It mirrors version-preview's chrome gating on the canvas.
|
|
601
|
-
|
|
727
|
+
// Capture (snapshot) mode is camera-only for the same reason: suppress
|
|
728
|
+
// selection, editing handles, and the tool manager (which mounts the site
|
|
729
|
+
// boundary flags) so the framed shot stays clean.
|
|
730
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
731
|
+
const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode || isCaptureMode
|
|
602
732
|
return (
|
|
603
733
|
<>
|
|
604
|
-
|
|
734
|
+
<SceneEnvironment />
|
|
735
|
+
{!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
|
|
605
736
|
{!noEditing && <BoxSelectTool />}
|
|
606
737
|
{!noEditing && <NodeArrowHandles />}
|
|
607
738
|
{!noEditing && <GroupRotateHandle />}
|
|
608
|
-
{!noEditing && <
|
|
739
|
+
{!noEditing && <GroupSelectionBox3D />}
|
|
609
740
|
{!noEditing && <WallOpeningHighlights />}
|
|
610
741
|
{!noEditing && <SlabHoleHighlights />}
|
|
611
742
|
{!noEditing && <WallMoveSideHandles />}
|
|
743
|
+
{!noEditing && <FenceTangentLines3D />}
|
|
612
744
|
{!noEditing && <FloatingActionMenu />}
|
|
745
|
+
{!noEditing && <GroupFloatingActionMenu />}
|
|
613
746
|
{!noEditing && <FloatingBuildingActionMenu />}
|
|
614
747
|
{!isFirstPersonMode && <WallMeasurementLabel />}
|
|
615
748
|
<ExportManager />
|
|
616
749
|
{isFirstPersonMode ? <ViewerZoneSystem /> : <ZoneSystem />}
|
|
617
750
|
<CeilingSystem />
|
|
618
751
|
<CeilingSelectionAffordanceSystem />
|
|
752
|
+
{!noEditing && <SelectionAffordanceManager />}
|
|
619
753
|
<RoofEditSystem />
|
|
620
754
|
<StairEditSystem />
|
|
621
755
|
{!(isLoading || isFirstPersonMode) && <SnapAwareGrid />}
|
|
@@ -715,6 +849,9 @@ function PaintCursorLayer({
|
|
|
715
849
|
}) {
|
|
716
850
|
const mode = useEditor((s) => s.mode)
|
|
717
851
|
const activePaintMaterial = useEditor((s) => s.activePaintMaterial)
|
|
852
|
+
const paintEraser = useEditor((s) => s.paintEraser)
|
|
853
|
+
const paintHover = useEditor((s) => s.paintHover)
|
|
854
|
+
const sceneMaterials = useScene((s) => s.materials)
|
|
718
855
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
|
|
719
856
|
const active = mode === 'material-paint' && !isVersionPreviewMode
|
|
720
857
|
|
|
@@ -764,11 +901,14 @@ function PaintCursorLayer({
|
|
|
764
901
|
}
|
|
765
902
|
}, [active, containerRef])
|
|
766
903
|
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
904
|
+
const hasPaint = paintEraser || hasActivePaintMaterial(activePaintMaterial)
|
|
905
|
+
const badgeState: PaintCursorBadgeState = !hasPaint
|
|
906
|
+
? 'empty'
|
|
907
|
+
: paintHover != null
|
|
908
|
+
? 'ready'
|
|
909
|
+
: 'blocked'
|
|
910
|
+
const swatchColor = getActivePaintMaterialSwatchColor(activePaintMaterial, sceneMaterials)
|
|
911
|
+
const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(activePaintMaterial, sceneMaterials)
|
|
772
912
|
|
|
773
913
|
if (!active || !position) return null
|
|
774
914
|
|
|
@@ -777,7 +917,13 @@ function PaintCursorLayer({
|
|
|
777
917
|
className="pointer-events-none absolute z-40"
|
|
778
918
|
style={{ left: 0, top: 0, transform: `translate(${position.x}px, ${position.y}px)` }}
|
|
779
919
|
>
|
|
780
|
-
<PaintCursorBadge
|
|
920
|
+
<PaintCursorBadge
|
|
921
|
+
isEraser={paintEraser}
|
|
922
|
+
position={{ x: 0, y: 0 }}
|
|
923
|
+
state={badgeState}
|
|
924
|
+
swatchColor={swatchColor}
|
|
925
|
+
swatchImageUrl={swatchImageUrl}
|
|
926
|
+
/>
|
|
781
927
|
</div>
|
|
782
928
|
)
|
|
783
929
|
}
|
|
@@ -816,6 +962,13 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
816
962
|
)
|
|
817
963
|
|
|
818
964
|
const viewerAreaRef = useRef<HTMLDivElement>(null)
|
|
965
|
+
// State mirror of `viewerAreaRef` so the floorplan compass portal re-renders
|
|
966
|
+
// once the container exists (a plain ref mutation wouldn't trigger it).
|
|
967
|
+
const [viewerAreaEl, setViewerAreaEl] = useState<HTMLDivElement | null>(null)
|
|
968
|
+
const setViewerAreaNode = useCallback((el: HTMLDivElement | null) => {
|
|
969
|
+
viewerAreaRef.current = el
|
|
970
|
+
setViewerAreaEl(el)
|
|
971
|
+
}, [])
|
|
819
972
|
const viewer3dRef = useRef<HTMLDivElement>(null)
|
|
820
973
|
const isResizingFloorplan = useRef(false)
|
|
821
974
|
|
|
@@ -861,7 +1014,10 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
861
1014
|
|
|
862
1015
|
return (
|
|
863
1016
|
<ErrorBoundary fallback={<EditorSceneCrashFallback />}>
|
|
864
|
-
|
|
1017
|
+
{/* `relative` so the floorplan compass (portaled here to stay visible in
|
|
1018
|
+
2d / 3d / split alike) can anchor to this container's bottom-left. */}
|
|
1019
|
+
<div className="relative flex h-full" ref={setViewerAreaNode}>
|
|
1020
|
+
<QuickMeasurementHud />
|
|
865
1021
|
{/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
|
|
866
1022
|
<div
|
|
867
1023
|
className="relative h-full flex-shrink-0"
|
|
@@ -871,7 +1027,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
871
1027
|
}}
|
|
872
1028
|
>
|
|
873
1029
|
<div className="h-full w-full overflow-hidden">
|
|
874
|
-
<FloorplanPanel />
|
|
1030
|
+
<FloorplanPanel compassHost={viewerAreaEl} />
|
|
875
1031
|
</div>
|
|
876
1032
|
{viewMode === 'split' && (
|
|
877
1033
|
<div
|
|
@@ -886,6 +1042,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
886
1042
|
{/* 3D viewer — always mounted, hidden via CSS to avoid destroying the WebGL context */}
|
|
887
1043
|
<div
|
|
888
1044
|
className="relative min-w-0 flex-1 overflow-hidden"
|
|
1045
|
+
data-pascal-viewer-3d
|
|
889
1046
|
ref={viewer3dRef}
|
|
890
1047
|
style={{ display: show3d ? undefined : 'none' }}
|
|
891
1048
|
>
|
|
@@ -935,6 +1092,7 @@ export default function Editor({
|
|
|
935
1092
|
sidebarTabs,
|
|
936
1093
|
viewerToolbarLeft,
|
|
937
1094
|
viewerToolbarRight,
|
|
1095
|
+
stageOverlay,
|
|
938
1096
|
inspectorFooter,
|
|
939
1097
|
projectId,
|
|
940
1098
|
onLoad,
|
|
@@ -944,6 +1102,7 @@ export default function Editor({
|
|
|
944
1102
|
previewScene,
|
|
945
1103
|
isVersionPreviewMode = false,
|
|
946
1104
|
isLoading = false,
|
|
1105
|
+
onLoaderChange,
|
|
947
1106
|
onThumbnailCapture,
|
|
948
1107
|
sidebarOverlay,
|
|
949
1108
|
viewerBanner,
|
|
@@ -974,11 +1133,22 @@ export default function Editor({
|
|
|
974
1133
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
975
1134
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
976
1135
|
|
|
1136
|
+
// Host-registered rail panels. Called unconditionally so
|
|
1137
|
+
// hook order is stable across the v1 / v2 layout branches below; the v1
|
|
1138
|
+
// AppSidebar path merges its own copy internally, the v2 path merges these
|
|
1139
|
+
// into its tab bar.
|
|
1140
|
+
const hostRailPanels = useHostPanels()
|
|
1141
|
+
|
|
977
1142
|
useEffect(() => {
|
|
978
1143
|
const teardown = initializeEditorRuntime()
|
|
979
1144
|
return teardown
|
|
980
1145
|
}, [])
|
|
981
1146
|
|
|
1147
|
+
useEffect(() => {
|
|
1148
|
+
void useEditor.persist.rehydrate()
|
|
1149
|
+
void useSidebarStore.persist.rehydrate()
|
|
1150
|
+
}, [])
|
|
1151
|
+
|
|
982
1152
|
useEffect(() => {
|
|
983
1153
|
useViewer.getState().setProjectId(projectId ?? null)
|
|
984
1154
|
|
|
@@ -1059,8 +1229,25 @@ export default function Editor({
|
|
|
1059
1229
|
setIsViewerSceneReady(ready)
|
|
1060
1230
|
}, [])
|
|
1061
1231
|
|
|
1232
|
+
useEffect(() => {
|
|
1233
|
+
if (isLoading || isSceneLoading || !hasLoadedInitialScene || isViewerSceneReady) return
|
|
1234
|
+
|
|
1235
|
+
const timer = window.setTimeout(() => {
|
|
1236
|
+
console.warn('[editor] viewer scene readiness timed out; showing editor shell anyway', {
|
|
1237
|
+
sceneReadyKey,
|
|
1238
|
+
})
|
|
1239
|
+
setIsViewerSceneReady(true)
|
|
1240
|
+
}, SCENE_READY_FALLBACK_MS)
|
|
1241
|
+
|
|
1242
|
+
return () => window.clearTimeout(timer)
|
|
1243
|
+
}, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
|
|
1244
|
+
|
|
1062
1245
|
const showLoader = isLoading || isSceneLoading || !hasLoadedInitialScene || !isViewerSceneReady
|
|
1063
1246
|
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
onLoaderChange?.(showLoader)
|
|
1249
|
+
}, [showLoader, onLoaderChange])
|
|
1250
|
+
|
|
1064
1251
|
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
1065
1252
|
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
1066
1253
|
|
|
@@ -1109,6 +1296,7 @@ export default function Editor({
|
|
|
1109
1296
|
<CeilingSystem />
|
|
1110
1297
|
<RoofEditSystem />
|
|
1111
1298
|
<StairEditSystem />
|
|
1299
|
+
{isFirstPersonMode && <FirstPersonControls />}
|
|
1112
1300
|
<CustomCameraControls />
|
|
1113
1301
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
1114
1302
|
<InteractiveSystem />
|
|
@@ -1131,7 +1319,16 @@ export default function Editor({
|
|
|
1131
1319
|
|
|
1132
1320
|
// ── V2 layout ──
|
|
1133
1321
|
if (layoutVersion === 'v2') {
|
|
1134
|
-
|
|
1322
|
+
// Registered host panels join the host's `sidebarTabs` as first-class tabs.
|
|
1323
|
+
// Explicit tabs keep precedence because they are already in the map first.
|
|
1324
|
+
const tabMap = new Map<string, SidebarTab & { component: React.ComponentType }>(
|
|
1325
|
+
sidebarTabs?.map((t) => [t.id, t]) ?? [],
|
|
1326
|
+
)
|
|
1327
|
+
for (const p of hostRailPanels) {
|
|
1328
|
+
if (!tabMap.has(p.id)) {
|
|
1329
|
+
tabMap.set(p.id, { id: p.id, label: p.label, icon: p.icon, component: p.component })
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1135
1332
|
|
|
1136
1333
|
const renderTabContent = (tabId: string) => {
|
|
1137
1334
|
// Built-in panels
|
|
@@ -1148,14 +1345,24 @@ export default function Editor({
|
|
|
1148
1345
|
return <Component />
|
|
1149
1346
|
}
|
|
1150
1347
|
|
|
1151
|
-
const tabBarTabs =
|
|
1152
|
-
sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
|
|
1348
|
+
const tabBarTabs = [
|
|
1349
|
+
...(sidebarTabs?.map(({ id, label, mobileDefaultSnap, mobileIcon, icon }) => ({
|
|
1153
1350
|
id,
|
|
1154
1351
|
label,
|
|
1155
1352
|
mobileDefaultSnap,
|
|
1156
1353
|
mobileIcon,
|
|
1157
1354
|
icon,
|
|
1158
|
-
})) ?? []
|
|
1355
|
+
})) ?? []),
|
|
1356
|
+
// Host panels appear after the explicit tabs in the rail. The icon
|
|
1357
|
+
// doubles as the mobile icon; a half-height sheet is a sensible default.
|
|
1358
|
+
...hostRailPanels.map((p) => ({
|
|
1359
|
+
id: p.id,
|
|
1360
|
+
label: p.label,
|
|
1361
|
+
mobileDefaultSnap: 0.5,
|
|
1362
|
+
mobileIcon: p.icon,
|
|
1363
|
+
icon: p.icon,
|
|
1364
|
+
})),
|
|
1365
|
+
]
|
|
1159
1366
|
|
|
1160
1367
|
return (
|
|
1161
1368
|
<>
|
|
@@ -1167,8 +1374,14 @@ export default function Editor({
|
|
|
1167
1374
|
|
|
1168
1375
|
{!isLoading && isPreviewMode ? (
|
|
1169
1376
|
<div className="dark flex h-full w-full flex-col bg-neutral-100 text-foreground">
|
|
1170
|
-
|
|
1171
|
-
|
|
1377
|
+
{isFirstPersonMode ? (
|
|
1378
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1379
|
+
) : (
|
|
1380
|
+
<ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
|
|
1381
|
+
)}
|
|
1382
|
+
<div className="h-full w-full" data-pascal-viewer-3d>
|
|
1383
|
+
{previewViewerContent}
|
|
1384
|
+
</div>
|
|
1172
1385
|
</div>
|
|
1173
1386
|
) : (
|
|
1174
1387
|
<>
|
|
@@ -1176,7 +1389,7 @@ export default function Editor({
|
|
|
1176
1389
|
navbarSlot={navbarSlot}
|
|
1177
1390
|
overlays={
|
|
1178
1391
|
<>
|
|
1179
|
-
{!isCaptureMode && <FloatingLevelSelector />}
|
|
1392
|
+
{!(isCaptureMode || stageOverlay) && <FloatingLevelSelector />}
|
|
1180
1393
|
{!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
|
|
1181
1394
|
<div className="pointer-events-auto">
|
|
1182
1395
|
<ActionMenu />
|
|
@@ -1204,6 +1417,7 @@ export default function Editor({
|
|
|
1204
1417
|
renderTabContent={renderTabContent}
|
|
1205
1418
|
sidebarOverlay={sidebarOverlay}
|
|
1206
1419
|
sidebarTabs={tabBarTabs}
|
|
1420
|
+
stageOverlay={stageOverlay}
|
|
1207
1421
|
viewerContent={viewerCanvas}
|
|
1208
1422
|
viewerToolbarLeft={viewerToolbarLeft}
|
|
1209
1423
|
viewerToolbarRight={viewerToolbarRight}
|
|
@@ -1232,8 +1446,14 @@ export default function Editor({
|
|
|
1232
1446
|
|
|
1233
1447
|
{!isLoading && isPreviewMode ? (
|
|
1234
1448
|
<>
|
|
1235
|
-
|
|
1236
|
-
|
|
1449
|
+
{isFirstPersonMode ? (
|
|
1450
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1451
|
+
) : (
|
|
1452
|
+
<ViewerOverlay onBack={() => useEditor.getState().setPreviewMode(false)} />
|
|
1453
|
+
)}
|
|
1454
|
+
<div className="h-full w-full" data-pascal-viewer-3d>
|
|
1455
|
+
{previewViewerContent}
|
|
1456
|
+
</div>
|
|
1237
1457
|
</>
|
|
1238
1458
|
) : (
|
|
1239
1459
|
<>
|
|
@@ -1263,6 +1483,7 @@ export default function Editor({
|
|
|
1263
1483
|
<div className="pointer-events-auto">
|
|
1264
1484
|
<HelperManager />
|
|
1265
1485
|
</div>
|
|
1486
|
+
<RiserDiagramPanel />
|
|
1266
1487
|
{isFirstPersonMode && (
|
|
1267
1488
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1268
1489
|
)}
|
|
@@ -23,11 +23,64 @@ const PART_ORDER: { key: MeasurePart; prefix: string }[] = [
|
|
|
23
23
|
{ key: 'thickness', prefix: 'T' },
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
export interface DimensionPillPart {
|
|
27
|
+
key: string
|
|
28
|
+
prefix: string
|
|
29
|
+
value: number
|
|
30
|
+
/** Render an explicit +/- sign — for deltas rather than absolute sizes. */
|
|
31
|
+
signed?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generic floating dimension pill: a row of `prefix value` readouts with the
|
|
36
|
+
* active one emphasised. Styled to match the top-center floating info bar
|
|
37
|
+
* (rounded-full, design-token colours) so it tracks the app theme.
|
|
38
|
+
*
|
|
39
|
+
* `primaryRef` points at the primary value's `<span>` so a caller driving a
|
|
40
|
+
* per-frame drag can rewrite its text imperatively without a React re-render.
|
|
41
|
+
*/
|
|
42
|
+
export function DimensionPill({
|
|
43
|
+
parts,
|
|
44
|
+
unit,
|
|
45
|
+
primary,
|
|
46
|
+
primaryRef,
|
|
47
|
+
}: {
|
|
48
|
+
parts: DimensionPillPart[]
|
|
49
|
+
unit: 'metric' | 'imperial'
|
|
50
|
+
primary?: string
|
|
51
|
+
primaryRef?: ForwardedRef<HTMLSpanElement>
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
|
|
55
|
+
{parts.map((part, index) => {
|
|
56
|
+
const text = part.signed
|
|
57
|
+
? `${part.value < 0 ? '-' : '+'}${formatMeasurement(Math.abs(part.value), unit)}`
|
|
58
|
+
: formatMeasurement(part.value, unit)
|
|
59
|
+
return (
|
|
60
|
+
<Fragment key={part.key}>
|
|
61
|
+
{index > 0 ? (
|
|
62
|
+
<span aria-hidden className="text-muted-foreground">
|
|
63
|
+
·
|
|
64
|
+
</span>
|
|
65
|
+
) : null}
|
|
66
|
+
<span
|
|
67
|
+
className={
|
|
68
|
+
part.key === primary ? 'font-medium text-foreground' : 'text-muted-foreground'
|
|
69
|
+
}
|
|
70
|
+
ref={part.key === primary ? primaryRef : undefined}
|
|
71
|
+
>
|
|
72
|
+
{`${part.prefix} ${text}`}
|
|
73
|
+
</span>
|
|
74
|
+
</Fragment>
|
|
75
|
+
)
|
|
76
|
+
})}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
/**
|
|
27
82
|
* Floating dimension pill shown during wall / fence drags: `H · L · T` with
|
|
28
|
-
* the actively-dragged dimension emphasised.
|
|
29
|
-
* floating info bar (rounded-full, design-token colours) so it tracks the
|
|
30
|
-
* app theme.
|
|
83
|
+
* the actively-dragged dimension emphasised.
|
|
31
84
|
*
|
|
32
85
|
* The forwarded ref points at the `primary` value's `<span>` so a caller
|
|
33
86
|
* driving a per-frame drag (the height arrow) can rewrite its text
|
|
@@ -52,24 +105,11 @@ export const MeasurementPill = forwardRef(function MeasurementPill(
|
|
|
52
105
|
) {
|
|
53
106
|
const values: Record<MeasurePart, number> = { height, length, thickness }
|
|
54
107
|
return (
|
|
55
|
-
<
|
|
56
|
-
{PART_ORDER.map((part
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</span>
|
|
62
|
-
) : null}
|
|
63
|
-
<span
|
|
64
|
-
className={
|
|
65
|
-
part.key === primary ? 'font-medium text-foreground' : 'text-muted-foreground'
|
|
66
|
-
}
|
|
67
|
-
ref={part.key === primary ? primaryRef : undefined}
|
|
68
|
-
>
|
|
69
|
-
{`${part.prefix} ${formatMeasurement(values[part.key], unit)}`}
|
|
70
|
-
</span>
|
|
71
|
-
</Fragment>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
108
|
+
<DimensionPill
|
|
109
|
+
parts={PART_ORDER.map((part) => ({ ...part, value: values[part.key] }))}
|
|
110
|
+
primary={primary}
|
|
111
|
+
primaryRef={primaryRef}
|
|
112
|
+
unit={unit}
|
|
113
|
+
/>
|
|
74
114
|
)
|
|
75
115
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { Copy, Move, Spline, Trash2 } from 'lucide-react'
|
|
4
|
+
import { Copy, Move, Search, Spline, Trash2 } from 'lucide-react'
|
|
5
5
|
import type { MouseEventHandler, PointerEventHandler } from 'react'
|
|
6
6
|
|
|
7
7
|
type NodeActionMenuProps = {
|
|
8
|
+
onFind?: MouseEventHandler<HTMLButtonElement>
|
|
8
9
|
onAddHole?: MouseEventHandler<HTMLButtonElement>
|
|
9
10
|
onDelete?: MouseEventHandler<HTMLButtonElement>
|
|
10
11
|
onDuplicate?: MouseEventHandler<HTMLButtonElement>
|
|
@@ -17,6 +18,7 @@ type NodeActionMenuProps = {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function NodeActionMenu({
|
|
21
|
+
onFind,
|
|
20
22
|
onAddHole,
|
|
21
23
|
onDelete,
|
|
22
24
|
onDuplicate,
|
|
@@ -35,6 +37,17 @@ export function NodeActionMenu({
|
|
|
35
37
|
onPointerLeave={onPointerLeave}
|
|
36
38
|
onPointerUp={onPointerUp}
|
|
37
39
|
>
|
|
40
|
+
{onFind && (
|
|
41
|
+
<button
|
|
42
|
+
aria-label="Find in catalog"
|
|
43
|
+
className="tooltip-trigger rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
44
|
+
onClick={onFind}
|
|
45
|
+
title="Find in catalog"
|
|
46
|
+
type="button"
|
|
47
|
+
>
|
|
48
|
+
<Search className="h-4 w-4" />
|
|
49
|
+
</button>
|
|
50
|
+
)}
|
|
38
51
|
{onMove && (
|
|
39
52
|
<button
|
|
40
53
|
aria-label="Move"
|