@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
|
@@ -1,47 +1,25 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { emitter
|
|
3
|
+
import { emitter } from '@pascal-app/core'
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
computeHeroFraming,
|
|
6
|
+
createSnapshotPipeline,
|
|
7
7
|
GRID_LAYER,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
packNormalToRGB,
|
|
11
|
-
SSGI_PARAMS,
|
|
8
|
+
heroCameraPose,
|
|
9
|
+
type SnapshotPipeline,
|
|
12
10
|
snapLevelsToTruePositions,
|
|
13
|
-
|
|
11
|
+
THUMBNAIL_HEIGHT,
|
|
12
|
+
THUMBNAIL_WIDTH,
|
|
13
|
+
temporarilyHideNodeTypes,
|
|
14
14
|
useViewer,
|
|
15
15
|
} from '@pascal-app/viewer'
|
|
16
16
|
import type { CameraControls } from '@react-three/drei'
|
|
17
17
|
import { useThree } from '@react-three/fiber'
|
|
18
18
|
import { useCallback, useEffect, useRef } from 'react'
|
|
19
19
|
import * as THREE from 'three'
|
|
20
|
-
import {
|
|
21
|
-
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
|
22
|
-
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
|
|
23
|
-
import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js'
|
|
24
|
-
import {
|
|
25
|
-
convertToTexture,
|
|
26
|
-
diffuseColor,
|
|
27
|
-
float,
|
|
28
|
-
mix,
|
|
29
|
-
mrt,
|
|
30
|
-
normalView,
|
|
31
|
-
output,
|
|
32
|
-
pass,
|
|
33
|
-
sample,
|
|
34
|
-
screenUV,
|
|
35
|
-
smoothstep,
|
|
36
|
-
uniform,
|
|
37
|
-
vec4,
|
|
38
|
-
} from 'three/tsl'
|
|
39
|
-
import { RenderPipeline, RenderTarget, type WebGPURenderer } from 'three/webgpu'
|
|
20
|
+
import type { WebGPURenderer } from 'three/webgpu'
|
|
40
21
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
41
22
|
|
|
42
|
-
const THUMBNAIL_WIDTH = 1920
|
|
43
|
-
const THUMBNAIL_HEIGHT = 1080
|
|
44
|
-
|
|
45
23
|
export interface SnapshotCameraData {
|
|
46
24
|
position: [number, number, number]
|
|
47
25
|
target: [number, number, number] | null
|
|
@@ -64,20 +42,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
64
42
|
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
|
65
43
|
|
|
66
44
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
|
67
|
-
const pipelineRef = useRef<
|
|
68
|
-
const renderTargetRef = useRef<RenderTarget | null>(null)
|
|
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))
|
|
45
|
+
const pipelineRef = useRef<SnapshotPipeline | null>(null)
|
|
81
46
|
|
|
82
47
|
useEffect(() => {
|
|
83
48
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
@@ -93,116 +58,24 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
93
58
|
let mounted = true
|
|
94
59
|
|
|
95
60
|
const buildPipeline = async () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
scenePass.setMRT(
|
|
105
|
-
mrt({
|
|
106
|
-
output,
|
|
107
|
-
diffuseColor,
|
|
108
|
-
normal: packNormalToRGB(normalView),
|
|
109
|
-
}),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
const scenePassColor = scenePass.getTextureNode('output')
|
|
113
|
-
const scenePassDepth = scenePass.getTextureNode('depth')
|
|
114
|
-
const scenePassNormal = scenePass.getTextureNode('normal')
|
|
115
|
-
|
|
116
|
-
scenePass.getTexture('diffuseColor').type = UnsignedByteType
|
|
117
|
-
scenePass.getTexture('normal').type = UnsignedByteType
|
|
118
|
-
|
|
119
|
-
const sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)))
|
|
120
|
-
|
|
121
|
-
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, cam as any)
|
|
122
|
-
giPass.sliceCount.value = SSGI_PARAMS.sliceCount
|
|
123
|
-
giPass.stepCount.value = SSGI_PARAMS.stepCount
|
|
124
|
-
giPass.radius.value = SSGI_PARAMS.radius
|
|
125
|
-
giPass.expFactor.value = SSGI_PARAMS.expFactor
|
|
126
|
-
giPass.thickness.value = SSGI_PARAMS.thickness
|
|
127
|
-
giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting
|
|
128
|
-
giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity
|
|
129
|
-
giPass.giIntensity.value = SSGI_PARAMS.giIntensity
|
|
130
|
-
giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness
|
|
131
|
-
giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling
|
|
132
|
-
giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering
|
|
133
|
-
|
|
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))
|
|
138
|
-
const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, cam)
|
|
139
|
-
denoisePass.index.value = 0
|
|
140
|
-
denoisePass.radius.value = 4
|
|
141
|
-
|
|
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
|
-
)
|
|
177
|
-
|
|
178
|
-
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
|
179
|
-
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
|
180
|
-
const aaOutput = fxaa(convertToTexture(finalOutput))
|
|
181
|
-
|
|
182
|
-
const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer)
|
|
183
|
-
pipeline.outputNode = aaOutput
|
|
184
|
-
pipelineRef.current = pipeline
|
|
185
|
-
|
|
186
|
-
// Dedicated render target — pipeline outputs here instead of the canvas,
|
|
187
|
-
// so R3F's main render loop can never overwrite our capture.
|
|
188
|
-
const { width, height } = gl.domElement
|
|
189
|
-
renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true })
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.error(
|
|
192
|
-
'[thumbnail] Failed to build post-processing pipeline, will use fallback render.',
|
|
193
|
-
error,
|
|
194
|
-
)
|
|
61
|
+
const pipeline = await createSnapshotPipeline({
|
|
62
|
+
renderer: gl as unknown as WebGPURenderer,
|
|
63
|
+
scene,
|
|
64
|
+
camera: cam,
|
|
65
|
+
})
|
|
66
|
+
if (!mounted) {
|
|
67
|
+
pipeline?.dispose()
|
|
68
|
+
return
|
|
195
69
|
}
|
|
70
|
+
pipelineRef.current = pipeline
|
|
196
71
|
}
|
|
197
72
|
|
|
198
|
-
buildPipeline()
|
|
73
|
+
void buildPipeline()
|
|
199
74
|
|
|
200
75
|
return () => {
|
|
201
76
|
mounted = false
|
|
202
77
|
pipelineRef.current?.dispose()
|
|
203
78
|
pipelineRef.current = null
|
|
204
|
-
renderTargetRef.current?.dispose()
|
|
205
|
-
renderTargetRef.current = null
|
|
206
79
|
}
|
|
207
80
|
}, [gl, scene])
|
|
208
81
|
|
|
@@ -242,16 +115,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
242
115
|
// uniforms below.
|
|
243
116
|
thumbnailCamera.updateMatrixWorld()
|
|
244
117
|
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
bgMixUniform.current.value = transparent ? 0 : 1
|
|
118
|
+
const pipeline = pipelineRef.current
|
|
119
|
+
pipeline?.applyEnvironment({
|
|
120
|
+
theme: useViewer.getState().sceneTheme,
|
|
121
|
+
transparent,
|
|
122
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
123
|
+
// Preset/item captures stay clean; scene captures mirror the canvas.
|
|
124
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
125
|
+
camera: thumbnailCamera,
|
|
126
|
+
})
|
|
255
127
|
|
|
256
128
|
// Capture camera data for snapshot storage
|
|
257
129
|
const pos = mainCamera.position
|
|
@@ -286,166 +158,66 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
286
158
|
// are registered. Spawn renders on SCENE_LAYER for occlusion, so the
|
|
287
159
|
// thumbnail camera's layer mask can't filter it either. Returns a
|
|
288
160
|
// function that restores the original visibility.
|
|
289
|
-
const restoreNodeVisibility = (
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
161
|
+
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
|
|
162
|
+
|
|
163
|
+
// Auto-save shots don't copy the user's mid-edit camera — they re-pose
|
|
164
|
+
// onto the same computed hero angle the published thumbnail uses, so a
|
|
165
|
+
// project's card never shows a half-zoomed working view. Measured after
|
|
166
|
+
// the level snap so stacked positions frame correctly. User-driven
|
|
167
|
+
// captures (captureMode set) keep the exact viewport pose.
|
|
168
|
+
if (snapLevels) {
|
|
169
|
+
const framing = computeHeroFraming()
|
|
170
|
+
if (framing) {
|
|
171
|
+
const pose = heroCameraPose({
|
|
172
|
+
boxes: framing.boxes,
|
|
173
|
+
aim: framing.aim,
|
|
174
|
+
azimuthRad: framing.azimuthRad,
|
|
175
|
+
aspect: width / height,
|
|
299
176
|
})
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
177
|
+
thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
178
|
+
thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
179
|
+
thumbnailCamera.updateMatrixWorld()
|
|
180
|
+
pipeline?.applyEnvironment({
|
|
181
|
+
theme: useViewer.getState().sceneTheme,
|
|
182
|
+
transparent,
|
|
183
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
184
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
185
|
+
camera: thumbnailCamera,
|
|
304
186
|
})
|
|
187
|
+
cameraData.position = pose.position
|
|
188
|
+
cameraData.target = pose.target
|
|
305
189
|
}
|
|
306
|
-
}
|
|
190
|
+
}
|
|
307
191
|
|
|
308
192
|
let blob: Blob
|
|
309
193
|
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Resize RT if the canvas dimensions changed
|
|
314
|
-
if (rt.width !== width || rt.height !== height) {
|
|
315
|
-
rt.setSize(width, height)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const renderer = gl as unknown as WebGPURenderer
|
|
194
|
+
if (pipeline) {
|
|
195
|
+
let capturePromise: ReturnType<SnapshotPipeline['capture']>
|
|
319
196
|
|
|
320
197
|
// Notify other systems (wall cutouts, selection manager) to restore
|
|
321
198
|
// their overrides before capture and re-apply them after.
|
|
322
199
|
try {
|
|
323
200
|
emitter.emit('thumbnail:before-capture', undefined)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
201
|
+
capturePromise = pipeline.capture({
|
|
202
|
+
captureMode,
|
|
203
|
+
cropRegion,
|
|
204
|
+
standardSize,
|
|
205
|
+
})
|
|
327
206
|
} finally {
|
|
328
207
|
// Restore level positions, levelMode, and node visibility immediately
|
|
329
208
|
// after the render — before the async GPU readback. Runs in `finally`
|
|
330
209
|
// so a render failure can't leave helpers permanently hidden.
|
|
331
|
-
renderer.setRenderTarget(null)
|
|
332
210
|
emitter.emit('thumbnail:after-capture', undefined)
|
|
333
211
|
restoreLevels()
|
|
334
212
|
restoreLevelMode?.()
|
|
335
213
|
restoreNodeVisibility()
|
|
336
214
|
}
|
|
337
215
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
// depad the rows before constructing ImageData.
|
|
341
|
-
const pixels = (await (renderer as any).readRenderTargetPixelsAsync(
|
|
342
|
-
rt,
|
|
343
|
-
0,
|
|
344
|
-
0,
|
|
345
|
-
width,
|
|
346
|
-
height,
|
|
347
|
-
)) as Uint8Array
|
|
348
|
-
|
|
349
|
-
const actualBytesPerRow = width * 4
|
|
350
|
-
const tightTotal = actualBytesPerRow * height
|
|
351
|
-
const paddedBytesPerRow = Math.ceil(actualBytesPerRow / 256) * 256
|
|
352
|
-
// Two readback shapes to handle:
|
|
353
|
-
// - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
|
|
354
|
-
// when width*4 isn't already a multiple of 256.
|
|
355
|
-
// - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
|
|
356
|
-
// (OpenGL framebuffer convention).
|
|
357
|
-
// `isWebGPURenderer` lies — it stays true even when the renderer
|
|
358
|
-
// falls back to the WebGL backend. Inspect the actual backend
|
|
359
|
-
// instead (presence of a GPU device, or backend constructor name).
|
|
360
|
-
const backend = (renderer as any).backend
|
|
361
|
-
const isWebGPU =
|
|
362
|
-
!!backend?.device ||
|
|
363
|
-
backend?.isWebGPUBackend === true ||
|
|
364
|
-
backend?.constructor?.name === 'WebGPUBackend'
|
|
365
|
-
let tightPixels: Uint8ClampedArray
|
|
366
|
-
if (isWebGPU) {
|
|
367
|
-
// WebGPU: depad rows if needed; orientation is already top-down.
|
|
368
|
-
if (paddedBytesPerRow === actualBytesPerRow) {
|
|
369
|
-
tightPixels = new Uint8ClampedArray(
|
|
370
|
-
pixels.buffer,
|
|
371
|
-
pixels.byteOffset,
|
|
372
|
-
Math.min(pixels.byteLength, tightTotal),
|
|
373
|
-
)
|
|
374
|
-
} else {
|
|
375
|
-
tightPixels = new Uint8ClampedArray(tightTotal)
|
|
376
|
-
for (let row = 0; row < height; row++) {
|
|
377
|
-
tightPixels.set(
|
|
378
|
-
pixels.subarray(
|
|
379
|
-
row * paddedBytesPerRow,
|
|
380
|
-
row * paddedBytesPerRow + actualBytesPerRow,
|
|
381
|
-
),
|
|
382
|
-
row * actualBytesPerRow,
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
// WebGL2: tight buffer in bottom-up order — flip rows.
|
|
388
|
-
tightPixels = new Uint8ClampedArray(tightTotal)
|
|
389
|
-
for (let row = 0; row < height; row++) {
|
|
390
|
-
const srcStart = (height - 1 - row) * actualBytesPerRow
|
|
391
|
-
tightPixels.set(
|
|
392
|
-
pixels.subarray(srcStart, srcStart + actualBytesPerRow),
|
|
393
|
-
row * actualBytesPerRow,
|
|
394
|
-
)
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const imageData = new ImageData(
|
|
399
|
-
tightPixels as unknown as Uint8ClampedArray<ArrayBuffer>,
|
|
400
|
-
width,
|
|
401
|
-
height,
|
|
402
|
-
)
|
|
403
|
-
const srcCanvas = new OffscreenCanvas(width, height)
|
|
404
|
-
srcCanvas.getContext('2d')!.putImageData(imageData, 0, 0)
|
|
405
|
-
|
|
406
|
-
let outW: number
|
|
407
|
-
let outH: number
|
|
408
|
-
|
|
409
|
-
if (captureMode === 'viewport') {
|
|
410
|
-
outW = width
|
|
411
|
-
outH = height
|
|
412
|
-
const offscreen = new OffscreenCanvas(outW, outH)
|
|
413
|
-
offscreen.getContext('2d')!.drawImage(srcCanvas, 0, 0)
|
|
414
|
-
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
|
415
|
-
} else if (captureMode === 'area' && cropRegion) {
|
|
416
|
-
const sx = Math.round(cropRegion.x * width)
|
|
417
|
-
const sy = Math.round(cropRegion.y * height)
|
|
418
|
-
outW = Math.round(cropRegion.width * width)
|
|
419
|
-
outH = Math.round(cropRegion.height * height)
|
|
420
|
-
const offscreen = new OffscreenCanvas(outW, outH)
|
|
421
|
-
offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
|
|
422
|
-
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
|
423
|
-
} else {
|
|
424
|
-
// Standard: center-crop to the requested aspect (default 1920×1080)
|
|
425
|
-
const srcAspect = width / height
|
|
426
|
-
const dstAspect = standardW / standardH
|
|
427
|
-
let sx = 0,
|
|
428
|
-
sy = 0,
|
|
429
|
-
sWidth = width,
|
|
430
|
-
sHeight = height
|
|
431
|
-
if (srcAspect > dstAspect) {
|
|
432
|
-
sWidth = Math.round(height * dstAspect)
|
|
433
|
-
sx = Math.round((width - sWidth) / 2)
|
|
434
|
-
} else if (srcAspect < dstAspect) {
|
|
435
|
-
sHeight = Math.round(width / dstAspect)
|
|
436
|
-
sy = Math.round((height - sHeight) / 2)
|
|
437
|
-
}
|
|
438
|
-
outW = standardW
|
|
439
|
-
outH = standardH
|
|
440
|
-
const offscreen = new OffscreenCanvas(outW, outH)
|
|
441
|
-
offscreen
|
|
442
|
-
.getContext('2d')!
|
|
443
|
-
.drawImage(srcCanvas, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
444
|
-
blob = await offscreen.convertToBlob({ type: 'image/png' })
|
|
445
|
-
}
|
|
216
|
+
const result = await capturePromise
|
|
217
|
+
blob = result.blob
|
|
446
218
|
|
|
447
219
|
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
448
|
-
cameraData.resolution = { w: outW, h: outH }
|
|
220
|
+
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
449
221
|
} else {
|
|
450
222
|
// Fallback: plain render directly to the canvas
|
|
451
223
|
try {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { collectRegistrySelectionIdsInBounds } from './use-floorplan-hit-testing'
|
|
3
|
+
|
|
4
|
+
const identityMatrix = {
|
|
5
|
+
inverse() {
|
|
6
|
+
return this
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pointFactory() {
|
|
11
|
+
return {
|
|
12
|
+
x: 0,
|
|
13
|
+
y: 0,
|
|
14
|
+
matrixTransform() {
|
|
15
|
+
return pointFactoryWithPosition(this.x, this.y)
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pointFactoryWithPosition(x: number, y: number) {
|
|
21
|
+
return {
|
|
22
|
+
x,
|
|
23
|
+
y,
|
|
24
|
+
matrixTransform() {
|
|
25
|
+
return pointFactoryWithPosition(x, y)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function geometry(x: number, y: number, width: number, height: number) {
|
|
31
|
+
return {
|
|
32
|
+
getBBox: () => ({ x, y, width, height }),
|
|
33
|
+
getScreenCTM: () => identityMatrix,
|
|
34
|
+
} as unknown as SVGGraphicsElement
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entry(nodeId: string, children: SVGGraphicsElement[]) {
|
|
38
|
+
return {
|
|
39
|
+
dataset: { nodeId },
|
|
40
|
+
querySelectorAll: () => children,
|
|
41
|
+
} as unknown as SVGGElement
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scene(entries: SVGGElement[]) {
|
|
45
|
+
return {
|
|
46
|
+
getScreenCTM: () => identityMatrix,
|
|
47
|
+
ownerSVGElement: {
|
|
48
|
+
createSVGPoint: pointFactory,
|
|
49
|
+
},
|
|
50
|
+
querySelectorAll: () => entries,
|
|
51
|
+
} as unknown as SVGGElement
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('collectRegistrySelectionIdsInBounds', () => {
|
|
55
|
+
test('selects rendered registry geometry and preserves candidate order', () => {
|
|
56
|
+
const renderedScene = scene([
|
|
57
|
+
entry('wall_1', [geometry(0, 0, 4, 0.2)]),
|
|
58
|
+
entry('door_1', [geometry(1, 1, 0.9, 0.9)]),
|
|
59
|
+
entry('outside_1', [geometry(20, 20, 1, 1)]),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
collectRegistrySelectionIdsInBounds(renderedScene, { minX: -1, minY: -1, maxX: 5, maxY: 5 }, [
|
|
64
|
+
'door_1',
|
|
65
|
+
'outside_1',
|
|
66
|
+
'wall_1',
|
|
67
|
+
]),
|
|
68
|
+
).toEqual(['door_1', 'wall_1'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('ignores rendered entries outside the selectable registry candidates', () => {
|
|
72
|
+
const renderedScene = scene([entry('zone_1', [geometry(0, 0, 2, 2)])])
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
collectRegistrySelectionIdsInBounds(
|
|
76
|
+
renderedScene,
|
|
77
|
+
{ minX: -1, minY: -1, maxX: 3, maxY: 3 },
|
|
78
|
+
[],
|
|
79
|
+
),
|
|
80
|
+
).toEqual([])
|
|
81
|
+
})
|
|
82
|
+
})
|