@pascal-app/editor 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -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 +284 -237
- 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-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 +180 -32
- 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/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/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 +135 -2
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +45 -8
- 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/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 +9 -12
- 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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +22 -5
- 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 +263 -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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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 +390 -36
- 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 +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- 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 +122 -0
- 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 +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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 +1 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- 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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -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/usdz-export.ts +38 -0
- 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-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/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { AnyNodeId, LevelNode, SceneApi } from '@pascal-app/core'
|
|
4
|
+
import { createContext, type ReactNode, useContext } from 'react'
|
|
5
|
+
|
|
6
|
+
export type RegistryToolContextValue = {
|
|
7
|
+
activeLevelId: LevelNode['id'] | null
|
|
8
|
+
isCameraDragging: () => boolean
|
|
9
|
+
sceneApi: SceneApi
|
|
10
|
+
selectNode: (nodeId: AnyNodeId) => void
|
|
11
|
+
unit: 'metric' | 'imperial'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RegistryToolContext = createContext<RegistryToolContextValue | null>(null)
|
|
15
|
+
|
|
16
|
+
export function RegistryToolProvider({
|
|
17
|
+
children,
|
|
18
|
+
value,
|
|
19
|
+
}: {
|
|
20
|
+
children: ReactNode
|
|
21
|
+
value: RegistryToolContextValue
|
|
22
|
+
}) {
|
|
23
|
+
return <RegistryToolContext.Provider value={value}>{children}</RegistryToolContext.Provider>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useRegistryToolContext(): RegistryToolContextValue {
|
|
27
|
+
const value = useContext(RegistryToolContext)
|
|
28
|
+
if (!value) throw new Error('Registry tools must be mounted by ToolManager')
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isBoxSelectPointerSuppressed,
|
|
20
20
|
markBoxSelectHandled,
|
|
21
21
|
} from './box-select-state'
|
|
22
|
+
import { marqueePolygon } from './marquee-footprint'
|
|
22
23
|
import {
|
|
23
24
|
convexHull2D,
|
|
24
25
|
type Point2,
|
|
@@ -223,7 +224,8 @@ function collectNodeIdsInScreenRect(
|
|
|
223
224
|
| { start?: unknown; end?: unknown; polygon?: unknown }
|
|
224
225
|
| undefined
|
|
225
226
|
if (node) {
|
|
226
|
-
const { start, end
|
|
227
|
+
const { start, end } = node
|
|
228
|
+
const polygon = marqueePolygon(node)
|
|
227
229
|
if (isVec2(start) && isVec2(end)) {
|
|
228
230
|
if (segmentIntersectsPolygon(start, end, quad)) result.push(id)
|
|
229
231
|
continue
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { marqueePolygon } from './marquee-footprint'
|
|
3
|
+
import { type Point2, polygonsIntersect } from './marquee-geometry'
|
|
4
|
+
|
|
5
|
+
const polygon: Point2[] = [
|
|
6
|
+
[-4, -2],
|
|
7
|
+
[4, -2],
|
|
8
|
+
[4, 2],
|
|
9
|
+
[-4, 2],
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
test('a translated pool is not selected by a pipe marquee over its untransformed outline', () => {
|
|
13
|
+
const pool = { polygon, position: [12, 0, 0], rotation: [0, 0, 0] }
|
|
14
|
+
const pipeMarquee: Point2[] = [
|
|
15
|
+
[-1, -0.1],
|
|
16
|
+
[1, -0.1],
|
|
17
|
+
[1, 0.1],
|
|
18
|
+
[-1, 0.1],
|
|
19
|
+
]
|
|
20
|
+
expect(polygonsIntersect(polygon, pipeMarquee)).toBe(true)
|
|
21
|
+
expect(polygonsIntersect(marqueePolygon(pool)!, pipeMarquee)).toBe(false)
|
|
22
|
+
expect(
|
|
23
|
+
polygonsIntersect(marqueePolygon(pool)!, [
|
|
24
|
+
[11, -1],
|
|
25
|
+
[13, -1],
|
|
26
|
+
[13, 1],
|
|
27
|
+
[11, 1],
|
|
28
|
+
]),
|
|
29
|
+
).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('pool rotation is applied before its position', () => {
|
|
33
|
+
const footprint = marqueePolygon({
|
|
34
|
+
polygon,
|
|
35
|
+
position: [12, 0, 5],
|
|
36
|
+
rotation: [0, Math.PI / 2, 0],
|
|
37
|
+
})!
|
|
38
|
+
expect(footprint[0]![0]).toBeCloseTo(10)
|
|
39
|
+
expect(footprint[0]![1]).toBeCloseTo(9)
|
|
40
|
+
expect(
|
|
41
|
+
polygonsIntersect(footprint, [
|
|
42
|
+
[15, 4],
|
|
43
|
+
[16, 4],
|
|
44
|
+
[16, 6],
|
|
45
|
+
[15, 6],
|
|
46
|
+
]),
|
|
47
|
+
).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('level-space polygons keep their existing coordinates', () => {
|
|
51
|
+
expect(marqueePolygon({ polygon })).toBe(polygon)
|
|
52
|
+
expect(marqueePolygon({ polygon: undefined })).toBeNull()
|
|
53
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Euler, Vector3 } from 'three'
|
|
2
|
+
import type { Point2 } from './marquee-geometry'
|
|
3
|
+
|
|
4
|
+
export function marqueePolygon(node: {
|
|
5
|
+
polygon?: unknown
|
|
6
|
+
position?: unknown
|
|
7
|
+
rotation?: unknown
|
|
8
|
+
}): Point2[] | null {
|
|
9
|
+
const { polygon, position, rotation } = node
|
|
10
|
+
if (
|
|
11
|
+
!Array.isArray(polygon) ||
|
|
12
|
+
polygon.length === 0 ||
|
|
13
|
+
!polygon.every(
|
|
14
|
+
(point) => Array.isArray(point) && point.length === 2 && point.every(Number.isFinite),
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
return null
|
|
18
|
+
// Position-based kinds author their footprint locally, unlike slabs and zones.
|
|
19
|
+
if (!Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite))
|
|
20
|
+
return polygon as Point2[]
|
|
21
|
+
const angles =
|
|
22
|
+
Array.isArray(rotation) && rotation.length === 3 && rotation.every(Number.isFinite)
|
|
23
|
+
? new Euler(rotation[0], rotation[1], rotation[2])
|
|
24
|
+
: new Euler()
|
|
25
|
+
return polygon.map(([x, z]) => {
|
|
26
|
+
const point = new Vector3(x, 0, z).applyEuler(angles)
|
|
27
|
+
return [point.x + position[0], point.z + position[2]]
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -104,15 +104,24 @@ function getMeasurementGuidePoints(width: number, height: number, depth: number)
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function MeasurementPill({
|
|
107
|
+
active,
|
|
107
108
|
label,
|
|
109
|
+
onSelect,
|
|
108
110
|
position,
|
|
109
111
|
}: {
|
|
112
|
+
active?: boolean
|
|
110
113
|
label: string
|
|
114
|
+
onSelect?: () => void
|
|
111
115
|
position: [number, number, number]
|
|
112
116
|
}) {
|
|
113
117
|
return (
|
|
114
|
-
<Html center position={position} style={{ pointerEvents: 'none' }}>
|
|
115
|
-
<
|
|
118
|
+
<Html center position={position} style={{ pointerEvents: onSelect ? 'auto' : 'none' }}>
|
|
119
|
+
<button
|
|
120
|
+
className={active ? 'ring-1 ring-indigo-200' : undefined}
|
|
121
|
+
onClick={(event) => {
|
|
122
|
+
event.stopPropagation()
|
|
123
|
+
onSelect?.()
|
|
124
|
+
}}
|
|
116
125
|
style={{
|
|
117
126
|
background: 'rgba(15, 23, 42, 0.86)',
|
|
118
127
|
border: '1px solid rgba(15, 23, 42, 0.65)',
|
|
@@ -123,12 +132,13 @@ function MeasurementPill({
|
|
|
123
132
|
fontWeight: 600,
|
|
124
133
|
lineHeight: 1,
|
|
125
134
|
padding: '4px 8px',
|
|
126
|
-
pointerEvents: 'none',
|
|
135
|
+
pointerEvents: onSelect ? 'auto' : 'none',
|
|
127
136
|
whiteSpace: 'nowrap',
|
|
128
137
|
}}
|
|
138
|
+
type="button"
|
|
129
139
|
>
|
|
130
140
|
{label}
|
|
131
|
-
</
|
|
141
|
+
</button>
|
|
132
142
|
</Html>
|
|
133
143
|
)
|
|
134
144
|
}
|
|
@@ -147,8 +157,12 @@ function MeasurementPill({
|
|
|
147
157
|
* a shelf — lines up without an extra offset.
|
|
148
158
|
*/
|
|
149
159
|
export function PlacementBox({
|
|
160
|
+
activeDimensionId,
|
|
150
161
|
dimensions,
|
|
162
|
+
dimensionInput = '',
|
|
151
163
|
measurements,
|
|
164
|
+
measurementValues,
|
|
165
|
+
onDimensionSelect,
|
|
152
166
|
position,
|
|
153
167
|
rotationY = 0,
|
|
154
168
|
valid,
|
|
@@ -157,14 +171,20 @@ export function PlacementBox({
|
|
|
157
171
|
dimensions: [number, number, number]
|
|
158
172
|
/** Optional dimension guide labels matching the GLB item placement cursor. */
|
|
159
173
|
measurements?: PlacementBoxMeasurements
|
|
174
|
+
/** Values represented by the editable dimension pills; defaults to the box dimensions. */
|
|
175
|
+
measurementValues?: [width: number, height: number, depth: number]
|
|
160
176
|
/** World-plan position of the footprint centre (floor level). */
|
|
161
177
|
position: [number, number, number]
|
|
162
178
|
/** Y-rotation in radians, applied to the whole box. */
|
|
163
179
|
rotationY?: number
|
|
164
180
|
/** Drives the colour: green when placeable, red otherwise. */
|
|
165
181
|
valid: boolean
|
|
182
|
+
activeDimensionId?: string | null
|
|
183
|
+
dimensionInput?: string
|
|
184
|
+
onDimensionSelect?: (id: string) => void
|
|
166
185
|
}) {
|
|
167
186
|
const [width, height, depth] = dimensions
|
|
187
|
+
const [measurementWidth, measurementHeight, measurementDepth] = measurementValues ?? dimensions
|
|
168
188
|
|
|
169
189
|
const edgeGeometry = useMemo(
|
|
170
190
|
() =>
|
|
@@ -276,15 +296,45 @@ export function PlacementBox({
|
|
|
276
296
|
renderOrder={998}
|
|
277
297
|
/>
|
|
278
298
|
<MeasurementPill
|
|
279
|
-
|
|
299
|
+
active={activeDimensionId === 'cabinet-width'}
|
|
300
|
+
label={
|
|
301
|
+
activeDimensionId === 'cabinet-width' && dimensionInput
|
|
302
|
+
? dimensionInput
|
|
303
|
+
: formatLinearMeasurement(
|
|
304
|
+
measurementWidth,
|
|
305
|
+
measurements.unit,
|
|
306
|
+
measurements.metricNotation,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-width') : undefined}
|
|
280
310
|
position={[0, 0.04, depth / 2 + 0.24]}
|
|
281
311
|
/>
|
|
282
312
|
<MeasurementPill
|
|
283
|
-
|
|
313
|
+
active={activeDimensionId === 'cabinet-depth'}
|
|
314
|
+
label={
|
|
315
|
+
activeDimensionId === 'cabinet-depth' && dimensionInput
|
|
316
|
+
? dimensionInput
|
|
317
|
+
: formatLinearMeasurement(
|
|
318
|
+
measurementDepth,
|
|
319
|
+
measurements.unit,
|
|
320
|
+
measurements.metricNotation,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-depth') : undefined}
|
|
284
324
|
position={[width / 2 + 0.24, 0.04, 0]}
|
|
285
325
|
/>
|
|
286
326
|
<MeasurementPill
|
|
287
|
-
|
|
327
|
+
active={activeDimensionId === 'cabinet-height'}
|
|
328
|
+
label={
|
|
329
|
+
activeDimensionId === 'cabinet-height' && dimensionInput
|
|
330
|
+
? dimensionInput
|
|
331
|
+
: formatLinearMeasurement(
|
|
332
|
+
measurementHeight,
|
|
333
|
+
measurements.unit,
|
|
334
|
+
measurements.metricNotation,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
onSelect={onDimensionSelect ? () => onDimensionSelect('cabinet-height') : undefined}
|
|
288
338
|
position={[-width / 2 - 0.24, height / 2, -depth / 2]}
|
|
289
339
|
/>
|
|
290
340
|
</>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { Html } from '@react-three/drei'
|
|
5
|
+
import { useLayoutEffect, useMemo } from 'react'
|
|
6
|
+
import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
|
|
7
|
+
import { LineBasicNodeMaterial } from 'three/webgpu'
|
|
8
|
+
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
9
|
+
import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
|
|
10
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
11
|
+
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
12
|
+
|
|
13
|
+
const DIMENSION_COLOR = 0x63_66_f1
|
|
14
|
+
const dimensionMaterial = new LineBasicNodeMaterial({
|
|
15
|
+
color: DIMENSION_COLOR,
|
|
16
|
+
depthTest: false,
|
|
17
|
+
depthWrite: false,
|
|
18
|
+
toneMapped: false,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
export function PlacementDimensionGuides() {
|
|
22
|
+
const dimensions = usePlacementPreview((state) => state.dimensions)
|
|
23
|
+
const activeDimensionId = usePlacementPreview((state) => state.activeDimensionId)
|
|
24
|
+
const dimensionInput = usePlacementPreview((state) => state.dimensionInput)
|
|
25
|
+
const unit = useViewer((state) => state.unit)
|
|
26
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
27
|
+
if (dimensions.length === 0) return null
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
{dimensions
|
|
32
|
+
.filter((dimension) => dimension.renderIn3d !== false)
|
|
33
|
+
.map((dimension) => (
|
|
34
|
+
<PlacementDimensionGuide
|
|
35
|
+
dimension={dimension}
|
|
36
|
+
dimensionInput={dimension.id === activeDimensionId ? dimensionInput : null}
|
|
37
|
+
key={dimension.id}
|
|
38
|
+
metricNotation={metricNotation}
|
|
39
|
+
unit={unit}
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function PlacementDimensionGuide({
|
|
47
|
+
dimension,
|
|
48
|
+
dimensionInput,
|
|
49
|
+
metricNotation,
|
|
50
|
+
unit,
|
|
51
|
+
}: {
|
|
52
|
+
dimension: PlacementPreviewDimension
|
|
53
|
+
dimensionInput: string | null
|
|
54
|
+
metricNotation: 'meters' | 'millimeters'
|
|
55
|
+
unit: 'metric' | 'imperial'
|
|
56
|
+
}) {
|
|
57
|
+
const { line, position } = useMemo(() => {
|
|
58
|
+
const position = new Float32BufferAttribute(new Float32Array(6), 3)
|
|
59
|
+
const geometry = new BufferGeometry()
|
|
60
|
+
geometry.setAttribute('position', position)
|
|
61
|
+
const line = new ThreeLine(geometry, dimensionMaterial)
|
|
62
|
+
line.frustumCulled = false
|
|
63
|
+
line.layers.set(EDITOR_LAYER)
|
|
64
|
+
line.renderOrder = 1000
|
|
65
|
+
return { line, position }
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const start = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
[
|
|
71
|
+
dimension.start[0] + dimension.offsetNormal[0] * dimension.offsetDistance,
|
|
72
|
+
dimension.start[1],
|
|
73
|
+
dimension.start[2] + dimension.offsetNormal[1] * dimension.offsetDistance,
|
|
74
|
+
] as [number, number, number],
|
|
75
|
+
[dimension.offsetDistance, dimension.offsetNormal, dimension.start],
|
|
76
|
+
)
|
|
77
|
+
const end = useMemo(
|
|
78
|
+
() =>
|
|
79
|
+
[
|
|
80
|
+
dimension.end[0] + dimension.offsetNormal[0] * dimension.offsetDistance,
|
|
81
|
+
dimension.end[1],
|
|
82
|
+
dimension.end[2] + dimension.offsetNormal[1] * dimension.offsetDistance,
|
|
83
|
+
] as [number, number, number],
|
|
84
|
+
[dimension.end, dimension.offsetDistance, dimension.offsetNormal],
|
|
85
|
+
)
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
position.setXYZ(0, ...start)
|
|
88
|
+
position.setXYZ(1, ...end)
|
|
89
|
+
position.needsUpdate = true
|
|
90
|
+
}, [end, position, start])
|
|
91
|
+
|
|
92
|
+
useLayoutEffect(() => () => line.geometry.dispose(), [line])
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
<primitive object={line} />
|
|
96
|
+
<Html
|
|
97
|
+
center
|
|
98
|
+
position={[
|
|
99
|
+
(start[0] + end[0]) / 2,
|
|
100
|
+
(start[1] + end[1]) / 2 + 0.015,
|
|
101
|
+
(start[2] + end[2]) / 2,
|
|
102
|
+
]}
|
|
103
|
+
style={{ pointerEvents: 'auto', userSelect: 'none' }}
|
|
104
|
+
zIndexRange={[100, 0]}
|
|
105
|
+
>
|
|
106
|
+
<div
|
|
107
|
+
className={`rounded-[3px] px-1.5 py-0.5 font-mono text-[10px] font-semibold text-white shadow-sm ${
|
|
108
|
+
dimensionInput !== null ? 'bg-indigo-700 ring-1 ring-indigo-200' : 'bg-indigo-500/90'
|
|
109
|
+
}`}
|
|
110
|
+
onClick={(event) => {
|
|
111
|
+
event.stopPropagation()
|
|
112
|
+
usePlacementPreview.getState().selectDimension(dimension.id)
|
|
113
|
+
}}
|
|
114
|
+
style={{ cursor: 'text', pointerEvents: 'auto' }}
|
|
115
|
+
>
|
|
116
|
+
{dimensionInput || formatMeasurement(dimension.value, unit, metricNotation)}
|
|
117
|
+
</div>
|
|
118
|
+
</Html>
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
121
|
+
}
|