@pascal-app/viewer 1.0.3 → 1.0.4
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/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +8 -3
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +252 -85
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +420 -506
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
- package/dist/components/viewer/index.d.ts +14 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +84 -26
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +8 -3
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +16 -2
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +10 -0
- package/dist/components/viewer/viewer-camera.d.ts +4 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -14
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/hero-pose.d.ts +12 -2
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +4 -2
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +14 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +31 -5
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +27 -4
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-visibility.d.ts +6 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +15 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/snapshot-pipeline.d.ts +32 -2
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +215 -105
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +11 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +13 -6
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +86 -6
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +55 -17
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +40 -15
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +47 -52
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +3 -5
- package/dist/systems/level/level-utils.d.ts +7 -4
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +17 -6
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/roof-materials.d.ts +14 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +54 -15
- package/dist/systems/roof/roof-system.d.ts +13 -3
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +188 -13
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +7 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +46 -17
- package/dist/systems/wall/wall-cutout.d.ts +6 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +42 -28
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-materials.d.ts +9 -4
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +99 -152
- package/dist/systems/wall/wall-system.d.ts +28 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +582 -143
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +41 -22
- package/package.json +5 -5
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Color, Matrix4, UnsignedByteType } from 'three';
|
|
1
|
+
import { Color, Matrix4, SRGBColorSpace, UnsignedByteType } from 'three';
|
|
2
2
|
import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
|
|
3
3
|
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
|
|
4
4
|
import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js';
|
|
5
|
-
import { convertToTexture, diffuseColor, float, mix, mrt, normalView, output, sample, saturation, screenUV, smoothstep, uniform, vec3, vec4, } from 'three/tsl';
|
|
5
|
+
import { clamp, convertToTexture, diffuseColor, float, mix, mrt, normalView, output, renderOutput, sample, saturation, screenUV, smoothstep, uniform, vec2, vec3, vec4, } from 'three/tsl';
|
|
6
6
|
import { PassNode, RenderPipeline, RenderTarget, } from 'three/webgpu';
|
|
7
7
|
import { GRADE_PARAMS, SSGI_PARAMS } from '../components/viewer/post-processing';
|
|
8
8
|
import { backdropGradient, deepSkyColor, horizonHazeColor } from './backdrop';
|
|
9
9
|
import { edgeColorFor, edgeOpacityScaleFor } from './edge-style';
|
|
10
|
-
import { inkedEdges } from './ink-edges';
|
|
10
|
+
import { createEdgeDepthSampler, inkedEdges } from './ink-edges';
|
|
11
11
|
import { getSceneTheme } from './scene-themes';
|
|
12
12
|
import { packNormalToRGB, unpackRGBToNormal } from './tsl-compat';
|
|
13
13
|
export const THUMBNAIL_WIDTH = 1920;
|
|
@@ -29,6 +29,11 @@ function clampSnapshotSize(width, height) {
|
|
|
29
29
|
const scale = SNAPSHOT_MAX_EDGE / maxEdge;
|
|
30
30
|
return { w: Math.round(width * scale), h: Math.round(height * scale) };
|
|
31
31
|
}
|
|
32
|
+
function srgbHexToVec3(hex) {
|
|
33
|
+
const color = new Color(hex);
|
|
34
|
+
const { r, g, b } = color.getRGB({ r: 0, g: 0, b: 0 }, SRGBColorSpace);
|
|
35
|
+
return vec3(r, g, b);
|
|
36
|
+
}
|
|
32
37
|
class SnapshotScenePass extends PassNode {
|
|
33
38
|
updateBefore(frame) {
|
|
34
39
|
const renderer = frame.renderer;
|
|
@@ -44,7 +49,7 @@ class SnapshotScenePass extends PassNode {
|
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
|
-
export async function createSnapshotPipeline({ renderer, scene, camera, atmosphere = null, }) {
|
|
52
|
+
export async function createSnapshotPipeline({ renderer, scene, camera, atmosphere = null, studioBackdrop, }) {
|
|
48
53
|
try {
|
|
49
54
|
if (renderer.init)
|
|
50
55
|
await renderer.init();
|
|
@@ -74,6 +79,7 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
74
79
|
}));
|
|
75
80
|
const scenePassColor = scenePass.getTextureNode('output');
|
|
76
81
|
const scenePassDepth = scenePass.getTextureNode('depth');
|
|
82
|
+
const sampleEdgeDepth = createEdgeDepthSampler(scenePassDepth, camera);
|
|
77
83
|
const scenePassNormal = scenePass.getTextureNode('normal');
|
|
78
84
|
scenePass.getTexture('diffuseColor').type = UnsignedByteType;
|
|
79
85
|
scenePass.getTexture('normal').type = UnsignedByteType;
|
|
@@ -99,7 +105,7 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
99
105
|
denoisePass.radius.value = 4;
|
|
100
106
|
// Same far-field AO fade as the viewport pipeline — without it the
|
|
101
107
|
// horizon picks up a visible AO line in captures.
|
|
102
|
-
const aoFarFade = smoothstep(float(0.9994), float(0.9998),
|
|
108
|
+
const aoFarFade = smoothstep(float(0.9994), float(0.9998), sampleEdgeDepth(screenUV));
|
|
103
109
|
const ao = mix(denoisePass.r, float(1), aoFarFade);
|
|
104
110
|
const aoRgb = scenePassColor.rgb.mul(ao);
|
|
105
111
|
// Ink edges, mirroring the viewport pipeline (AO → ink → grade) so
|
|
@@ -110,7 +116,7 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
110
116
|
const inkRadius = Math.max(1, Math.round(renderer.domElement.height / 1080));
|
|
111
117
|
const inkedRgb = inkedEdges({
|
|
112
118
|
sceneRgb: aoRgb,
|
|
113
|
-
|
|
119
|
+
sampleDepth: sampleEdgeDepth,
|
|
114
120
|
normalTex: scenePassNormal,
|
|
115
121
|
inkColor: inkColorUniform,
|
|
116
122
|
radius: inkRadius,
|
|
@@ -137,11 +143,32 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
137
143
|
const bgGradient = mix(ungradedBgGradient, gradeRgb(ungradedBgGradient), gradeMixUniform);
|
|
138
144
|
const alpha = scenePassColor.a;
|
|
139
145
|
const finalOutput = vec4(mix(sceneRgb, mix(bgGradient, sceneRgb, alpha), bgMixUniform), mix(alpha, float(1), bgMixUniform));
|
|
140
|
-
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
|
141
|
-
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
|
142
|
-
const aaOutput = fxaa(convertToTexture(finalOutput));
|
|
143
146
|
const pipeline = new RenderPipeline(renderer);
|
|
144
|
-
|
|
147
|
+
if (studioBackdrop) {
|
|
148
|
+
// Composite in display space: the scene gets the renderer's own output
|
|
149
|
+
// transform (tone mapping + sRGB), the backdrop is already sRGB — so
|
|
150
|
+
// the pipeline's final transform is off.
|
|
151
|
+
const { width, height } = renderer.domElement;
|
|
152
|
+
const aspect = width / height;
|
|
153
|
+
const [cx, cy] = studioBackdrop.center;
|
|
154
|
+
const radius = Math.max(...[
|
|
155
|
+
[0, 0],
|
|
156
|
+
[1, 0],
|
|
157
|
+
[0, 1],
|
|
158
|
+
[1, 1],
|
|
159
|
+
].map(([x, y]) => Math.hypot((x - cx) * aspect, y - cy)));
|
|
160
|
+
const offset = screenUV.sub(vec2(cx, cy)).mul(vec2(aspect, 1));
|
|
161
|
+
const t = smoothstep(float(0), float(1), clamp(offset.length().div(radius), 0, 1));
|
|
162
|
+
const backdrop = mix(srgbHexToVec3(studioBackdrop.inner), srgbHexToVec3(studioBackdrop.outer), t);
|
|
163
|
+
const sceneDisplay = renderOutput(vec4(sceneRgb, 1), renderer.toneMapping, renderer.outputColorSpace).rgb;
|
|
164
|
+
pipeline.outputColorTransform = false;
|
|
165
|
+
pipeline.outputNode = fxaa(convertToTexture(vec4(mix(backdrop, sceneDisplay, alpha), 1)));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// FXAA requires a texture node as input; convertToTexture renders finalOutput
|
|
169
|
+
// into an intermediate RT so FXAA can sample it with neighbour UV offsets.
|
|
170
|
+
pipeline.outputNode = fxaa(convertToTexture(finalOutput));
|
|
171
|
+
}
|
|
145
172
|
// Dedicated render target — pipeline outputs here instead of the canvas,
|
|
146
173
|
// so R3F's main render loop can never overwrite our capture.
|
|
147
174
|
const { width, height } = renderer.domElement;
|
|
@@ -166,7 +193,7 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
166
193
|
bgProjInvUniform.value.copy(captureCamera.projectionMatrixInverse);
|
|
167
194
|
bgCamWorldUniform.value.copy(captureCamera.matrixWorld);
|
|
168
195
|
},
|
|
169
|
-
capture: async ({ captureMode, cropRegion, standardSize }) => {
|
|
196
|
+
capture: async ({ captureMode, cropRegion, standardSize, mime }) => {
|
|
170
197
|
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH;
|
|
171
198
|
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT;
|
|
172
199
|
const { width: captureWidth, height: captureHeight } = renderer.domElement;
|
|
@@ -184,100 +211,13 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
184
211
|
// Let callers restore visibility and other capture policy immediately
|
|
185
212
|
// after the render, before the asynchronous GPU readback begins.
|
|
186
213
|
await Promise.resolve();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Two readback shapes to handle:
|
|
195
|
-
// - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
|
|
196
|
-
// when width*4 isn't already a multiple of 256.
|
|
197
|
-
// - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
|
|
198
|
-
// (OpenGL framebuffer convention).
|
|
199
|
-
// `isWebGPURenderer` lies — it stays true even when the renderer
|
|
200
|
-
// falls back to the WebGL backend. Inspect the actual backend
|
|
201
|
-
// instead (presence of a GPU device, or backend constructor name).
|
|
202
|
-
const backend = renderer.backend;
|
|
203
|
-
const isWebGPU = !!backend?.device ||
|
|
204
|
-
backend?.isWebGPUBackend === true ||
|
|
205
|
-
backend?.constructor?.name === 'WebGPUBackend';
|
|
206
|
-
let tightPixels;
|
|
207
|
-
if (isWebGPU) {
|
|
208
|
-
// WebGPU: depad rows if needed; orientation is already top-down.
|
|
209
|
-
if (paddedBytesPerRow === actualBytesPerRow) {
|
|
210
|
-
tightPixels = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, Math.min(pixels.byteLength, tightTotal));
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
tightPixels = new Uint8ClampedArray(tightTotal);
|
|
214
|
-
for (let row = 0; row < captureHeight; row++) {
|
|
215
|
-
tightPixels.set(pixels.subarray(row * paddedBytesPerRow, row * paddedBytesPerRow + actualBytesPerRow), row * actualBytesPerRow);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
// WebGL2: tight buffer in bottom-up order — flip rows.
|
|
221
|
-
tightPixels = new Uint8ClampedArray(tightTotal);
|
|
222
|
-
for (let row = 0; row < captureHeight; row++) {
|
|
223
|
-
const srcStart = (captureHeight - 1 - row) * actualBytesPerRow;
|
|
224
|
-
tightPixels.set(pixels.subarray(srcStart, srcStart + actualBytesPerRow), row * actualBytesPerRow);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const imageData = new ImageData(tightPixels, captureWidth, captureHeight);
|
|
228
|
-
const srcCanvas = new OffscreenCanvas(captureWidth, captureHeight);
|
|
229
|
-
srcCanvas.getContext('2d').putImageData(imageData, 0, 0);
|
|
230
|
-
let outW;
|
|
231
|
-
let outH;
|
|
232
|
-
let blob;
|
|
233
|
-
if (captureMode === 'viewport') {
|
|
234
|
-
;
|
|
235
|
-
({ w: outW, h: outH } = clampSnapshotSize(captureWidth, captureHeight));
|
|
236
|
-
const offscreen = new OffscreenCanvas(outW, outH);
|
|
237
|
-
const ctx = offscreen.getContext('2d');
|
|
238
|
-
if (outW !== captureWidth || outH !== captureHeight)
|
|
239
|
-
ctx.imageSmoothingQuality = 'high';
|
|
240
|
-
ctx.drawImage(srcCanvas, 0, 0, captureWidth, captureHeight, 0, 0, outW, outH);
|
|
241
|
-
blob = await offscreen.convertToBlob({ type: SNAPSHOT_MIME, quality: SNAPSHOT_QUALITY });
|
|
242
|
-
}
|
|
243
|
-
else if (captureMode === 'area' && cropRegion) {
|
|
244
|
-
const sx = Math.round(cropRegion.x * captureWidth);
|
|
245
|
-
const sy = Math.round(cropRegion.y * captureHeight);
|
|
246
|
-
const sourceW = Math.round(cropRegion.width * captureWidth);
|
|
247
|
-
const sourceH = Math.round(cropRegion.height * captureHeight);
|
|
248
|
-
({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH));
|
|
249
|
-
const offscreen = new OffscreenCanvas(outW, outH);
|
|
250
|
-
const ctx = offscreen.getContext('2d');
|
|
251
|
-
if (outW !== sourceW || outH !== sourceH)
|
|
252
|
-
ctx.imageSmoothingQuality = 'high';
|
|
253
|
-
ctx.drawImage(srcCanvas, sx, sy, sourceW, sourceH, 0, 0, outW, outH);
|
|
254
|
-
blob = await offscreen.convertToBlob({ type: SNAPSHOT_MIME, quality: SNAPSHOT_QUALITY });
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
// Standard: center-crop to the requested aspect (default 1920×1080)
|
|
258
|
-
const srcAspect = captureWidth / captureHeight;
|
|
259
|
-
const dstAspect = standardW / standardH;
|
|
260
|
-
let sx = 0;
|
|
261
|
-
let sy = 0;
|
|
262
|
-
let sWidth = captureWidth;
|
|
263
|
-
let sHeight = captureHeight;
|
|
264
|
-
if (srcAspect > dstAspect) {
|
|
265
|
-
sWidth = Math.round(captureHeight * dstAspect);
|
|
266
|
-
sx = Math.round((captureWidth - sWidth) / 2);
|
|
267
|
-
}
|
|
268
|
-
else if (srcAspect < dstAspect) {
|
|
269
|
-
sHeight = Math.round(captureWidth / dstAspect);
|
|
270
|
-
sy = Math.round((captureHeight - sHeight) / 2);
|
|
271
|
-
}
|
|
272
|
-
outW = standardW;
|
|
273
|
-
outH = standardH;
|
|
274
|
-
const offscreen = new OffscreenCanvas(outW, outH);
|
|
275
|
-
offscreen
|
|
276
|
-
.getContext('2d')
|
|
277
|
-
.drawImage(srcCanvas, sx, sy, sWidth, sHeight, 0, 0, outW, outH);
|
|
278
|
-
blob = await offscreen.convertToBlob({ type: SNAPSHOT_MIME, quality: SNAPSHOT_QUALITY });
|
|
279
|
-
}
|
|
280
|
-
return { blob, outW, outH };
|
|
214
|
+
return encodeCapture(renderer, renderTarget, captureWidth, captureHeight, {
|
|
215
|
+
captureMode,
|
|
216
|
+
cropRegion,
|
|
217
|
+
standardW,
|
|
218
|
+
standardH,
|
|
219
|
+
mime,
|
|
220
|
+
});
|
|
281
221
|
},
|
|
282
222
|
dispose: () => {
|
|
283
223
|
pipeline.dispose();
|
|
@@ -292,3 +232,173 @@ export async function createSnapshotPipeline({ renderer, scene, camera, atmosphe
|
|
|
292
232
|
return null;
|
|
293
233
|
}
|
|
294
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The frame in `renderTarget`, read back and encoded: WebGPU's 256-byte row
|
|
237
|
+
* padding undone (the WebGL fallback's bottom-up rows flipped), then cut to
|
|
238
|
+
* the capture mode — the viewport as is, an area, or the standard aspect.
|
|
239
|
+
* Shared by the post-processed pipeline and the plain one.
|
|
240
|
+
*/
|
|
241
|
+
async function encodeCapture(renderer, renderTarget, captureWidth, captureHeight, { captureMode, cropRegion, standardW, standardH, mime, }) {
|
|
242
|
+
const encoding = mime ? { type: mime } : { type: SNAPSHOT_MIME, quality: SNAPSHOT_QUALITY };
|
|
243
|
+
// Read pixels from the RT asynchronously.
|
|
244
|
+
// WebGPU copyTextureToBuffer aligns each row to 256 bytes, so we must
|
|
245
|
+
// depad the rows before constructing ImageData.
|
|
246
|
+
const pixels = (await renderer.readRenderTargetPixelsAsync(renderTarget, 0, 0, captureWidth, captureHeight));
|
|
247
|
+
const actualBytesPerRow = captureWidth * 4;
|
|
248
|
+
const tightTotal = actualBytesPerRow * captureHeight;
|
|
249
|
+
const paddedBytesPerRow = Math.ceil(actualBytesPerRow / 256) * 256;
|
|
250
|
+
// Two readback shapes to handle:
|
|
251
|
+
// - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
|
|
252
|
+
// when width*4 isn't already a multiple of 256.
|
|
253
|
+
// - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
|
|
254
|
+
// (OpenGL framebuffer convention).
|
|
255
|
+
// `isWebGPURenderer` lies — it stays true even when the renderer
|
|
256
|
+
// falls back to the WebGL backend. Inspect the actual backend
|
|
257
|
+
// instead (presence of a GPU device, or backend constructor name).
|
|
258
|
+
const backend = renderer.backend;
|
|
259
|
+
const isWebGPU = !!backend?.device ||
|
|
260
|
+
backend?.isWebGPUBackend === true ||
|
|
261
|
+
backend?.constructor?.name === 'WebGPUBackend';
|
|
262
|
+
let tightPixels;
|
|
263
|
+
if (isWebGPU) {
|
|
264
|
+
// WebGPU: depad rows if needed; orientation is already top-down.
|
|
265
|
+
if (paddedBytesPerRow === actualBytesPerRow) {
|
|
266
|
+
tightPixels = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, Math.min(pixels.byteLength, tightTotal));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
tightPixels = new Uint8ClampedArray(tightTotal);
|
|
270
|
+
for (let row = 0; row < captureHeight; row++) {
|
|
271
|
+
tightPixels.set(pixels.subarray(row * paddedBytesPerRow, row * paddedBytesPerRow + actualBytesPerRow), row * actualBytesPerRow);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// WebGL2: tight buffer in bottom-up order — flip rows.
|
|
277
|
+
tightPixels = new Uint8ClampedArray(tightTotal);
|
|
278
|
+
for (let row = 0; row < captureHeight; row++) {
|
|
279
|
+
const srcStart = (captureHeight - 1 - row) * actualBytesPerRow;
|
|
280
|
+
tightPixels.set(pixels.subarray(srcStart, srcStart + actualBytesPerRow), row * actualBytesPerRow);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const imageData = new ImageData(tightPixels, captureWidth, captureHeight);
|
|
284
|
+
const srcCanvas = new OffscreenCanvas(captureWidth, captureHeight);
|
|
285
|
+
srcCanvas.getContext('2d').putImageData(imageData, 0, 0);
|
|
286
|
+
let outW;
|
|
287
|
+
let outH;
|
|
288
|
+
let blob;
|
|
289
|
+
if (captureMode === 'viewport') {
|
|
290
|
+
;
|
|
291
|
+
({ w: outW, h: outH } = clampSnapshotSize(captureWidth, captureHeight));
|
|
292
|
+
const offscreen = new OffscreenCanvas(outW, outH);
|
|
293
|
+
const ctx = offscreen.getContext('2d');
|
|
294
|
+
if (outW !== captureWidth || outH !== captureHeight)
|
|
295
|
+
ctx.imageSmoothingQuality = 'high';
|
|
296
|
+
ctx.drawImage(srcCanvas, 0, 0, captureWidth, captureHeight, 0, 0, outW, outH);
|
|
297
|
+
blob = await offscreen.convertToBlob(encoding);
|
|
298
|
+
}
|
|
299
|
+
else if (captureMode === 'area' && cropRegion) {
|
|
300
|
+
const sx = Math.round(cropRegion.x * captureWidth);
|
|
301
|
+
const sy = Math.round(cropRegion.y * captureHeight);
|
|
302
|
+
const sourceW = Math.round(cropRegion.width * captureWidth);
|
|
303
|
+
const sourceH = Math.round(cropRegion.height * captureHeight);
|
|
304
|
+
({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH));
|
|
305
|
+
const offscreen = new OffscreenCanvas(outW, outH);
|
|
306
|
+
const ctx = offscreen.getContext('2d');
|
|
307
|
+
if (outW !== sourceW || outH !== sourceH)
|
|
308
|
+
ctx.imageSmoothingQuality = 'high';
|
|
309
|
+
ctx.drawImage(srcCanvas, sx, sy, sourceW, sourceH, 0, 0, outW, outH);
|
|
310
|
+
blob = await offscreen.convertToBlob(encoding);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Standard: center-crop to the requested aspect (default 1920×1080)
|
|
314
|
+
const srcAspect = captureWidth / captureHeight;
|
|
315
|
+
const dstAspect = standardW / standardH;
|
|
316
|
+
let sx = 0;
|
|
317
|
+
let sy = 0;
|
|
318
|
+
let sWidth = captureWidth;
|
|
319
|
+
let sHeight = captureHeight;
|
|
320
|
+
if (srcAspect > dstAspect) {
|
|
321
|
+
sWidth = Math.round(captureHeight * dstAspect);
|
|
322
|
+
sx = Math.round((captureWidth - sWidth) / 2);
|
|
323
|
+
}
|
|
324
|
+
else if (srcAspect < dstAspect) {
|
|
325
|
+
sHeight = Math.round(captureWidth / dstAspect);
|
|
326
|
+
sy = Math.round((captureHeight - sHeight) / 2);
|
|
327
|
+
}
|
|
328
|
+
outW = standardW;
|
|
329
|
+
outH = standardH;
|
|
330
|
+
const offscreen = new OffscreenCanvas(outW, outH);
|
|
331
|
+
offscreen.getContext('2d').drawImage(srcCanvas, sx, sy, sWidth, sHeight, 0, 0, outW, outH);
|
|
332
|
+
blob = await offscreen.convertToBlob(encoding);
|
|
333
|
+
}
|
|
334
|
+
return { blob, outW, outH };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* A capture WITHOUT the post-processing stack: the scene as the renderer
|
|
338
|
+
* draws it, into an offscreen target, read back and encoded like the
|
|
339
|
+
* post-processed frames. The sheets' orthographic elevation captures use
|
|
340
|
+
* it (2026-09-10): an SSGI pass built for a second, orthographic camera
|
|
341
|
+
* never delivered a frame and held the generator's guard forever.
|
|
342
|
+
*/
|
|
343
|
+
export async function createPlainSnapshotPipeline({ renderer, scene, camera, }) {
|
|
344
|
+
try {
|
|
345
|
+
if (renderer.init)
|
|
346
|
+
await renderer.init();
|
|
347
|
+
const { width, height } = renderer.domElement;
|
|
348
|
+
const renderTarget = new RenderTarget(width, height, { depthBuffer: true });
|
|
349
|
+
let transparent = true;
|
|
350
|
+
const background = new Color('#ffffff');
|
|
351
|
+
return {
|
|
352
|
+
applyEnvironment: ({ theme, transparent: alpha }) => {
|
|
353
|
+
transparent = alpha;
|
|
354
|
+
background.set(getSceneTheme(theme).background);
|
|
355
|
+
},
|
|
356
|
+
capture: async ({ captureMode, cropRegion, standardSize }) => {
|
|
357
|
+
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH;
|
|
358
|
+
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT;
|
|
359
|
+
const { width: captureWidth, height: captureHeight } = renderer.domElement;
|
|
360
|
+
if (renderTarget.width !== captureWidth || renderTarget.height !== captureHeight) {
|
|
361
|
+
renderTarget.setSize(captureWidth, captureHeight);
|
|
362
|
+
}
|
|
363
|
+
const previousBackground = scene.background;
|
|
364
|
+
const previousClear = renderer.getClearColor(new Color());
|
|
365
|
+
const previousAlpha = renderer.getClearAlpha();
|
|
366
|
+
try {
|
|
367
|
+
scene.background = null;
|
|
368
|
+
renderer.setClearColor(background, transparent ? 0 : 1);
|
|
369
|
+
renderer.setRenderTarget(renderTarget);
|
|
370
|
+
// outside the frame loop the async render is the one that submits
|
|
371
|
+
// its work and settles once the GPU took it
|
|
372
|
+
const asyncRender = renderer.renderAsync;
|
|
373
|
+
if (asyncRender)
|
|
374
|
+
await asyncRender.call(renderer, scene, camera);
|
|
375
|
+
else
|
|
376
|
+
renderer.render(scene, camera);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
renderer.setRenderTarget(null);
|
|
380
|
+
scene.background = previousBackground;
|
|
381
|
+
renderer.setClearColor(previousClear, previousAlpha);
|
|
382
|
+
}
|
|
383
|
+
await Promise.resolve();
|
|
384
|
+
// a readback that never returns must not hold a caller for the session
|
|
385
|
+
return Promise.race([
|
|
386
|
+
encodeCapture(renderer, renderTarget, captureWidth, captureHeight, {
|
|
387
|
+
captureMode,
|
|
388
|
+
cropRegion,
|
|
389
|
+
standardW,
|
|
390
|
+
standardH,
|
|
391
|
+
}),
|
|
392
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('the plain capture readback did not return within 12 s')), 12000)),
|
|
393
|
+
]);
|
|
394
|
+
},
|
|
395
|
+
dispose: () => {
|
|
396
|
+
renderTarget.dispose();
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
console.error('[thumbnail] Failed to build the plain capture pipeline.', error);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terrain-perimeter-fill.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-perimeter-fill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA+C,MAAM,OAAO,CAAA;AAGnF,MAAM,MAAM,qBAAqB,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,qBAAqB,EAAE,EACxC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAO,GACb,cAAc,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"terrain-perimeter-fill.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-perimeter-fill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA+C,MAAM,OAAO,CAAA;AAGnF,MAAM,MAAM,qBAAqB,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5D,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,SAAS,qBAAqB,EAAE,EACxC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,SAAO,GACb,cAAc,GAAG,IAAI,CAkFvB"}
|
|
@@ -13,32 +13,39 @@ export function buildTerrainPerimeterFillGeometry(points, bottomY, topY, epsilon
|
|
|
13
13
|
}
|
|
14
14
|
const counterClockwise = signedArea > 0;
|
|
15
15
|
const positions = [];
|
|
16
|
-
const
|
|
16
|
+
const uvs = [];
|
|
17
|
+
const push = (point, y, u = point.x, v = point.z) => {
|
|
17
18
|
positions.push(point.x, y, point.z);
|
|
19
|
+
uvs.push(u, v);
|
|
18
20
|
};
|
|
21
|
+
let perimeter = 0;
|
|
19
22
|
for (let index = 0; index < points.length; index += 1) {
|
|
20
23
|
const next = (index + 1) % points.length;
|
|
21
24
|
const a = points[index];
|
|
22
25
|
const b = points[next];
|
|
23
26
|
const ay = bottomY[index];
|
|
24
27
|
const by = bottomY[next];
|
|
25
|
-
|
|
28
|
+
const length = Math.hypot(b.x - a.x, b.z - a.z);
|
|
29
|
+
const ua = perimeter;
|
|
30
|
+
perimeter += length;
|
|
31
|
+
const ub = perimeter;
|
|
32
|
+
if (length < epsilon || (ay >= topY - epsilon && by >= topY - epsilon))
|
|
26
33
|
continue;
|
|
27
34
|
if (counterClockwise) {
|
|
28
|
-
push(a, topY);
|
|
29
|
-
push(b, by);
|
|
30
|
-
push(a, ay);
|
|
31
|
-
push(a, topY);
|
|
32
|
-
push(b, topY);
|
|
33
|
-
push(b, by);
|
|
35
|
+
push(a, topY, ua, topY);
|
|
36
|
+
push(b, by, ub, by);
|
|
37
|
+
push(a, ay, ua, ay);
|
|
38
|
+
push(a, topY, ua, topY);
|
|
39
|
+
push(b, topY, ub, topY);
|
|
40
|
+
push(b, by, ub, by);
|
|
34
41
|
}
|
|
35
42
|
else {
|
|
36
|
-
push(a, topY);
|
|
37
|
-
push(a, ay);
|
|
38
|
-
push(b, by);
|
|
39
|
-
push(a, topY);
|
|
40
|
-
push(b, by);
|
|
41
|
-
push(b, topY);
|
|
43
|
+
push(a, topY, ua, topY);
|
|
44
|
+
push(a, ay, ua, ay);
|
|
45
|
+
push(b, by, ub, by);
|
|
46
|
+
push(a, topY, ua, topY);
|
|
47
|
+
push(b, by, ub, by);
|
|
48
|
+
push(b, topY, ub, topY);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
const faces = ShapeUtils.triangulateShape(points.map((point) => new Vector2(point.x, point.z)), []);
|
|
@@ -69,6 +76,7 @@ export function buildTerrainPerimeterFillGeometry(points, bottomY, topY, epsilon
|
|
|
69
76
|
return null;
|
|
70
77
|
const geometry = new BufferGeometry();
|
|
71
78
|
geometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
|
|
79
|
+
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2));
|
|
72
80
|
geometry.computeVertexNormals();
|
|
73
81
|
ensureRenderableGeometryAttributes(geometry);
|
|
74
82
|
return geometry;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ring } from '@pascal-app/core';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
type ZoneFootprint = {
|
|
4
|
+
polygon: Ring;
|
|
5
|
+
holes?: Ring[];
|
|
6
|
+
};
|
|
7
|
+
export declare function createZoneShape({ polygon, holes }: ZoneFootprint): THREE.Shape;
|
|
8
|
+
/** Vertical quads along each polygon edge (UV.y 0 at the floor, 1 at the top). */
|
|
9
|
+
export declare function createZoneWallGeometry(footprint: ZoneFootprint): THREE.BufferGeometry;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=zone-geometry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zone-geometry.d.ts","sourceRoot":"","sources":["../../src/lib/zone-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAC5C,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,KAAK,aAAa,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CAAE,CAAA;AAItD,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,KAAU,EAAE,EAAE,aAAa,GAAG,KAAK,CAAC,KAAK,CAiBnF;AAED,kFAAkF;AAClF,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,aAAa,GAAG,KAAK,CAAC,cAAc,CAiCrF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
const Y_OFFSET = 0.01;
|
|
3
|
+
const ZONE_WALL_HEIGHT = 2.3;
|
|
4
|
+
export function createZoneShape({ polygon, holes = [] }) {
|
|
5
|
+
const shape = new THREE.Shape();
|
|
6
|
+
const draw = (path, ring) => {
|
|
7
|
+
ring.forEach(([x, z], i) => {
|
|
8
|
+
if (i === 0)
|
|
9
|
+
path.moveTo(x, -z);
|
|
10
|
+
else
|
|
11
|
+
path.lineTo(x, -z);
|
|
12
|
+
});
|
|
13
|
+
path.closePath();
|
|
14
|
+
};
|
|
15
|
+
draw(shape, polygon);
|
|
16
|
+
for (const ring of holes) {
|
|
17
|
+
if (ring.length < 3)
|
|
18
|
+
continue;
|
|
19
|
+
const path = new THREE.Path();
|
|
20
|
+
draw(path, ring);
|
|
21
|
+
shape.holes.push(path);
|
|
22
|
+
}
|
|
23
|
+
return shape;
|
|
24
|
+
}
|
|
25
|
+
/** Vertical quads along each polygon edge (UV.y 0 at the floor, 1 at the top). */
|
|
26
|
+
export function createZoneWallGeometry(footprint) {
|
|
27
|
+
const positions = [];
|
|
28
|
+
const uvs = [];
|
|
29
|
+
const indices = [];
|
|
30
|
+
for (const polygon of [footprint.polygon, ...(footprint.holes ?? [])]) {
|
|
31
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
32
|
+
const [cx, cz] = polygon[i];
|
|
33
|
+
const [nx, nz] = polygon[(i + 1) % polygon.length];
|
|
34
|
+
const base = positions.length / 3;
|
|
35
|
+
positions.push(cx, Y_OFFSET, cz, nx, Y_OFFSET, nz, nx, Y_OFFSET + ZONE_WALL_HEIGHT, nz, cx, Y_OFFSET + ZONE_WALL_HEIGHT, cz);
|
|
36
|
+
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
|
37
|
+
indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const geometry = new THREE.BufferGeometry();
|
|
41
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
42
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
43
|
+
geometry.setIndex(indices);
|
|
44
|
+
geometry.computeVertexNormals();
|
|
45
|
+
return geometry;
|
|
46
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type AnyNodeId, type Interactive, type LightEffect } from '@pascal-app/core';
|
|
2
|
+
import type { Object3D, Vector3 } from 'three';
|
|
3
|
+
export type LightSource = {
|
|
4
|
+
key: string;
|
|
3
5
|
nodeId: AnyNodeId;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
hasSlider: boolean;
|
|
6
|
+
color: string;
|
|
7
|
+
distance: number;
|
|
8
|
+
getWorldPosition: (out: Vector3) => boolean;
|
|
9
|
+
getIntensity: () => number;
|
|
10
|
+
isEligible: () => boolean;
|
|
10
11
|
};
|
|
12
|
+
export declare function catalogLightSource(key: string, nodeId: AnyNodeId, effect: LightEffect, interactive: Interactive, localScale?: readonly [number, number, number]): LightSource;
|
|
11
13
|
type ItemLightPoolStore = {
|
|
12
|
-
registrations: Map<string,
|
|
13
|
-
|
|
14
|
+
registrations: Map<string, LightSource>;
|
|
15
|
+
bakedCanvases: Set<Object3D>;
|
|
16
|
+
setBakedCanvas: (scene: Object3D, active: boolean) => void;
|
|
17
|
+
register: (source: LightSource) => void;
|
|
14
18
|
unregister: (key: string) => void;
|
|
15
19
|
};
|
|
16
20
|
export declare const useItemLightPool: import("zustand").UseBoundStore<import("zustand").StoreApi<ItemLightPoolStore>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-item-light-pool.d.ts","sourceRoot":"","sources":["../../src/store/use-item-light-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"use-item-light-pool.d.ts","sourceRoot":"","sources":["../../src/store/use-item-light-pool.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,EAIjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAG9C,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,SAAS,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;IAC3C,YAAY,EAAE,MAAM,MAAM,CAAA;IAC1B,UAAU,EAAE,MAAM,OAAO,CAAA;CAC1B,CAAA;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,WAAW,EACxB,UAAU,CAAC,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC7C,WAAW,CAwCb;AAED,KAAK,kBAAkB,GAAG;IACxB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACvC,aAAa,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC5B,cAAc,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;IAC1D,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;IACvC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC,CAAA;AAED,eAAO,MAAM,gBAAgB,iFAkB1B,CAAA"}
|
|
@@ -1,30 +1,56 @@
|
|
|
1
|
+
import { sceneRegistry, useInteractive, } from '@pascal-app/core';
|
|
1
2
|
import { create } from 'zustand';
|
|
3
|
+
export function catalogLightSource(key, nodeId, effect, interactive, localScale) {
|
|
4
|
+
const toggleIndex = interactive.controls.findIndex((control) => control.kind === 'toggle');
|
|
5
|
+
const sliderIndex = interactive.controls.findIndex((control) => control.kind === 'slider');
|
|
6
|
+
const slider = sliderIndex >= 0 ? interactive.controls[sliderIndex] : null;
|
|
7
|
+
return {
|
|
8
|
+
key,
|
|
9
|
+
nodeId,
|
|
10
|
+
color: effect.color,
|
|
11
|
+
distance: effect.distance ?? 0,
|
|
12
|
+
getWorldPosition: (out) => {
|
|
13
|
+
const object = sceneRegistry.nodes.get(nodeId);
|
|
14
|
+
if (!object)
|
|
15
|
+
return false;
|
|
16
|
+
if (localScale) {
|
|
17
|
+
out.set(effect.offset[0] * localScale[0], effect.offset[1] * localScale[1], effect.offset[2] * localScale[2]);
|
|
18
|
+
object.localToWorld(out);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
object.getWorldPosition(out);
|
|
22
|
+
out.set(out.x + effect.offset[0], out.y + effect.offset[1], out.z + effect.offset[2]);
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
getIntensity: () => {
|
|
26
|
+
const values = useInteractive.getState().items[nodeId]?.controlValues;
|
|
27
|
+
if (toggleIndex >= 0 && !values?.[toggleIndex])
|
|
28
|
+
return effect.intensityRange[0];
|
|
29
|
+
const raw = slider ? (values?.[sliderIndex] ?? slider.min) : 1;
|
|
30
|
+
const fraction = slider && slider.max > slider.min ? (raw - slider.min) / (slider.max - slider.min) : 1;
|
|
31
|
+
return (effect.intensityRange[0] + (effect.intensityRange[1] - effect.intensityRange[0]) * fraction);
|
|
32
|
+
},
|
|
33
|
+
isEligible: () => {
|
|
34
|
+
const values = useInteractive.getState().items[nodeId]?.controlValues;
|
|
35
|
+
return toggleIndex < 0 || Boolean(values?.[toggleIndex]);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
2
39
|
export const useItemLightPool = create((set) => ({
|
|
3
40
|
registrations: new Map(),
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
next.set(key, registration);
|
|
20
|
-
return { registrations: next };
|
|
21
|
-
});
|
|
22
|
-
},
|
|
23
|
-
unregister: (key) => {
|
|
24
|
-
set((s) => {
|
|
25
|
-
const next = new Map(s.registrations);
|
|
26
|
-
next.delete(key);
|
|
27
|
-
return { registrations: next };
|
|
28
|
-
});
|
|
29
|
-
},
|
|
41
|
+
bakedCanvases: new Set(),
|
|
42
|
+
setBakedCanvas: (scene, active) => set((state) => {
|
|
43
|
+
const bakedCanvases = new Set(state.bakedCanvases);
|
|
44
|
+
if (active)
|
|
45
|
+
bakedCanvases.add(scene);
|
|
46
|
+
else
|
|
47
|
+
bakedCanvases.delete(scene);
|
|
48
|
+
return { bakedCanvases };
|
|
49
|
+
}),
|
|
50
|
+
register: (source) => set((state) => ({ registrations: new Map(state.registrations).set(source.key, source) })),
|
|
51
|
+
unregister: (key) => set((state) => {
|
|
52
|
+
const registrations = new Map(state.registrations);
|
|
53
|
+
registrations.delete(key);
|
|
54
|
+
return { registrations };
|
|
55
|
+
}),
|
|
30
56
|
}));
|