3dtiles-inspector 0.2.13 → 0.2.15
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/CHANGELOG.md +25 -0
- package/README.md +11 -4
- package/dist/inspector-assets/viewer/app.js +4673 -787
- package/package.json +3 -3
- package/src/server/splatCrop/gaussianPrimitives.js +37 -7
- package/src/server/splatCrop/index.js +10 -2
- package/src/server/splatCrop/normalize.js +43 -6
- package/src/server/splatCrop/traversal.js +162 -0
- package/src/server/splatCrop/worker.js +43 -8
- package/src/server/splatCrop/workerPool.js +2 -0
- package/src/server/viewerHtml.js +54 -10
- package/src/viewer/app.js +16 -1
- package/src/viewer/dom/cropUi.js +50 -0
- package/src/viewer/dom/elements.js +8 -0
- package/src/viewer/dom/events.js +63 -0
- package/src/viewer/scene/cameraController.js +18 -3
- package/src/viewer/screenSelection/cropController.js +578 -6
- package/src/viewer/screenSelection/index.js +15 -2
- package/src/viewer/screenSelection/sdf.js +51 -6
- package/src/viewer/screenSelection/state.js +1 -0
- package/src/viewer/transform/geoCamera.js +1 -1
- package/src/viewer/transform/transformModeController.js +19 -2
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BufferGeometry,
|
|
3
|
+
Float32BufferAttribute,
|
|
4
|
+
Group,
|
|
5
|
+
LineBasicMaterial,
|
|
6
|
+
LineSegments,
|
|
7
|
+
Matrix4,
|
|
8
|
+
Quaternion,
|
|
9
|
+
Sphere,
|
|
10
|
+
Vector3,
|
|
11
|
+
} from 'three';
|
|
2
12
|
import {
|
|
3
13
|
SCREEN_SELECTION_ACTION_EXCLUDE,
|
|
14
|
+
SCREEN_SELECTION_ACTION_INCLUDE,
|
|
4
15
|
createScreenSelection,
|
|
5
16
|
createScreenSelectionEdit,
|
|
6
17
|
createScreenSelectionFarHandle,
|
|
@@ -13,6 +24,7 @@ import {
|
|
|
13
24
|
setScreenSelectionFarDepth,
|
|
14
25
|
updateScreenSelectionWorldState,
|
|
15
26
|
} from './index.js';
|
|
27
|
+
import { applySphereSelectionSdf } from './sdf.js';
|
|
16
28
|
import {
|
|
17
29
|
clearOverlay,
|
|
18
30
|
createSelectionData,
|
|
@@ -37,6 +49,122 @@ import { updateCropControls } from '../dom/cropUi.js';
|
|
|
37
49
|
const CAMERA_POSITION_EPSILON_SQ = 1e-12;
|
|
38
50
|
const CAMERA_QUATERNION_EPSILON = 1e-10;
|
|
39
51
|
const CAMERA_PROJECTION_EPSILON = 1e-10;
|
|
52
|
+
const KEEP_SPHERE_RADIUS_TRACK_PIXELS_PER_EXPONENT = 90;
|
|
53
|
+
const KEEP_SPHERE_MIN_RADIUS = 1e-6;
|
|
54
|
+
const KEEP_SPHERE_WIREFRAME_COLOR = 0xffffff;
|
|
55
|
+
const KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS = 20;
|
|
56
|
+
const KEEP_SPHERE_WIREFRAME_LONGITUDE_SEGMENTS = 80;
|
|
57
|
+
const KEEP_SPHERE_WIREFRAME_MERIDIANS = 20;
|
|
58
|
+
const KEEP_SPHERE_WIREFRAME_OVERLAY_OPACITY = 0.35;
|
|
59
|
+
const KEEP_SPHERE_WIREFRAME_RENDER_ORDER = 1000001;
|
|
60
|
+
const RAYCAST_CROP_EPSILON = 1e-6;
|
|
61
|
+
|
|
62
|
+
function pushKeepSphereSegment(vertices, start, end) {
|
|
63
|
+
vertices.push(start[0], start[1], start[2], end[0], end[1], end[2]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createKeepSphereWireframeGeometry() {
|
|
67
|
+
const vertices = [];
|
|
68
|
+
|
|
69
|
+
for (
|
|
70
|
+
let latitudeIndex = 1;
|
|
71
|
+
latitudeIndex < KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS;
|
|
72
|
+
latitudeIndex++
|
|
73
|
+
) {
|
|
74
|
+
const phi =
|
|
75
|
+
(Math.PI * latitudeIndex) / KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS;
|
|
76
|
+
const z = Math.cos(phi);
|
|
77
|
+
const ringRadius = Math.sin(phi);
|
|
78
|
+
|
|
79
|
+
for (
|
|
80
|
+
let longitudeIndex = 0;
|
|
81
|
+
longitudeIndex < KEEP_SPHERE_WIREFRAME_LONGITUDE_SEGMENTS;
|
|
82
|
+
longitudeIndex++
|
|
83
|
+
) {
|
|
84
|
+
const theta0 =
|
|
85
|
+
(Math.PI * 2 * longitudeIndex) /
|
|
86
|
+
KEEP_SPHERE_WIREFRAME_LONGITUDE_SEGMENTS;
|
|
87
|
+
const theta1 =
|
|
88
|
+
(Math.PI * 2 * (longitudeIndex + 1)) /
|
|
89
|
+
KEEP_SPHERE_WIREFRAME_LONGITUDE_SEGMENTS;
|
|
90
|
+
pushKeepSphereSegment(
|
|
91
|
+
vertices,
|
|
92
|
+
[
|
|
93
|
+
Math.cos(theta0) * ringRadius,
|
|
94
|
+
Math.sin(theta0) * ringRadius,
|
|
95
|
+
z,
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
Math.cos(theta1) * ringRadius,
|
|
99
|
+
Math.sin(theta1) * ringRadius,
|
|
100
|
+
z,
|
|
101
|
+
],
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (
|
|
107
|
+
let meridianIndex = 0;
|
|
108
|
+
meridianIndex < KEEP_SPHERE_WIREFRAME_MERIDIANS;
|
|
109
|
+
meridianIndex++
|
|
110
|
+
) {
|
|
111
|
+
const theta =
|
|
112
|
+
(Math.PI * 2 * meridianIndex) / KEEP_SPHERE_WIREFRAME_MERIDIANS;
|
|
113
|
+
const cosTheta = Math.cos(theta);
|
|
114
|
+
const sinTheta = Math.sin(theta);
|
|
115
|
+
|
|
116
|
+
for (
|
|
117
|
+
let latitudeIndex = 0;
|
|
118
|
+
latitudeIndex < KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS;
|
|
119
|
+
latitudeIndex++
|
|
120
|
+
) {
|
|
121
|
+
const phi0 =
|
|
122
|
+
(Math.PI * latitudeIndex) / KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS;
|
|
123
|
+
const phi1 =
|
|
124
|
+
(Math.PI * (latitudeIndex + 1)) /
|
|
125
|
+
KEEP_SPHERE_WIREFRAME_LATITUDE_SEGMENTS;
|
|
126
|
+
pushKeepSphereSegment(
|
|
127
|
+
vertices,
|
|
128
|
+
[
|
|
129
|
+
cosTheta * Math.sin(phi0),
|
|
130
|
+
sinTheta * Math.sin(phi0),
|
|
131
|
+
Math.cos(phi0),
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
cosTheta * Math.sin(phi1),
|
|
135
|
+
sinTheta * Math.sin(phi1),
|
|
136
|
+
Math.cos(phi1),
|
|
137
|
+
],
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const geometry = new BufferGeometry();
|
|
143
|
+
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
|
|
144
|
+
geometry.computeBoundingSphere();
|
|
145
|
+
return geometry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createKeepSphereWireframeMaterial({ depthTest, opacity = 1 }) {
|
|
149
|
+
return new LineBasicMaterial({
|
|
150
|
+
color: KEEP_SPHERE_WIREFRAME_COLOR,
|
|
151
|
+
depthTest,
|
|
152
|
+
depthWrite: depthTest,
|
|
153
|
+
opacity,
|
|
154
|
+
transparent: opacity < 1,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isGaussianSplatObject(object) {
|
|
159
|
+
let current = object;
|
|
160
|
+
while (current) {
|
|
161
|
+
if (current.userData?.gaussianSplat) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
current = current.parent;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
40
168
|
|
|
41
169
|
export function createCropController({
|
|
42
170
|
camera,
|
|
@@ -53,6 +181,7 @@ export function createCropController({
|
|
|
53
181
|
viewerElements,
|
|
54
182
|
cancelOtherPositionPickModes,
|
|
55
183
|
getCurrentRootTransformArray,
|
|
184
|
+
getLocalFrameQuaternion,
|
|
56
185
|
getTilesetBoundingSphere,
|
|
57
186
|
}) {
|
|
58
187
|
let selections = [];
|
|
@@ -62,9 +191,22 @@ export function createCropController({
|
|
|
62
191
|
let pendingMode = false;
|
|
63
192
|
let pendingScreenEdit = null;
|
|
64
193
|
let pendingEditDrag = null;
|
|
194
|
+
let keepSphere = null;
|
|
195
|
+
let keepSphereTrackDrag = null;
|
|
65
196
|
let editCursor = '';
|
|
66
197
|
let hasGaussianSplats = false;
|
|
67
198
|
const sphere = new Sphere();
|
|
199
|
+
const rootMatrix = new Matrix4();
|
|
200
|
+
const rootInverseMatrix = new Matrix4();
|
|
201
|
+
const sphereWorldCenter = new Vector3();
|
|
202
|
+
const sphereLocalCenter = new Vector3();
|
|
203
|
+
const sphereWorldQuaternion = new Quaternion();
|
|
204
|
+
const raycastPlaneMatrix = new Matrix4();
|
|
205
|
+
const raycastPlanePosition = new Vector3();
|
|
206
|
+
const raycastPlaneQuaternion = new Quaternion();
|
|
207
|
+
const raycastPlaneScale = new Vector3();
|
|
208
|
+
const raycastPlaneNormal = new Vector3(0, 0, 1);
|
|
209
|
+
const raycastSphereCenter = new Vector3();
|
|
68
210
|
const cameraForward = new Vector3();
|
|
69
211
|
const screenEditOverlay = createScreenEditOverlay({ overlayEl, rectEl });
|
|
70
212
|
|
|
@@ -75,14 +217,182 @@ export function createCropController({
|
|
|
75
217
|
return findSelection(activeSelectionId)?.selection || null;
|
|
76
218
|
}
|
|
77
219
|
|
|
220
|
+
function isPointInsideScreenSelection(selection, point) {
|
|
221
|
+
if (!selection?.planeMatrices) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const matrixArray of selection.planeMatrices) {
|
|
226
|
+
raycastPlaneMatrix.fromArray(matrixArray);
|
|
227
|
+
raycastPlaneMatrix.decompose(
|
|
228
|
+
raycastPlanePosition,
|
|
229
|
+
raycastPlaneQuaternion,
|
|
230
|
+
raycastPlaneScale,
|
|
231
|
+
);
|
|
232
|
+
raycastPlaneNormal
|
|
233
|
+
.set(0, 0, 1)
|
|
234
|
+
.applyQuaternion(raycastPlaneQuaternion)
|
|
235
|
+
.normalize();
|
|
236
|
+
const distance =
|
|
237
|
+
raycastPlaneNormal.dot(point) -
|
|
238
|
+
raycastPlaneNormal.dot(raycastPlanePosition);
|
|
239
|
+
if (distance > RAYCAST_CROP_EPSILON) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isPointInsideCropSphere(selection, point) {
|
|
247
|
+
const radius = Number(selection?.worldRadius);
|
|
248
|
+
if (
|
|
249
|
+
!Array.isArray(selection?.worldCenter) ||
|
|
250
|
+
!Number.isFinite(radius) ||
|
|
251
|
+
radius <= 0
|
|
252
|
+
) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
raycastSphereCenter.fromArray(selection.worldCenter);
|
|
257
|
+
return (
|
|
258
|
+
point.distanceToSquared(raycastSphereCenter) <=
|
|
259
|
+
radius * radius + RAYCAST_CROP_EPSILON
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isSplatPointVisibleForRaycast(point) {
|
|
264
|
+
for (const selection of selections) {
|
|
265
|
+
if (selection.id === activeSelectionId) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (isPointInsideScreenSelection(selection, point)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
keepSphere?.confirmed &&
|
|
275
|
+
keepSphere.id !== activeSelectionId &&
|
|
276
|
+
!isPointInsideCropSphere(keepSphere, point)
|
|
277
|
+
) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getCurrentRootMatrix(target = rootMatrix) {
|
|
285
|
+
return target.fromArray(getCurrentRootTransformArray());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getCurrentRootScale() {
|
|
289
|
+
const scale = getCurrentRootMatrix(rootMatrix).getMaxScaleOnAxis();
|
|
290
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function updateKeepSphereWorldState(selection = keepSphere) {
|
|
294
|
+
if (!selection) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getCurrentRootMatrix(rootMatrix);
|
|
299
|
+
sphereLocalCenter.fromArray(selection.localCenter);
|
|
300
|
+
sphereWorldCenter.copy(sphereLocalCenter).applyMatrix4(rootMatrix);
|
|
301
|
+
const rootScale = rootMatrix.getMaxScaleOnAxis();
|
|
302
|
+
const safeScale = Number.isFinite(rootScale) && rootScale > 0 ? rootScale : 1;
|
|
303
|
+
selection.worldCenter = sphereWorldCenter.toArray();
|
|
304
|
+
if (typeof getLocalFrameQuaternion === 'function') {
|
|
305
|
+
getLocalFrameQuaternion(sphereWorldCenter, sphereWorldQuaternion);
|
|
306
|
+
} else {
|
|
307
|
+
sphereWorldQuaternion.identity();
|
|
308
|
+
}
|
|
309
|
+
selection.worldQuaternion = sphereWorldQuaternion.toArray();
|
|
310
|
+
selection.worldRadius = Math.max(
|
|
311
|
+
KEEP_SPHERE_MIN_RADIUS,
|
|
312
|
+
selection.localRadius * safeScale,
|
|
313
|
+
);
|
|
314
|
+
applySphereSelectionSdf(selection);
|
|
315
|
+
updateKeepSphereWireframe(selection);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createKeepSphereWireframe(selection) {
|
|
319
|
+
const group = new Group();
|
|
320
|
+
const geometry = createKeepSphereWireframeGeometry();
|
|
321
|
+
const visibleMaterial = createKeepSphereWireframeMaterial({
|
|
322
|
+
depthTest: true,
|
|
323
|
+
});
|
|
324
|
+
const overlayMaterial = createKeepSphereWireframeMaterial({
|
|
325
|
+
depthTest: false,
|
|
326
|
+
opacity: KEEP_SPHERE_WIREFRAME_OVERLAY_OPACITY,
|
|
327
|
+
});
|
|
328
|
+
const visibleWireframe = new LineSegments(geometry, visibleMaterial);
|
|
329
|
+
const overlayWireframe = new LineSegments(geometry, overlayMaterial);
|
|
330
|
+
visibleWireframe.renderOrder = KEEP_SPHERE_WIREFRAME_RENDER_ORDER;
|
|
331
|
+
overlayWireframe.renderOrder = KEEP_SPHERE_WIREFRAME_RENDER_ORDER + 1;
|
|
332
|
+
group.add(visibleWireframe);
|
|
333
|
+
group.add(overlayWireframe);
|
|
334
|
+
group.renderOrder = KEEP_SPHERE_WIREFRAME_RENDER_ORDER;
|
|
335
|
+
group.userData.keepSphereHandle = true;
|
|
336
|
+
group.userData.screenSelectionId = selection.id;
|
|
337
|
+
selection.wireframe = group;
|
|
338
|
+
selection.wireframeGeometry = geometry;
|
|
339
|
+
scene.add(group);
|
|
340
|
+
return group;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function updateKeepSphereWireframe(selection = keepSphere) {
|
|
344
|
+
if (!selection) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const wireframe = selection.wireframe || createKeepSphereWireframe(selection);
|
|
348
|
+
wireframe.position.fromArray(selection.worldCenter);
|
|
349
|
+
wireframe.quaternion.fromArray(selection.worldQuaternion);
|
|
350
|
+
wireframe.scale.setScalar(selection.worldRadius);
|
|
351
|
+
wireframe.visible =
|
|
352
|
+
!selection.confirmed || selection.id === activeSelectionId;
|
|
353
|
+
wireframe.updateMatrix();
|
|
354
|
+
wireframe.updateMatrixWorld(true);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function disposeKeepSphere(selection = keepSphere) {
|
|
358
|
+
if (!selection) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (selection.edit) {
|
|
362
|
+
setScreenSelectionEditSelection(selection.edit, null, true);
|
|
363
|
+
selection.edit.removeFromParent();
|
|
364
|
+
selection.edit = null;
|
|
365
|
+
}
|
|
366
|
+
selection.sdfs?.forEach((sdf) => {
|
|
367
|
+
sdf.removeFromParent();
|
|
368
|
+
});
|
|
369
|
+
selection.sdfs = null;
|
|
370
|
+
if (selection.wireframe) {
|
|
371
|
+
selection.wireframe.traverse((object) => {
|
|
372
|
+
object.material?.dispose?.();
|
|
373
|
+
});
|
|
374
|
+
selection.wireframeGeometry?.dispose?.();
|
|
375
|
+
selection.wireframe.removeFromParent();
|
|
376
|
+
selection.wireframe = null;
|
|
377
|
+
selection.wireframeGeometry = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
78
381
|
function getEntries() {
|
|
79
|
-
|
|
382
|
+
const entries = [
|
|
80
383
|
...selections.map((selection) => ({ style: 'exclude', selection })),
|
|
81
384
|
...pendingSelections.map((selection) => ({
|
|
82
385
|
style: 'preview',
|
|
83
386
|
selection,
|
|
84
387
|
})),
|
|
85
388
|
];
|
|
389
|
+
if (keepSphere) {
|
|
390
|
+
entries.push({
|
|
391
|
+
style: keepSphere.confirmed ? 'include' : 'preview',
|
|
392
|
+
selection: keepSphere,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return entries;
|
|
86
396
|
}
|
|
87
397
|
|
|
88
398
|
function setEditCursor(cursor) {
|
|
@@ -218,6 +528,10 @@ export function createCropController({
|
|
|
218
528
|
function syncWorldState() {
|
|
219
529
|
const transform = getCurrentRootTransformArray();
|
|
220
530
|
getEntries().forEach(({ selection }) => {
|
|
531
|
+
if (selection.type === 'sphere') {
|
|
532
|
+
updateKeepSphereWorldState(selection);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
221
535
|
updateScreenSelectionWorldState(
|
|
222
536
|
selection,
|
|
223
537
|
transform,
|
|
@@ -239,6 +553,15 @@ export function createCropController({
|
|
|
239
553
|
selection,
|
|
240
554
|
})),
|
|
241
555
|
];
|
|
556
|
+
if (keepSphere) {
|
|
557
|
+
styled.push({
|
|
558
|
+
style:
|
|
559
|
+
keepSphere.confirmed && keepSphere.id !== activeSelectionId
|
|
560
|
+
? 'include'
|
|
561
|
+
: 'preview',
|
|
562
|
+
selection: keepSphere,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
242
565
|
|
|
243
566
|
styled.forEach(({ style, selection }) => {
|
|
244
567
|
if (!selection.edit) {
|
|
@@ -255,10 +578,13 @@ export function createCropController({
|
|
|
255
578
|
}
|
|
256
579
|
|
|
257
580
|
function syncFarHandles() {
|
|
258
|
-
const
|
|
581
|
+
const allEntries = getEntries();
|
|
582
|
+
const entries = allEntries.filter(
|
|
583
|
+
({ selection }) => selection.type !== 'sphere',
|
|
584
|
+
);
|
|
259
585
|
if (
|
|
260
586
|
activeSelectionId != null &&
|
|
261
|
-
!
|
|
587
|
+
!allEntries.some(({ selection }) => selection.id === activeSelectionId)
|
|
262
588
|
) {
|
|
263
589
|
activeSelectionId = null;
|
|
264
590
|
}
|
|
@@ -280,6 +606,9 @@ export function createCropController({
|
|
|
280
606
|
pendingScreenSelections: pendingSelections,
|
|
281
607
|
onScreenSelectionRemove: handleSelectionRemove,
|
|
282
608
|
onScreenSelectionSelect: handleSelectionSelect,
|
|
609
|
+
keepSphere,
|
|
610
|
+
onKeepSphereRemove: removeKeepSphere,
|
|
611
|
+
onKeepSphereSelect: selectKeepSphere,
|
|
283
612
|
tilesetHasGaussianSplats: hasGaussianSplats,
|
|
284
613
|
});
|
|
285
614
|
}
|
|
@@ -568,6 +897,206 @@ export function createCropController({
|
|
|
568
897
|
);
|
|
569
898
|
}
|
|
570
899
|
|
|
900
|
+
function formatKeepSphereRadius(radius) {
|
|
901
|
+
const value = Number(radius);
|
|
902
|
+
if (!Number.isFinite(value)) {
|
|
903
|
+
return '0';
|
|
904
|
+
}
|
|
905
|
+
const abs = Math.abs(value);
|
|
906
|
+
if (abs > 0 && (abs < 0.001 || abs >= 1000000)) {
|
|
907
|
+
return value.toExponential(3).replace(/\.?0+e/, 'e');
|
|
908
|
+
}
|
|
909
|
+
return (abs < 1 ? value.toFixed(3) : value.toFixed(2)).replace(
|
|
910
|
+
/\.?0+$/,
|
|
911
|
+
'',
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function createKeepSphere() {
|
|
916
|
+
if (!hasGaussianSplats) {
|
|
917
|
+
setStatus('Crop sphere is available for 3D Gaussian Splat tilesets only.', true);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (keepSphere) {
|
|
921
|
+
setStatus('Only one crop sphere can be active.', true);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (!getTilesetBoundingSphere(sphere)) {
|
|
925
|
+
setStatus('Tileset bounds are not ready yet.', true);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
cancelMode();
|
|
930
|
+
cancelOtherPositionPickModes();
|
|
931
|
+
setTransformMode(null);
|
|
932
|
+
|
|
933
|
+
getCurrentRootMatrix(rootMatrix);
|
|
934
|
+
rootInverseMatrix.copy(rootMatrix).invert();
|
|
935
|
+
const rootScale = getCurrentRootScale();
|
|
936
|
+
const localRadius = Math.max(
|
|
937
|
+
KEEP_SPHERE_MIN_RADIUS,
|
|
938
|
+
sphere.radius / rootScale,
|
|
939
|
+
);
|
|
940
|
+
sphereLocalCenter.copy(sphere.center).applyMatrix4(rootInverseMatrix);
|
|
941
|
+
keepSphere = {
|
|
942
|
+
action: SCREEN_SELECTION_ACTION_INCLUDE,
|
|
943
|
+
confirmed: false,
|
|
944
|
+
id: nextSelectionId++,
|
|
945
|
+
localBaseRadius: localRadius,
|
|
946
|
+
localCenter: sphereLocalCenter.toArray(),
|
|
947
|
+
localRadius,
|
|
948
|
+
radiusExponent: 0,
|
|
949
|
+
type: 'sphere',
|
|
950
|
+
worldCenter: sphere.center.toArray(),
|
|
951
|
+
worldRadius: sphere.radius,
|
|
952
|
+
};
|
|
953
|
+
activeSelectionId = keepSphere.id;
|
|
954
|
+
updateKeepSphereWorldState();
|
|
955
|
+
syncEditSdfs();
|
|
956
|
+
syncFarHandles();
|
|
957
|
+
syncTransformControlsState();
|
|
958
|
+
refreshUi();
|
|
959
|
+
setStatus('Created crop sphere at the tileset center. Adjust radius or move it, then Confirm or Cancel.');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function confirmKeepSphere() {
|
|
963
|
+
if (!keepSphere || keepSphere.confirmed) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
keepSphere.confirmed = true;
|
|
967
|
+
activeSelectionId = null;
|
|
968
|
+
syncEditSdfs();
|
|
969
|
+
syncFarHandles();
|
|
970
|
+
syncTransformControlsState();
|
|
971
|
+
refreshUi();
|
|
972
|
+
setStatus('Confirmed crop sphere. Splats outside the sphere are hidden and will be removed on Save. Select its row to adjust it again.');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function cancelKeepSphere() {
|
|
976
|
+
if (!keepSphere || keepSphere.confirmed) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (activeSelectionId === keepSphere.id) {
|
|
980
|
+
activeSelectionId = null;
|
|
981
|
+
}
|
|
982
|
+
disposeKeepSphere(keepSphere);
|
|
983
|
+
keepSphere = null;
|
|
984
|
+
keepSphereTrackDrag = null;
|
|
985
|
+
syncEditSdfs();
|
|
986
|
+
syncFarHandles();
|
|
987
|
+
syncTransformControlsState();
|
|
988
|
+
refreshUi();
|
|
989
|
+
setStatus('Crop sphere cancelled.');
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function removeKeepSphere() {
|
|
993
|
+
if (!keepSphere) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (activeSelectionId === keepSphere.id) {
|
|
997
|
+
activeSelectionId = null;
|
|
998
|
+
}
|
|
999
|
+
disposeKeepSphere(keepSphere);
|
|
1000
|
+
keepSphere = null;
|
|
1001
|
+
keepSphereTrackDrag = null;
|
|
1002
|
+
syncEditSdfs();
|
|
1003
|
+
syncFarHandles();
|
|
1004
|
+
syncTransformControlsState();
|
|
1005
|
+
refreshUi();
|
|
1006
|
+
setStatus('Removed crop sphere.');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function selectKeepSphere() {
|
|
1010
|
+
if (!keepSphere) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
activeSelectionId = activeSelectionId === keepSphere.id ? null : keepSphere.id;
|
|
1014
|
+
setTransformMode(null);
|
|
1015
|
+
syncEditSdfs();
|
|
1016
|
+
syncFarHandles();
|
|
1017
|
+
syncTransformControlsState();
|
|
1018
|
+
refreshUi();
|
|
1019
|
+
setStatus(
|
|
1020
|
+
activeSelectionId === keepSphere?.id
|
|
1021
|
+
? 'Drag the crop sphere transform controls or adjust radius.'
|
|
1022
|
+
: 'Crop sphere deactivated.',
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function setKeepSphereRadiusExponent(exponent, { commit = false } = {}) {
|
|
1027
|
+
if (!keepSphere) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
const nextExponent = Number(exponent);
|
|
1031
|
+
if (!Number.isFinite(nextExponent)) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
keepSphere.radiusExponent = nextExponent;
|
|
1035
|
+
keepSphere.localRadius = Math.max(
|
|
1036
|
+
KEEP_SPHERE_MIN_RADIUS,
|
|
1037
|
+
keepSphere.localBaseRadius * 2 ** nextExponent,
|
|
1038
|
+
);
|
|
1039
|
+
updateKeepSphereWorldState();
|
|
1040
|
+
syncEditSdfs();
|
|
1041
|
+
refreshUi();
|
|
1042
|
+
if (commit) {
|
|
1043
|
+
setStatus(`Crop sphere radius set to ${formatKeepSphereRadius(keepSphere.worldRadius)}.`);
|
|
1044
|
+
}
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function beginKeepSphereRadiusTrackDrag(clientX) {
|
|
1049
|
+
if (!keepSphere) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
keepSphereTrackDrag = {
|
|
1053
|
+
startClientX: clientX,
|
|
1054
|
+
startExponent: keepSphere.radiusExponent,
|
|
1055
|
+
};
|
|
1056
|
+
viewerElements.keepSphereRadiusTrackEl?.style.setProperty(
|
|
1057
|
+
'--scale-track-offset',
|
|
1058
|
+
'0px',
|
|
1059
|
+
);
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function setKeepSphereRadiusFromTrackClientX(clientX) {
|
|
1064
|
+
if (!keepSphere || !keepSphereTrackDrag) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
const deltaX = clientX - keepSphereTrackDrag.startClientX;
|
|
1068
|
+
viewerElements.keepSphereRadiusTrackEl?.style.setProperty(
|
|
1069
|
+
'--scale-track-offset',
|
|
1070
|
+
`${deltaX}px`,
|
|
1071
|
+
);
|
|
1072
|
+
return setKeepSphereRadiusExponent(
|
|
1073
|
+
keepSphereTrackDrag.startExponent +
|
|
1074
|
+
deltaX / KEEP_SPHERE_RADIUS_TRACK_PIXELS_PER_EXPONENT,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function endKeepSphereRadiusTrackDrag({ commit = false } = {}) {
|
|
1079
|
+
if (!keepSphereTrackDrag) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
keepSphereTrackDrag = null;
|
|
1083
|
+
viewerElements.keepSphereRadiusTrackEl?.style.setProperty(
|
|
1084
|
+
'--scale-track-offset',
|
|
1085
|
+
'0px',
|
|
1086
|
+
);
|
|
1087
|
+
if (commit && keepSphere) {
|
|
1088
|
+
setStatus(`Crop sphere radius set to ${formatKeepSphereRadius(keepSphere.worldRadius)}.`);
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function nudgeKeepSphereRadiusExponent(delta) {
|
|
1094
|
+
return setKeepSphereRadiusExponent(
|
|
1095
|
+
(keepSphere?.radiusExponent || 0) + delta,
|
|
1096
|
+
{ commit: true },
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
571
1100
|
function toggle() {
|
|
572
1101
|
if (!hasGaussianSplats) {
|
|
573
1102
|
setStatus(
|
|
@@ -640,6 +1169,9 @@ export function createCropController({
|
|
|
640
1169
|
|
|
641
1170
|
function findSelection(selectionId) {
|
|
642
1171
|
const id = Number(selectionId);
|
|
1172
|
+
if (keepSphere?.id === id) {
|
|
1173
|
+
return { confirmed: keepSphere.confirmed, selection: keepSphere };
|
|
1174
|
+
}
|
|
643
1175
|
let selection = selections.find((entry) => entry.id === id);
|
|
644
1176
|
if (selection) {
|
|
645
1177
|
return { confirmed: true, selection };
|
|
@@ -670,6 +1202,20 @@ export function createCropController({
|
|
|
670
1202
|
}
|
|
671
1203
|
|
|
672
1204
|
function handleTransformControlObjectChange(object) {
|
|
1205
|
+
if (object?.userData?.keepSphereHandle) {
|
|
1206
|
+
if (!keepSphere || object.userData.screenSelectionId !== keepSphere.id) {
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
getCurrentRootMatrix(rootMatrix);
|
|
1210
|
+
rootInverseMatrix.copy(rootMatrix).invert();
|
|
1211
|
+
sphereLocalCenter.copy(object.position).applyMatrix4(rootInverseMatrix);
|
|
1212
|
+
keepSphere.localCenter = sphereLocalCenter.toArray();
|
|
1213
|
+
updateKeepSphereWorldState();
|
|
1214
|
+
syncEditSdfs();
|
|
1215
|
+
refreshUi();
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
673
1219
|
if (!object?.userData?.screenSelectionFarHandle) {
|
|
674
1220
|
return false;
|
|
675
1221
|
}
|
|
@@ -751,15 +1297,22 @@ export function createCropController({
|
|
|
751
1297
|
|
|
752
1298
|
function getPayload() {
|
|
753
1299
|
syncWorldState();
|
|
754
|
-
|
|
1300
|
+
const payload = selections.map(getScreenSelectionPayload);
|
|
1301
|
+
if (keepSphere?.confirmed) {
|
|
1302
|
+
payload.push(getScreenSelectionPayload(keepSphere));
|
|
1303
|
+
}
|
|
1304
|
+
return payload;
|
|
755
1305
|
}
|
|
756
1306
|
|
|
757
1307
|
function clearAll() {
|
|
758
1308
|
cancelMode();
|
|
759
1309
|
selections.forEach(disposeScreenSelection);
|
|
760
1310
|
pendingSelections.forEach(disposeScreenSelection);
|
|
1311
|
+
disposeKeepSphere(keepSphere);
|
|
761
1312
|
selections = [];
|
|
762
1313
|
pendingSelections = [];
|
|
1314
|
+
keepSphere = null;
|
|
1315
|
+
keepSphereTrackDrag = null;
|
|
763
1316
|
activeSelectionId = null;
|
|
764
1317
|
clearPendingScreenEdit();
|
|
765
1318
|
syncEditSdfs();
|
|
@@ -828,6 +1381,16 @@ export function createCropController({
|
|
|
828
1381
|
return handlePendingEditPointerCancel(event);
|
|
829
1382
|
}
|
|
830
1383
|
|
|
1384
|
+
function isRaycastHitVisible(intersection) {
|
|
1385
|
+
if (!hasGaussianSplats || !isGaussianSplatObject(intersection?.object)) {
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
if (!intersection?.point) {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
return isSplatPointVisibleForRaycast(intersection.point);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
831
1394
|
cameraController.addEventListener(
|
|
832
1395
|
'update',
|
|
833
1396
|
freezePendingScreenEditIfCameraChanged,
|
|
@@ -853,8 +1416,17 @@ export function createCropController({
|
|
|
853
1416
|
handleSelectionRemove,
|
|
854
1417
|
handleSelectionSelect,
|
|
855
1418
|
handleTransformControlObjectChange,
|
|
856
|
-
|
|
1419
|
+
isRaycastHitVisible,
|
|
1420
|
+
hasPendingSelections: () =>
|
|
1421
|
+
pendingSelections.length > 0 || (keepSphere && !keepSphere.confirmed),
|
|
1422
|
+
beginKeepSphereRadiusTrackDrag,
|
|
1423
|
+
cancelKeepSphere,
|
|
1424
|
+
confirmKeepSphere,
|
|
1425
|
+
createKeepSphere,
|
|
1426
|
+
endKeepSphereRadiusTrackDrag,
|
|
1427
|
+
nudgeKeepSphereRadiusExponent,
|
|
857
1428
|
notifyTransformModeChanged,
|
|
1429
|
+
setKeepSphereRadiusFromTrackClientX,
|
|
858
1430
|
setHasGaussianSplats,
|
|
859
1431
|
shouldCapturePointerDown,
|
|
860
1432
|
syncWorldState,
|