@pascal-app/editor 0.9.2 → 1.0.0-beta.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 +9 -7
- package/src/components/editor/bake-exporter.tsx +3 -1
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +307 -212
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +66 -2
- package/src/components/editor/first-person/build-collider-world.ts +20 -0
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +295 -132
- package/src/components/editor/floating-action-menu.tsx +59 -65
- package/src/components/editor/floorplan-background-selection.test.ts +16 -2
- package/src/components/editor/floorplan-background-selection.ts +4 -18
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +931 -861
- package/src/components/editor/group-actions.ts +155 -15
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-rotate-handle.tsx +1 -1
- package/src/components/editor/group-selection-box-3d.tsx +1 -1
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +17 -7
- package/src/components/editor/index.tsx +42 -9
- package/src/components/editor/measurement-pill.tsx +6 -1
- package/src/components/editor/node-arrow-handles.tsx +51 -15
- package/src/components/editor/quick-measurement-card.tsx +9 -3
- package/src/components/editor/quick-measurement-hud.tsx +7 -1
- package/src/components/editor/selection-manager.tsx +3 -1
- package/src/components/editor/site-edge-labels.tsx +24 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -1
- package/src/components/editor/thumbnail-generator.tsx +66 -294
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-mesh-settle-epoch.ts +1 -0
- package/src/components/editor/wall-measurement-label.tsx +8 -16
- package/src/components/editor/wall-move-side-handles.tsx +193 -18
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +6 -4
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +39 -8
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +47 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +21 -5
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +93 -59
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +27 -0
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +75 -1
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +92 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +93 -28
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +279 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +621 -281
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +12 -7
- package/src/components/tools/fence/fence-drafting.ts +27 -0
- package/src/components/tools/item/use-draft-node.ts +131 -72
- package/src/components/tools/item/use-placement-coordinator.tsx +52 -4
- package/src/components/tools/registry/move-registry-node-tool.tsx +75 -4
- package/src/components/tools/roof/roof-tool.tsx +16 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/site/site-boundary-editor.tsx +13 -6
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-tool.tsx +98 -47
- package/src/components/tools/tool-manager.tsx +67 -9
- package/src/components/tools/wall/wall-drafting.test.ts +118 -1
- package/src/components/tools/wall/wall-drafting.ts +128 -9
- package/src/components/tools/wall/wall-snap-geometry.test.ts +8 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +70 -3
- package/src/components/tools/zone/zone-tool.tsx +16 -0
- package/src/components/ui/action-menu/control-modes.tsx +4 -1
- package/src/components/ui/action-menu/measurement-control.tsx +107 -28
- package/src/components/ui/command-palette/editor-commands.tsx +14 -1
- package/src/components/ui/controls/material-paint-panel.tsx +7 -1
- package/src/components/ui/controls/material-picker.tsx +86 -7
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +73 -19
- package/src/components/ui/helpers/helper-manager.tsx +46 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/panel-manager.tsx +14 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -2
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +14 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +16 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +20 -655
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +53 -30
- package/src/index.tsx +137 -4
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +8 -1
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/direct-manipulation.test.ts +83 -0
- package/src/lib/direct-manipulation.ts +9 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +265 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +745 -131
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +2 -3
- package/src/lib/fresh-planar-placement.test.ts +44 -1
- package/src/lib/fresh-planar-placement.ts +21 -4
- package/src/lib/glb-export.test.ts +116 -1
- package/src/lib/glb-export.ts +204 -7
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +22 -0
- package/src/lib/interaction/overlay-policy.test.ts +2 -1
- package/src/lib/interaction/scope.ts +40 -8
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/measurements.test.ts +5 -0
- package/src/lib/measurements.ts +12 -1
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/scene-clipboard.test.ts +130 -1
- package/src/lib/scene-clipboard.ts +237 -13
- package/src/lib/selection-routing.test.ts +16 -1
- package/src/lib/selection-routing.ts +5 -0
- package/src/lib/sfx-bus.ts +33 -2
- package/src/lib/sfx-player.test.ts +148 -6
- package/src/lib/sfx-player.ts +156 -8
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/snapping-mode.test.ts +29 -2
- package/src/lib/snapping-mode.ts +15 -3
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +57 -85
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +184 -15
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-fence-curve-draft.ts +38 -8
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +54 -4
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +23 -3
- package/src/store/use-interaction-scope.ts +8 -0
- package/src/store/use-measurement-draft.test.ts +29 -0
- package/src/store/use-measurement-draft.ts +33 -6
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-stair-build-preview.ts +12 -2
- package/src/lib/floorplan/selection-tool.ts +0 -271
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
4
|
import {
|
|
5
|
+
acquireSceneReadOnlyLease,
|
|
5
6
|
getCatalogMaterialById,
|
|
6
7
|
getLibraryMaterialIdFromRef,
|
|
7
8
|
getSceneMaterialIdFromRef,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
} from '../../lib/scene'
|
|
32
33
|
import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
|
|
33
34
|
import useEditor from '../../store/use-editor'
|
|
35
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
34
36
|
import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
|
|
35
37
|
import { CeilingSystem } from '../systems/ceiling/ceiling-system'
|
|
36
38
|
import { RoofEditSystem } from '../systems/roof/roof-edit-system'
|
|
@@ -57,12 +59,14 @@ import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
|
|
|
57
59
|
import type { SidebarTab } from '../ui/sidebar/tab-bar'
|
|
58
60
|
import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
|
|
59
61
|
import { CustomCameraControls } from './custom-camera-controls'
|
|
62
|
+
import { DeleteConfirmationDialog } from './delete-confirmation-dialog'
|
|
60
63
|
import { EditorLayoutV2 } from './editor-layout-v2'
|
|
61
64
|
import { ExportManager } from './export-manager'
|
|
62
65
|
import { FenceTangentLines3D } from './fence-tangent-lines-3d'
|
|
63
66
|
import { FirstPersonControls, FirstPersonOverlay } from './first-person-controls'
|
|
64
67
|
import { FloatingActionMenu } from './floating-action-menu'
|
|
65
68
|
import { FloatingBuildingActionMenu } from './floating-building-action-menu'
|
|
69
|
+
import { FloorplanModeCoordinator } from './floorplan-mode-coordinator'
|
|
66
70
|
import { FloorplanPanel } from './floorplan-panel'
|
|
67
71
|
import { Grid } from './grid'
|
|
68
72
|
import { GroupFloatingActionMenu } from './group-floating-action-menu'
|
|
@@ -152,6 +156,17 @@ export interface EditorProps {
|
|
|
152
156
|
* only while a node is selected.
|
|
153
157
|
*/
|
|
154
158
|
inspectorFooter?: ReactNode
|
|
159
|
+
/**
|
|
160
|
+
* Docked below the multi-selection panel (v2). Hosts mount whole-selection
|
|
161
|
+
* affordances here (e.g. "Save to my catalog"); shows only while more than
|
|
162
|
+
* one node is selected.
|
|
163
|
+
*/
|
|
164
|
+
multiSelectionFooter?: ReactNode
|
|
165
|
+
|
|
166
|
+
/** Host-owned content mounted inside the editor's React Three Fiber scene. */
|
|
167
|
+
viewerSceneSlot?: ReactNode
|
|
168
|
+
/** Host-owned SVG content mounted in the transformed floor-plan scene. */
|
|
169
|
+
floorplanSceneSlot?: ReactNode
|
|
155
170
|
|
|
156
171
|
projectId?: string | null
|
|
157
172
|
|
|
@@ -715,12 +730,14 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
715
730
|
isFirstPersonMode,
|
|
716
731
|
isStudioMode,
|
|
717
732
|
onThumbnailCapture,
|
|
733
|
+
viewerSceneSlot,
|
|
718
734
|
}: {
|
|
719
735
|
isVersionPreviewMode: boolean
|
|
720
736
|
isLoading: boolean
|
|
721
737
|
isFirstPersonMode: boolean
|
|
722
738
|
isStudioMode: boolean
|
|
723
739
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
740
|
+
viewerSceneSlot?: ReactNode
|
|
724
741
|
}) {
|
|
725
742
|
// Studio mode is a clean render/snapshot surface — no selection or editing
|
|
726
743
|
// affordances. It mirrors version-preview's chrome gating on the canvas.
|
|
@@ -759,6 +776,7 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
|
|
|
759
776
|
<ThumbnailGenerator onThumbnailCapture={onThumbnailCapture} />
|
|
760
777
|
{!isFirstPersonMode && <SiteEdgeLabels />}
|
|
761
778
|
<InteractiveSystem />
|
|
779
|
+
{!noEditing && viewerSceneSlot}
|
|
762
780
|
</>
|
|
763
781
|
)
|
|
764
782
|
})
|
|
@@ -941,6 +959,8 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
941
959
|
sceneReadyKey,
|
|
942
960
|
onSceneReadyChange,
|
|
943
961
|
onThumbnailCapture,
|
|
962
|
+
viewerSceneSlot,
|
|
963
|
+
floorplanSceneSlot,
|
|
944
964
|
}: {
|
|
945
965
|
isVersionPreviewMode: boolean
|
|
946
966
|
isLoading: boolean
|
|
@@ -951,6 +971,8 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
951
971
|
sceneReadyKey: number
|
|
952
972
|
onSceneReadyChange: (ready: boolean) => void
|
|
953
973
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
974
|
+
viewerSceneSlot?: ReactNode
|
|
975
|
+
floorplanSceneSlot?: ReactNode
|
|
954
976
|
}) {
|
|
955
977
|
const viewMode = useEditor((s) => s.viewMode)
|
|
956
978
|
const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio)
|
|
@@ -1018,6 +1040,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1018
1040
|
2d / 3d / split alike) can anchor to this container's bottom-left. */}
|
|
1019
1041
|
<div className="relative flex h-full" ref={setViewerAreaNode}>
|
|
1020
1042
|
<QuickMeasurementHud />
|
|
1043
|
+
<DeleteConfirmationDialog />
|
|
1021
1044
|
{/* 2D floorplan — always mounted once shown, hidden via CSS to preserve state */}
|
|
1022
1045
|
<div
|
|
1023
1046
|
className="relative h-full flex-shrink-0"
|
|
@@ -1027,7 +1050,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1027
1050
|
}}
|
|
1028
1051
|
>
|
|
1029
1052
|
<div className="h-full w-full overflow-hidden">
|
|
1030
|
-
<FloorplanPanel compassHost={viewerAreaEl} />
|
|
1053
|
+
<FloorplanPanel compassHost={viewerAreaEl} floorplanSceneSlot={floorplanSceneSlot} />
|
|
1031
1054
|
</div>
|
|
1032
1055
|
{viewMode === 'split' && (
|
|
1033
1056
|
<div
|
|
@@ -1075,6 +1098,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
1075
1098
|
isStudioMode={isStudioMode}
|
|
1076
1099
|
isVersionPreviewMode={isVersionPreviewMode}
|
|
1077
1100
|
onThumbnailCapture={onThumbnailCapture}
|
|
1101
|
+
viewerSceneSlot={viewerSceneSlot}
|
|
1078
1102
|
/>
|
|
1079
1103
|
</Viewer>
|
|
1080
1104
|
</div>
|
|
@@ -1094,6 +1118,9 @@ export default function Editor({
|
|
|
1094
1118
|
viewerToolbarRight,
|
|
1095
1119
|
stageOverlay,
|
|
1096
1120
|
inspectorFooter,
|
|
1121
|
+
multiSelectionFooter,
|
|
1122
|
+
viewerSceneSlot,
|
|
1123
|
+
floorplanSceneSlot,
|
|
1097
1124
|
projectId,
|
|
1098
1125
|
onLoad,
|
|
1099
1126
|
onSave,
|
|
@@ -1151,9 +1178,11 @@ export default function Editor({
|
|
|
1151
1178
|
|
|
1152
1179
|
useEffect(() => {
|
|
1153
1180
|
useViewer.getState().setProjectId(projectId ?? null)
|
|
1181
|
+
useFloorplanMode.getState().setProjectId(projectId ?? null)
|
|
1154
1182
|
|
|
1155
1183
|
return () => {
|
|
1156
1184
|
useViewer.getState().setProjectId(null)
|
|
1185
|
+
useFloorplanMode.getState().setProjectId(null)
|
|
1157
1186
|
}
|
|
1158
1187
|
}, [projectId])
|
|
1159
1188
|
|
|
@@ -1209,13 +1238,10 @@ export default function Editor({
|
|
|
1209
1238
|
|
|
1210
1239
|
// Lock scene graph and reset to select mode when entering version preview
|
|
1211
1240
|
useEffect(() => {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
return () => {
|
|
1217
|
-
useScene.getState().setReadOnly(false)
|
|
1218
|
-
}
|
|
1241
|
+
if (!isVersionPreviewMode) return
|
|
1242
|
+
const releaseReadOnly = acquireSceneReadOnlyLease()
|
|
1243
|
+
useEditor.getState().setMode('select')
|
|
1244
|
+
return releaseReadOnly
|
|
1219
1245
|
}, [isVersionPreviewMode])
|
|
1220
1246
|
|
|
1221
1247
|
useEffect(() => {
|
|
@@ -1314,6 +1340,8 @@ export default function Editor({
|
|
|
1314
1340
|
onThumbnailCapture={onThumbnailCapture}
|
|
1315
1341
|
sceneReadyKey={sceneReadyKey}
|
|
1316
1342
|
showLoader={showLoader}
|
|
1343
|
+
viewerSceneSlot={viewerSceneSlot}
|
|
1344
|
+
floorplanSceneSlot={floorplanSceneSlot}
|
|
1317
1345
|
/>
|
|
1318
1346
|
)
|
|
1319
1347
|
|
|
@@ -1366,6 +1394,7 @@ export default function Editor({
|
|
|
1366
1394
|
|
|
1367
1395
|
return (
|
|
1368
1396
|
<>
|
|
1397
|
+
<FloorplanModeCoordinator />
|
|
1369
1398
|
{showLoader && (
|
|
1370
1399
|
<div className="fixed inset-0 z-60">
|
|
1371
1400
|
<SceneLoader className="bg-background" />
|
|
@@ -1397,7 +1426,10 @@ export default function Editor({
|
|
|
1397
1426
|
)}
|
|
1398
1427
|
{!(isVersionPreviewMode || isCaptureMode || isStudioMode) && (
|
|
1399
1428
|
<div className="pointer-events-auto">
|
|
1400
|
-
<PanelManager
|
|
1429
|
+
<PanelManager
|
|
1430
|
+
inspectorFooter={inspectorFooter}
|
|
1431
|
+
multiSelectionFooter={multiSelectionFooter}
|
|
1432
|
+
/>
|
|
1401
1433
|
</div>
|
|
1402
1434
|
)}
|
|
1403
1435
|
{!isCaptureMode && (
|
|
@@ -1438,6 +1470,7 @@ export default function Editor({
|
|
|
1438
1470
|
|
|
1439
1471
|
return (
|
|
1440
1472
|
<div className="dark flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
|
|
1473
|
+
<FloorplanModeCoordinator />
|
|
1441
1474
|
{showLoader && (
|
|
1442
1475
|
<div className="fixed inset-0 z-60">
|
|
1443
1476
|
<SceneLoader className="bg-background" />
|
|
@@ -4,7 +4,11 @@ import { type ForwardedRef, Fragment, forwardRef } from 'react'
|
|
|
4
4
|
|
|
5
5
|
// Canonical in-world dimension formatter — metric metres or imperial
|
|
6
6
|
// feet/inches. Shared by every measurement readout so they read the same.
|
|
7
|
-
export function formatMeasurement(
|
|
7
|
+
export function formatMeasurement(
|
|
8
|
+
value: number,
|
|
9
|
+
unit: 'metric' | 'imperial',
|
|
10
|
+
metricNotation: 'meters' | 'millimeters' = 'meters',
|
|
11
|
+
): string {
|
|
8
12
|
if (unit === 'imperial') {
|
|
9
13
|
const feet = value * 3.280_84
|
|
10
14
|
const wholeFeet = Math.floor(feet)
|
|
@@ -12,6 +16,7 @@ export function formatMeasurement(value: number, unit: 'metric' | 'imperial'): s
|
|
|
12
16
|
if (inches === 12) return `${wholeFeet + 1}'0"`
|
|
13
17
|
return `${wholeFeet}'${inches}"`
|
|
14
18
|
}
|
|
19
|
+
if (metricNotation === 'millimeters') return `${Math.round(value * 1000)}mm`
|
|
15
20
|
return `${Number.parseFloat(value.toFixed(2))}m`
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
nodeRegistry,
|
|
15
15
|
type RadialResizeHandle,
|
|
16
16
|
sceneRegistry,
|
|
17
|
-
snapScalar,
|
|
18
17
|
type TapActionHandle,
|
|
19
18
|
useLiveNodeOverrides,
|
|
20
19
|
useScene,
|
|
@@ -44,11 +43,10 @@ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js
|
|
|
44
43
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
45
44
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
46
45
|
import { RESIZE_HANDLE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
47
|
-
import { resolveDirectManipulationNode } from '../../lib/direct-manipulation'
|
|
48
46
|
import { createEditorApi } from '../../lib/editor-api'
|
|
49
47
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
50
48
|
import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
|
|
51
|
-
import useEditor from '../../store/use-editor'
|
|
49
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
52
50
|
import useInteractionScope, {
|
|
53
51
|
useEndpointReshape,
|
|
54
52
|
useIsCurveReshape,
|
|
@@ -64,6 +62,8 @@ import {
|
|
|
64
62
|
HandleArrow,
|
|
65
63
|
NO_RAYCAST,
|
|
66
64
|
} from './handles/handle-arrow'
|
|
65
|
+
import { replacePreviewOverrideIds } from './handles/preview-overrides'
|
|
66
|
+
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
67
67
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
68
68
|
|
|
69
69
|
// Pooled scratch for the handle rig's world-relative pose mapping.
|
|
@@ -213,8 +213,7 @@ export function NodeArrowHandles() {
|
|
|
213
213
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : activeRotateNodeId
|
|
214
214
|
const rawNode = useScene((state) => {
|
|
215
215
|
if (!selectedId) return null
|
|
216
|
-
|
|
217
|
-
return selectedNode ? resolveDirectManipulationNode(selectedNode, state.nodes) : null
|
|
216
|
+
return state.nodes[selectedId as AnyNodeId] ?? null
|
|
218
217
|
})
|
|
219
218
|
|
|
220
219
|
// Merge any live drag override so the arrows themselves (positions,
|
|
@@ -234,7 +233,7 @@ export function NodeArrowHandles() {
|
|
|
234
233
|
if (!(node && def?.handles)) return null
|
|
235
234
|
const all =
|
|
236
235
|
typeof def.handles === 'function'
|
|
237
|
-
? def.handles(node as never)
|
|
236
|
+
? def.handles(node as never, descriptorSceneApi)
|
|
238
237
|
: (def.handles as HandleDescriptor[])
|
|
239
238
|
// The whole-node move-cross gizmo is gone: moving is now click-to-move on
|
|
240
239
|
// the selected node body (see selection-manager). Drop both flavours — the
|
|
@@ -718,10 +717,6 @@ function LinearArrow({
|
|
|
718
717
|
const initialValue = descriptor.currentValue(initialNode)
|
|
719
718
|
const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
|
|
720
719
|
const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
|
|
721
|
-
const gridSnapStep =
|
|
722
|
-
descriptor.kind === 'linear-resize' && descriptor.gridSnap
|
|
723
|
-
? useEditor.getState().gridSnapStep
|
|
724
|
-
: null
|
|
725
720
|
const factor =
|
|
726
721
|
descriptor.kind === 'radial-resize'
|
|
727
722
|
? 1
|
|
@@ -735,6 +730,7 @@ function LinearArrow({
|
|
|
735
730
|
// when the (snapped + clamped) value actually changes, so the cue
|
|
736
731
|
// tracks real size steps instead of every sub-pixel pointer jitter.
|
|
737
732
|
let lastTickValue = initialValue
|
|
733
|
+
let previewOverrideIds = new Set<AnyNodeId>()
|
|
738
734
|
|
|
739
735
|
return {
|
|
740
736
|
overrideId,
|
|
@@ -755,7 +751,14 @@ function LinearArrow({
|
|
|
755
751
|
},
|
|
756
752
|
onEnd: () => {
|
|
757
753
|
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
754
|
+
if (descriptor.kind === 'linear-resize') {
|
|
755
|
+
descriptor.onDragEnd?.(initialNode as never, sceneApi)
|
|
756
|
+
}
|
|
758
757
|
if (onDrag) useOpeningGuides.getState().clear()
|
|
758
|
+
for (const previewId of previewOverrideIds) {
|
|
759
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
760
|
+
useScene.getState().markDirty(previewId)
|
|
761
|
+
}
|
|
759
762
|
},
|
|
760
763
|
move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
|
|
761
764
|
const currentPointer =
|
|
@@ -766,16 +769,46 @@ function LinearArrow({
|
|
|
766
769
|
) / localToWorldScale
|
|
767
770
|
const delta = currentPointer - initialPointer
|
|
768
771
|
const rawNext = initialValue + delta * factor
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
772
|
+
const linearDescriptor = descriptor.kind === 'linear-resize' ? descriptor : null
|
|
773
|
+
const snappedNext = resolveResizeSnapValue({
|
|
774
|
+
rawValue: rawNext,
|
|
775
|
+
gridSnapEnabled: linearDescriptor?.gridSnap === true,
|
|
776
|
+
gridSnapActive: isGridSnapActive(),
|
|
777
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
778
|
+
magneticSnapActive: isMagneticSnapActive(),
|
|
779
|
+
magneticSnap: linearDescriptor?.magneticSnap
|
|
780
|
+
? (value) => linearDescriptor.magneticSnap?.(initialNode, value, sceneApi) ?? value
|
|
781
|
+
: undefined,
|
|
782
|
+
})
|
|
773
783
|
const next = Math.min(maxBound, Math.max(minBound, snappedNext))
|
|
774
784
|
if (next !== lastTickValue) {
|
|
775
785
|
lastTickValue = next
|
|
776
786
|
sfxEmitter.emit('sfx:resize')
|
|
777
787
|
}
|
|
778
788
|
const patch = descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>
|
|
789
|
+
if (descriptor.kind === 'linear-resize' && descriptor.previewOverrides) {
|
|
790
|
+
const previewEntries = descriptor.previewOverrides(initialNode as never, next, sceneApi)
|
|
791
|
+
const nextPreviewOverrideIds = replacePreviewOverrideIds(
|
|
792
|
+
previewOverrideIds,
|
|
793
|
+
previewEntries,
|
|
794
|
+
(previewId) => {
|
|
795
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
796
|
+
useScene.getState().markDirty(previewId)
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
useLiveNodeOverrides
|
|
800
|
+
.getState()
|
|
801
|
+
.setMany(
|
|
802
|
+
previewEntries.map(([id, previewPatch]) => [
|
|
803
|
+
id,
|
|
804
|
+
previewPatch as Record<string, unknown>,
|
|
805
|
+
]),
|
|
806
|
+
)
|
|
807
|
+
for (const [previewId] of previewEntries) {
|
|
808
|
+
useScene.getState().markDirty(previewId)
|
|
809
|
+
}
|
|
810
|
+
previewOverrideIds = nextPreviewOverrideIds
|
|
811
|
+
}
|
|
779
812
|
// Let the kind publish live guides for the edge being resized.
|
|
780
813
|
onDrag?.({ ...(initialNode as object), ...patch } as AnyNode, sceneApi)
|
|
781
814
|
return patch
|
|
@@ -1550,7 +1583,10 @@ function CornerPickerShape({
|
|
|
1550
1583
|
position={position}
|
|
1551
1584
|
renderOrder={1001}
|
|
1552
1585
|
/>
|
|
1553
|
-
<group
|
|
1586
|
+
<group
|
|
1587
|
+
position={[position[0], position[1] + CORNER_FLOOR_OFFSET, position[2]]}
|
|
1588
|
+
ref={billboardRef}
|
|
1589
|
+
>
|
|
1554
1590
|
<HandleArrow
|
|
1555
1591
|
cursor={cursor}
|
|
1556
1592
|
hover={hover}
|
|
@@ -5,19 +5,25 @@ import { Crosshair, MapPin } from 'lucide-react'
|
|
|
5
5
|
import { memo } from 'react'
|
|
6
6
|
import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '../../lib/measurements'
|
|
7
7
|
|
|
8
|
-
function formatMetric(
|
|
8
|
+
function formatMetric(
|
|
9
|
+
metric: QuickMeasurementMetric,
|
|
10
|
+
unit: 'metric' | 'imperial',
|
|
11
|
+
metricNotation: 'meters' | 'millimeters',
|
|
12
|
+
): string {
|
|
9
13
|
if (metric.quantity === 'area') return formatAreaLabel(metric.value, unit, 2)
|
|
10
14
|
if (metric.quantity === 'volume') return formatVolumeLabel(metric.value, unit, 2)
|
|
11
|
-
return formatLinearMeasurement(metric.value, unit)
|
|
15
|
+
return formatLinearMeasurement(metric.value, unit, metricNotation)
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export const QuickMeasurementCard = memo(function QuickMeasurementCard({
|
|
15
19
|
report,
|
|
16
20
|
unit,
|
|
21
|
+
metricNotation,
|
|
17
22
|
lensState,
|
|
18
23
|
}: {
|
|
19
24
|
report: QuickMeasurementReport
|
|
20
25
|
unit: 'metric' | 'imperial'
|
|
26
|
+
metricNotation: 'meters' | 'millimeters'
|
|
21
27
|
lensState: 'live' | 'pinned'
|
|
22
28
|
}) {
|
|
23
29
|
const pinned = lensState === 'pinned'
|
|
@@ -53,7 +59,7 @@ export const QuickMeasurementCard = memo(function QuickMeasurementCard({
|
|
|
53
59
|
<span className="truncate">{metric.label}</span>
|
|
54
60
|
</div>
|
|
55
61
|
<div className="mt-0.5 truncate font-mono font-medium text-xs tabular-nums">
|
|
56
|
-
{formatMetric(metric, unit)}
|
|
62
|
+
{formatMetric(metric, unit, metricNotation)}
|
|
57
63
|
</div>
|
|
58
64
|
</div>
|
|
59
65
|
))}
|
|
@@ -12,6 +12,7 @@ export function QuickMeasurementHud() {
|
|
|
12
12
|
const viewMode = useEditor((state) => state.viewMode)
|
|
13
13
|
const entry = useQuickMeasurementHud((state) => selectQuickMeasurementHudEntry(state, viewMode))
|
|
14
14
|
const unit = useViewer((state) => state.unit)
|
|
15
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
15
16
|
|
|
16
17
|
if (!entry) return null
|
|
17
18
|
|
|
@@ -21,7 +22,12 @@ export function QuickMeasurementHud() {
|
|
|
21
22
|
data-quick-measure-hud
|
|
22
23
|
>
|
|
23
24
|
<div className="w-full max-w-[34rem]">
|
|
24
|
-
<QuickMeasurementCard
|
|
25
|
+
<QuickMeasurementCard
|
|
26
|
+
lensState={entry.lensState}
|
|
27
|
+
metricNotation={metricNotation}
|
|
28
|
+
report={entry.report}
|
|
29
|
+
unit={unit}
|
|
30
|
+
/>
|
|
25
31
|
</div>
|
|
26
32
|
</div>
|
|
27
33
|
)
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
} from '../../lib/paint-scope'
|
|
61
61
|
import { getHoveredRoofSegmentOutlineProxy } from '../../lib/roof-hover-outline-proxy'
|
|
62
62
|
import {
|
|
63
|
+
emitCanvasNodeSelection,
|
|
63
64
|
resolveCanvasSelectionNode,
|
|
64
65
|
resolveNodeSelectionTarget,
|
|
65
66
|
resolveSelectedIdsForNodeClick,
|
|
@@ -1365,7 +1366,7 @@ export const SelectionManager = () => {
|
|
|
1365
1366
|
useEffect(() => {
|
|
1366
1367
|
if (mode !== 'select') return
|
|
1367
1368
|
let owns = false
|
|
1368
|
-
let prevKey = '
|
|
1369
|
+
let prevKey = '\0'
|
|
1369
1370
|
const applyCursor = () => {
|
|
1370
1371
|
const { selection, hoveredId } = useViewer.getState()
|
|
1371
1372
|
const selectedIds = selection.selectedIds
|
|
@@ -1648,6 +1649,7 @@ export const SelectionManager = () => {
|
|
|
1648
1649
|
modifierKeysRef.current,
|
|
1649
1650
|
selectedIdsBeforeRouting,
|
|
1650
1651
|
)
|
|
1652
|
+
emitCanvasNodeSelection(nodeToSelect)
|
|
1651
1653
|
|
|
1652
1654
|
let nextMaterialTargetHandled = false
|
|
1653
1655
|
|
|
@@ -7,6 +7,7 @@ import { Html } from '@react-three/drei'
|
|
|
7
7
|
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
8
8
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
9
9
|
import { type Camera, type Object3D, Vector3 } from 'three'
|
|
10
|
+
import { groundHeightAt } from '../../lib/ground-surface'
|
|
10
11
|
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
11
12
|
import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary'
|
|
12
13
|
import { useActiveHandleDrag } from '../../store/use-interaction-scope'
|
|
@@ -16,6 +17,16 @@ type ViewportSize = {
|
|
|
16
17
|
height: number
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How far the label floats above the ground under it.
|
|
22
|
+
*
|
|
23
|
+
* Was a bare `0.5` in the position literal, which on flat ground is the same thing.
|
|
24
|
+
* Named now because it is no longer the label's *height* but its clearance: on a
|
|
25
|
+
* sculpted lot the number underneath moves, and half a metre is what keeps the text
|
|
26
|
+
* clear of a rim the boundary line is draped over.
|
|
27
|
+
*/
|
|
28
|
+
const LABEL_LIFT = 0.5
|
|
29
|
+
|
|
19
30
|
const htmlPosition = new Vector3()
|
|
20
31
|
|
|
21
32
|
function calculateHtmlPosition(el: Object3D, camera: Camera, size: ViewportSize) {
|
|
@@ -78,14 +89,25 @@ export function SiteEdgeLabels() {
|
|
|
78
89
|
if (obj) setSiteObj(obj)
|
|
79
90
|
})
|
|
80
91
|
|
|
92
|
+
// No live-terrain subscription here, deliberately: activating a site-boundary
|
|
93
|
+
// handle exits sculpt mode before the handle-drag scope begins, so the ground
|
|
94
|
+
// cannot move while these labels are on screen. The drag itself rewrites
|
|
95
|
+
// `polygon` every frame, which re-runs this memo against the current field.
|
|
81
96
|
const edges = useMemo(() => {
|
|
82
97
|
if (polygon.length < 2) return []
|
|
83
98
|
return polygon.map(([x1, z1], i) => {
|
|
84
99
|
const [x2, z2] = polygon[(i + 1) % polygon.length]!
|
|
85
100
|
const midX = (x1! + x2) / 2
|
|
86
101
|
const midZ = (z1! + z2) / 2
|
|
102
|
+
// Length stays the *plan* length. A property line is a horizontal measurement
|
|
103
|
+
// — a deed says 30 m whether the lot is flat or a hillside — and reporting the
|
|
104
|
+
// slope distance would make the same lot appear to grow as it is sculpted.
|
|
105
|
+
// Only the label's Y follows the ground.
|
|
87
106
|
const dist = Math.sqrt((x2 - x1!) ** 2 + (z2 - z1!) ** 2)
|
|
88
|
-
|
|
107
|
+
// `?? 0` covers both no-terrain cases, and 0 *is* the datum, so this reduces
|
|
108
|
+
// to the flat constant it replaced whenever the lot is unsculpted.
|
|
109
|
+
const groundY = groundHeightAt(midX, midZ, 0) ?? 0
|
|
110
|
+
return { midX, midY: groundY + LABEL_LIFT, midZ, dist }
|
|
89
111
|
})
|
|
90
112
|
}, [polygon])
|
|
91
113
|
|
|
@@ -99,7 +121,7 @@ export function SiteEdgeLabels() {
|
|
|
99
121
|
calculatePosition={calculateLabelPosition}
|
|
100
122
|
key={`${cameraMode}-${camera.uuid}-edge-${i}`}
|
|
101
123
|
occlude
|
|
102
|
-
position={[edge.midX,
|
|
124
|
+
position={[edge.midX, edge.midY, edge.midZ]}
|
|
103
125
|
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
104
126
|
zIndexRange={[10, 0]}
|
|
105
127
|
>
|
|
@@ -109,6 +109,9 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
109
109
|
const isPreset = captureMode.mode === 'preset'
|
|
110
110
|
const requestedCrop = captureMode.mode === 'standard' ? captureMode.crop : undefined
|
|
111
111
|
const requestedAspect = captureMode.mode === 'standard' ? captureMode.standardAspect : undefined
|
|
112
|
+
// A host-preselected crop means the host needs that exact output shape
|
|
113
|
+
// (e.g. the publish-cover capture) — hide the crop/aspect switcher.
|
|
114
|
+
const isCropLocked = isPreset || requestedCrop !== undefined
|
|
112
115
|
|
|
113
116
|
const [mode, setMode] = useState<CropMode>('standard')
|
|
114
117
|
const [standardAspect, setStandardAspect] = useState<StandardAspect>('16:9')
|
|
@@ -538,7 +541,7 @@ export function SnapshotCaptureOverlay({ projectId }: { projectId: string }) {
|
|
|
538
541
|
|
|
539
542
|
{/* Bottom-center: crop switcher, caption + shutter */}
|
|
540
543
|
<div className="pointer-events-none absolute right-0 bottom-5 left-0 flex flex-col items-center gap-2.5">
|
|
541
|
-
{!
|
|
544
|
+
{!isCropLocked && (
|
|
542
545
|
<div className="pointer-events-auto relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-950/85 px-1.5 py-1.5 shadow-xl backdrop-blur-md">
|
|
543
546
|
{/* Clicking Standard while it's active opens the aspect picker */}
|
|
544
547
|
<ModeButton
|