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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "3dtiles-inspector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Inspect, align, and save local 3D Tiles root transforms in an interactive browser session.",
|
|
5
5
|
"author": "William Liu <lyz15972107087@gmail.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sparkjsdev/spark": "2.1.0",
|
|
58
|
-
"3d-tiles-renderer": "0.4.
|
|
59
|
-
"3d-tiles-rendererjs-3dgs-plugin": "0.1.
|
|
58
|
+
"3d-tiles-renderer": "0.4.28",
|
|
59
|
+
"3d-tiles-rendererjs-3dgs-plugin": "0.1.14",
|
|
60
60
|
"esbuild": "^0.25.11"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
@@ -2,6 +2,23 @@ const { InspectorError } = require('../../errors');
|
|
|
2
2
|
const { normalizeMatrix4Array } = require('./normalize');
|
|
3
3
|
const { getNodeLocalMatrix } = require('./gltfResource');
|
|
4
4
|
|
|
5
|
+
const KHR_GAUSSIAN_SPLATTING_EXTENSION = 'KHR_gaussian_splatting';
|
|
6
|
+
const KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION =
|
|
7
|
+
'KHR_gaussian_splatting_compression_spz_2';
|
|
8
|
+
const EXT_SPLAT_OPACITY_EXTENSION = 'EXT_splat_opacity';
|
|
9
|
+
|
|
10
|
+
function getGaussianSplattingExtensions(primitive) {
|
|
11
|
+
return primitive?.extensions?.[KHR_GAUSSIAN_SPLATTING_EXTENSION]?.extensions;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getGaussianSplatOpacityExtension(primitive) {
|
|
15
|
+
const extensions = getGaussianSplattingExtensions(primitive);
|
|
16
|
+
return (
|
|
17
|
+
extensions?.[EXT_SPLAT_OPACITY_EXTENSION] ??
|
|
18
|
+
primitive?.extensions?.[EXT_SPLAT_OPACITY_EXTENSION]
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
function getTileLocalTransform(THREE, tile) {
|
|
6
23
|
const matrix = new THREE.Matrix4();
|
|
7
24
|
if (Array.isArray(tile?.transform)) {
|
|
@@ -37,10 +54,22 @@ function getRootUpRotationMatrix(THREE, tileset) {
|
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
function primitiveHasGaussianSpzExtension(primitive) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
const extensions = getGaussianSplattingExtensions(primitive);
|
|
58
|
+
return extensions?.[KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION] != null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getGaussianSplatOpacityAccessorIndex(primitive) {
|
|
62
|
+
const opacityAccessor = getGaussianSplatOpacityExtension(primitive)
|
|
63
|
+
?.opacityAccessor;
|
|
64
|
+
if (opacityAccessor == null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isInteger(opacityAccessor) || opacityAccessor < 0) {
|
|
68
|
+
throw new InspectorError(
|
|
69
|
+
'EXT_splat_opacity opacityAccessor must be a non-negative integer.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return opacityAccessor;
|
|
44
73
|
}
|
|
45
74
|
|
|
46
75
|
function collectGaussianPrimitiveDescriptors(
|
|
@@ -87,12 +116,12 @@ function collectGaussianPrimitiveDescriptors(
|
|
|
87
116
|
const mesh = meshes[node.mesh];
|
|
88
117
|
if (mesh && Array.isArray(mesh.primitives)) {
|
|
89
118
|
mesh.primitives.forEach((primitive, primitiveIndex) => {
|
|
119
|
+
const extensions = getGaussianSplattingExtensions(primitive);
|
|
90
120
|
const gaussianExtension =
|
|
91
|
-
|
|
92
|
-
?.KHR_gaussian_splatting_compression_spz_2;
|
|
121
|
+
extensions?.[KHR_GAUSSIAN_SPLATTING_SPZ_EXTENSION];
|
|
93
122
|
const bufferView = gaussianExtension?.bufferView;
|
|
94
123
|
if (bufferView == null) {
|
|
95
|
-
if (primitive?.extensions?.
|
|
124
|
+
if (primitive?.extensions?.[KHR_GAUSSIAN_SPLATTING_EXTENSION]) {
|
|
96
125
|
throw new InspectorError(
|
|
97
126
|
'Only KHR_gaussian_splatting_compression_spz_2 Gaussian primitives are supported for crop deletion.',
|
|
98
127
|
);
|
|
@@ -288,6 +317,7 @@ function updateGaussianPrimitiveAccessorCounts(resource, descriptors, count) {
|
|
|
288
317
|
|
|
289
318
|
module.exports = {
|
|
290
319
|
collectGaussianPrimitiveDescriptors,
|
|
320
|
+
getGaussianSplatOpacityAccessorIndex,
|
|
291
321
|
getRootUpRotationMatrix,
|
|
292
322
|
getTileLocalTransform,
|
|
293
323
|
getTileWorldTransform,
|
|
@@ -75,8 +75,16 @@ async function deleteSplatsInNormalizedSelections(
|
|
|
75
75
|
planeMatrices: selection.planeMatrices
|
|
76
76
|
? selection.planeMatrices.map((matrix) => matrix.slice())
|
|
77
77
|
: null,
|
|
78
|
-
rect: { ...selection.rect },
|
|
79
|
-
|
|
78
|
+
rect: selection.rect ? { ...selection.rect } : null,
|
|
79
|
+
sphere: selection.sphere
|
|
80
|
+
? {
|
|
81
|
+
center: selection.sphere.center.slice(),
|
|
82
|
+
radius: selection.sphere.radius,
|
|
83
|
+
}
|
|
84
|
+
: null,
|
|
85
|
+
viewProjectionMatrix: selection.viewProjectionMatrix
|
|
86
|
+
? selection.viewProjectionMatrix.slice()
|
|
87
|
+
: null,
|
|
80
88
|
}));
|
|
81
89
|
const workerPool = new SplatCropWorkerPool(SPLAT_CROP_WORKER_COUNT);
|
|
82
90
|
|
|
@@ -2,6 +2,7 @@ const { InspectorError } = require('../../errors');
|
|
|
2
2
|
|
|
3
3
|
const MAX_SCREEN_SELECTIONS = 256;
|
|
4
4
|
const SCREEN_SELECTION_ACTION_EXCLUDE = 'exclude';
|
|
5
|
+
const SCREEN_SELECTION_ACTION_INCLUDE = 'include';
|
|
5
6
|
|
|
6
7
|
function normalizeMatrix4Array(value, name = 'matrix') {
|
|
7
8
|
if (!Array.isArray(value) || value.length !== 16) {
|
|
@@ -56,12 +57,35 @@ function normalizeScreenSelectionAction(value, name) {
|
|
|
56
57
|
if (value == null) {
|
|
57
58
|
return SCREEN_SELECTION_ACTION_EXCLUDE;
|
|
58
59
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
60
|
+
if (
|
|
61
|
+
value !== SCREEN_SELECTION_ACTION_EXCLUDE &&
|
|
62
|
+
value !== SCREEN_SELECTION_ACTION_INCLUDE
|
|
63
|
+
) {
|
|
64
|
+
throw new InspectorError(`${name} must be "exclude" or "include".`);
|
|
61
65
|
}
|
|
62
66
|
return value;
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
function normalizeSphereSelection(value, name) {
|
|
70
|
+
if (value == null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (!value || typeof value !== 'object') {
|
|
74
|
+
throw new InspectorError(`${name} must be an object.`);
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(value.center) || value.center.length !== 3) {
|
|
77
|
+
throw new InspectorError(`${name}.center must be a 3-number array.`);
|
|
78
|
+
}
|
|
79
|
+
const center = value.center.map((entry, index) =>
|
|
80
|
+
normalizeFiniteNumber(entry, `${name}.center[${index}]`),
|
|
81
|
+
);
|
|
82
|
+
const radius = normalizeFiniteNumber(value.radius, `${name}.radius`);
|
|
83
|
+
if (radius <= 0) {
|
|
84
|
+
throw new InspectorError(`${name}.radius must be greater than 0.`);
|
|
85
|
+
}
|
|
86
|
+
return { center, radius };
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
function normalizeScreenSelectionPlaneMatrices(value, name) {
|
|
66
90
|
if (value == null) {
|
|
67
91
|
return null;
|
|
@@ -96,11 +120,23 @@ function normalizeSplatScreenSelections(value) {
|
|
|
96
120
|
);
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
const action = normalizeScreenSelectionAction(
|
|
124
|
+
entry.action,
|
|
125
|
+
`splatScreenSelections[${index}].action`,
|
|
126
|
+
);
|
|
127
|
+
const sphere = normalizeSphereSelection(
|
|
128
|
+
entry.sphere,
|
|
129
|
+
`splatScreenSelections[${index}].sphere`,
|
|
130
|
+
);
|
|
131
|
+
if (sphere) {
|
|
132
|
+
return {
|
|
133
|
+
action,
|
|
134
|
+
sphere,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
99
138
|
return {
|
|
100
|
-
action
|
|
101
|
-
entry.action,
|
|
102
|
-
`splatScreenSelections[${index}].action`,
|
|
103
|
-
),
|
|
139
|
+
action,
|
|
104
140
|
rect: normalizeScreenSelectionRect(
|
|
105
141
|
entry.rect,
|
|
106
142
|
`splatScreenSelections[${index}].rect`,
|
|
@@ -119,6 +155,7 @@ function normalizeSplatScreenSelections(value) {
|
|
|
119
155
|
|
|
120
156
|
module.exports = {
|
|
121
157
|
SCREEN_SELECTION_ACTION_EXCLUDE,
|
|
158
|
+
SCREEN_SELECTION_ACTION_INCLUDE,
|
|
122
159
|
normalizeMatrix4Array,
|
|
123
160
|
normalizeSplatScreenSelections,
|
|
124
161
|
};
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
} = require('./gltfResource');
|
|
17
17
|
const {
|
|
18
18
|
collectGaussianPrimitiveDescriptors,
|
|
19
|
+
getGaussianSplatOpacityAccessorIndex,
|
|
19
20
|
getTileLocalTransform,
|
|
20
21
|
hasNonGaussianScenePrimitives,
|
|
21
22
|
hasScenePrimitives,
|
|
@@ -24,6 +25,10 @@ const {
|
|
|
24
25
|
} = require('./gaussianPrimitives');
|
|
25
26
|
const { SPLAT_CROP_WORKER_COUNT } = require('./workerPool');
|
|
26
27
|
|
|
28
|
+
const ACCESSOR_COMPONENT_TYPE_FLOAT = 5126;
|
|
29
|
+
const ACCESSOR_TYPE_SCALAR = 'SCALAR';
|
|
30
|
+
const FLOAT32_BYTE_LENGTH = 4;
|
|
31
|
+
|
|
27
32
|
class AsyncLimiter {
|
|
28
33
|
constructor(limit) {
|
|
29
34
|
this.active = 0;
|
|
@@ -618,6 +623,154 @@ function markSplatResourceProgress(context, resourcePath) {
|
|
|
618
623
|
});
|
|
619
624
|
}
|
|
620
625
|
|
|
626
|
+
function getPrimitiveForDescriptor(resource, descriptor) {
|
|
627
|
+
if (
|
|
628
|
+
!Number.isInteger(descriptor.meshIndex) ||
|
|
629
|
+
!Number.isInteger(descriptor.primitiveIndex)
|
|
630
|
+
) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
return resource.json.meshes?.[descriptor.meshIndex]?.primitives?.[
|
|
634
|
+
descriptor.primitiveIndex
|
|
635
|
+
];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function getAccessorDefinition(resource, accessorIndex, label) {
|
|
639
|
+
const accessor = resource.json.accessors?.[accessorIndex];
|
|
640
|
+
if (!accessor || typeof accessor !== 'object') {
|
|
641
|
+
throw new InspectorError(`${label} references missing accessor ${accessorIndex}.`);
|
|
642
|
+
}
|
|
643
|
+
if (accessor.componentType !== ACCESSOR_COMPONENT_TYPE_FLOAT) {
|
|
644
|
+
throw new InspectorError(`${label} must use FLOAT componentType.`);
|
|
645
|
+
}
|
|
646
|
+
if (accessor.type !== ACCESSOR_TYPE_SCALAR) {
|
|
647
|
+
throw new InspectorError(`${label} must use SCALAR type.`);
|
|
648
|
+
}
|
|
649
|
+
if (accessor.sparse) {
|
|
650
|
+
throw new InspectorError(`${label} sparse accessors are not supported.`);
|
|
651
|
+
}
|
|
652
|
+
if (!Number.isInteger(accessor.bufferView)) {
|
|
653
|
+
throw new InspectorError(`${label} must reference a bufferView.`);
|
|
654
|
+
}
|
|
655
|
+
if (!Number.isInteger(accessor.count) || accessor.count < 0) {
|
|
656
|
+
throw new InspectorError(`${label} count must be a non-negative integer.`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const byteOffset = Number(accessor.byteOffset ?? 0);
|
|
660
|
+
if (!Number.isInteger(byteOffset) || byteOffset < 0) {
|
|
661
|
+
throw new InspectorError(`${label} byteOffset must be a non-negative integer.`);
|
|
662
|
+
}
|
|
663
|
+
const bufferView = resource.json.bufferViews?.[accessor.bufferView];
|
|
664
|
+
if (!bufferView || typeof bufferView !== 'object') {
|
|
665
|
+
throw new InspectorError(`${label} references missing bufferView.`);
|
|
666
|
+
}
|
|
667
|
+
const byteStride = Number(bufferView.byteStride ?? FLOAT32_BYTE_LENGTH);
|
|
668
|
+
if (!Number.isInteger(byteStride) || byteStride < FLOAT32_BYTE_LENGTH) {
|
|
669
|
+
throw new InspectorError(
|
|
670
|
+
`${label} byteStride must be at least ${FLOAT32_BYTE_LENGTH}.`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { accessor, bufferView, byteOffset, byteStride };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function makeFloat32ScalarAccessorSubsetReplacement(
|
|
678
|
+
resource,
|
|
679
|
+
accessorIndex,
|
|
680
|
+
survivors,
|
|
681
|
+
) {
|
|
682
|
+
const label = `EXT_splat_opacity accessor ${accessorIndex}`;
|
|
683
|
+
const { accessor, bufferView, byteOffset, byteStride } = getAccessorDefinition(
|
|
684
|
+
resource,
|
|
685
|
+
accessorIndex,
|
|
686
|
+
label,
|
|
687
|
+
);
|
|
688
|
+
const slice = getBufferViewSlice(resource, accessor.bufferView);
|
|
689
|
+
const sourceLength =
|
|
690
|
+
accessor.count === 0
|
|
691
|
+
? 0
|
|
692
|
+
: (accessor.count - 1) * byteStride + FLOAT32_BYTE_LENGTH;
|
|
693
|
+
const sourceEnd = byteOffset + sourceLength;
|
|
694
|
+
|
|
695
|
+
if (sourceEnd > slice.bytes.length) {
|
|
696
|
+
throw new InspectorError(`${label} exceeds its bufferView.`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const out = Buffer.allocUnsafe(survivors.length * FLOAT32_BYTE_LENGTH);
|
|
700
|
+
let min = Infinity;
|
|
701
|
+
let max = -Infinity;
|
|
702
|
+
|
|
703
|
+
for (let index = 0; index < survivors.length; index++) {
|
|
704
|
+
const sourceIndex = survivors[index];
|
|
705
|
+
if (
|
|
706
|
+
!Number.isInteger(sourceIndex) ||
|
|
707
|
+
sourceIndex < 0 ||
|
|
708
|
+
sourceIndex >= accessor.count
|
|
709
|
+
) {
|
|
710
|
+
throw new InspectorError(`${label} survivor index is out of range.`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const value = slice.bytes.readFloatLE(
|
|
714
|
+
byteOffset + sourceIndex * byteStride,
|
|
715
|
+
);
|
|
716
|
+
out.writeFloatLE(value, index * FLOAT32_BYTE_LENGTH);
|
|
717
|
+
if (value < min) min = value;
|
|
718
|
+
if (value > max) max = value;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
accessor.count = survivors.length;
|
|
722
|
+
delete accessor.byteOffset;
|
|
723
|
+
delete bufferView.byteStride;
|
|
724
|
+
if (survivors.length > 0) {
|
|
725
|
+
accessor.min = [min];
|
|
726
|
+
accessor.max = [max];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
bufferIndex: slice.bufferIndex,
|
|
731
|
+
bufferViewIndex: accessor.bufferView,
|
|
732
|
+
bytes: out,
|
|
733
|
+
end: slice.end,
|
|
734
|
+
start: slice.start,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function addGaussianSplatOpacityAccessorReplacements(
|
|
739
|
+
resource,
|
|
740
|
+
descriptors,
|
|
741
|
+
survivors,
|
|
742
|
+
replacementsByBuffer,
|
|
743
|
+
) {
|
|
744
|
+
if (!(survivors instanceof Uint32Array) || survivors.length === 0) {
|
|
745
|
+
return 0;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const opacityAccessors = new Set();
|
|
749
|
+
descriptors.forEach((descriptor) => {
|
|
750
|
+
const primitive = getPrimitiveForDescriptor(resource, descriptor);
|
|
751
|
+
const accessorIndex = getGaussianSplatOpacityAccessorIndex(primitive);
|
|
752
|
+
if (accessorIndex !== null) {
|
|
753
|
+
opacityAccessors.add(accessorIndex);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
let replacementCount = 0;
|
|
758
|
+
opacityAccessors.forEach((accessorIndex) => {
|
|
759
|
+
const replacement = makeFloat32ScalarAccessorSubsetReplacement(
|
|
760
|
+
resource,
|
|
761
|
+
accessorIndex,
|
|
762
|
+
survivors,
|
|
763
|
+
);
|
|
764
|
+
if (!replacementsByBuffer.has(replacement.bufferIndex)) {
|
|
765
|
+
replacementsByBuffer.set(replacement.bufferIndex, []);
|
|
766
|
+
}
|
|
767
|
+
addReplacement(replacementsByBuffer.get(replacement.bufferIndex), replacement);
|
|
768
|
+
replacementCount += 1;
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
return replacementCount;
|
|
772
|
+
}
|
|
773
|
+
|
|
621
774
|
async function processGltfResource({
|
|
622
775
|
THREE,
|
|
623
776
|
filePath,
|
|
@@ -679,6 +832,15 @@ async function processGltfResource({
|
|
|
679
832
|
continue;
|
|
680
833
|
}
|
|
681
834
|
if (Number.isInteger(rewrite.survivorCount)) {
|
|
835
|
+
if (rewrite.deleted > 0) {
|
|
836
|
+
modified =
|
|
837
|
+
addGaussianSplatOpacityAccessorReplacements(
|
|
838
|
+
resource,
|
|
839
|
+
viewDescriptors,
|
|
840
|
+
rewrite.survivors,
|
|
841
|
+
replacementsByBuffer,
|
|
842
|
+
) > 0 || modified;
|
|
843
|
+
}
|
|
682
844
|
modified =
|
|
683
845
|
updateGaussianPrimitiveAccessorCounts(
|
|
684
846
|
resource,
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
} = require('./spzSubsetWriter');
|
|
6
6
|
|
|
7
7
|
const SPZ_POSITION_STRIDE = 9;
|
|
8
|
+
const SCREEN_SELECTION_ACTION_INCLUDE = 'include';
|
|
8
9
|
|
|
9
10
|
function multiplyMatrix4(left, right) {
|
|
10
11
|
const out = new Float64Array(16);
|
|
@@ -81,8 +82,18 @@ function buildSourceToScreenSelections(screenSelections, descriptors) {
|
|
|
81
82
|
for (let j = 0; j < descriptors.length; j++) {
|
|
82
83
|
const descriptor = descriptors[j];
|
|
83
84
|
const sourceToWorldMatrix = descriptor.sourceToWorldMatrix;
|
|
85
|
+
if (selection.sphere) {
|
|
86
|
+
sourceToScreenSelections.push({
|
|
87
|
+
action: selection.action,
|
|
88
|
+
radius: selection.sphere.radius,
|
|
89
|
+
sphereCenter: selection.sphere.center,
|
|
90
|
+
sourceToWorldMatrix,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
84
94
|
if (selection.planeMatrices) {
|
|
85
95
|
sourceToScreenSelections.push({
|
|
96
|
+
action: selection.action,
|
|
86
97
|
planes: buildSourceSpacePlaneValues(
|
|
87
98
|
selection.planeMatrices,
|
|
88
99
|
sourceToWorldMatrix,
|
|
@@ -91,6 +102,7 @@ function buildSourceToScreenSelections(screenSelections, descriptors) {
|
|
|
91
102
|
} else {
|
|
92
103
|
const rect = selection.rect;
|
|
93
104
|
sourceToScreenSelections.push({
|
|
105
|
+
action: selection.action,
|
|
94
106
|
matrix: multiplyMatrix4(
|
|
95
107
|
selection.viewProjectionMatrix,
|
|
96
108
|
sourceToWorldMatrix,
|
|
@@ -119,7 +131,25 @@ function buildSourceSpacePlaneValues(planeMatrices, sourceToWorldMatrix) {
|
|
|
119
131
|
return values;
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
function transformPoint(matrix, x, y, z) {
|
|
135
|
+
const w = matrix[3] * x + matrix[7] * y + matrix[11] * z + matrix[15];
|
|
136
|
+
const inverseW = Number.isFinite(w) && w !== 0 ? 1 / w : 1;
|
|
137
|
+
return {
|
|
138
|
+
x: (matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]) * inverseW,
|
|
139
|
+
y: (matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]) * inverseW,
|
|
140
|
+
z: (matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]) * inverseW,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
function centerIsInsideScreenSelection(x, y, z, selection) {
|
|
145
|
+
if (selection.sphereCenter) {
|
|
146
|
+
const point = transformPoint(selection.sourceToWorldMatrix, x, y, z);
|
|
147
|
+
const dx = point.x - selection.sphereCenter[0];
|
|
148
|
+
const dy = point.y - selection.sphereCenter[1];
|
|
149
|
+
const dz = point.z - selection.sphereCenter[2];
|
|
150
|
+
return dx * dx + dy * dy + dz * dz <= selection.radius * selection.radius;
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
if (selection.planes) {
|
|
124
154
|
const planes = selection.planes;
|
|
125
155
|
for (let i = 0; i < planes.length; i += 4) {
|
|
@@ -166,14 +196,12 @@ function centerIsSelectedForDeletion(
|
|
|
166
196
|
sourceToScreenSelections,
|
|
167
197
|
) {
|
|
168
198
|
for (let i = 0; i < sourceToScreenSelections.length; i++) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
) {
|
|
199
|
+
const selection = sourceToScreenSelections[i];
|
|
200
|
+
const inside = centerIsInsideScreenSelection(x, y, z, selection);
|
|
201
|
+
if (selection.action === SCREEN_SELECTION_ACTION_INCLUDE && !inside) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
if (selection.action !== SCREEN_SELECTION_ACTION_INCLUDE && inside) {
|
|
177
205
|
return true;
|
|
178
206
|
}
|
|
179
207
|
}
|
|
@@ -345,6 +373,7 @@ function rewriteSpzBytes({
|
|
|
345
373
|
deleted,
|
|
346
374
|
empty: false,
|
|
347
375
|
splatCount: parsed.sourceCount,
|
|
376
|
+
survivors,
|
|
348
377
|
survivorCount: survivors.length,
|
|
349
378
|
};
|
|
350
379
|
}
|
|
@@ -373,6 +402,11 @@ parentPort.on('message', async (message) => {
|
|
|
373
402
|
bytes = getTransferableBytes(result.bytes);
|
|
374
403
|
transferList.push(bytes.buffer);
|
|
375
404
|
}
|
|
405
|
+
let survivors = null;
|
|
406
|
+
if (result.survivors) {
|
|
407
|
+
survivors = result.survivors;
|
|
408
|
+
transferList.push(survivors.buffer);
|
|
409
|
+
}
|
|
376
410
|
|
|
377
411
|
parentPort.postMessage(
|
|
378
412
|
{
|
|
@@ -383,6 +417,7 @@ parentPort.on('message', async (message) => {
|
|
|
383
417
|
deleted: result.deleted,
|
|
384
418
|
empty: result.empty,
|
|
385
419
|
splatCount: result.splatCount,
|
|
420
|
+
survivors,
|
|
386
421
|
survivorCount: result.survivorCount,
|
|
387
422
|
},
|
|
388
423
|
},
|
|
@@ -110,6 +110,8 @@ class SplatCropWorkerPool {
|
|
|
110
110
|
deleted: Number(result.deleted || 0),
|
|
111
111
|
empty: !!result.empty,
|
|
112
112
|
splatCount: Number(result.splatCount || 0),
|
|
113
|
+
survivors:
|
|
114
|
+
result.survivors instanceof Uint32Array ? result.survivors : null,
|
|
113
115
|
survivorCount: Number(result.survivorCount || 0),
|
|
114
116
|
});
|
|
115
117
|
}
|
package/src/server/viewerHtml.js
CHANGED
|
@@ -612,6 +612,11 @@ function buildViewerHtml(viewerConfig) {
|
|
|
612
612
|
inset 0 -1px 0 rgba(73, 80, 87, 0.08);
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
.scale-track.disabled {
|
|
616
|
+
cursor: default;
|
|
617
|
+
opacity: 0.48;
|
|
618
|
+
}
|
|
619
|
+
|
|
615
620
|
.scale-track:focus {
|
|
616
621
|
outline: none;
|
|
617
622
|
}
|
|
@@ -700,6 +705,18 @@ function buildViewerHtml(viewerConfig) {
|
|
|
700
705
|
min-height: 0;
|
|
701
706
|
}
|
|
702
707
|
|
|
708
|
+
.crop-subsection {
|
|
709
|
+
display: grid;
|
|
710
|
+
gap: 8px;
|
|
711
|
+
min-width: 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.crop-subsection + .crop-subsection {
|
|
715
|
+
margin-top: 6px;
|
|
716
|
+
padding-top: 10px;
|
|
717
|
+
border-top: 1px solid rgba(22, 50, 79, 0.1);
|
|
718
|
+
}
|
|
719
|
+
|
|
703
720
|
.crop-list:empty {
|
|
704
721
|
display: none;
|
|
705
722
|
}
|
|
@@ -1024,18 +1041,45 @@ function buildViewerHtml(viewerConfig) {
|
|
|
1024
1041
|
</label>
|
|
1025
1042
|
</div>
|
|
1026
1043
|
<div id="crop-section" class="toolbar-section" hidden>
|
|
1027
|
-
<div class="
|
|
1028
|
-
<
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
<
|
|
1044
|
+
<div class="crop-subsection">
|
|
1045
|
+
<div class="toolbar-section-header">
|
|
1046
|
+
<p class="toolbar-section-title">Crop Sphere</p>
|
|
1047
|
+
<p id="keep-sphere-radius-value" class="toolbar-value">None</p>
|
|
1048
|
+
</div>
|
|
1049
|
+
<div class="coordinate-actions">
|
|
1050
|
+
<button id="keep-sphere-create" class="wide" type="button">Create Sphere</button>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="range-field">
|
|
1053
|
+
<div
|
|
1054
|
+
id="keep-sphere-radius"
|
|
1055
|
+
class="scale-track disabled"
|
|
1056
|
+
tabindex="0"
|
|
1057
|
+
aria-label="Crop sphere radius"
|
|
1058
|
+
aria-disabled="true"
|
|
1059
|
+
>
|
|
1060
|
+
<span class="scale-track-center" aria-hidden="true"></span>
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div class="status-actions">
|
|
1064
|
+
<button id="keep-sphere-confirm" type="button">Confirm</button>
|
|
1065
|
+
<button id="keep-sphere-cancel" type="button">Cancel</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div id="keep-sphere-list" class="crop-list"></div>
|
|
1033
1068
|
</div>
|
|
1034
|
-
<div class="
|
|
1035
|
-
<
|
|
1036
|
-
|
|
1069
|
+
<div class="crop-subsection">
|
|
1070
|
+
<div class="toolbar-section-header">
|
|
1071
|
+
<p class="toolbar-section-title">Crop Regions</p>
|
|
1072
|
+
<p id="crop-count-value" class="toolbar-value">0</p>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div class="coordinate-actions">
|
|
1075
|
+
<button id="crop-screen-select" class="wide" type="button">Draw Region</button>
|
|
1076
|
+
</div>
|
|
1077
|
+
<div class="status-actions">
|
|
1078
|
+
<button id="crop-screen-confirm" type="button">Confirm</button>
|
|
1079
|
+
<button id="crop-screen-cancel" type="button">Cancel</button>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div id="crop-list" class="crop-list"></div>
|
|
1037
1082
|
</div>
|
|
1038
|
-
<div id="crop-list" class="crop-list"></div>
|
|
1039
1083
|
</div>
|
|
1040
1084
|
</div>
|
|
1041
1085
|
<div class="toolbar-section status-panel">
|
package/src/viewer/app.js
CHANGED
|
@@ -331,8 +331,13 @@ cropController = createCropController({
|
|
|
331
331
|
cancelOtherPositionPickModes: () => setPositionController.cancelMode(),
|
|
332
332
|
getCurrentRootTransformArray: () =>
|
|
333
333
|
rootTransform.getCurrentRootTransformArray(),
|
|
334
|
+
getLocalFrameQuaternion: (referencePoint, target) =>
|
|
335
|
+
geoCamera.getLocalFrameQuaternion(referencePoint, target),
|
|
334
336
|
getTilesetBoundingSphere,
|
|
335
337
|
});
|
|
338
|
+
cameraController.setRaycastHitFilter((intersection) =>
|
|
339
|
+
cropController?.isRaycastHitVisible(intersection) ?? true,
|
|
340
|
+
);
|
|
336
341
|
|
|
337
342
|
transformModeController = createTransformModeController({
|
|
338
343
|
cropController,
|
|
@@ -516,7 +521,7 @@ async function saveTransform() {
|
|
|
516
521
|
cancelPositionPickModes();
|
|
517
522
|
if (cropController.hasPendingSelections()) {
|
|
518
523
|
setStatus(
|
|
519
|
-
'Confirm or cancel pending
|
|
524
|
+
'Confirm or cancel pending crop selections before saving.',
|
|
520
525
|
true,
|
|
521
526
|
);
|
|
522
527
|
return;
|
|
@@ -601,8 +606,15 @@ bindViewerEvents({
|
|
|
601
606
|
getTiles: () => tiles,
|
|
602
607
|
handlers: {
|
|
603
608
|
cancelCropScreenSelection: cropController.cancel,
|
|
609
|
+
beginKeepSphereRadiusTrackDrag:
|
|
610
|
+
cropController.beginKeepSphereRadiusTrackDrag,
|
|
611
|
+
cancelKeepSphere: cropController.cancelKeepSphere,
|
|
604
612
|
cancelPositionPickModes,
|
|
605
613
|
confirmCropScreenSelection: cropController.confirm,
|
|
614
|
+
confirmKeepSphere: cropController.confirmKeepSphere,
|
|
615
|
+
createKeepSphere: cropController.createKeepSphere,
|
|
616
|
+
endKeepSphereRadiusTrackDrag:
|
|
617
|
+
cropController.endKeepSphereRadiusTrackDrag,
|
|
606
618
|
handleScreenSelectionPointerCancel: cropController.handlePointerCancel,
|
|
607
619
|
handleScreenSelectionPointerDown: cropController.handlePointerDown,
|
|
608
620
|
handleScreenSelectionPointerMove: cropController.handlePointerMove,
|
|
@@ -617,6 +629,9 @@ bindViewerEvents({
|
|
|
617
629
|
requestViewerShutdown,
|
|
618
630
|
resetToSaved,
|
|
619
631
|
saveTransform,
|
|
632
|
+
nudgeKeepSphereRadiusExponent: cropController.nudgeKeepSphereRadiusExponent,
|
|
633
|
+
setKeepSphereRadiusFromTrackClientX:
|
|
634
|
+
cropController.setKeepSphereRadiusFromTrackClientX,
|
|
620
635
|
setTerrainEnabled: viewerToggles.setTerrainEnabled,
|
|
621
636
|
toggleBoundingVolume: viewerToggles.toggleBoundingVolume,
|
|
622
637
|
toggleCropScreenSelectionMode: cropController.toggle,
|