@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
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { emitter, sceneRegistry } from '@pascal-app/core'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
backdropGradient,
|
|
6
|
+
deepSkyColor,
|
|
7
|
+
GRID_LAYER,
|
|
8
|
+
getSceneTheme,
|
|
9
|
+
horizonHazeColor,
|
|
10
|
+
packNormalToRGB,
|
|
11
|
+
SSGI_PARAMS,
|
|
12
|
+
snapLevelsToTruePositions,
|
|
13
|
+
unpackRGBToNormal,
|
|
14
|
+
useViewer,
|
|
15
|
+
} from '@pascal-app/viewer'
|
|
5
16
|
import type { CameraControls } from '@react-three/drei'
|
|
6
17
|
import { useThree } from '@react-three/fiber'
|
|
7
18
|
import { useCallback, useEffect, useRef } from 'react'
|
|
@@ -11,16 +22,18 @@ import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
|
|
11
22
|
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
|
12
23
|
import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js'
|
|
13
24
|
import {
|
|
14
|
-
colorToDirection,
|
|
15
25
|
convertToTexture,
|
|
16
26
|
diffuseColor,
|
|
17
|
-
directionToColor,
|
|
18
27
|
float,
|
|
28
|
+
mix,
|
|
19
29
|
mrt,
|
|
20
30
|
normalView,
|
|
21
31
|
output,
|
|
22
32
|
pass,
|
|
23
33
|
sample,
|
|
34
|
+
screenUV,
|
|
35
|
+
smoothstep,
|
|
36
|
+
uniform,
|
|
24
37
|
vec4,
|
|
25
38
|
} from 'three/tsl'
|
|
26
39
|
import { RenderPipeline, RenderTarget, type WebGPURenderer } from 'three/webgpu'
|
|
@@ -54,6 +67,18 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
54
67
|
const pipelineRef = useRef<RenderPipeline | null>(null)
|
|
55
68
|
const renderTargetRef = useRef<RenderTarget | null>(null)
|
|
56
69
|
|
|
70
|
+
// Backdrop compositing for scene snapshots (studio renders, project
|
|
71
|
+
// thumbnails): theme background + sky gradient, same world-ray math as the
|
|
72
|
+
// viewport backdrop in viewer's post-processing. Uniform-driven so the one
|
|
73
|
+
// cached pipeline serves both opaque and transparent (preset/item) captures.
|
|
74
|
+
const bgColorUniform = useRef(uniform(new THREE.Color('#ffffff')))
|
|
75
|
+
const bgSkyUniform = useRef(uniform(new THREE.Color('#ffffff')))
|
|
76
|
+
const bgSkyDeepUniform = useRef(uniform(new THREE.Color('#ffffff')))
|
|
77
|
+
const bgHazeUniform = useRef(uniform(new THREE.Color('#ffffff')))
|
|
78
|
+
const bgProjInvUniform = useRef(uniform(new THREE.Matrix4()))
|
|
79
|
+
const bgCamWorldUniform = useRef(uniform(new THREE.Matrix4()))
|
|
80
|
+
const bgMixUniform = useRef(uniform(1))
|
|
81
|
+
|
|
57
82
|
useEffect(() => {
|
|
58
83
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
59
84
|
}, [onThumbnailCapture])
|
|
@@ -80,7 +105,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
80
105
|
mrt({
|
|
81
106
|
output,
|
|
82
107
|
diffuseColor,
|
|
83
|
-
normal:
|
|
108
|
+
normal: packNormalToRGB(normalView),
|
|
84
109
|
}),
|
|
85
110
|
)
|
|
86
111
|
|
|
@@ -91,7 +116,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
91
116
|
scenePass.getTexture('diffuseColor').type = UnsignedByteType
|
|
92
117
|
scenePass.getTexture('normal').type = UnsignedByteType
|
|
93
118
|
|
|
94
|
-
const sceneNormal = sample((uv) =>
|
|
119
|
+
const sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)))
|
|
95
120
|
|
|
96
121
|
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, cam as any)
|
|
97
122
|
giPass.sliceCount.value = SSGI_PARAMS.sliceCount
|
|
@@ -106,14 +131,49 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
106
131
|
giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling
|
|
107
132
|
giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering
|
|
108
133
|
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
// r185: SSGI's AO lives in its own single-channel texture (getAONode)
|
|
135
|
+
// rather than the alpha of one packed rgba texture.
|
|
136
|
+
const aoTexture = (giPass as any).getAONode()
|
|
137
|
+
const aoAsRgb = vec4(aoTexture.r, aoTexture.r, aoTexture.r, float(1))
|
|
111
138
|
const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, cam)
|
|
112
139
|
denoisePass.index.value = 0
|
|
113
140
|
denoisePass.radius.value = 4
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
// Same far-field AO fade as the viewport pipeline — without it the
|
|
143
|
+
// horizon picks up a visible AO line in captures.
|
|
144
|
+
const aoFarFade = smoothstep(
|
|
145
|
+
float(0.9994),
|
|
146
|
+
float(0.9998),
|
|
147
|
+
scenePassDepth.sample(screenUV).r,
|
|
148
|
+
)
|
|
149
|
+
const ao = mix((denoisePass as any).r, float(1), aoFarFade)
|
|
150
|
+
const sceneRgb = scenePassColor.rgb.mul(ao)
|
|
151
|
+
|
|
152
|
+
// Per-pixel world ray from the capture camera → sky gradient above the
|
|
153
|
+
// horizon (dir.y = 0), flat background below — mirrors the viewport
|
|
154
|
+
// backdrop. bgMix 0 bypasses it and keeps the capture transparent.
|
|
155
|
+
const ndc = vec4(
|
|
156
|
+
screenUV.x.mul(2).sub(1),
|
|
157
|
+
float(1).sub(screenUV.y).mul(2).sub(1),
|
|
158
|
+
1,
|
|
159
|
+
1,
|
|
160
|
+
) as any
|
|
161
|
+
const viewRay = (bgProjInvUniform.current as any).mul(ndc)
|
|
162
|
+
const worldDir = (bgCamWorldUniform.current as any)
|
|
163
|
+
.mul(vec4(viewRay.xyz, 0))
|
|
164
|
+
.xyz.normalize()
|
|
165
|
+
const bgGradient = backdropGradient({
|
|
166
|
+
dirY: worldDir.y,
|
|
167
|
+
background: bgColorUniform.current,
|
|
168
|
+
haze: bgHazeUniform.current,
|
|
169
|
+
sky: bgSkyUniform.current,
|
|
170
|
+
skyDeep: bgSkyDeepUniform.current,
|
|
171
|
+
})
|
|
172
|
+
const alpha = scenePassColor.a
|
|
173
|
+
const finalOutput = vec4(
|
|
174
|
+
mix(sceneRgb, mix(bgGradient, sceneRgb, alpha), bgMixUniform.current),
|
|
175
|
+
mix(alpha, float(1), bgMixUniform.current),
|
|
176
|
+
)
|
|
117
177
|
|
|
118
178
|
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
|
119
179
|
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
|
@@ -151,7 +211,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
151
211
|
snapLevels: boolean,
|
|
152
212
|
captureMode?: 'standard' | 'viewport' | 'area',
|
|
153
213
|
cropRegion?: { x: number; y: number; width: number; height: number },
|
|
214
|
+
standardSize?: { w: number; h: number },
|
|
215
|
+
transparent = false,
|
|
154
216
|
) => {
|
|
217
|
+
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
|
|
218
|
+
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
|
|
155
219
|
if (isGenerating.current) return
|
|
156
220
|
if (!onThumbnailCaptureRef.current) return
|
|
157
221
|
|
|
@@ -173,6 +237,21 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
173
237
|
const { width, height } = gl.domElement
|
|
174
238
|
thumbnailCamera.aspect = width / height
|
|
175
239
|
thumbnailCamera.updateProjectionMatrix()
|
|
240
|
+
// The capture camera never joins the scene graph, so its matrixWorld
|
|
241
|
+
// is only refreshed by the render itself — too late for the backdrop
|
|
242
|
+
// uniforms below.
|
|
243
|
+
thumbnailCamera.updateMatrixWorld()
|
|
244
|
+
|
|
245
|
+
const theme = getSceneTheme(useViewer.getState().sceneTheme)
|
|
246
|
+
bgColorUniform.current.value.set(theme.background)
|
|
247
|
+
bgSkyUniform.current.value.set(theme.backgroundSky ?? theme.background)
|
|
248
|
+
bgSkyDeepUniform.current.value.set(deepSkyColor(theme.backgroundSky ?? theme.background))
|
|
249
|
+
bgHazeUniform.current.value.set(
|
|
250
|
+
horizonHazeColor(theme.backgroundSky ?? theme.background, theme.appearance),
|
|
251
|
+
)
|
|
252
|
+
bgProjInvUniform.current.value.copy(thumbnailCamera.projectionMatrixInverse)
|
|
253
|
+
bgCamWorldUniform.current.value.copy(thumbnailCamera.matrixWorld)
|
|
254
|
+
bgMixUniform.current.value = transparent ? 0 : 1
|
|
176
255
|
|
|
177
256
|
// Capture camera data for snapshot storage
|
|
178
257
|
const pos = mainCamera.position
|
|
@@ -202,12 +281,14 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
202
281
|
restoreLevels = snapLevelsToTruePositions()
|
|
203
282
|
}
|
|
204
283
|
|
|
205
|
-
// Hide scan and
|
|
206
|
-
// thumbnail regardless of whether ScanSystem/GuideSystem listeners
|
|
207
|
-
// registered.
|
|
284
|
+
// Hide scan, guide, and spawn nodes directly so they are excluded from
|
|
285
|
+
// the thumbnail regardless of whether ScanSystem/GuideSystem listeners
|
|
286
|
+
// are registered. Spawn renders on SCENE_LAYER for occlusion, so the
|
|
287
|
+
// thumbnail camera's layer mask can't filter it either. Returns a
|
|
288
|
+
// function that restores the original visibility.
|
|
208
289
|
const restoreNodeVisibility = (() => {
|
|
209
290
|
const saved = new Map<THREE.Object3D, boolean>()
|
|
210
|
-
for (const type of ['scan', 'guide'] as const) {
|
|
291
|
+
for (const type of ['scan', 'guide', 'spawn'] as const) {
|
|
211
292
|
const ids = sceneRegistry.byType[type]!
|
|
212
293
|
ids.forEach((id) => {
|
|
213
294
|
const node = sceneRegistry.nodes.get(id)
|
|
@@ -238,18 +319,21 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
238
319
|
|
|
239
320
|
// Notify other systems (wall cutouts, selection manager) to restore
|
|
240
321
|
// their overrides before capture and re-apply them after.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
322
|
+
try {
|
|
323
|
+
emitter.emit('thumbnail:before-capture', undefined)
|
|
324
|
+
;(renderer as any).setClearAlpha(0)
|
|
325
|
+
renderer.setRenderTarget(rt)
|
|
326
|
+
pipelineRef.current.render()
|
|
327
|
+
} finally {
|
|
328
|
+
// Restore level positions, levelMode, and node visibility immediately
|
|
329
|
+
// after the render — before the async GPU readback. Runs in `finally`
|
|
330
|
+
// so a render failure can't leave helpers permanently hidden.
|
|
331
|
+
renderer.setRenderTarget(null)
|
|
332
|
+
emitter.emit('thumbnail:after-capture', undefined)
|
|
333
|
+
restoreLevels()
|
|
334
|
+
restoreLevelMode?.()
|
|
335
|
+
restoreNodeVisibility()
|
|
336
|
+
}
|
|
253
337
|
|
|
254
338
|
// Read pixels from the RT asynchronously.
|
|
255
339
|
// WebGPU copyTextureToBuffer aligns each row to 256 bytes, so we must
|
|
@@ -337,9 +421,9 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
337
421
|
offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
|
|
338
422
|
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
|
339
423
|
} else {
|
|
340
|
-
// Standard: center-crop to 1920×1080
|
|
424
|
+
// Standard: center-crop to the requested aspect (default 1920×1080)
|
|
341
425
|
const srcAspect = width / height
|
|
342
|
-
const dstAspect =
|
|
426
|
+
const dstAspect = standardW / standardH
|
|
343
427
|
let sx = 0,
|
|
344
428
|
sy = 0,
|
|
345
429
|
sWidth = width,
|
|
@@ -351,8 +435,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
351
435
|
sHeight = Math.round(width / dstAspect)
|
|
352
436
|
sy = Math.round((height - sHeight) / 2)
|
|
353
437
|
}
|
|
354
|
-
outW =
|
|
355
|
-
outH =
|
|
438
|
+
outW = standardW
|
|
439
|
+
outH = standardH
|
|
356
440
|
const offscreen = new OffscreenCanvas(outW, outH)
|
|
357
441
|
offscreen
|
|
358
442
|
.getContext('2d')!
|
|
@@ -364,12 +448,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
364
448
|
cameraData.resolution = { w: outW, h: outH }
|
|
365
449
|
} else {
|
|
366
450
|
// Fallback: plain render directly to the canvas
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
451
|
+
try {
|
|
452
|
+
emitter.emit('thumbnail:before-capture', undefined)
|
|
453
|
+
gl.render(scene, thumbnailCamera)
|
|
454
|
+
} finally {
|
|
455
|
+
emitter.emit('thumbnail:after-capture', undefined)
|
|
456
|
+
restoreLevels()
|
|
457
|
+
restoreLevelMode?.()
|
|
458
|
+
restoreNodeVisibility()
|
|
459
|
+
}
|
|
373
460
|
|
|
374
461
|
let outW: number
|
|
375
462
|
let outH: number
|
|
@@ -406,7 +493,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
406
493
|
)
|
|
407
494
|
} else {
|
|
408
495
|
const srcAspect = width / height
|
|
409
|
-
const dstAspect =
|
|
496
|
+
const dstAspect = standardW / standardH
|
|
410
497
|
let sx = 0,
|
|
411
498
|
sy = 0,
|
|
412
499
|
sWidth = width,
|
|
@@ -418,8 +505,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
418
505
|
sHeight = Math.round(width / dstAspect)
|
|
419
506
|
sy = Math.round((height - sHeight) / 2)
|
|
420
507
|
}
|
|
421
|
-
outW =
|
|
422
|
-
outH =
|
|
508
|
+
outW = standardW
|
|
509
|
+
outH = standardH
|
|
423
510
|
const offscreen = document.createElement('canvas')
|
|
424
511
|
offscreen.width = outW
|
|
425
512
|
offscreen.height = outH
|
|
@@ -460,14 +547,20 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
460
547
|
const handleGenerateThumbnail = async (event: {
|
|
461
548
|
captureMode?: 'standard' | 'viewport' | 'area'
|
|
462
549
|
cropRegion?: { x: number; y: number; width: number; height: number }
|
|
550
|
+
standardSize?: { w: number; h: number }
|
|
463
551
|
snapLevels?: boolean
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// background bits) can branch on it without touching the emitter.
|
|
552
|
+
// Preset/item captures keep the alpha channel (their thumbnails compose
|
|
553
|
+
// onto arbitrary palette backgrounds); scene snapshots — studio renders
|
|
554
|
+
// and project thumbnails — composite the theme backdrop + sky.
|
|
468
555
|
transparent?: boolean
|
|
469
556
|
}) => {
|
|
470
|
-
await generate(
|
|
557
|
+
await generate(
|
|
558
|
+
event.snapLevels === true,
|
|
559
|
+
event.captureMode,
|
|
560
|
+
event.cropRegion,
|
|
561
|
+
event.standardSize,
|
|
562
|
+
event.transparent === true,
|
|
563
|
+
)
|
|
471
564
|
}
|
|
472
565
|
|
|
473
566
|
emitter.on('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
@@ -2,24 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import { emitter, type FenceNode, isCurvedWall, type WallNode } from '@pascal-app/core'
|
|
4
4
|
import { type MouseEvent as ReactMouseEvent, useCallback } from 'react'
|
|
5
|
+
import { resolveCeilingPlanPointSnap } from '../../lib/ceiling-plan-snap'
|
|
5
6
|
import { alignFloorplanDraftPoint, getPlanPointDistance } from '../../lib/floorplan'
|
|
7
|
+
import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
|
|
8
|
+
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
9
|
+
import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
10
|
+
import usePlacementPreview from '../../store/use-placement-preview'
|
|
11
|
+
import useSegmentDraftChain from '../../store/use-segment-draft-chain'
|
|
6
12
|
import { snapFenceDraftPoint } from '../tools/fence/fence-drafting'
|
|
7
|
-
import {
|
|
8
|
-
WALL_FINE_GRID_STEP,
|
|
9
|
-
WALL_GRID_STEP,
|
|
10
|
-
type WallPlanPoint,
|
|
11
|
-
} from '../tools/wall/wall-drafting'
|
|
13
|
+
import { getSegmentGridStep, type WallPlanPoint } from '../tools/wall/wall-drafting'
|
|
12
14
|
|
|
13
15
|
type UseFloorplanBackgroundPlacementArgs = {
|
|
14
16
|
activePolygonDraftPoints: WallPlanPoint[]
|
|
15
17
|
ceilingDraftPoints: WallPlanPoint[]
|
|
16
18
|
clearFencePlacementDraft: () => void
|
|
17
19
|
clearRoofPlacementDraft: () => void
|
|
20
|
+
clearWallPlacementDraft: () => void
|
|
18
21
|
emitFloorplanGridEvent: (
|
|
19
22
|
type: 'click' | 'double-click' | 'move',
|
|
20
23
|
planPoint: WallPlanPoint,
|
|
21
24
|
event: ReactMouseEvent<SVGSVGElement>,
|
|
22
|
-
) =>
|
|
25
|
+
) => void
|
|
23
26
|
fenceDraftStart: WallPlanPoint | null
|
|
24
27
|
fences: FenceNode[]
|
|
25
28
|
findClosestWallPoint: (
|
|
@@ -40,7 +43,7 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
40
43
|
) => boolean
|
|
41
44
|
handleCeilingPlacementPoint: (point: WallPlanPoint) => void
|
|
42
45
|
handleSlabPlacementPoint: (point: WallPlanPoint) => void
|
|
43
|
-
handleWallPlacementPoint: (point: WallPlanPoint
|
|
46
|
+
handleWallPlacementPoint: (point: WallPlanPoint) => void
|
|
44
47
|
handleZonePlacementPoint: (point: WallPlanPoint) => void
|
|
45
48
|
isCeilingBuildActive: boolean
|
|
46
49
|
isCeilingItemPlacementActive: boolean
|
|
@@ -51,18 +54,19 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
51
54
|
isRoofBuildActive: boolean
|
|
52
55
|
isWallBuildActive: boolean
|
|
53
56
|
isZoneBuildActive: boolean
|
|
57
|
+
levelId: string | null
|
|
54
58
|
roofDraftStart: WallPlanPoint | null
|
|
55
59
|
setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
56
60
|
setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
57
61
|
setFenceDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
58
62
|
setRoofDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
59
63
|
setRoofDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
|
|
60
|
-
shiftPressed: boolean
|
|
61
64
|
snapWallDraftPoint: (args: {
|
|
62
65
|
point: WallPlanPoint
|
|
63
66
|
walls: WallNode[]
|
|
64
67
|
start?: WallPlanPoint
|
|
65
68
|
angleSnap?: boolean
|
|
69
|
+
bypassSnap?: boolean
|
|
66
70
|
step?: number
|
|
67
71
|
gridSnap?: (point: WallPlanPoint) => WallPlanPoint
|
|
68
72
|
}) => WallPlanPoint
|
|
@@ -77,7 +81,7 @@ type UseFloorplanBackgroundPlacementArgs = {
|
|
|
77
81
|
* Snap a building-local plan point to the world XZ grid at `step`.
|
|
78
82
|
* Injected so the hook doesn't have to know the building's rotation
|
|
79
83
|
* or position — used by wall / fence branches that snap at variable
|
|
80
|
-
* step
|
|
84
|
+
* step.
|
|
81
85
|
*/
|
|
82
86
|
worldGridSnap: (point: WallPlanPoint, step: number) => WallPlanPoint
|
|
83
87
|
}
|
|
@@ -87,6 +91,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
87
91
|
ceilingDraftPoints,
|
|
88
92
|
clearFencePlacementDraft,
|
|
89
93
|
clearRoofPlacementDraft,
|
|
94
|
+
clearWallPlacementDraft,
|
|
90
95
|
emitFloorplanGridEvent,
|
|
91
96
|
fenceDraftStart,
|
|
92
97
|
fences,
|
|
@@ -107,13 +112,13 @@ export function useFloorplanBackgroundPlacement({
|
|
|
107
112
|
isRoofBuildActive,
|
|
108
113
|
isWallBuildActive,
|
|
109
114
|
isZoneBuildActive,
|
|
115
|
+
levelId,
|
|
110
116
|
roofDraftStart,
|
|
111
117
|
setCursorPoint,
|
|
112
118
|
setFenceDraftEnd,
|
|
113
119
|
setFenceDraftStart,
|
|
114
120
|
setRoofDraftEnd,
|
|
115
121
|
setRoofDraftStart,
|
|
116
|
-
shiftPressed,
|
|
117
122
|
snapWallDraftPoint,
|
|
118
123
|
snapPolygonDraftPoint,
|
|
119
124
|
toPoint2D,
|
|
@@ -144,22 +149,29 @@ export function useFloorplanBackgroundPlacement({
|
|
|
144
149
|
stopPropagation: () => {},
|
|
145
150
|
} as any)
|
|
146
151
|
}
|
|
152
|
+
// Drop the off-wall ghost on commit so it doesn't linger at the
|
|
153
|
+
// just-placed spot before the next pointer move re-evaluates.
|
|
154
|
+
usePlacementPreview.getState().clear()
|
|
147
155
|
return true
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
if (isCeilingBuildActive) {
|
|
151
|
-
// Align the committed vertex the same way the move-preview did, so
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
// Align the committed vertex the same way the move-preview did, so the
|
|
160
|
+
// placed point matches what the user saw — mode-driven (the chip):
|
|
161
|
+
// `grid` quantizes, `angles` locks 15° rays, `lines` snaps onto walls /
|
|
162
|
+
// alignment, `off` is free. Alt remains force/free at commit time.
|
|
163
|
+
const angleSnap = ceilingDraftPoints.length > 0 && isAngleSnapActive()
|
|
164
|
+
const fallbackPoint = snapPolygonDraftPoint({
|
|
156
165
|
point: planPoint,
|
|
157
166
|
start: ceilingDraftPoints[ceilingDraftPoints.length - 1],
|
|
158
167
|
angleSnap,
|
|
159
168
|
})
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
const snappedPoint = resolveCeilingPlanPointSnap({
|
|
170
|
+
rawPoint: planPoint,
|
|
171
|
+
fallbackPoint,
|
|
172
|
+
levelId,
|
|
173
|
+
align: !angleSnap,
|
|
174
|
+
}).point
|
|
163
175
|
|
|
164
176
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
165
177
|
handleCeilingPlacementPoint(snappedPoint)
|
|
@@ -167,8 +179,11 @@ export function useFloorplanBackgroundPlacement({
|
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
if (isRoofBuildActive) {
|
|
182
|
+
// Footprint placement (polygon context: grid / lines / off, no angle),
|
|
183
|
+
// mode-driven to match the chip. Alt is force/free at commit time;
|
|
184
|
+
// alignment display/pull follows the active magnetic mode.
|
|
170
185
|
const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
|
|
171
|
-
|
|
186
|
+
applySnap: isMagneticSnapActive(),
|
|
172
187
|
})
|
|
173
188
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
174
189
|
setCursorPoint(snappedPoint)
|
|
@@ -183,16 +198,19 @@ export function useFloorplanBackgroundPlacement({
|
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
if (isFenceBuildActive) {
|
|
186
|
-
// Fence draft:
|
|
187
|
-
//
|
|
188
|
-
// `gridSnap`
|
|
189
|
-
//
|
|
190
|
-
const fenceStep =
|
|
201
|
+
// Fence draft: mode-driven (matches the chip), same as the move
|
|
202
|
+
// preview. `grid` snaps to the world XZ grid (rotation-safe via the
|
|
203
|
+
// `gridSnap` callback), `angles` locks 15° rays from the start, `lines`
|
|
204
|
+
// pulls onto walls / fences / alignment, `off` is free.
|
|
205
|
+
const fenceStep = getSegmentGridStep()
|
|
206
|
+
const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
|
|
191
207
|
const fenceSnapped = snapFenceDraftPoint({
|
|
192
208
|
point: planPoint,
|
|
193
209
|
walls,
|
|
194
210
|
fences,
|
|
195
|
-
|
|
211
|
+
start: fenceDraftStart ?? undefined,
|
|
212
|
+
angleSnap: fenceAngleSnap,
|
|
213
|
+
magnetic: isMagneticSnapActive(),
|
|
196
214
|
gridSnap: (p) => worldGridSnap(p, fenceStep),
|
|
197
215
|
})
|
|
198
216
|
const fenceGridBase = worldGridSnap(planPoint, fenceStep)
|
|
@@ -200,18 +218,42 @@ export function useFloorplanBackgroundPlacement({
|
|
|
200
218
|
fenceSnapped[0] !== fenceGridBase[0] || fenceSnapped[1] !== fenceGridBase[1]
|
|
201
219
|
const snappedPoint = fenceLocked
|
|
202
220
|
? fenceSnapped
|
|
203
|
-
: alignFloorplanDraftPoint(fenceSnapped, {
|
|
221
|
+
: alignFloorplanDraftPoint(fenceSnapped, {
|
|
222
|
+
applySnap: isMagneticSnapActive() && !fenceAngleSnap,
|
|
223
|
+
})
|
|
204
224
|
|
|
205
225
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
206
226
|
setCursorPoint(snappedPoint)
|
|
207
227
|
|
|
228
|
+
// Double-click finishes the chain. The emit above already made the
|
|
229
|
+
// 3D fence tool stopDrafting (its detail >= 2 guard), so close the
|
|
230
|
+
// 2D draft too — leaving it open desyncs the two views.
|
|
231
|
+
if (fenceDraftStart && event.detail >= 2) {
|
|
232
|
+
clearFencePlacementDraft()
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
|
|
208
236
|
if (!fenceDraftStart) {
|
|
209
237
|
setFenceDraftStart(snappedPoint)
|
|
210
238
|
setFenceDraftEnd(snappedPoint)
|
|
211
239
|
} else if (
|
|
212
240
|
getPlanPointDistance(toPoint2D(fenceDraftStart), toPoint2D(snappedPoint)) >= 0.01
|
|
213
241
|
) {
|
|
214
|
-
|
|
242
|
+
// Single mode commits one segment per click: the same emit above
|
|
243
|
+
// already made the 3D fence tool stopDrafting, so close the 2D
|
|
244
|
+
// draft too instead of chaining.
|
|
245
|
+
if (useEditor.getState().getContinuation('fence') === 'single') {
|
|
246
|
+
clearFencePlacementDraft()
|
|
247
|
+
setCursorPoint(snappedPoint)
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
// The 3D fence tool owns creation and keeps chaining from the
|
|
251
|
+
// committed fence's resolved end — chain the 2D draft from the
|
|
252
|
+
// same published point so both views draft the next segment
|
|
253
|
+
// from the same start.
|
|
254
|
+
const nextStart = useSegmentDraftChain.getState().fence ?? snappedPoint
|
|
255
|
+
setFenceDraftStart(nextStart)
|
|
256
|
+
setFenceDraftEnd(nextStart)
|
|
215
257
|
} else {
|
|
216
258
|
setFenceDraftEnd(snappedPoint)
|
|
217
259
|
}
|
|
@@ -224,15 +266,20 @@ export function useFloorplanBackgroundPlacement({
|
|
|
224
266
|
// swallow the click and skip local draft state updates — leaving
|
|
225
267
|
// the 2D draft polygon invisible while the 3D tool builds fine).
|
|
226
268
|
if (isPolygonBuildActive) {
|
|
227
|
-
const angleSnap = activePolygonDraftPoints.length > 0 &&
|
|
228
|
-
|
|
269
|
+
const angleSnap = activePolygonDraftPoints.length > 0 && isAngleSnapActive()
|
|
270
|
+
const fallbackPoint = snapPolygonDraftPoint({
|
|
229
271
|
point: planPoint,
|
|
230
272
|
start: activePolygonDraftPoints[activePolygonDraftPoints.length - 1],
|
|
231
273
|
angleSnap,
|
|
232
274
|
})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
275
|
+
// Zone shares the slab surface snap (wall corners / midpoints /
|
|
276
|
+
// crossings + alignment) — it's the same polygon-on-a-level draw.
|
|
277
|
+
const snappedPoint = resolveSlabPlanPointSnap({
|
|
278
|
+
rawPoint: planPoint,
|
|
279
|
+
fallbackPoint,
|
|
280
|
+
levelId,
|
|
281
|
+
align: !angleSnap,
|
|
282
|
+
}).point
|
|
236
283
|
|
|
237
284
|
// Emit the grid event so the registry-driven slab tool also
|
|
238
285
|
// sees the click (parity with ceiling / fence / roof branches
|
|
@@ -255,26 +302,45 @@ export function useFloorplanBackgroundPlacement({
|
|
|
255
302
|
// / draftEnd state in the floor plan would never update, leaving
|
|
256
303
|
// the dashed-line draft preview invisible.
|
|
257
304
|
if (isWallBuildActive) {
|
|
258
|
-
// Wall draft:
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
const
|
|
305
|
+
// Wall draft: mode-driven (matches the chip + the move-preview branch).
|
|
306
|
+
// `grid` snaps to the world XZ grid (rotation-safe via `gridSnap`),
|
|
307
|
+
// `angles` locks 15° rays from the start, `lines` pulls the endpoint
|
|
308
|
+
// onto existing wall corners / edges + alignment, `off` is free.
|
|
309
|
+
const wallStep = getSegmentGridStep()
|
|
310
|
+
const wallAngleSnap = draftStart !== null && isAngleSnapActive()
|
|
264
311
|
const wallSnapped = snapWallDraftPoint({
|
|
265
312
|
point: planPoint,
|
|
266
313
|
walls,
|
|
267
|
-
|
|
314
|
+
start: draftStart ?? undefined,
|
|
315
|
+
angleSnap: wallAngleSnap,
|
|
268
316
|
gridSnap: (p) => worldGridSnap(p, wallStep),
|
|
269
317
|
})
|
|
270
318
|
const wallGridBase = worldGridSnap(planPoint, wallStep)
|
|
271
319
|
const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
320
|
+
let snappedPoint = wallSnapped
|
|
321
|
+
if (wallLocked) {
|
|
322
|
+
useAlignmentGuides.getState().clear()
|
|
323
|
+
} else {
|
|
324
|
+
// Alignment lines are shown in every mode; the pull applies only when
|
|
325
|
+
// magnetic ('lines') and the segment isn't angle-locked.
|
|
326
|
+
snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
|
|
327
|
+
applySnap: isMagneticSnapActive() && !wallAngleSnap,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
275
330
|
|
|
276
331
|
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
277
|
-
|
|
332
|
+
|
|
333
|
+
// Double-click finishes the chain. The emit above already made the
|
|
334
|
+
// 3D wall tool stopDrafting (its detail >= 2 guard), so close the
|
|
335
|
+
// 2D draft too — otherwise it stays open against a closed 3D tool
|
|
336
|
+
// and the next previewed segment is silently never created.
|
|
337
|
+
if (draftStart && event.detail >= 2) {
|
|
338
|
+
clearWallPlacementDraft()
|
|
339
|
+
setCursorPoint(snappedPoint)
|
|
340
|
+
return true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
handleWallPlacementPoint(snappedPoint)
|
|
278
344
|
return true
|
|
279
345
|
}
|
|
280
346
|
|
|
@@ -291,7 +357,8 @@ export function useFloorplanBackgroundPlacement({
|
|
|
291
357
|
// local floor-plan draft handler (column / spawn / shelf / etc.).
|
|
292
358
|
// The tool's `grid:click` subscriber owns the placement.
|
|
293
359
|
if (isFloorplanGridInteractionActive) {
|
|
294
|
-
const snappedPoint =
|
|
360
|
+
const snappedPoint = getSnappedFloorplanPoint(planPoint)
|
|
361
|
+
emitFloorplanGridEvent('click', snappedPoint, event)
|
|
295
362
|
setCursorPoint(snappedPoint)
|
|
296
363
|
return true
|
|
297
364
|
}
|
|
@@ -303,6 +370,7 @@ export function useFloorplanBackgroundPlacement({
|
|
|
303
370
|
ceilingDraftPoints,
|
|
304
371
|
clearFencePlacementDraft,
|
|
305
372
|
clearRoofPlacementDraft,
|
|
373
|
+
clearWallPlacementDraft,
|
|
306
374
|
emitFloorplanGridEvent,
|
|
307
375
|
fenceDraftStart,
|
|
308
376
|
fences,
|
|
@@ -322,13 +390,13 @@ export function useFloorplanBackgroundPlacement({
|
|
|
322
390
|
isRoofBuildActive,
|
|
323
391
|
isWallBuildActive,
|
|
324
392
|
isZoneBuildActive,
|
|
393
|
+
levelId,
|
|
325
394
|
roofDraftStart,
|
|
326
395
|
setCursorPoint,
|
|
327
396
|
setFenceDraftEnd,
|
|
328
397
|
setFenceDraftStart,
|
|
329
398
|
setRoofDraftEnd,
|
|
330
399
|
setRoofDraftStart,
|
|
331
|
-
shiftPressed,
|
|
332
400
|
snapWallDraftPoint,
|
|
333
401
|
snapPolygonDraftPoint,
|
|
334
402
|
toPoint2D,
|