@selvajs/visualization 1.6.0-beta.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{gpu-dispose-fRI12OCL.cjs → gpu-dispose-D66GqIWS.cjs} +48 -3
- package/dist/gpu-dispose-D66GqIWS.cjs.map +1 -0
- package/dist/{gpu-dispose-Cl3RXm-v.js → gpu-dispose-Di_nxeXF.js} +31 -4
- package/dist/gpu-dispose-Di_nxeXF.js.map +1 -0
- package/dist/parse.cjs +264 -17
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.d.cts +15 -2
- package/dist/parse.d.ts +15 -2
- package/dist/parse.js +264 -17
- package/dist/parse.js.map +1 -1
- package/dist/render.cjs +762 -142
- package/dist/render.cjs.map +1 -1
- package/dist/render.d.cts +20 -6
- package/dist/render.d.ts +20 -6
- package/dist/render.js +762 -142
- package/dist/render.js.map +1 -1
- package/package.json +13 -7
- package/dist/gpu-dispose-Cl3RXm-v.js.map +0 -1
- package/dist/gpu-dispose-fRI12OCL.cjs.map +0 -1
package/dist/render.cjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_gpu_ownership = require("./gpu-ownership-ADRZBxxs.cjs");
|
|
3
|
-
const require_gpu_dispose = require("./gpu-dispose-
|
|
3
|
+
const require_gpu_dispose = require("./gpu-dispose-D66GqIWS.cjs");
|
|
4
4
|
const require_merged_draw = require("./merged-draw-ma_2r4N8.cjs");
|
|
5
5
|
let three = require("three");
|
|
6
6
|
three = require_gpu_ownership.__toESM(three, 1);
|
|
7
7
|
let three_addons_lines_Line2_js = require("three/addons/lines/Line2.js");
|
|
8
8
|
let three_addons_lines_LineGeometry_js = require("three/addons/lines/LineGeometry.js");
|
|
9
9
|
let three_addons_lines_LineMaterial_js = require("three/addons/lines/LineMaterial.js");
|
|
10
|
-
let three_addons_environments_RoomEnvironment_js = require("three/addons/environments/RoomEnvironment.js");
|
|
11
10
|
let three_addons_lines_LineSegments2_js = require("three/addons/lines/LineSegments2.js");
|
|
12
11
|
let three_addons_lines_LineSegmentsGeometry_js = require("three/addons/lines/LineSegmentsGeometry.js");
|
|
13
12
|
let three_addons_renderers_CSS2DRenderer_js = require("three/addons/renderers/CSS2DRenderer.js");
|
|
13
|
+
let three_mesh_bvh = require("three-mesh-bvh");
|
|
14
14
|
let three_addons_helpers_ViewHelper_js = require("three/addons/helpers/ViewHelper.js");
|
|
15
15
|
let three_addons_postprocessing_EffectComposer_js = require("three/addons/postprocessing/EffectComposer.js");
|
|
16
16
|
let three_addons_postprocessing_RenderPass_js = require("three/addons/postprocessing/RenderPass.js");
|
|
@@ -18,8 +18,8 @@ let three_addons_postprocessing_ShaderPass_js = require("three/addons/postproces
|
|
|
18
18
|
let three_addons_postprocessing_GTAOPass_js = require("three/addons/postprocessing/GTAOPass.js");
|
|
19
19
|
let three_addons_postprocessing_SMAAPass_js = require("three/addons/postprocessing/SMAAPass.js");
|
|
20
20
|
let three_addons_postprocessing_OutputPass_js = require("three/addons/postprocessing/OutputPass.js");
|
|
21
|
-
let three_addons_postprocessing_UnrealBloomPass_js = require("three/addons/postprocessing/UnrealBloomPass.js");
|
|
22
21
|
let three_addons_postprocessing_Pass_js = require("three/addons/postprocessing/Pass.js");
|
|
22
|
+
let three_addons_postprocessing_UnrealBloomPass_js = require("three/addons/postprocessing/UnrealBloomPass.js");
|
|
23
23
|
let three_addons_controls_OrbitControls_js = require("three/addons/controls/OrbitControls.js");
|
|
24
24
|
let three_addons_loaders_HDRLoader_js = require("three/addons/loaders/HDRLoader.js");
|
|
25
25
|
// src/shared/types.ts
|
|
@@ -68,8 +68,84 @@ function isOwnedBy(object, id) {
|
|
|
68
68
|
if (id === void 0) return isHostOwned(object);
|
|
69
69
|
return object.userData?.source === appSource(id);
|
|
70
70
|
}
|
|
71
|
+
// src/render/scene-setup/studio-environment.ts
|
|
72
|
+
/** What a softbox costs to set up, in radiance: only their ratio to the room matters to a reflection. */
|
|
73
|
+
const OVERHEAD = 2.5;
|
|
74
|
+
const KEY_STRIP = 11;
|
|
75
|
+
const FILL_STRIP = 4.5;
|
|
76
|
+
const RIM = 3.5;
|
|
77
|
+
const FLOOR = .05;
|
|
78
|
+
const WALL_LOW = .06;
|
|
79
|
+
const WALL_HORIZON = .65;
|
|
80
|
+
const WALL_HIGH = .8;
|
|
81
|
+
const ROOM_SIZE = 10;
|
|
82
|
+
function softbox(radiance, width, height, position) {
|
|
83
|
+
const panel = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshBasicMaterial({
|
|
84
|
+
color: new three.Color().setScalar(radiance),
|
|
85
|
+
side: three.DoubleSide
|
|
86
|
+
}));
|
|
87
|
+
panel.position.set(...position);
|
|
88
|
+
panel.lookAt(0, 0, 0);
|
|
89
|
+
return panel;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A photographer's table, for PMREM to bake into a reflection probe: softboxes on a dark room.
|
|
93
|
+
* Shape comes from the gaps between lights. A surface that reflects light everywhere has no contour,
|
|
94
|
+
* which is why three's `RoomEnvironment`, bright on all sides, leaves metal looking like flat paint.
|
|
95
|
+
* The floor stays dark so an underside reads as turned away from the light.
|
|
96
|
+
*/
|
|
97
|
+
function createStudioScene() {
|
|
98
|
+
const scene = new three.Scene();
|
|
99
|
+
const room = new three.Mesh(gradedRoom(), new three.MeshBasicMaterial({
|
|
100
|
+
vertexColors: true,
|
|
101
|
+
side: three.BackSide
|
|
102
|
+
}));
|
|
103
|
+
const floor = new three.Mesh(new three.PlaneGeometry(ROOM_SIZE, ROOM_SIZE), new three.MeshBasicMaterial({ color: new three.Color().setScalar(FLOOR) }));
|
|
104
|
+
floor.rotation.x = -Math.PI / 2;
|
|
105
|
+
floor.position.y = -4;
|
|
106
|
+
scene.add(room, floor, softbox(OVERHEAD, 6.5, 4.5, [
|
|
107
|
+
0,
|
|
108
|
+
4.8,
|
|
109
|
+
.6
|
|
110
|
+
]), softbox(KEY_STRIP, 1.4, 5.5, [
|
|
111
|
+
-4.7,
|
|
112
|
+
.8,
|
|
113
|
+
1.6
|
|
114
|
+
]), softbox(FILL_STRIP, .9, 5, [
|
|
115
|
+
4.7,
|
|
116
|
+
.5,
|
|
117
|
+
2.4
|
|
118
|
+
]), softbox(RIM, 5, .8, [
|
|
119
|
+
.6,
|
|
120
|
+
2.2,
|
|
121
|
+
-4.7
|
|
122
|
+
]));
|
|
123
|
+
return scene;
|
|
124
|
+
}
|
|
125
|
+
function gradedRoom() {
|
|
126
|
+
const geometry = new three.BoxGeometry(ROOM_SIZE, ROOM_SIZE, ROOM_SIZE, 1, 16, 1);
|
|
127
|
+
const position = geometry.attributes.position;
|
|
128
|
+
const colors = new Float32Array(position.count * 3);
|
|
129
|
+
for (let i = 0; i < position.count; i++) {
|
|
130
|
+
const t = position.getY(i) / (ROOM_SIZE / 2);
|
|
131
|
+
const radiance = t < 0 ? three.MathUtils.lerp(WALL_HORIZON, WALL_LOW, -t) : three.MathUtils.lerp(WALL_HORIZON, WALL_HIGH, t);
|
|
132
|
+
colors.fill(radiance, i * 3, i * 3 + 3);
|
|
133
|
+
}
|
|
134
|
+
geometry.setAttribute("color", new three.BufferAttribute(colors, 3));
|
|
135
|
+
return geometry;
|
|
136
|
+
}
|
|
137
|
+
/** Frees what {@link createStudioScene} built, once PMREM has baked it. */
|
|
138
|
+
function disposeStudioScene(scene) {
|
|
139
|
+
scene.traverse((object) => {
|
|
140
|
+
const mesh = object;
|
|
141
|
+
mesh.geometry?.dispose();
|
|
142
|
+
mesh.material?.dispose();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
71
145
|
// src/render/scene-setup/reflection-environment.ts
|
|
72
146
|
const METAL = .5;
|
|
147
|
+
/** Above PMREM's blur for a mirror, so a softbox has an edge in chrome without showing as a shape. */
|
|
148
|
+
const STUDIO_BLUR = .03;
|
|
73
149
|
const KEY = "selvaReflectionEnvironment";
|
|
74
150
|
/**
|
|
75
151
|
* Builds the studio probe once per scene. `rotation` turns its Y-up room to the scene's up, the same
|
|
@@ -77,9 +153,9 @@ const KEY = "selvaReflectionEnvironment";
|
|
|
77
153
|
*/
|
|
78
154
|
function createReflectionEnvironment(scene, renderer, rotation) {
|
|
79
155
|
const pmrem = new three.PMREMGenerator(renderer);
|
|
80
|
-
const
|
|
81
|
-
const texture = pmrem.fromScene(
|
|
82
|
-
|
|
156
|
+
const studio = createStudioScene();
|
|
157
|
+
const texture = pmrem.fromScene(studio, STUDIO_BLUR).texture;
|
|
158
|
+
disposeStudioScene(studio);
|
|
83
159
|
pmrem.dispose();
|
|
84
160
|
installReflectionEnvironment(scene, texture, rotation);
|
|
85
161
|
}
|
|
@@ -295,6 +371,8 @@ function applyDefaults(options) {
|
|
|
295
371
|
toneMappingExposure: options.render?.toneMappingExposure ?? preset.toneMappingExposure,
|
|
296
372
|
preserveDrawingBuffer: options.render?.preserveDrawingBuffer ?? false,
|
|
297
373
|
lowPower,
|
|
374
|
+
idleRefineFrames: options.render?.idleRefineFrames ?? (lowPower ? 0 : 32),
|
|
375
|
+
lightOrbitFrameMs: options.render?.lightOrbitFrameMs ?? 22,
|
|
298
376
|
ambientOcclusion: options.render?.ambientOcclusion ?? preset.ambientOcclusion,
|
|
299
377
|
aoIntensity: options.render?.aoIntensity ?? 1,
|
|
300
378
|
bloom: options.render?.bloom ?? preset.bloom,
|
|
@@ -357,6 +435,108 @@ function applyDefaults(options) {
|
|
|
357
435
|
onMaxAnisotropy: options.onMaxAnisotropy
|
|
358
436
|
};
|
|
359
437
|
}
|
|
438
|
+
// src/render/scene-setup/idle-refine.ts
|
|
439
|
+
/** Long enough to skip the pauses inside an orbit, or a drag would keep restarting the average. */
|
|
440
|
+
const SETTLE_MS = 150;
|
|
441
|
+
/** Ambient occlusion is the largest cost of a frame, and the only one that can fade without a pop. */
|
|
442
|
+
const AO_FADE_FRAMES = 8;
|
|
443
|
+
/** Enough moving frames that one hitch can't decide it. */
|
|
444
|
+
const ORBIT_SAMPLES = 12;
|
|
445
|
+
/** Low-discrepancy sequence in [0, 1): N samples cover the range far more evenly than random ones. */
|
|
446
|
+
function halton(index, base) {
|
|
447
|
+
let result = 0;
|
|
448
|
+
let fraction = 1 / base;
|
|
449
|
+
for (let i = index; i > 0; i = Math.floor(i / base)) {
|
|
450
|
+
result += i % base * fraction;
|
|
451
|
+
fraction /= base;
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
/** Counts what a forgotten `invalidate()` most likely changed: visibility and materials. */
|
|
456
|
+
function sceneStamp(scene) {
|
|
457
|
+
let stamp = 0;
|
|
458
|
+
scene.traverseVisible((object) => {
|
|
459
|
+
stamp += 1;
|
|
460
|
+
const { material } = object;
|
|
461
|
+
for (const each of Array.isArray(material) ? material : material ? [material] : []) stamp += each.version * 31;
|
|
462
|
+
});
|
|
463
|
+
return stamp;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Softens the picture while nothing moves. Each frame moves the camera by under a pixel and every
|
|
467
|
+
* light within its real size, and the average of those frames has true penumbras and clean edges at
|
|
468
|
+
* no cost to orbiting. The averaged image would be lost to the render loop's safety repaint, so
|
|
469
|
+
* while it holds, that repaint only checks whether the scene changed.
|
|
470
|
+
*
|
|
471
|
+
* It also keeps orbiting smooth on a slow GPU. Once frames run over `lightOrbitFrameMs`, ambient
|
|
472
|
+
* occlusion fades out while the camera moves and back in as it rests. Decided once, so a machine
|
|
473
|
+
* near the limit doesn't flip between the two, and a fast one never sees it.
|
|
474
|
+
*/
|
|
475
|
+
function createIdleRefiner(params) {
|
|
476
|
+
const { scene, renderer, getPipeline, frames, lightOrbitFrameMs, jitterLights, restoreLights } = params;
|
|
477
|
+
let index = 0;
|
|
478
|
+
let changedAt = 0;
|
|
479
|
+
let stamp = 0;
|
|
480
|
+
const size = new three.Vector2();
|
|
481
|
+
let lightOrbit = frames > 0 && lightOrbitFrameMs <= 0;
|
|
482
|
+
let aoWeight = 1;
|
|
483
|
+
const orbitSamples = [];
|
|
484
|
+
const setAoWeight = (weight) => {
|
|
485
|
+
aoWeight = Math.min(1, Math.max(0, weight));
|
|
486
|
+
getPipeline()?.setAoWeight(aoWeight);
|
|
487
|
+
};
|
|
488
|
+
const measureOrbit = (deltaMs) => {
|
|
489
|
+
orbitSamples.push(deltaMs);
|
|
490
|
+
if (orbitSamples.length < ORBIT_SAMPLES) return;
|
|
491
|
+
orbitSamples.sort((a, b) => a - b);
|
|
492
|
+
lightOrbit = orbitSamples[6] > lightOrbitFrameMs;
|
|
493
|
+
orbitSamples.length = 0;
|
|
494
|
+
};
|
|
495
|
+
const jitterCamera = (camera, sample) => {
|
|
496
|
+
const lens = camera;
|
|
497
|
+
if (!lens.setViewOffset) return;
|
|
498
|
+
renderer.getDrawingBufferSize(size);
|
|
499
|
+
const x = sample ? halton(sample, 2) - .5 : 0;
|
|
500
|
+
const y = sample ? halton(sample, 3) - .5 : 0;
|
|
501
|
+
lens.setViewOffset(size.x, size.y, x, y, size.x, size.y);
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
reset: (now) => {
|
|
505
|
+
index = 0;
|
|
506
|
+
changedAt = now;
|
|
507
|
+
},
|
|
508
|
+
moving: (deltaMs) => {
|
|
509
|
+
if (frames === 0) return;
|
|
510
|
+
if (lightOrbit) setAoWeight(aoWeight - 1 / AO_FADE_FRAMES);
|
|
511
|
+
else measureOrbit(deltaMs);
|
|
512
|
+
},
|
|
513
|
+
wants: (now) => (index < frames || aoWeight < 1) && now - changedAt >= SETTLE_MS && !!getPipeline(),
|
|
514
|
+
step: (camera) => {
|
|
515
|
+
const pipeline = getPipeline();
|
|
516
|
+
if (!pipeline) return;
|
|
517
|
+
if (aoWeight < 1) {
|
|
518
|
+
setAoWeight(aoWeight + 1 / AO_FADE_FRAMES);
|
|
519
|
+
pipeline.setCamera(camera);
|
|
520
|
+
pipeline.accumulate(0);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (index > 0) jitterLights(index);
|
|
524
|
+
jitterCamera(camera, index);
|
|
525
|
+
pipeline.setCamera(camera);
|
|
526
|
+
pipeline.accumulate(index);
|
|
527
|
+
camera.clearViewOffset?.();
|
|
528
|
+
if (index > 0) restoreLights();
|
|
529
|
+
index += 1;
|
|
530
|
+
if (index === frames) stamp = sceneStamp(scene);
|
|
531
|
+
},
|
|
532
|
+
present: () => {
|
|
533
|
+
if (frames === 0 || index < frames) return false;
|
|
534
|
+
getPipeline()?.presentAccumulated();
|
|
535
|
+
return !!getPipeline();
|
|
536
|
+
},
|
|
537
|
+
isHolding: () => frames > 0 && index >= frames && !!getPipeline() && stamp === sceneStamp(scene)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
360
540
|
// src/render/scene-setup/setup-lighting.ts
|
|
361
541
|
const SHADOW_NORMAL_BIAS = .02;
|
|
362
542
|
/**
|
|
@@ -366,6 +546,11 @@ const SHADOW_NORMAL_BIAS = .02;
|
|
|
366
546
|
const SUN_SHADOW_BLUR = 6;
|
|
367
547
|
/** Sky and bounce always fill a shadow in, so a fully dark one looks painted on. */
|
|
368
548
|
const SUN_SHADOW_DENSITY = .62;
|
|
549
|
+
/**
|
|
550
|
+
* How far the sun moves while the view refines, as a tangent: about 1 degree. The real sun's 0.27
|
|
551
|
+
* looks hard on a model this size, and much past 1 a small part's shadow dissolves completely.
|
|
552
|
+
*/
|
|
553
|
+
const SUN_DISC_RADIUS = .018;
|
|
369
554
|
/** Survives a solve with the sun, which would otherwise aim at a target outside the scene. */
|
|
370
555
|
const SUN_TARGET_ID = "sun-target";
|
|
371
556
|
function setupLighting(scene, config) {
|
|
@@ -411,6 +596,19 @@ function setupLighting(scene, config) {
|
|
|
411
596
|
sun: sunlight
|
|
412
597
|
};
|
|
413
598
|
}
|
|
599
|
+
/** Moves the sun within its disc and returns how to put it back. */
|
|
600
|
+
function jitterSun(light, index) {
|
|
601
|
+
const home = light.position.clone();
|
|
602
|
+
const toSun = home.clone().sub(light.target.position);
|
|
603
|
+
const side = new three.Vector3(0, 0, 1).cross(toSun);
|
|
604
|
+
if (side.lengthSq() === 0) side.set(1, 0, 0);
|
|
605
|
+
side.setLength(toSun.length() * SUN_DISC_RADIUS);
|
|
606
|
+
const across = toSun.clone().cross(side).setLength(side.length());
|
|
607
|
+
const angle = halton(index, 2) * Math.PI * 2;
|
|
608
|
+
const reach = Math.sqrt(halton(index, 3));
|
|
609
|
+
light.position.addScaledVector(side, Math.cos(angle) * reach).addScaledVector(across, Math.sin(angle) * reach);
|
|
610
|
+
return () => light.position.copy(home);
|
|
611
|
+
}
|
|
414
612
|
/**
|
|
415
613
|
* Sizes a directional light's orthographic shadow frustum to the scene content's bounding sphere —
|
|
416
614
|
* the dominant lever on shadow crispness. No-op on an empty or zero-size box: the frustum would
|
|
@@ -1612,10 +1810,143 @@ function createLabelLayer(container, scene) {
|
|
|
1612
1810
|
}
|
|
1613
1811
|
};
|
|
1614
1812
|
}
|
|
1813
|
+
// src/render/scene-pick.ts
|
|
1814
|
+
const bvhCache = /* @__PURE__ */ new WeakMap();
|
|
1815
|
+
function versionOf$1(attribute) {
|
|
1816
|
+
return attribute?.version ?? 0;
|
|
1817
|
+
}
|
|
1818
|
+
/** BVH for a mesh geometry, building it on first use. Null when the geometry can't take one. */
|
|
1819
|
+
function bvhFor(geometry) {
|
|
1820
|
+
const position = geometry.attributes.position;
|
|
1821
|
+
if (!position || position.count === 0) return null;
|
|
1822
|
+
const index = geometry.index;
|
|
1823
|
+
const key = index ?? position;
|
|
1824
|
+
const cached = bvhCache.get(key);
|
|
1825
|
+
if (cached === null) return null;
|
|
1826
|
+
if (cached && cached.position === position && cached.positionVersion === versionOf$1(position) && cached.indexVersion === versionOf$1(index)) return cached.bvh;
|
|
1827
|
+
const proxy = new three.BufferGeometry();
|
|
1828
|
+
proxy.setAttribute("position", position);
|
|
1829
|
+
if (index) proxy.setIndex(index);
|
|
1830
|
+
try {
|
|
1831
|
+
const bvh = new three_mesh_bvh.MeshBVH(proxy, { indirect: true });
|
|
1832
|
+
bvhCache.set(key, {
|
|
1833
|
+
position,
|
|
1834
|
+
positionVersion: versionOf$1(position),
|
|
1835
|
+
indexVersion: versionOf$1(index),
|
|
1836
|
+
bvh
|
|
1837
|
+
});
|
|
1838
|
+
return bvh;
|
|
1839
|
+
} catch {
|
|
1840
|
+
bvhCache.set(key, null);
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function isBvhMesh(object) {
|
|
1845
|
+
const o = object;
|
|
1846
|
+
return o.isMesh === true && !o.isInstancedMesh && !o.isSkinnedMesh && !o.isBatchedMesh && !o.isLineSegments2 && !object.morphTargetInfluences?.length;
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Visible, pickable objects under `root`: hidden subtrees, viewer aids and edge overlays are
|
|
1850
|
+
* skipped. Overlays are skipped because they double the mesh's own edges and, as fat lines, cost a
|
|
1851
|
+
* linear scan of every segment per ray.
|
|
1852
|
+
*/
|
|
1853
|
+
function collectPickable(root, out = []) {
|
|
1854
|
+
for (const child of root.children) {
|
|
1855
|
+
if (!child.visible || isViewerAidRoot(child) || isEdgeOverlay(child)) continue;
|
|
1856
|
+
out.push(child);
|
|
1857
|
+
collectPickable(child, out);
|
|
1858
|
+
}
|
|
1859
|
+
return out;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Index-buffer ranges a merged mesh currently draws, or null when it draws everything. Hidden
|
|
1863
|
+
* members are the gaps between groups (`shared/merged-draw.ts`).
|
|
1864
|
+
*/
|
|
1865
|
+
function drawnRanges(mesh) {
|
|
1866
|
+
const { groups } = mesh.geometry;
|
|
1867
|
+
if (!Array.isArray(mesh.material) || groups.length === 0) return null;
|
|
1868
|
+
const materials = mesh.material;
|
|
1869
|
+
return groups.filter((g) => materials[g.materialIndex ?? 0]?.visible !== false).map((g) => ({
|
|
1870
|
+
start: g.start,
|
|
1871
|
+
end: g.start + g.count
|
|
1872
|
+
}));
|
|
1873
|
+
}
|
|
1874
|
+
const _inverse = new three.Matrix4();
|
|
1875
|
+
const _localRay = new three.Ray();
|
|
1876
|
+
const _sphere = new three.Sphere();
|
|
1877
|
+
function raycastMeshBvh(mesh, bvh, raycaster) {
|
|
1878
|
+
_inverse.copy(mesh.matrixWorld).invert();
|
|
1879
|
+
_localRay.copy(raycaster.ray).applyMatrix4(_inverse);
|
|
1880
|
+
const ranges = drawnRanges(mesh);
|
|
1881
|
+
let best = null;
|
|
1882
|
+
if (!ranges) best = bvh.raycastFirst(_localRay, three.DoubleSide);
|
|
1883
|
+
else for (const hit of bvh.raycast(_localRay, three.DoubleSide)) {
|
|
1884
|
+
const at = (hit.faceIndex ?? 0) * 3;
|
|
1885
|
+
if (!ranges.some((r) => at >= r.start && at < r.end)) continue;
|
|
1886
|
+
if (!best || hit.distance < best.distance) best = hit;
|
|
1887
|
+
}
|
|
1888
|
+
if (!best) return null;
|
|
1889
|
+
best.point.applyMatrix4(mesh.matrixWorld);
|
|
1890
|
+
best.distance = best.point.distanceTo(raycaster.ray.origin);
|
|
1891
|
+
best.object = mesh;
|
|
1892
|
+
if (best.distance < raycaster.near || best.distance > raycaster.far) return null;
|
|
1893
|
+
return best;
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Every hit along `raycaster`'s ray over the visible content of `scene`, nearest first. Meshes use
|
|
1897
|
+
* the BVH; lines, points and instanced meshes fall back to their own `raycast`.
|
|
1898
|
+
*/
|
|
1899
|
+
function raycastVisible(scene, raycaster) {
|
|
1900
|
+
const hits = [];
|
|
1901
|
+
for (const object of collectPickable(scene)) {
|
|
1902
|
+
if (!object.layers.test(raycaster.layers)) continue;
|
|
1903
|
+
if (isBvhMesh(object)) {
|
|
1904
|
+
const { geometry } = object;
|
|
1905
|
+
if (!geometry.boundingSphere) geometry.computeBoundingSphere();
|
|
1906
|
+
if (!geometry.boundingSphere) continue;
|
|
1907
|
+
_sphere.copy(geometry.boundingSphere).applyMatrix4(object.matrixWorld);
|
|
1908
|
+
if (!raycaster.ray.intersectsSphere(_sphere)) continue;
|
|
1909
|
+
const bvh = bvhFor(geometry);
|
|
1910
|
+
if (bvh) {
|
|
1911
|
+
const hit = raycastMeshBvh(object, bvh, raycaster);
|
|
1912
|
+
if (hit) hits.push(hit);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
object.raycast(raycaster, hits);
|
|
1917
|
+
}
|
|
1918
|
+
return hits.sort((a, b) => a.distance - b.distance);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Builds BVHs for the visible meshes under `root` one per idle slice, so the first hover over a
|
|
1922
|
+
* large model doesn't pay for all of them at once. Returns a cancel function.
|
|
1923
|
+
*/
|
|
1924
|
+
function warmPickIndex(root) {
|
|
1925
|
+
const queue = collectPickable(root).filter(isBvhMesh);
|
|
1926
|
+
const idle = typeof requestIdleCallback === "function" ? (fn) => requestIdleCallback(fn, { timeout: 500 }) : (fn) => setTimeout(fn, 16);
|
|
1927
|
+
const cancelIdle = typeof cancelIdleCallback === "function" ? cancelIdleCallback : (id) => clearTimeout(id);
|
|
1928
|
+
let handle = 0;
|
|
1929
|
+
let cancelled = false;
|
|
1930
|
+
const step = () => {
|
|
1931
|
+
if (cancelled) return;
|
|
1932
|
+
const mesh = queue.shift();
|
|
1933
|
+
if (!mesh) return;
|
|
1934
|
+
bvhFor(mesh.geometry);
|
|
1935
|
+
handle = idle(step);
|
|
1936
|
+
};
|
|
1937
|
+
handle = idle(step);
|
|
1938
|
+
return () => {
|
|
1939
|
+
cancelled = true;
|
|
1940
|
+
cancelIdle(handle);
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1615
1943
|
// src/render/measure.ts
|
|
1616
1944
|
const DEFAULT_SNAP_PIXELS = 12;
|
|
1617
1945
|
const DEFAULT_COLOR = 16763904;
|
|
1618
1946
|
const LINE_PICK_FRACTION = .015;
|
|
1947
|
+
const RENDER_ORDER_LINE = 998;
|
|
1948
|
+
const RENDER_ORDER_MARKER = 999;
|
|
1949
|
+
const RENDER_ORDER_HOVER = 1e3;
|
|
1619
1950
|
const UNIT_DISPLAY = {
|
|
1620
1951
|
Millimeters: {
|
|
1621
1952
|
metersPerUnit: 1 / 1e3,
|
|
@@ -1638,10 +1969,33 @@ const UNIT_DISPLAY = {
|
|
|
1638
1969
|
suffix: "ft"
|
|
1639
1970
|
}
|
|
1640
1971
|
};
|
|
1641
|
-
/**
|
|
1972
|
+
/**
|
|
1973
|
+
* Three significant figures below 1000, whole units above. `toPrecision(3)` alone switches to
|
|
1974
|
+
* exponent notation past 999 ("1.23e+4 mm"), which is unreadable on a building-scale model.
|
|
1975
|
+
*
|
|
1976
|
+
* @internal exported for tests
|
|
1977
|
+
*/
|
|
1642
1978
|
function makeFormatter(displayUnit) {
|
|
1643
1979
|
const unit = displayUnit && UNIT_DISPLAY[displayUnit] || UNIT_DISPLAY.Meters;
|
|
1644
|
-
return (meters) =>
|
|
1980
|
+
return (meters) => {
|
|
1981
|
+
const value = meters / unit.metersPerUnit;
|
|
1982
|
+
const abs = Math.abs(value);
|
|
1983
|
+
const decimals = abs === 0 ? 0 : Math.min(6, Math.max(0, 2 - Math.floor(Math.log10(abs))));
|
|
1984
|
+
return `${value.toFixed(decimals)} ${unit.suffix}`;
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
/** @internal exported for tests */
|
|
1988
|
+
function makeDefaultFormat(fmt) {
|
|
1989
|
+
return (distance, delta) => {
|
|
1990
|
+
const eps = distance * 1e-6;
|
|
1991
|
+
const axes = [
|
|
1992
|
+
"x",
|
|
1993
|
+
"y",
|
|
1994
|
+
"z"
|
|
1995
|
+
].filter((axis) => delta[axis] > eps);
|
|
1996
|
+
if (axes.length < 2) return fmt(distance);
|
|
1997
|
+
return `${fmt(distance)}\n${axes.map((axis) => `Δ${axis} ${fmt(delta[axis])}`).join(" ")}`;
|
|
1998
|
+
};
|
|
1645
1999
|
}
|
|
1646
2000
|
/**
|
|
1647
2001
|
* Raycast threshold for picking lines/points, as a fixed fraction of view size so the grab band
|
|
@@ -1659,138 +2013,129 @@ function pickThreshold(camera, viewTarget) {
|
|
|
1659
2013
|
return ((viewTarget ? camera.position.distanceTo(viewTarget) : camera.position.length()) || 1) * LINE_PICK_FRACTION;
|
|
1660
2014
|
}
|
|
1661
2015
|
/**
|
|
1662
|
-
*
|
|
1663
|
-
* Line/LineSegments → struck segment's 2 endpoints; Points →
|
|
1664
|
-
* carries no usable index
|
|
2016
|
+
* Local-space vertices to consider snapping to, by object type: Mesh → struck triangle's 3
|
|
2017
|
+
* vertices; Line/LineSegments and fat Line2/LineSegments2 → struck segment's 2 endpoints; Points →
|
|
2018
|
+
* the struck vertex. Null when the hit carries no usable index, so the caller keeps the raw point.
|
|
1665
2019
|
*/
|
|
1666
|
-
function
|
|
2020
|
+
function snapCandidates(hit) {
|
|
1667
2021
|
const obj = hit.object;
|
|
1668
|
-
|
|
2022
|
+
const geometry = obj.geometry;
|
|
2023
|
+
if (!geometry) return null;
|
|
2024
|
+
if (obj.isLineSegments2) {
|
|
2025
|
+
const start = geometry.attributes.instanceStart;
|
|
2026
|
+
const end = geometry.attributes.instanceEnd;
|
|
2027
|
+
const i = hit.faceIndex;
|
|
2028
|
+
if (!start || !end || i == null || i >= start.count) return null;
|
|
2029
|
+
return [new three.Vector3().fromBufferAttribute(start, i), new three.Vector3().fromBufferAttribute(end, i)];
|
|
2030
|
+
}
|
|
2031
|
+
const pos = geometry.attributes.position;
|
|
2032
|
+
if (!pos) return null;
|
|
2033
|
+
const at = (indices) => indices.filter((i) => i < pos.count).map((i) => new three.Vector3().fromBufferAttribute(pos, i));
|
|
2034
|
+
if (obj instanceof three.Mesh) return hit.face ? at([
|
|
1669
2035
|
hit.face.a,
|
|
1670
2036
|
hit.face.b,
|
|
1671
2037
|
hit.face.c
|
|
1672
|
-
] : null;
|
|
1673
|
-
if (obj instanceof three.Points) return hit.index != null ? [hit.index] : null;
|
|
2038
|
+
]) : null;
|
|
2039
|
+
if (obj instanceof three.Points) return hit.index != null ? at([hit.index]) : null;
|
|
1674
2040
|
if (obj instanceof three.Line) {
|
|
1675
2041
|
if (hit.index == null) return null;
|
|
1676
|
-
const index =
|
|
2042
|
+
const index = geometry.index;
|
|
1677
2043
|
if (index) {
|
|
1678
2044
|
if (hit.index + 1 >= index.count) return null;
|
|
1679
|
-
return [index.getX(hit.index), index.getX(hit.index + 1)];
|
|
2045
|
+
return at([index.getX(hit.index), index.getX(hit.index + 1)]);
|
|
1680
2046
|
}
|
|
1681
|
-
return [hit.index, hit.index + 1];
|
|
2047
|
+
return at([hit.index, hit.index + 1]);
|
|
1682
2048
|
}
|
|
1683
2049
|
return null;
|
|
1684
2050
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2051
|
+
function toScreen(world, camera, screenSize) {
|
|
2052
|
+
const ndc = world.clone().project(camera);
|
|
2053
|
+
return new three.Vector2((ndc.x + 1) / 2 * screenSize.width, (1 - ndc.y) / 2 * screenSize.height);
|
|
2054
|
+
}
|
|
2055
|
+
/** Where a hit sits on screen: fat lines report the nearest point on the line separately. */
|
|
2056
|
+
function hitPoint(hit) {
|
|
2057
|
+
return hit.pointOnLine ?? hit.point;
|
|
2058
|
+
}
|
|
2059
|
+
function snapHit(hit, camera, screenSize, snapPixels) {
|
|
2060
|
+
const raw = hitPoint(hit).clone();
|
|
2061
|
+
const candidates = snapCandidates(hit);
|
|
2062
|
+
if (!candidates) return {
|
|
2063
|
+
point: raw,
|
|
2064
|
+
snapped: false
|
|
2065
|
+
};
|
|
2066
|
+
const rawScreen = toScreen(raw, camera, screenSize);
|
|
2067
|
+
let best = {
|
|
2068
|
+
point: raw,
|
|
2069
|
+
snapped: false
|
|
2070
|
+
};
|
|
1699
2071
|
let bestPx = snapPixels;
|
|
1700
|
-
for (const
|
|
1701
|
-
|
|
1702
|
-
const
|
|
1703
|
-
const px = toScreen(world).distanceTo(rawScreen);
|
|
2072
|
+
for (const local of candidates) {
|
|
2073
|
+
const world = local.applyMatrix4(hit.object.matrixWorld);
|
|
2074
|
+
const px = toScreen(world, camera, screenSize).distanceTo(rawScreen);
|
|
1704
2075
|
if (px < bestPx) {
|
|
1705
2076
|
bestPx = px;
|
|
1706
|
-
best =
|
|
2077
|
+
best = {
|
|
2078
|
+
point: world,
|
|
2079
|
+
snapped: true
|
|
2080
|
+
};
|
|
1707
2081
|
}
|
|
1708
2082
|
}
|
|
1709
2083
|
return best;
|
|
1710
2084
|
}
|
|
2085
|
+
/** Snap a raycast hit to the nearest geometry vertex within `snapPixels` on screen, else the raw hit point. */
|
|
2086
|
+
function snapToVertex(hit, camera, screenSize, snapPixels) {
|
|
2087
|
+
return snapHit(hit, camera, screenSize, snapPixels).point;
|
|
2088
|
+
}
|
|
1711
2089
|
function createMeasureTool(deps) {
|
|
1712
2090
|
const { canvas, scene, getActiveCamera, getViewTarget, labelLayer, options = {} } = deps;
|
|
2091
|
+
const requestRender = deps.requestRender ?? (() => {});
|
|
1713
2092
|
const snapPixels = options.snapPixels ?? DEFAULT_SNAP_PIXELS;
|
|
1714
2093
|
const color = new three.Color(options.color ?? DEFAULT_COLOR);
|
|
1715
|
-
const
|
|
1716
|
-
const defaultFormat = (d, delta) => `${fmt(d)}\nΔx ${fmt(delta.x)} Δy ${fmt(delta.y)} Δz ${fmt(delta.z)}`;
|
|
1717
|
-
const format = options.format ?? defaultFormat;
|
|
2094
|
+
const format = options.format ?? makeDefaultFormat(makeFormatter(options.displayUnit));
|
|
1718
2095
|
const raycaster = new three.Raycaster();
|
|
1719
2096
|
const pointer = new three.Vector2();
|
|
1720
2097
|
let enabled = false;
|
|
1721
|
-
|
|
1722
|
-
const
|
|
1723
|
-
let line = null;
|
|
1724
|
-
let label = null;
|
|
2098
|
+
let start = null;
|
|
2099
|
+
const measurements = [];
|
|
1725
2100
|
const markerMaterial = new three.PointsMaterial({
|
|
1726
2101
|
color,
|
|
1727
2102
|
size: 8,
|
|
1728
2103
|
sizeAttenuation: false,
|
|
1729
2104
|
depthTest: false
|
|
1730
2105
|
});
|
|
1731
|
-
const
|
|
2106
|
+
const hoverSnappedMaterial = new three.PointsMaterial({
|
|
2107
|
+
color,
|
|
2108
|
+
size: 12,
|
|
2109
|
+
sizeAttenuation: false,
|
|
2110
|
+
depthTest: false
|
|
2111
|
+
});
|
|
2112
|
+
const hoverFreeMaterial = new three.PointsMaterial({
|
|
1732
2113
|
color,
|
|
1733
|
-
size:
|
|
2114
|
+
size: 7,
|
|
1734
2115
|
sizeAttenuation: false,
|
|
1735
2116
|
depthTest: false,
|
|
1736
2117
|
transparent: true,
|
|
1737
|
-
opacity: .
|
|
2118
|
+
opacity: .6
|
|
1738
2119
|
});
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
if (!hoverMarker) {
|
|
1746
|
-
const geometry = new three.BufferGeometry();
|
|
1747
|
-
geometry.setAttribute("position", new three.Float32BufferAttribute([
|
|
1748
|
-
0,
|
|
1749
|
-
0,
|
|
1750
|
-
0
|
|
1751
|
-
], 3));
|
|
1752
|
-
hoverMarker = new three.Points(geometry, hoverMaterial);
|
|
1753
|
-
hoverMarker.renderOrder = 1e3;
|
|
1754
|
-
hoverMarker.userData.id = "measure";
|
|
1755
|
-
hoverMarker.raycast = () => {};
|
|
1756
|
-
scene.add(hoverMarker);
|
|
1757
|
-
}
|
|
1758
|
-
hoverMarker.position.copy(p);
|
|
1759
|
-
hoverMarker.visible = true;
|
|
2120
|
+
const tag = (object, renderOrder) => {
|
|
2121
|
+
object.renderOrder = renderOrder;
|
|
2122
|
+
object.userData.id = "measure";
|
|
2123
|
+
object.raycast = () => {};
|
|
2124
|
+
object.frustumCulled = false;
|
|
2125
|
+
return object;
|
|
1760
2126
|
};
|
|
1761
|
-
const
|
|
2127
|
+
const makePoints = (...ps) => {
|
|
1762
2128
|
const geometry = new three.BufferGeometry();
|
|
1763
|
-
geometry.setAttribute("position", new three.Float32BufferAttribute([
|
|
2129
|
+
geometry.setAttribute("position", new three.Float32BufferAttribute(ps.flatMap((p) => [
|
|
1764
2130
|
p.x,
|
|
1765
2131
|
p.y,
|
|
1766
2132
|
p.z
|
|
1767
|
-
], 3));
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
marker.raycast = () => {};
|
|
1772
|
-
scene.add(marker);
|
|
1773
|
-
return marker;
|
|
2133
|
+
]), 3));
|
|
2134
|
+
const points = tag(new three.Points(geometry, markerMaterial), RENDER_ORDER_MARKER);
|
|
2135
|
+
scene.add(points);
|
|
2136
|
+
return points;
|
|
1774
2137
|
};
|
|
1775
|
-
const
|
|
1776
|
-
points.length = 0;
|
|
1777
|
-
markers.forEach((m) => {
|
|
1778
|
-
m.geometry.dispose();
|
|
1779
|
-
m.removeFromParent();
|
|
1780
|
-
});
|
|
1781
|
-
markers.length = 0;
|
|
1782
|
-
if (line) {
|
|
1783
|
-
line.geometry.dispose();
|
|
1784
|
-
line.material.dispose();
|
|
1785
|
-
line.removeFromParent();
|
|
1786
|
-
line = null;
|
|
1787
|
-
}
|
|
1788
|
-
label?.remove();
|
|
1789
|
-
label = null;
|
|
1790
|
-
};
|
|
1791
|
-
const drawMeasurement = () => {
|
|
1792
|
-
if (points.length !== 2) return;
|
|
1793
|
-
const [a, b] = points;
|
|
2138
|
+
const makeLine = (a, b, opacity = 1) => {
|
|
1794
2139
|
const geometry = new three_addons_lines_LineGeometry_js.LineGeometry();
|
|
1795
2140
|
geometry.setPositions([
|
|
1796
2141
|
a.x,
|
|
@@ -1800,33 +2145,102 @@ function createMeasureTool(deps) {
|
|
|
1800
2145
|
b.y,
|
|
1801
2146
|
b.z
|
|
1802
2147
|
]);
|
|
1803
|
-
const material = new three_addons_lines_LineMaterial_js.LineMaterial({
|
|
2148
|
+
const material = new three_addons_lines_LineMaterial_js.LineMaterial({
|
|
2149
|
+
color,
|
|
2150
|
+
transparent: opacity < 1,
|
|
2151
|
+
opacity
|
|
2152
|
+
});
|
|
1804
2153
|
material.linewidth = 2;
|
|
1805
2154
|
material.depthTest = false;
|
|
1806
|
-
line = new three_addons_lines_Line2_js.Line2(geometry, material);
|
|
1807
|
-
line.renderOrder = 998;
|
|
1808
|
-
line.userData.id = "measure";
|
|
1809
|
-
line.raycast = () => {};
|
|
2155
|
+
const line = tag(new three_addons_lines_Line2_js.Line2(geometry, material), RENDER_ORDER_LINE);
|
|
1810
2156
|
scene.add(line);
|
|
1811
|
-
|
|
2157
|
+
return line;
|
|
2158
|
+
};
|
|
2159
|
+
const moveLine = (line, a, b) => {
|
|
2160
|
+
const buffer = line.geometry.attributes.instanceStart.data;
|
|
2161
|
+
buffer.array.set([
|
|
2162
|
+
a.x,
|
|
2163
|
+
a.y,
|
|
2164
|
+
a.z,
|
|
2165
|
+
b.x,
|
|
2166
|
+
b.y,
|
|
2167
|
+
b.z
|
|
2168
|
+
]);
|
|
2169
|
+
buffer.needsUpdate = true;
|
|
2170
|
+
};
|
|
2171
|
+
const disposeLine = (line) => {
|
|
2172
|
+
line.geometry.dispose();
|
|
2173
|
+
line.material.dispose();
|
|
2174
|
+
line.removeFromParent();
|
|
2175
|
+
};
|
|
2176
|
+
const disposePoints = (points) => {
|
|
2177
|
+
points.geometry.dispose();
|
|
2178
|
+
points.removeFromParent();
|
|
2179
|
+
};
|
|
2180
|
+
const labelText = (a, b) => {
|
|
1812
2181
|
const delta = new three.Vector3(Math.abs(b.x - a.x), Math.abs(b.y - a.y), Math.abs(b.z - a.z));
|
|
1813
|
-
|
|
2182
|
+
return format(a.distanceTo(b), delta);
|
|
2183
|
+
};
|
|
2184
|
+
const midpoint = (a, b) => a.clone().add(b).multiplyScalar(.5);
|
|
2185
|
+
let hoverMarker = null;
|
|
2186
|
+
let rubberBand = null;
|
|
2187
|
+
let rubberLabel = null;
|
|
2188
|
+
const hidePreview = () => {
|
|
2189
|
+
if (hoverMarker) hoverMarker.visible = false;
|
|
2190
|
+
if (rubberBand) rubberBand.visible = false;
|
|
2191
|
+
rubberLabel?.remove();
|
|
2192
|
+
rubberLabel = null;
|
|
2193
|
+
};
|
|
2194
|
+
const showPreview = (snap) => {
|
|
2195
|
+
if (!snap) {
|
|
2196
|
+
hidePreview();
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
if (!hoverMarker) {
|
|
2200
|
+
hoverMarker = makePoints(new three.Vector3());
|
|
2201
|
+
hoverMarker.renderOrder = RENDER_ORDER_HOVER;
|
|
2202
|
+
}
|
|
2203
|
+
hoverMarker.material = snap.snapped ? hoverSnappedMaterial : hoverFreeMaterial;
|
|
2204
|
+
hoverMarker.position.copy(snap.point);
|
|
2205
|
+
hoverMarker.visible = true;
|
|
2206
|
+
if (!start) {
|
|
2207
|
+
if (rubberBand) rubberBand.visible = false;
|
|
2208
|
+
rubberLabel?.remove();
|
|
2209
|
+
rubberLabel = null;
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (!rubberBand) rubberBand = makeLine(start.point, snap.point, .7);
|
|
2213
|
+
moveLine(rubberBand, start.point, snap.point);
|
|
2214
|
+
rubberBand.visible = true;
|
|
2215
|
+
const text = labelText(start.point, snap.point);
|
|
2216
|
+
const mid = midpoint(start.point, snap.point);
|
|
2217
|
+
if (rubberLabel) {
|
|
2218
|
+
rubberLabel.setText(text);
|
|
2219
|
+
rubberLabel.setPosition(mid);
|
|
2220
|
+
} else rubberLabel = labelLayer.addLabel(text, mid, options.labelClassName);
|
|
1814
2221
|
};
|
|
1815
|
-
const
|
|
2222
|
+
const pick = (event) => {
|
|
1816
2223
|
const rect = canvas.getBoundingClientRect();
|
|
1817
|
-
|
|
1818
|
-
|
|
2224
|
+
const cursorX = event.clientX - rect.left;
|
|
2225
|
+
const cursorY = event.clientY - rect.top;
|
|
2226
|
+
pointer.x = cursorX / rect.width * 2 - 1;
|
|
2227
|
+
pointer.y = -(cursorY / rect.height) * 2 + 1;
|
|
1819
2228
|
const camera = getActiveCamera();
|
|
1820
2229
|
raycaster.setFromCamera(pointer, camera);
|
|
1821
|
-
const threshold = pickThreshold(camera, getViewTarget?.());
|
|
2230
|
+
const threshold = pickThreshold(camera, getViewTarget?.()) * 2;
|
|
1822
2231
|
raycaster.params.Line.threshold = threshold;
|
|
1823
2232
|
raycaster.params.Points.threshold = threshold;
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
return snapToVertex(hits[0], camera, {
|
|
2233
|
+
raycaster.params.Line2 = { threshold: snapPixels / 2 };
|
|
2234
|
+
const screen = {
|
|
1827
2235
|
width: rect.width,
|
|
1828
2236
|
height: rect.height
|
|
1829
|
-
}
|
|
2237
|
+
};
|
|
2238
|
+
const cursor = new three.Vector2(cursorX, cursorY);
|
|
2239
|
+
for (const hit of raycastVisible(scene, raycaster)) {
|
|
2240
|
+
if (!(hit.object.isMesh && hit.face != null) && toScreen(hitPoint(hit), camera, screen).distanceTo(cursor) > snapPixels) continue;
|
|
2241
|
+
return snapHit(hit, camera, screen, snapPixels);
|
|
2242
|
+
}
|
|
2243
|
+
return null;
|
|
1830
2244
|
};
|
|
1831
2245
|
let pendingMove = null;
|
|
1832
2246
|
let moveRaf = 0;
|
|
@@ -1846,42 +2260,104 @@ function createMeasureTool(deps) {
|
|
|
1846
2260
|
const latest = pendingMove;
|
|
1847
2261
|
pendingMove = null;
|
|
1848
2262
|
if (!enabled || !latest) return;
|
|
1849
|
-
|
|
2263
|
+
showPreview(pick(latest));
|
|
2264
|
+
requestRender();
|
|
1850
2265
|
});
|
|
1851
2266
|
};
|
|
2267
|
+
const handleLeave = () => {
|
|
2268
|
+
cancelPendingMove();
|
|
2269
|
+
hidePreview();
|
|
2270
|
+
requestRender();
|
|
2271
|
+
};
|
|
2272
|
+
const dropStart = () => {
|
|
2273
|
+
if (!start) return;
|
|
2274
|
+
disposePoints(start.marker);
|
|
2275
|
+
start = null;
|
|
2276
|
+
};
|
|
2277
|
+
const clear = () => {
|
|
2278
|
+
dropStart();
|
|
2279
|
+
for (const m of measurements) {
|
|
2280
|
+
disposePoints(m.markers);
|
|
2281
|
+
disposeLine(m.line);
|
|
2282
|
+
m.label.remove();
|
|
2283
|
+
}
|
|
2284
|
+
measurements.length = 0;
|
|
2285
|
+
hidePreview();
|
|
2286
|
+
requestRender();
|
|
2287
|
+
};
|
|
2288
|
+
const cancel = () => {
|
|
2289
|
+
if (start) {
|
|
2290
|
+
dropStart();
|
|
2291
|
+
hidePreview();
|
|
2292
|
+
requestRender();
|
|
2293
|
+
} else clear();
|
|
2294
|
+
};
|
|
1852
2295
|
const handleClick = (event) => {
|
|
1853
2296
|
if (!enabled) return false;
|
|
1854
|
-
|
|
1855
|
-
const
|
|
1856
|
-
if (
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2297
|
+
cancelPendingMove();
|
|
2298
|
+
const snap = pick(event);
|
|
2299
|
+
if (!snap) return true;
|
|
2300
|
+
if (!start) start = {
|
|
2301
|
+
point: snap.point,
|
|
2302
|
+
marker: makePoints(snap.point)
|
|
2303
|
+
};
|
|
2304
|
+
else if (!snap.point.equals(start.point)) {
|
|
2305
|
+
const a = start.point;
|
|
2306
|
+
const b = snap.point;
|
|
2307
|
+
dropStart();
|
|
2308
|
+
measurements.push({
|
|
2309
|
+
markers: makePoints(a, b),
|
|
2310
|
+
line: makeLine(a, b),
|
|
2311
|
+
label: labelLayer.addLabel(labelText(a, b), midpoint(a, b), options.labelClassName)
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
showPreview(snap);
|
|
2315
|
+
requestRender();
|
|
1860
2316
|
return true;
|
|
1861
2317
|
};
|
|
2318
|
+
const handleKey = (event) => {
|
|
2319
|
+
if (event.key !== "Escape") return;
|
|
2320
|
+
if (event.target?.closest?.("input, textarea, select, [contenteditable]")) return;
|
|
2321
|
+
cancel();
|
|
2322
|
+
};
|
|
2323
|
+
let previousCursor = "";
|
|
2324
|
+
let cancelWarm = null;
|
|
2325
|
+
const setEnabled = (value) => {
|
|
2326
|
+
if (value === enabled) return;
|
|
2327
|
+
enabled = value;
|
|
2328
|
+
if (value) {
|
|
2329
|
+
previousCursor = canvas.style.cursor;
|
|
2330
|
+
canvas.style.cursor = "crosshair";
|
|
2331
|
+
canvas.addEventListener("mouseleave", handleLeave);
|
|
2332
|
+
window.addEventListener("keydown", handleKey);
|
|
2333
|
+
cancelWarm = warmPickIndex(scene);
|
|
2334
|
+
} else {
|
|
2335
|
+
canvas.style.cursor = previousCursor;
|
|
2336
|
+
canvas.removeEventListener("mouseleave", handleLeave);
|
|
2337
|
+
window.removeEventListener("keydown", handleKey);
|
|
2338
|
+
cancelWarm?.();
|
|
2339
|
+
cancelWarm = null;
|
|
2340
|
+
cancelPendingMove();
|
|
2341
|
+
clear();
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
1862
2344
|
return {
|
|
1863
|
-
setEnabled
|
|
1864
|
-
enabled = value;
|
|
1865
|
-
if (!value) {
|
|
1866
|
-
cancelPendingMove();
|
|
1867
|
-
clear();
|
|
1868
|
-
showHover(null);
|
|
1869
|
-
}
|
|
1870
|
-
},
|
|
2345
|
+
setEnabled,
|
|
1871
2346
|
isEnabled: () => enabled,
|
|
1872
2347
|
handleClick,
|
|
1873
2348
|
handleMove,
|
|
2349
|
+
cancel,
|
|
1874
2350
|
clear,
|
|
1875
2351
|
dispose: () => {
|
|
1876
|
-
|
|
2352
|
+
setEnabled(false);
|
|
1877
2353
|
clear();
|
|
1878
|
-
if (hoverMarker)
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
}
|
|
2354
|
+
if (hoverMarker) disposePoints(hoverMarker);
|
|
2355
|
+
if (rubberBand) disposeLine(rubberBand);
|
|
2356
|
+
hoverMarker = null;
|
|
2357
|
+
rubberBand = null;
|
|
1883
2358
|
markerMaterial.dispose();
|
|
1884
|
-
|
|
2359
|
+
hoverSnappedMaterial.dispose();
|
|
2360
|
+
hoverFreeMaterial.dispose();
|
|
1885
2361
|
}
|
|
1886
2362
|
};
|
|
1887
2363
|
}
|
|
@@ -2304,7 +2780,7 @@ function createViewGizmo(deps) {
|
|
|
2304
2780
|
};
|
|
2305
2781
|
}
|
|
2306
2782
|
// src/render/scene-setup/animation-loop.ts
|
|
2307
|
-
function createAnimationLoop(renderer, scene, camera, getActiveCamera, cameraController, controls, getCanvasSize, getPixelRatio, onFrame, grid, gizmo, getRenderPipeline, labelLayer, nearFitter, onDemand = true) {
|
|
2783
|
+
function createAnimationLoop(renderer, scene, camera, getActiveCamera, cameraController, controls, getCanvasSize, getPixelRatio, onFrame, grid, gizmo, getRenderPipeline, labelLayer, nearFitter, onDemand = true, refiner) {
|
|
2308
2784
|
let animationId = null;
|
|
2309
2785
|
let lastTime = performance.now();
|
|
2310
2786
|
let renderRequested = true;
|
|
@@ -2350,17 +2826,22 @@ function createAnimationLoop(renderer, scene, camera, getActiveCamera, cameraCon
|
|
|
2350
2826
|
invalidate();
|
|
2351
2827
|
}
|
|
2352
2828
|
};
|
|
2829
|
+
const drawOverlays = (activeCamera) => {
|
|
2830
|
+
if (labelLayer) labelLayer.render(scene, activeCamera);
|
|
2831
|
+
if (gizmo) gizmo.render(renderer);
|
|
2832
|
+
};
|
|
2353
2833
|
const drawFrame = (activeCamera, delta) => {
|
|
2354
2834
|
const renderPipeline = getRenderPipeline?.();
|
|
2355
2835
|
if (renderPipeline) {
|
|
2356
2836
|
renderPipeline.setCamera(activeCamera);
|
|
2357
2837
|
renderPipeline.render(delta);
|
|
2358
2838
|
} else renderer.render(scene, activeCamera);
|
|
2359
|
-
|
|
2360
|
-
if (gizmo) gizmo.render(renderer);
|
|
2839
|
+
drawOverlays(activeCamera);
|
|
2361
2840
|
};
|
|
2362
2841
|
const renderNow = () => {
|
|
2363
|
-
|
|
2842
|
+
const activeCamera = getActiveCamera();
|
|
2843
|
+
if (refiner?.present()) drawOverlays(activeCamera);
|
|
2844
|
+
else drawFrame(activeCamera, 0);
|
|
2364
2845
|
};
|
|
2365
2846
|
const animate = function() {
|
|
2366
2847
|
animationId = requestAnimationFrame(animate);
|
|
@@ -2374,9 +2855,20 @@ function createAnimationLoop(renderer, scene, camera, getActiveCamera, cameraCon
|
|
|
2374
2855
|
onFrame?.(delta);
|
|
2375
2856
|
const activeCamera = getActiveCamera();
|
|
2376
2857
|
if (onDemand) {
|
|
2377
|
-
|
|
2378
|
-
|
|
2858
|
+
const moved = cameraMoved(activeCamera);
|
|
2859
|
+
if (moved) refiner?.moving(delta * 1e3);
|
|
2860
|
+
const changed = renderRequested || moved;
|
|
2861
|
+
if (!changed && refiner?.wants(now)) {
|
|
2862
|
+
refiner.step(activeCamera);
|
|
2863
|
+
drawOverlays(activeCamera);
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const repaint = now - lastRenderTime >= IDLE_REPAINT_INTERVAL_MS;
|
|
2867
|
+
if (!changed && !repaint) return;
|
|
2379
2868
|
lastRenderTime = now;
|
|
2869
|
+
if (!changed && refiner?.isHolding()) return;
|
|
2870
|
+
renderRequested = false;
|
|
2871
|
+
refiner?.reset(now);
|
|
2380
2872
|
}
|
|
2381
2873
|
drawFrame(activeCamera, delta);
|
|
2382
2874
|
};
|
|
@@ -2493,6 +2985,7 @@ function pickLightSources(sources, maxLights = DEFAULT_MAX_LIGHTS) {
|
|
|
2493
2985
|
function createEmissiveLights(scene) {
|
|
2494
2986
|
const lights = [];
|
|
2495
2987
|
const silenced = /* @__PURE__ */ new Set();
|
|
2988
|
+
const offset = new three.Vector3();
|
|
2496
2989
|
const addLight = (shadowed) => {
|
|
2497
2990
|
const key = new three.PointLight(16777215, 1, 0, 2);
|
|
2498
2991
|
const bounce = new three.PointLight(16777215, 1, 0, 1);
|
|
@@ -2504,7 +2997,9 @@ function createEmissiveLights(scene) {
|
|
|
2504
2997
|
}
|
|
2505
2998
|
const pair = {
|
|
2506
2999
|
key,
|
|
2507
|
-
bounce
|
|
3000
|
+
bounce,
|
|
3001
|
+
home: new three.Vector3(),
|
|
3002
|
+
size: new three.Vector3()
|
|
2508
3003
|
};
|
|
2509
3004
|
lights.push(pair);
|
|
2510
3005
|
scene.add(key, bounce);
|
|
@@ -2545,12 +3040,14 @@ function createEmissiveLights(scene) {
|
|
|
2545
3040
|
};
|
|
2546
3041
|
const sources = pickLightSources(sourcesFrom(parts, maxLights), maxLights);
|
|
2547
3042
|
sources.forEach((source, index) => {
|
|
2548
|
-
const { key, bounce } = lights[index] ?? addLight(index < MAX_SHADOWED_LIGHTS);
|
|
3043
|
+
const { key, bounce, home, size } = lights[index] ?? addLight(index < MAX_SHADOWED_LIGHTS);
|
|
2549
3044
|
const intensity = source.light ?? source.strength * INTENSITY_PER_STRENGTH * radius * radius;
|
|
2550
3045
|
key.color.copy(source.color);
|
|
2551
3046
|
const spread = source.bounds.getSize(new three.Vector3()).length() / 2 / radius;
|
|
2552
3047
|
key.intensity = !source.light && spread > MAX_POINT_SOURCE_SPREAD ? 0 : intensity;
|
|
2553
3048
|
source.bounds.getCenter(key.position);
|
|
3049
|
+
home.copy(key.position);
|
|
3050
|
+
source.bounds.getSize(size);
|
|
2554
3051
|
bounce.color.copy(source.color);
|
|
2555
3052
|
bounce.intensity = intensity * BOUNCE_SHARE / radius;
|
|
2556
3053
|
bounce.position.copy(key.position);
|
|
@@ -2571,8 +3068,16 @@ function createEmissiveLights(scene) {
|
|
|
2571
3068
|
}
|
|
2572
3069
|
silence(parts, sources);
|
|
2573
3070
|
};
|
|
3071
|
+
const place = (position) => {
|
|
3072
|
+
for (const { key, home, size } of lights) {
|
|
3073
|
+
key.position.copy(position(home, size));
|
|
3074
|
+
if (key.castShadow) key.shadow.needsUpdate = true;
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
2574
3077
|
return {
|
|
2575
3078
|
sync,
|
|
3079
|
+
jitter: (index) => place((home, size) => offset.set(halton(index, 2) - .5, halton(index, 3) - .5, halton(index, 5) - .5).multiply(size).add(home)),
|
|
3080
|
+
settle: () => place((home) => home),
|
|
2576
3081
|
dispose: () => sync(false)
|
|
2577
3082
|
};
|
|
2578
3083
|
}
|
|
@@ -2750,6 +3255,8 @@ function createAppearanceController(params) {
|
|
|
2750
3255
|
setAoIntensity,
|
|
2751
3256
|
setLook,
|
|
2752
3257
|
syncEmissiveLights,
|
|
3258
|
+
jitterEmissiveLights: emissiveLights.jitter,
|
|
3259
|
+
settleEmissiveLights: emissiveLights.settle,
|
|
2753
3260
|
getMaterialAppearance: () => require_gpu_dispose.materialAppearanceForLook(activeLook)
|
|
2754
3261
|
};
|
|
2755
3262
|
}
|
|
@@ -2908,7 +3415,9 @@ var EdgeDetectionPass = class extends three_addons_postprocessing_Pass_js.Pass {
|
|
|
2908
3415
|
renderer.setClearColor(7829503, 1);
|
|
2909
3416
|
renderer.autoClear = true;
|
|
2910
3417
|
this.scene.overrideMaterial = this.normalMaterial;
|
|
3418
|
+
const showGlass = require_gpu_dispose.hideGlass(this.scene);
|
|
2911
3419
|
renderer.render(this.scene, this.camera);
|
|
3420
|
+
showGlass();
|
|
2912
3421
|
this.scene.overrideMaterial = previousOverride;
|
|
2913
3422
|
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
|
2914
3423
|
renderer.autoClear = previousAutoClear;
|
|
@@ -2959,6 +3468,68 @@ const HDR_CLAMP_SHADER = {
|
|
|
2959
3468
|
gl_FragColor = vec4(safe(color.r), safe(color.g), safe(color.b), color.a);
|
|
2960
3469
|
}`
|
|
2961
3470
|
};
|
|
3471
|
+
const AVERAGE_SHADER = {
|
|
3472
|
+
vertexShader: `
|
|
3473
|
+
varying vec2 vUv;
|
|
3474
|
+
void main() {
|
|
3475
|
+
vUv = uv;
|
|
3476
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
3477
|
+
}`,
|
|
3478
|
+
fragmentShader: `
|
|
3479
|
+
uniform sampler2D tFrame;
|
|
3480
|
+
varying vec2 vUv;
|
|
3481
|
+
void main() {
|
|
3482
|
+
gl_FragColor = texture2D(tFrame, vUv);
|
|
3483
|
+
}`
|
|
3484
|
+
};
|
|
3485
|
+
/**
|
|
3486
|
+
* Keeps the running average of the composer's finished frames. They're already tone mapped, which
|
|
3487
|
+
* suits what gets averaged here: shadows and edges, not HDR highlights. Half float, because 32
|
|
3488
|
+
* frames blended into 8 bits would band in a soft shadow.
|
|
3489
|
+
*/
|
|
3490
|
+
function createAccumulator(renderer) {
|
|
3491
|
+
const target = new three.WebGLRenderTarget(1, 1, {
|
|
3492
|
+
type: three.HalfFloatType,
|
|
3493
|
+
depthBuffer: false
|
|
3494
|
+
});
|
|
3495
|
+
const material = new three.ShaderMaterial({
|
|
3496
|
+
...AVERAGE_SHADER,
|
|
3497
|
+
uniforms: { tFrame: { value: null } },
|
|
3498
|
+
depthTest: false,
|
|
3499
|
+
depthWrite: false,
|
|
3500
|
+
toneMapped: false,
|
|
3501
|
+
blending: three.CustomBlending,
|
|
3502
|
+
blendSrc: three.ConstantAlphaFactor,
|
|
3503
|
+
blendDst: three.OneMinusConstantAlphaFactor
|
|
3504
|
+
});
|
|
3505
|
+
const quad = new three_addons_postprocessing_Pass_js.FullScreenQuad(material);
|
|
3506
|
+
const draw = (texture, weight) => {
|
|
3507
|
+
material.uniforms.tFrame.value = texture;
|
|
3508
|
+
material.blendAlpha = weight;
|
|
3509
|
+
const autoClear = renderer.autoClear;
|
|
3510
|
+
renderer.autoClear = false;
|
|
3511
|
+
quad.render(renderer);
|
|
3512
|
+
renderer.autoClear = autoClear;
|
|
3513
|
+
};
|
|
3514
|
+
const present = () => {
|
|
3515
|
+
renderer.setRenderTarget(null);
|
|
3516
|
+
draw(target.texture, 1);
|
|
3517
|
+
};
|
|
3518
|
+
return {
|
|
3519
|
+
add: (frame, index, width, height) => {
|
|
3520
|
+
if (target.width !== width || target.height !== height) target.setSize(width, height);
|
|
3521
|
+
renderer.setRenderTarget(target);
|
|
3522
|
+
draw(frame, 1 / (index + 1));
|
|
3523
|
+
present();
|
|
3524
|
+
},
|
|
3525
|
+
present,
|
|
3526
|
+
dispose: () => {
|
|
3527
|
+
target.dispose();
|
|
3528
|
+
material.dispose();
|
|
3529
|
+
quad.dispose();
|
|
3530
|
+
}
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
2962
3533
|
/**
|
|
2963
3534
|
* Without MSAA a sub-pixel feature (a sheet's bright hem edge) comes out as a dashed line, which
|
|
2964
3535
|
* SMAA can't rebuild. At 2× DPR each CSS pixel already holds four samples, and 4× MSAA on top
|
|
@@ -2967,6 +3538,20 @@ const HDR_CLAMP_SHADER = {
|
|
|
2967
3538
|
function msaaSamplesFor(pixelRatio) {
|
|
2968
3539
|
return pixelRatio >= 2 ? 0 : 4;
|
|
2969
3540
|
}
|
|
3541
|
+
function skipGlass(pass) {
|
|
3542
|
+
const hooks = pass;
|
|
3543
|
+
const overrideVisibility = hooks._overrideVisibility.bind(pass);
|
|
3544
|
+
const restoreVisibility = hooks._restoreVisibility.bind(pass);
|
|
3545
|
+
let showGlass = () => {};
|
|
3546
|
+
hooks._overrideVisibility = () => {
|
|
3547
|
+
overrideVisibility();
|
|
3548
|
+
showGlass = require_gpu_dispose.hideGlass(pass.scene);
|
|
3549
|
+
};
|
|
3550
|
+
hooks._restoreVisibility = () => {
|
|
3551
|
+
showGlass();
|
|
3552
|
+
restoreVisibility();
|
|
3553
|
+
};
|
|
3554
|
+
}
|
|
2970
3555
|
function createRenderPipeline(renderer, scene, camera, width, height, options) {
|
|
2971
3556
|
const sceneTarget = new three.WebGLRenderTarget(width, height, { type: three.HalfFloatType });
|
|
2972
3557
|
const composer = new three_addons_postprocessing_EffectComposer_js.EffectComposer(renderer, sceneTarget);
|
|
@@ -2983,6 +3568,7 @@ function createRenderPipeline(renderer, scene, camera, width, height, options) {
|
|
|
2983
3568
|
gtaoPass = new three_addons_postprocessing_GTAOPass_js.GTAOPass(scene, camera, width, height);
|
|
2984
3569
|
gtaoPass.blendIntensity = options.aoIntensity ?? 1;
|
|
2985
3570
|
gtaoPass.updateGtaoMaterial({ screenSpaceRadius: true });
|
|
3571
|
+
skipGlass(gtaoPass);
|
|
2986
3572
|
composer.addPass(gtaoPass);
|
|
2987
3573
|
}
|
|
2988
3574
|
let clampPass = null;
|
|
@@ -3003,6 +3589,7 @@ function createRenderPipeline(renderer, scene, camera, width, height, options) {
|
|
|
3003
3589
|
composer.addPass(outputPass);
|
|
3004
3590
|
renderer.toneMapping = options.toneMapping;
|
|
3005
3591
|
renderer.toneMappingExposure = options.toneMappingExposure;
|
|
3592
|
+
let accumulator = null;
|
|
3006
3593
|
const aoPixelRatioCap = options.aoPixelRatio ?? 1;
|
|
3007
3594
|
let aoScale = 1;
|
|
3008
3595
|
if (gtaoPass) {
|
|
@@ -3041,8 +3628,23 @@ function createRenderPipeline(renderer, scene, camera, width, height, options) {
|
|
|
3041
3628
|
edgePass.enabled = enabled;
|
|
3042
3629
|
},
|
|
3043
3630
|
edgeDetectionEnabled: () => edgePass.enabled,
|
|
3631
|
+
accumulate: (index) => {
|
|
3632
|
+
composer.renderToScreen = false;
|
|
3633
|
+
renderFrame(0);
|
|
3634
|
+
composer.renderToScreen = true;
|
|
3635
|
+
accumulator ??= createAccumulator(renderer);
|
|
3636
|
+
const { texture, width, height } = composer.readBuffer;
|
|
3637
|
+
accumulator.add(texture, index, width, height);
|
|
3638
|
+
},
|
|
3639
|
+
presentAccumulated: () => accumulator?.present(),
|
|
3640
|
+
setAoWeight: (weight) => {
|
|
3641
|
+
if (!gtaoPass) return;
|
|
3642
|
+
gtaoPass.enabled = weight > 0;
|
|
3643
|
+
gtaoPass.blendIntensity = (options.aoIntensity ?? 1) * weight;
|
|
3644
|
+
},
|
|
3044
3645
|
dispose: () => {
|
|
3045
3646
|
composer.dispose();
|
|
3647
|
+
accumulator?.dispose();
|
|
3046
3648
|
gtaoPass?.dispose();
|
|
3047
3649
|
clampPass?.dispose();
|
|
3048
3650
|
bloomPass?.dispose();
|
|
@@ -3604,6 +4206,8 @@ const initThree = function(canvas, options) {
|
|
|
3604
4206
|
canvas,
|
|
3605
4207
|
scene,
|
|
3606
4208
|
getActiveCamera,
|
|
4209
|
+
getViewTarget: () => controls.target,
|
|
4210
|
+
requestRender: () => requestRender(),
|
|
3607
4211
|
labelLayer,
|
|
3608
4212
|
options: {
|
|
3609
4213
|
snapPixels: config.measure.snapPixels,
|
|
@@ -3704,7 +4308,23 @@ const initThree = function(canvas, options) {
|
|
|
3704
4308
|
fullPixelRatio,
|
|
3705
4309
|
requestRender: () => requestRender()
|
|
3706
4310
|
});
|
|
3707
|
-
|
|
4311
|
+
let restoreSun;
|
|
4312
|
+
const refiner = createIdleRefiner({
|
|
4313
|
+
scene,
|
|
4314
|
+
renderer,
|
|
4315
|
+
getPipeline: () => pipeline.get(),
|
|
4316
|
+
frames: config.render.idleRefineFrames,
|
|
4317
|
+
lightOrbitFrameMs: config.render.lightOrbitFrameMs,
|
|
4318
|
+
jitterLights: (index) => {
|
|
4319
|
+
if (sunlight) restoreSun = jitterSun(sunlight, index);
|
|
4320
|
+
appearance.jitterEmissiveLights(index);
|
|
4321
|
+
},
|
|
4322
|
+
restoreLights: () => {
|
|
4323
|
+
restoreSun?.();
|
|
4324
|
+
appearance.settleEmissiveLights();
|
|
4325
|
+
}
|
|
4326
|
+
});
|
|
4327
|
+
const { animate, dispose: disposeAnimation, invalidate, renderNow } = createAnimationLoop(renderer, scene, camera, getActiveCamera, cameraController, controls, getCanvasSize, () => config.render.pixelRatio, config.events.onFrame, grid, gizmo, () => pipeline.get(), labelLayer, nearFitter, config.render.onDemand ?? true, refiner);
|
|
3708
4328
|
requestRender = invalidate;
|
|
3709
4329
|
animate();
|
|
3710
4330
|
scene.up.set(sceneUp.x, sceneUp.y, sceneUp.z);
|