@d5techs/3dgs-lib 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/3dgs-lib.js CHANGED
@@ -7125,6 +7125,12 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7125
7125
  __publicField(this, "sortStateInitialized", false);
7126
7126
  __publicField(this, "sortFrequency", 1);
7127
7127
  __publicField(this, "frameCounter", 0);
7128
+ // 编辑器状态
7129
+ __publicField(this, "editorStateBuffer", null);
7130
+ __publicField(this, "editorPipeline", null);
7131
+ __publicField(this, "editorBindGroupLayout", null);
7132
+ __publicField(this, "editorBindGroup", null);
7133
+ __publicField(this, "editorEnabled", false);
7128
7134
  // 深度法线Pass依赖资源
7129
7135
  __publicField(this, "depthNormalPipeline", null);
7130
7136
  __publicField(this, "depthRT", null);
@@ -7485,6 +7491,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7485
7491
  { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
7486
7492
  ]
7487
7493
  });
7494
+ if (this.editorEnabled) this.rebuildEditorBindGroup();
7488
7495
  }
7489
7496
  setCompactData(compactData) {
7490
7497
  const device = this.renderer.device;
@@ -7531,6 +7538,7 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7531
7538
  { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } }
7532
7539
  ]
7533
7540
  });
7541
+ if (this.editorEnabled) this.rebuildEditorBindGroup();
7534
7542
  }
7535
7543
  render(pass) {
7536
7544
  if (this.splatCount === 0 || !this.bindGroup || !this.sorter) {
@@ -7574,8 +7582,13 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7574
7582
  });
7575
7583
  this.sorter.sort();
7576
7584
  }
7577
- pass.setPipeline(this.pipeline);
7578
- pass.setBindGroup(0, this.bindGroup);
7585
+ if (this.editorEnabled && this.editorPipeline && this.editorBindGroup) {
7586
+ pass.setPipeline(this.editorPipeline);
7587
+ pass.setBindGroup(0, this.editorBindGroup);
7588
+ } else {
7589
+ pass.setPipeline(this.pipeline);
7590
+ pass.setBindGroup(0, this.bindGroup);
7591
+ }
7579
7592
  pass.drawIndirect(this.sorter.getDrawIndirectBuffer(), 0);
7580
7593
  }
7581
7594
  getSplatCount() {
@@ -7824,7 +7837,358 @@ const _GSSplatRenderer = class _GSSplatRenderer {
7824
7837
  maxSplatCount: 0
7825
7838
  };
7826
7839
  }
7840
+ // ============================================
7841
+ // 编辑器状态管理
7842
+ // ============================================
7843
+ /**
7844
+ * 启用编辑器模式,传入每个 splat 的状态数据
7845
+ */
7846
+ setEditorState(stateData) {
7847
+ var _a2;
7848
+ if (this.splatCount === 0 || !this.splatBuffer) return;
7849
+ const device = this.renderer.device;
7850
+ const bufSize = Math.ceil(stateData.byteLength / 4) * 4;
7851
+ if (!this.editorStateBuffer || this.editorStateBuffer.size < bufSize) {
7852
+ (_a2 = this.editorStateBuffer) == null ? void 0 : _a2.destroy();
7853
+ this.editorStateBuffer = device.createBuffer({
7854
+ size: bufSize,
7855
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
7856
+ });
7857
+ }
7858
+ const aligned = this.alignStateData(stateData);
7859
+ device.queue.writeBuffer(this.editorStateBuffer, 0, aligned);
7860
+ if (!this.editorPipeline) {
7861
+ this.createEditorPipeline();
7862
+ }
7863
+ this.rebuildEditorBindGroup();
7864
+ this.editorEnabled = true;
7865
+ }
7866
+ /**
7867
+ * 更新编辑器状态数据
7868
+ */
7869
+ updateEditorState(stateData) {
7870
+ if (!this.editorStateBuffer || !this.editorEnabled) return;
7871
+ const aligned = this.alignStateData(stateData);
7872
+ this.renderer.device.queue.writeBuffer(this.editorStateBuffer, 0, aligned);
7873
+ }
7874
+ alignStateData(data) {
7875
+ const alignedSize = Math.ceil(data.byteLength / 4) * 4;
7876
+ const buf = new ArrayBuffer(alignedSize);
7877
+ new Uint8Array(buf).set(data);
7878
+ return buf;
7879
+ }
7880
+ /**
7881
+ * 禁用编辑器模式
7882
+ */
7883
+ clearEditorState() {
7884
+ this.editorEnabled = false;
7885
+ this.editorBindGroup = null;
7886
+ if (this.editorStateBuffer) {
7887
+ this.editorStateBuffer.destroy();
7888
+ this.editorStateBuffer = null;
7889
+ }
7890
+ }
7891
+ createEditorPipeline() {
7892
+ const device = this.renderer.device;
7893
+ const editorShaderCode = this.buildEditorShader();
7894
+ const shaderModule = device.createShaderModule({ code: editorShaderCode });
7895
+ this.editorBindGroupLayout = device.createBindGroupLayout({
7896
+ entries: [
7897
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
7898
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
7899
+ { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
7900
+ { binding: 3, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }
7901
+ ]
7902
+ });
7903
+ const pipelineLayout = device.createPipelineLayout({
7904
+ bindGroupLayouts: [this.editorBindGroupLayout]
7905
+ });
7906
+ this.editorPipeline = device.createRenderPipeline({
7907
+ layout: pipelineLayout,
7908
+ vertex: { module: shaderModule, entryPoint: "vs_editor" },
7909
+ fragment: {
7910
+ module: shaderModule,
7911
+ entryPoint: "fs_editor",
7912
+ targets: [{
7913
+ format: this.renderer.format,
7914
+ blend: {
7915
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
7916
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
7917
+ }
7918
+ }]
7919
+ },
7920
+ primitive: { topology: "triangle-strip" },
7921
+ depthStencil: {
7922
+ format: this.renderer.depthFormat,
7923
+ depthWriteEnabled: true,
7924
+ depthCompare: "always"
7925
+ }
7926
+ });
7927
+ }
7928
+ rebuildEditorBindGroup() {
7929
+ if (!this.editorBindGroupLayout || !this.splatBuffer || !this.sorter || !this.editorStateBuffer) return;
7930
+ this.editorBindGroup = this.renderer.device.createBindGroup({
7931
+ layout: this.editorBindGroupLayout,
7932
+ entries: [
7933
+ { binding: 0, resource: { buffer: this.uniformBuffer } },
7934
+ { binding: 1, resource: { buffer: this.splatBuffer } },
7935
+ { binding: 2, resource: { buffer: this.sorter.getIndicesBuffer() } },
7936
+ { binding: 3, resource: { buffer: this.editorStateBuffer } }
7937
+ ]
7938
+ });
7939
+ }
7940
+ buildEditorShader() {
7941
+ return (
7942
+ /* wgsl */
7943
+ `
7944
+ const SQRT_8: f32 = 2.82842712475;
7945
+ const SH_C0: f32 = 0.28209479177387814;
7946
+ const SH_C1: f32 = 0.4886025119029199;
7947
+ const SH_C2_0: f32 = 1.0925484305920792;
7948
+ const SH_C2_1: f32 = -1.0925484305920792;
7949
+ const SH_C2_2: f32 = 0.31539156525252005;
7950
+ const SH_C2_3: f32 = -1.0925484305920792;
7951
+ const SH_C2_4: f32 = 0.5462742152960396;
7952
+ const SH_C3_0: f32 = -0.5900435899266435;
7953
+ const SH_C3_1: f32 = 2.890611442640554;
7954
+ const SH_C3_2: f32 = -0.4570457994644658;
7955
+ const SH_C3_3: f32 = 0.3731763325901154;
7956
+ const SH_C3_4: f32 = -0.4570457994644658;
7957
+ const SH_C3_5: f32 = 1.4453057213202769;
7958
+ const SH_C3_6: f32 = -0.5900435899266435;
7959
+ const LOW_PASS_FILTER: f32 = 0.3;
7960
+ const ALPHA_CULL_THRESHOLD: f32 = 0.00392156863;
7961
+ const GAUSSIAN_K: f32 = 4.0;
7962
+ const EXP_NEG_K: f32 = 0.01831563888873418;
7963
+ const INV_ONE_MINUS_EXP_NEG_K: f32 = 1.01865736036377408;
7964
+
7965
+ const STATE_SELECTED: u32 = 1u;
7966
+ const STATE_HIDDEN: u32 = 2u;
7967
+ const STATE_DELETED: u32 = 4u;
7968
+
7969
+ struct Uniforms {
7970
+ view: mat4x4<f32>,
7971
+ proj: mat4x4<f32>,
7972
+ model: mat4x4<f32>,
7973
+ cameraPos: vec3<f32>,
7974
+ _pad: f32,
7975
+ screenSize: vec2<f32>,
7976
+ _pad2: vec2<f32>,
7977
+ }
7978
+
7979
+ struct Splat {
7980
+ mean: vec3<f32>, _pad0: f32,
7981
+ scale: vec3<f32>, _pad1: f32,
7982
+ rotation: vec4<f32>,
7983
+ colorDC: vec3<f32>,
7984
+ opacity: f32,
7985
+ sh1: array<f32, 9>,
7986
+ sh2: array<f32, 15>,
7987
+ sh3: array<f32, 21>,
7988
+ _pad2: array<f32, 3>,
7989
+ }
7990
+
7991
+ fn evalSH(splat: Splat, dir: vec3<f32>) -> vec3<f32> {
7992
+ let x = dir.x; let y = dir.y; let z = dir.z;
7993
+ var result = vec3<f32>(0.0);
7994
+ result += (-SH_C1 * y) * vec3<f32>(splat.sh1[0], splat.sh1[1], splat.sh1[2]);
7995
+ result += ( SH_C1 * z) * vec3<f32>(splat.sh1[3], splat.sh1[4], splat.sh1[5]);
7996
+ result += (-SH_C1 * x) * vec3<f32>(splat.sh1[6], splat.sh1[7], splat.sh1[8]);
7997
+ let xx = x * x; let yy = y * y; let zz = z * z;
7998
+ let xy = x * y; let yz = y * z; let xz = x * z;
7999
+ result += (SH_C2_0 * xy) * vec3<f32>(splat.sh2[0], splat.sh2[1], splat.sh2[2]);
8000
+ result += (SH_C2_1 * yz) * vec3<f32>(splat.sh2[3], splat.sh2[4], splat.sh2[5]);
8001
+ result += (SH_C2_2 * (2.0 * zz - xx - yy)) * vec3<f32>(splat.sh2[6], splat.sh2[7], splat.sh2[8]);
8002
+ result += (SH_C2_3 * xz) * vec3<f32>(splat.sh2[9], splat.sh2[10], splat.sh2[11]);
8003
+ result += (SH_C2_4 * (xx - yy)) * vec3<f32>(splat.sh2[12], splat.sh2[13], splat.sh2[14]);
8004
+ result += (SH_C3_0 * y * (3.0 * xx - yy)) * vec3<f32>(splat.sh3[0], splat.sh3[1], splat.sh3[2]);
8005
+ result += (SH_C3_1 * xy * z) * vec3<f32>(splat.sh3[3], splat.sh3[4], splat.sh3[5]);
8006
+ result += (SH_C3_2 * y * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[6], splat.sh3[7], splat.sh3[8]);
8007
+ result += (SH_C3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy)) * vec3<f32>(splat.sh3[9], splat.sh3[10], splat.sh3[11]);
8008
+ result += (SH_C3_4 * x * (4.0 * zz - xx - yy)) * vec3<f32>(splat.sh3[12], splat.sh3[13], splat.sh3[14]);
8009
+ result += (SH_C3_5 * z * (xx - yy)) * vec3<f32>(splat.sh3[15], splat.sh3[16], splat.sh3[17]);
8010
+ result += (SH_C3_6 * x * (xx - 3.0 * yy)) * vec3<f32>(splat.sh3[18], splat.sh3[19], splat.sh3[20]);
8011
+ return result;
8012
+ }
8013
+
8014
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
8015
+ @group(0) @binding(1) var<storage, read> splats: array<Splat>;
8016
+ @group(0) @binding(2) var<storage, read> sortedIndices: array<u32>;
8017
+ @group(0) @binding(3) var<storage, read> splatState: array<u32>;
8018
+
8019
+ fn getState(splatIndex: u32) -> u32 {
8020
+ let wordIndex = splatIndex / 4u;
8021
+ let byteIndex = splatIndex % 4u;
8022
+ return (splatState[wordIndex] >> (byteIndex * 8u)) & 0xFFu;
8023
+ }
8024
+
8025
+ struct VertexOutput {
8026
+ @builtin(position) position: vec4<f32>,
8027
+ @location(0) fragPos: vec2<f32>,
8028
+ @location(1) color: vec3<f32>,
8029
+ @location(2) opacity: f32,
8030
+ @location(3) @interpolate(flat) stateFlags: u32,
8031
+ }
8032
+
8033
+ const QUAD_POSITIONS = array<vec2<f32>, 4>(
8034
+ vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 1.0),
8035
+ vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
8036
+ );
8037
+
8038
+ fn computeClipFactor(alpha: f32) -> f32 {
8039
+ if alpha <= ALPHA_CULL_THRESHOLD { return 0.0; }
8040
+ let threshold = 1.0 / (255.0 * alpha);
8041
+ if threshold >= 1.0 { return 0.0; }
8042
+ let targetExp = threshold * (1.0 - EXP_NEG_K) + EXP_NEG_K;
8043
+ if targetExp >= 1.0 { return 0.0; }
8044
+ let A = -log(targetExp) / GAUSSIAN_K;
8045
+ return min(1.0, sqrt(A));
8046
+ }
8047
+
8048
+ fn quatToMat3(q: vec4<f32>) -> mat3x3<f32> {
8049
+ let r = q.x; let x = q.y; let y = q.z; let z = q.w;
8050
+ return mat3x3<f32>(
8051
+ vec3<f32>(1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + r * z), 2.0 * (x * z - r * y)),
8052
+ vec3<f32>(2.0 * (x * y - r * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + r * x)),
8053
+ vec3<f32>(2.0 * (x * z + r * y), 2.0 * (y * z - r * x), 1.0 - 2.0 * (x * x + y * y))
8054
+ );
8055
+ }
8056
+
8057
+ fn computeCovariance3D(scale: vec3<f32>, rotation: vec4<f32>) -> mat3x3<f32> {
8058
+ let R = quatToMat3(rotation);
8059
+ let S = mat3x3<f32>(vec3<f32>(scale.x, 0.0, 0.0), vec3<f32>(0.0, scale.y, 0.0), vec3<f32>(0.0, 0.0, scale.z));
8060
+ let M = R * S;
8061
+ return M * transpose(M);
8062
+ }
8063
+
8064
+ fn projectCovariance(cov3d: mat3x3<f32>, viewCenter: vec4<f32>, focal: vec2<f32>, modelViewMat: mat4x4<f32>) -> vec3<f32> {
8065
+ let v = viewCenter.xyz;
8066
+ let s = 1.0 / (v.z * v.z);
8067
+ let J = mat3x3<f32>(
8068
+ vec3<f32>(focal.x / v.z, 0.0, 0.0),
8069
+ vec3<f32>(0.0, focal.y / v.z, 0.0),
8070
+ vec3<f32>(-(focal.x * v.x) * s, -(focal.y * v.y) * s, 0.0)
8071
+ );
8072
+ let W = mat3x3<f32>(
8073
+ vec3<f32>(modelViewMat[0][0], modelViewMat[0][1], modelViewMat[0][2]),
8074
+ vec3<f32>(modelViewMat[1][0], modelViewMat[1][1], modelViewMat[1][2]),
8075
+ vec3<f32>(modelViewMat[2][0], modelViewMat[2][1], modelViewMat[2][2])
8076
+ );
8077
+ let T = J * W;
8078
+ let cov2d = T * cov3d * transpose(T);
8079
+ return vec3<f32>(cov2d[0][0], cov2d[0][1], cov2d[1][1]);
8080
+ }
8081
+
8082
+ struct ExtentResult { basis: vec4<f32>, adjustedOpacity: f32, }
8083
+
8084
+ fn computeExtentBasisAA(cov2dIn: vec3<f32>, opacity: f32, viewportSize: vec2<f32>) -> ExtentResult {
8085
+ var result: ExtentResult;
8086
+ var cov2d = cov2dIn;
8087
+ var alpha = opacity;
8088
+ cov2d.x += LOW_PASS_FILTER;
8089
+ cov2d.z += LOW_PASS_FILTER;
8090
+ let a = cov2d.x; let d = cov2d.z; let b = cov2d.y;
8091
+ let mid = 0.5 * (a + d);
8092
+ let radius = length(vec2<f32>((a - d) * 0.5, b));
8093
+ let lambda1 = mid + radius;
8094
+ let lambda2 = mid - radius;
8095
+ if lambda2 <= 0.0 { result.basis = vec4<f32>(0.0); result.adjustedOpacity = 0.0; return result; }
8096
+ let vmin = min(1024.0, min(viewportSize.x, viewportSize.y));
8097
+ let l1 = min(2.0 * sqrt(2.0 * lambda1), vmin);
8098
+ let l2 = min(2.0 * sqrt(2.0 * lambda2), vmin);
8099
+ if l1 < 0.5 && l2 < 0.5 { result.basis = vec4<f32>(0.0); result.adjustedOpacity = 0.0; return result; }
8100
+ let diagVec = normalize(vec2<f32>(b, lambda1 - a));
8101
+ result.basis = vec4<f32>(diagVec * l1, vec2<f32>(diagVec.y, -diagVec.x) * l2);
8102
+ result.adjustedOpacity = alpha;
8103
+ return result;
8104
+ }
8105
+
8106
+ @vertex
8107
+ fn vs_editor(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
8108
+ var output: VertexOutput;
8109
+ let splatIndex = sortedIndices[instanceIndex];
8110
+ let splat = splats[splatIndex];
8111
+ let quadPos = QUAD_POSITIONS[vertexIndex];
8112
+
8113
+ let state = getState(splatIndex);
8114
+ output.stateFlags = state;
8115
+
8116
+ // 隐藏和删除的 splat 直接剔除
8117
+ if (state & STATE_HIDDEN) != 0u || (state & STATE_DELETED) != 0u {
8118
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
8119
+ }
8120
+
8121
+ if splat.opacity < ALPHA_CULL_THRESHOLD { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
8122
+ let quatNormSqr = dot(splat.rotation, splat.rotation);
8123
+ if quatNormSqr < 1e-6 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
8124
+
8125
+ let worldPos = uniforms.model * vec4<f32>(splat.mean, 1.0);
8126
+ let viewPos = uniforms.view * worldPos;
8127
+ let clipPos = uniforms.proj * viewPos;
8128
+ if viewPos.z >= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
8129
+ let pW = 1.0 / (clipPos.w + 0.0000001);
8130
+ let ndcPos = clipPos * pW;
8131
+ let clipBound = 1.3;
8132
+ if abs(ndcPos.x) > clipBound || abs(ndcPos.y) > clipBound || ndcPos.z < -0.2 || ndcPos.z > 1.0 {
8133
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
8134
+ }
8135
+
8136
+ let cov3d = computeCovariance3D(splat.scale, splat.rotation);
8137
+ let focal = vec2<f32>(abs(uniforms.proj[0][0]) * 0.5 * uniforms.screenSize.x, abs(uniforms.proj[1][1]) * 0.5 * uniforms.screenSize.y);
8138
+ let modelViewMat = uniforms.view * uniforms.model;
8139
+ let cov2d = projectCovariance(cov3d, viewPos, focal, modelViewMat);
8140
+ let extentResult = computeExtentBasisAA(cov2d, splat.opacity, uniforms.screenSize);
8141
+ let basis = extentResult.basis;
8142
+ let adjustedOpacity = extentResult.adjustedOpacity;
8143
+ if basis.x == 0.0 && basis.y == 0.0 && basis.z == 0.0 && basis.w == 0.0 {
8144
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
8145
+ }
8146
+ let maxExtentPixels = max(length(basis.xy), length(basis.zw));
8147
+ let pixelToClip = 2.0 * vec2<f32>(clipPos.w, clipPos.w) / uniforms.screenSize;
8148
+ let splatExtentClip = vec2<f32>(maxExtentPixels, maxExtentPixels) * pixelToClip;
8149
+ if any((abs(clipPos.xy) - splatExtentClip) > vec2<f32>(clipPos.w, clipPos.w)) {
8150
+ output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output;
8151
+ }
8152
+ let clipFactor = computeClipFactor(adjustedOpacity);
8153
+ if clipFactor <= 0.0 { output.position = vec4<f32>(0.0, 0.0, 2.0, 1.0); return output; }
8154
+ let basisViewport = vec2<f32>(1.0 / uniforms.screenSize.x, 1.0 / uniforms.screenSize.y);
8155
+ let basisVector1 = basis.xy * clipFactor;
8156
+ let basisVector2 = basis.zw * clipFactor;
8157
+ let ndcOffset = (quadPos.x * basisVector1 + quadPos.y * basisVector2) * basisViewport * 2.0;
8158
+ output.position = vec4<f32>(ndcPos.xy + ndcOffset, ndcPos.z, 1.0);
8159
+ output.fragPos = quadPos * clipFactor;
8160
+ let shDir = normalize(vec3<f32>(
8161
+ dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[0].xyz),
8162
+ dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[1].xyz),
8163
+ dot(worldPos.xyz - uniforms.cameraPos, uniforms.model[2].xyz)
8164
+ ));
8165
+ output.color = splat.colorDC + evalSH(splat, shDir);
8166
+ output.opacity = adjustedOpacity;
8167
+ return output;
8168
+ }
8169
+
8170
+ @fragment
8171
+ fn fs_editor(input: VertexOutput) -> @location(0) vec4<f32> {
8172
+ if input.opacity <= 0.0 { discard; }
8173
+ let A = dot(input.fragPos, input.fragPos);
8174
+ if A > 1.0 { discard; }
8175
+ let weight = (exp(-GAUSSIAN_K * A) - EXP_NEG_K) * INV_ONE_MINUS_EXP_NEG_K;
8176
+ let opacity = weight * input.opacity;
8177
+ if opacity < ALPHA_CULL_THRESHOLD { discard; }
8178
+ var color = max(input.color, vec3<f32>(0.0));
8179
+
8180
+ // 选中高亮:叠加橙色调
8181
+ if (input.stateFlags & STATE_SELECTED) != 0u {
8182
+ color = mix(color, vec3<f32>(1.0, 0.5, 0.1), 0.4);
8183
+ }
8184
+
8185
+ return vec4<f32>(color * opacity, opacity);
8186
+ }
8187
+ `
8188
+ );
8189
+ }
7827
8190
  destroy() {
8191
+ this.clearEditorState();
7828
8192
  if (this.splatBuffer) {
7829
8193
  this.splatBuffer.destroy();
7830
8194
  this.splatBuffer = null;
@@ -13594,7 +13958,7 @@ class HotspotManager {
13594
13958
  const device = this.renderer.device;
13595
13959
  const segments = 48;
13596
13960
  const outerRadius = this.indicatorBaseRadius;
13597
- const innerRadius = outerRadius * 0.92;
13961
+ const innerRadius = outerRadius * 0.78;
13598
13962
  const vertexCount = segments * 2;
13599
13963
  const vertexData = new Float32Array(vertexCount * 6);
13600
13964
  for (let i = 0; i < segments; i++) {
@@ -13662,7 +14026,7 @@ class HotspotManager {
13662
14026
  showIndicator() {
13663
14027
  if (!this.indicatorMesh || this.indicatorAdded) return;
13664
14028
  const material = {
13665
- baseColorFactor: [1, 1, 1, 0.85],
14029
+ baseColorFactor: [0, 0.75, 1, 0.95],
13666
14030
  baseColorTexture: null,
13667
14031
  metallicFactor: 0,
13668
14032
  roughnessFactor: 1,
@@ -13900,6 +14264,7 @@ class HotspotManager {
13900
14264
  if (info.billboard === enabled) return true;
13901
14265
  info.billboard = enabled;
13902
14266
  if (!enabled) {
14267
+ info._lastBillboardMeshPos = void 0;
13903
14268
  this.restoreHotspotOrientation(info);
13904
14269
  }
13905
14270
  return true;
@@ -14037,6 +14402,19 @@ class HotspotManager {
14037
14402
  const camPos = this.camera.position;
14038
14403
  for (const info of this.hotspots) {
14039
14404
  if (!info.billboard) continue;
14405
+ if (info._lastBillboardMeshPos) {
14406
+ const firstMesh = this.meshRenderer.getOverlayMeshByIndex(info.meshStartIndex);
14407
+ if (firstMesh) {
14408
+ const dx = firstMesh.position[0] - info._lastBillboardMeshPos[0];
14409
+ const dy = firstMesh.position[1] - info._lastBillboardMeshPos[1];
14410
+ const dz = firstMesh.position[2] - info._lastBillboardMeshPos[2];
14411
+ if (Math.abs(dx) > 1e-6 || Math.abs(dy) > 1e-6 || Math.abs(dz) > 1e-6) {
14412
+ info.position[0] += dx;
14413
+ info.position[1] += dy;
14414
+ info.position[2] += dz;
14415
+ }
14416
+ }
14417
+ }
14040
14418
  const {
14041
14419
  position,
14042
14420
  normal,
@@ -14088,6 +14466,10 @@ class HotspotManager {
14088
14466
  const owx = (rx * lcx + fx * lcy + zx * lcz) * s;
14089
14467
  const owy = (ry * lcx + fy * lcy + zy * lcz) * s;
14090
14468
  const owz = (rz * lcx + fz * lcy + zz * lcz) * s;
14469
+ const finalMeshX = wx - owx;
14470
+ const finalMeshY = wy - owy;
14471
+ const finalMeshZ = wz - owz;
14472
+ info._lastBillboardMeshPos = [finalMeshX, finalMeshY, finalMeshZ];
14091
14473
  for (let i = 0; i < info.meshCount; i++) {
14092
14474
  const mesh = this.meshRenderer.getOverlayMeshByIndex(
14093
14475
  info.meshStartIndex + i
@@ -14210,6 +14592,1490 @@ class HotspotManager {
14210
14592
  this.hotspots = [];
14211
14593
  }
14212
14594
  }
14595
+ var State = /* @__PURE__ */ ((State2) => {
14596
+ State2[State2["selected"] = 1] = "selected";
14597
+ State2[State2["hidden"] = 2] = "hidden";
14598
+ State2[State2["deleted"] = 4] = "deleted";
14599
+ return State2;
14600
+ })(State || {});
14601
+ class SplatState {
14602
+ constructor(count) {
14603
+ __publicField(this, "count");
14604
+ __publicField(this, "data");
14605
+ __publicField(this, "_numSelected", 0);
14606
+ __publicField(this, "_numHidden", 0);
14607
+ __publicField(this, "_numDeleted", 0);
14608
+ this.count = count;
14609
+ this.data = new Uint8Array(count);
14610
+ }
14611
+ get numSelected() {
14612
+ return this._numSelected;
14613
+ }
14614
+ get numHidden() {
14615
+ return this._numHidden;
14616
+ }
14617
+ get numDeleted() {
14618
+ return this._numDeleted;
14619
+ }
14620
+ recalcCounts() {
14621
+ let sel = 0, hid = 0, del = 0;
14622
+ for (let i = 0; i < this.count; i++) {
14623
+ const v = this.data[i];
14624
+ if (v & 1) sel++;
14625
+ if (v & 2) hid++;
14626
+ if (v & 4) del++;
14627
+ }
14628
+ this._numSelected = sel;
14629
+ this._numHidden = hid;
14630
+ this._numDeleted = del;
14631
+ }
14632
+ clear() {
14633
+ this.data.fill(0);
14634
+ this._numSelected = 0;
14635
+ this._numHidden = 0;
14636
+ this._numDeleted = 0;
14637
+ }
14638
+ }
14639
+ class EditHistory {
14640
+ constructor(onChange) {
14641
+ __publicField(this, "history", []);
14642
+ __publicField(this, "cursor", 0);
14643
+ __publicField(this, "onChange");
14644
+ this.onChange = onChange;
14645
+ }
14646
+ add(op) {
14647
+ var _a2, _b2, _c;
14648
+ while (this.cursor < this.history.length) {
14649
+ (_b2 = (_a2 = this.history.pop()) == null ? void 0 : _a2.destroy) == null ? void 0 : _b2.call(_a2);
14650
+ }
14651
+ this.history.push(op);
14652
+ op.do();
14653
+ this.cursor++;
14654
+ (_c = this.onChange) == null ? void 0 : _c.call(this);
14655
+ }
14656
+ canUndo() {
14657
+ return this.cursor > 0;
14658
+ }
14659
+ canRedo() {
14660
+ return this.cursor < this.history.length;
14661
+ }
14662
+ undo() {
14663
+ var _a2;
14664
+ if (!this.canUndo()) return;
14665
+ this.history[--this.cursor].undo();
14666
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
14667
+ }
14668
+ redo() {
14669
+ var _a2;
14670
+ if (!this.canRedo()) return;
14671
+ this.history[this.cursor++].do();
14672
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
14673
+ }
14674
+ clear() {
14675
+ var _a2;
14676
+ this.history.forEach((op) => {
14677
+ var _a3;
14678
+ return (_a3 = op.destroy) == null ? void 0 : _a3.call(op);
14679
+ });
14680
+ this.history = [];
14681
+ this.cursor = 0;
14682
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
14683
+ }
14684
+ }
14685
+ class StateOp {
14686
+ constructor(name, state, indices, mask, op) {
14687
+ __publicField(this, "name");
14688
+ __publicField(this, "state");
14689
+ __publicField(this, "indices");
14690
+ __publicField(this, "mask");
14691
+ __publicField(this, "op");
14692
+ this.name = name;
14693
+ this.state = state;
14694
+ this.indices = indices;
14695
+ this.mask = mask;
14696
+ this.op = op;
14697
+ }
14698
+ apply(op) {
14699
+ const d = this.state.data;
14700
+ const { mask, indices } = this;
14701
+ switch (op) {
14702
+ case 0:
14703
+ for (let i = 0; i < indices.length; i++) d[indices[i]] |= mask;
14704
+ break;
14705
+ case 1:
14706
+ for (let i = 0; i < indices.length; i++) d[indices[i]] &= ~mask;
14707
+ break;
14708
+ case 2:
14709
+ for (let i = 0; i < indices.length; i++) d[indices[i]] ^= mask;
14710
+ break;
14711
+ }
14712
+ }
14713
+ do() {
14714
+ this.apply(this.op);
14715
+ this.state.recalcCounts();
14716
+ }
14717
+ undo() {
14718
+ const undoOp = this.op === 2 ? 2 : this.op === 0 ? 1 : 0;
14719
+ this.apply(undoOp);
14720
+ this.state.recalcCounts();
14721
+ }
14722
+ }
14723
+ class SelectOp {
14724
+ constructor(state, op, predicate) {
14725
+ __publicField(this, "name", "select");
14726
+ __publicField(this, "inner");
14727
+ const d = state.data;
14728
+ const indices = [];
14729
+ if (op === "add") {
14730
+ for (let i = 0; i < state.count; i++) {
14731
+ if (predicate(i) && d[i] === 0) indices.push(i);
14732
+ }
14733
+ this.inner = new StateOp(
14734
+ "selectAdd",
14735
+ state,
14736
+ new Uint32Array(indices),
14737
+ State.selected,
14738
+ 0
14739
+ /* SET */
14740
+ );
14741
+ } else if (op === "remove") {
14742
+ for (let i = 0; i < state.count; i++) {
14743
+ if (predicate(i) && d[i] === State.selected) indices.push(i);
14744
+ }
14745
+ this.inner = new StateOp(
14746
+ "selectRemove",
14747
+ state,
14748
+ new Uint32Array(indices),
14749
+ State.selected,
14750
+ 1
14751
+ /* CLEAR */
14752
+ );
14753
+ } else {
14754
+ for (let i = 0; i < state.count; i++) {
14755
+ if (d[i] === State.selected !== predicate(i)) indices.push(i);
14756
+ }
14757
+ this.inner = new StateOp(
14758
+ "selectSet",
14759
+ state,
14760
+ new Uint32Array(indices),
14761
+ State.selected,
14762
+ 2
14763
+ /* TOGGLE */
14764
+ );
14765
+ }
14766
+ }
14767
+ do() {
14768
+ this.inner.do();
14769
+ }
14770
+ undo() {
14771
+ this.inner.undo();
14772
+ }
14773
+ }
14774
+ class SelectAllOp {
14775
+ constructor(state) {
14776
+ __publicField(this, "name", "selectAll");
14777
+ __publicField(this, "inner");
14778
+ const indices = [];
14779
+ for (let i = 0; i < state.count; i++) {
14780
+ if (state.data[i] === 0) indices.push(i);
14781
+ }
14782
+ this.inner = new StateOp(
14783
+ "selectAll",
14784
+ state,
14785
+ new Uint32Array(indices),
14786
+ State.selected,
14787
+ 0
14788
+ /* SET */
14789
+ );
14790
+ }
14791
+ do() {
14792
+ this.inner.do();
14793
+ }
14794
+ undo() {
14795
+ this.inner.undo();
14796
+ }
14797
+ }
14798
+ class SelectNoneOp {
14799
+ constructor(state) {
14800
+ __publicField(this, "name", "selectNone");
14801
+ __publicField(this, "inner");
14802
+ const indices = [];
14803
+ for (let i = 0; i < state.count; i++) {
14804
+ if (state.data[i] === State.selected) indices.push(i);
14805
+ }
14806
+ this.inner = new StateOp(
14807
+ "selectNone",
14808
+ state,
14809
+ new Uint32Array(indices),
14810
+ State.selected,
14811
+ 1
14812
+ /* CLEAR */
14813
+ );
14814
+ }
14815
+ do() {
14816
+ this.inner.do();
14817
+ }
14818
+ undo() {
14819
+ this.inner.undo();
14820
+ }
14821
+ }
14822
+ class SelectInvertOp {
14823
+ constructor(state) {
14824
+ __publicField(this, "name", "selectInvert");
14825
+ __publicField(this, "inner");
14826
+ const indices = [];
14827
+ for (let i = 0; i < state.count; i++) {
14828
+ if ((state.data[i] & (State.hidden | State.deleted)) === 0) indices.push(i);
14829
+ }
14830
+ this.inner = new StateOp(
14831
+ "selectInvert",
14832
+ state,
14833
+ new Uint32Array(indices),
14834
+ State.selected,
14835
+ 2
14836
+ /* TOGGLE */
14837
+ );
14838
+ }
14839
+ do() {
14840
+ this.inner.do();
14841
+ }
14842
+ undo() {
14843
+ this.inner.undo();
14844
+ }
14845
+ }
14846
+ class DeleteSelectionOp {
14847
+ constructor(state) {
14848
+ __publicField(this, "name", "deleteSelection");
14849
+ __publicField(this, "inner");
14850
+ const indices = [];
14851
+ for (let i = 0; i < state.count; i++) {
14852
+ if (state.data[i] === State.selected) indices.push(i);
14853
+ }
14854
+ this.inner = new StateOp(
14855
+ "deleteSelection",
14856
+ state,
14857
+ new Uint32Array(indices),
14858
+ State.deleted,
14859
+ 0
14860
+ /* SET */
14861
+ );
14862
+ }
14863
+ do() {
14864
+ this.inner.do();
14865
+ }
14866
+ undo() {
14867
+ this.inner.undo();
14868
+ }
14869
+ }
14870
+ class HideSelectionOp {
14871
+ constructor(state) {
14872
+ __publicField(this, "name", "hideSelection");
14873
+ __publicField(this, "inner");
14874
+ const indices = [];
14875
+ for (let i = 0; i < state.count; i++) {
14876
+ if (state.data[i] === State.selected) indices.push(i);
14877
+ }
14878
+ this.inner = new StateOp(
14879
+ "hideSelection",
14880
+ state,
14881
+ new Uint32Array(indices),
14882
+ State.hidden,
14883
+ 0
14884
+ /* SET */
14885
+ );
14886
+ }
14887
+ do() {
14888
+ this.inner.do();
14889
+ }
14890
+ undo() {
14891
+ this.inner.undo();
14892
+ }
14893
+ }
14894
+ class UnhideAllOp {
14895
+ constructor(state) {
14896
+ __publicField(this, "name", "unhideAll");
14897
+ __publicField(this, "inner");
14898
+ const indices = [];
14899
+ for (let i = 0; i < state.count; i++) {
14900
+ if ((state.data[i] & (State.hidden | State.deleted)) === State.hidden) indices.push(i);
14901
+ }
14902
+ this.inner = new StateOp(
14903
+ "unhideAll",
14904
+ state,
14905
+ new Uint32Array(indices),
14906
+ State.hidden,
14907
+ 1
14908
+ /* CLEAR */
14909
+ );
14910
+ }
14911
+ do() {
14912
+ this.inner.do();
14913
+ }
14914
+ undo() {
14915
+ this.inner.undo();
14916
+ }
14917
+ }
14918
+ class ToolManager {
14919
+ constructor(onToolChanged) {
14920
+ __publicField(this, "tools", /* @__PURE__ */ new Map());
14921
+ __publicField(this, "_active", null);
14922
+ __publicField(this, "onToolChanged");
14923
+ this.onToolChanged = onToolChanged;
14924
+ }
14925
+ get active() {
14926
+ return this._active;
14927
+ }
14928
+ register(name, tool) {
14929
+ this.tools.set(name, tool);
14930
+ }
14931
+ activate(name) {
14932
+ var _a2, _b2, _c;
14933
+ if (name === this._active) {
14934
+ if (name) this.activate(null);
14935
+ return;
14936
+ }
14937
+ if (this._active) {
14938
+ (_a2 = this.tools.get(this._active)) == null ? void 0 : _a2.deactivate();
14939
+ }
14940
+ this._active = name;
14941
+ if (this._active) {
14942
+ (_b2 = this.tools.get(this._active)) == null ? void 0 : _b2.activate();
14943
+ }
14944
+ (_c = this.onToolChanged) == null ? void 0 : _c.call(this, this._active);
14945
+ }
14946
+ deactivateAll() {
14947
+ this.activate(null);
14948
+ }
14949
+ }
14950
+ class RectSelection {
14951
+ constructor(parent, onSelect) {
14952
+ __publicField(this, "parent");
14953
+ __publicField(this, "svg");
14954
+ __publicField(this, "rect");
14955
+ __publicField(this, "onSelect");
14956
+ __publicField(this, "start", { x: 0, y: 0 });
14957
+ __publicField(this, "end", { x: 0, y: 0 });
14958
+ __publicField(this, "dragId");
14959
+ __publicField(this, "dragMoved", false);
14960
+ this.parent = parent;
14961
+ this.onSelect = onSelect;
14962
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
14963
+ this.svg.classList.add("tool-svg");
14964
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
14965
+ parent.appendChild(this.svg);
14966
+ this.rect = document.createElementNS(this.svg.namespaceURI, "rect");
14967
+ this.rect.setAttribute("fill", "rgba(255,102,0,0.15)");
14968
+ this.rect.setAttribute("stroke", "#f60");
14969
+ this.rect.setAttribute("stroke-width", "1.5");
14970
+ this.svg.appendChild(this.rect);
14971
+ this.pointerdown = this.pointerdown.bind(this);
14972
+ this.pointermove = this.pointermove.bind(this);
14973
+ this.pointerup = this.pointerup.bind(this);
14974
+ }
14975
+ activate() {
14976
+ this.svg.style.display = "block";
14977
+ this.parent.style.cursor = "crosshair";
14978
+ this.parent.addEventListener("pointerdown", this.pointerdown);
14979
+ this.parent.addEventListener("pointermove", this.pointermove);
14980
+ this.parent.addEventListener("pointerup", this.pointerup);
14981
+ }
14982
+ deactivate() {
14983
+ if (this.dragId !== void 0) this.dragEnd();
14984
+ this.svg.style.display = "none";
14985
+ this.parent.style.cursor = "";
14986
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
14987
+ this.parent.removeEventListener("pointermove", this.pointermove);
14988
+ this.parent.removeEventListener("pointerup", this.pointerup);
14989
+ }
14990
+ updateRect() {
14991
+ const x = Math.min(this.start.x, this.end.x);
14992
+ const y = Math.min(this.start.y, this.end.y);
14993
+ this.rect.setAttribute("x", x.toString());
14994
+ this.rect.setAttribute("y", y.toString());
14995
+ this.rect.setAttribute("width", Math.abs(this.start.x - this.end.x).toString());
14996
+ this.rect.setAttribute("height", Math.abs(this.start.y - this.end.y).toString());
14997
+ }
14998
+ pointerdown(e) {
14999
+ if (this.dragId !== void 0) return;
15000
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15001
+ e.preventDefault();
15002
+ e.stopPropagation();
15003
+ this.dragId = e.pointerId;
15004
+ this.dragMoved = false;
15005
+ this.parent.setPointerCapture(this.dragId);
15006
+ this.start.x = this.end.x = e.offsetX;
15007
+ this.start.y = this.end.y = e.offsetY;
15008
+ this.updateRect();
15009
+ this.svg.style.pointerEvents = "none";
15010
+ this.rect.style.display = "block";
15011
+ }
15012
+ pointermove(e) {
15013
+ if (e.pointerId !== this.dragId) return;
15014
+ e.preventDefault();
15015
+ e.stopPropagation();
15016
+ this.dragMoved = true;
15017
+ this.end.x = e.offsetX;
15018
+ this.end.y = e.offsetY;
15019
+ this.updateRect();
15020
+ }
15021
+ dragEnd() {
15022
+ if (this.dragId !== void 0) {
15023
+ this.parent.releasePointerCapture(this.dragId);
15024
+ this.dragId = void 0;
15025
+ }
15026
+ this.rect.style.display = "none";
15027
+ }
15028
+ pointerup(e) {
15029
+ if (e.pointerId !== this.dragId) return;
15030
+ e.preventDefault();
15031
+ e.stopPropagation();
15032
+ const w = this.parent.clientWidth;
15033
+ const h = this.parent.clientHeight;
15034
+ const selectOp = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15035
+ if (this.dragMoved) {
15036
+ this.onSelect(selectOp, {
15037
+ startX: Math.min(this.start.x, this.end.x) / w,
15038
+ startY: Math.min(this.start.y, this.end.y) / h,
15039
+ endX: Math.max(this.start.x, this.end.x) / w,
15040
+ endY: Math.max(this.start.y, this.end.y) / h
15041
+ });
15042
+ } else {
15043
+ const px = e.offsetX / w;
15044
+ const py = e.offsetY / h;
15045
+ const r = 4 / Math.max(w, h);
15046
+ this.onSelect(selectOp, {
15047
+ startX: px - r,
15048
+ startY: py - r,
15049
+ endX: px + r,
15050
+ endY: py + r
15051
+ });
15052
+ }
15053
+ this.dragEnd();
15054
+ }
15055
+ }
15056
+ class LassoSelection {
15057
+ constructor(parent, maskCanvas, maskCtx, onMaskSelect) {
15058
+ __publicField(this, "parent");
15059
+ __publicField(this, "svg");
15060
+ __publicField(this, "polygon");
15061
+ __publicField(this, "maskCanvas");
15062
+ __publicField(this, "maskCtx");
15063
+ __publicField(this, "onMaskSelect");
15064
+ __publicField(this, "points", []);
15065
+ __publicField(this, "currentPoint", null);
15066
+ __publicField(this, "lastPointTime", 0);
15067
+ __publicField(this, "dragId");
15068
+ this.parent = parent;
15069
+ this.maskCanvas = maskCanvas;
15070
+ this.maskCtx = maskCtx;
15071
+ this.onMaskSelect = onMaskSelect;
15072
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
15073
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
15074
+ parent.appendChild(this.svg);
15075
+ this.polygon = document.createElementNS(this.svg.namespaceURI, "polygon");
15076
+ this.polygon.setAttribute("fill", "rgba(255,102,0,0.15)");
15077
+ this.polygon.setAttribute("stroke", "#f60");
15078
+ this.polygon.setAttribute("stroke-width", "1.5");
15079
+ this.svg.appendChild(this.polygon);
15080
+ this.pointerdown = this.pointerdown.bind(this);
15081
+ this.pointermove = this.pointermove.bind(this);
15082
+ this.pointerup = this.pointerup.bind(this);
15083
+ }
15084
+ activate() {
15085
+ this.svg.style.display = "block";
15086
+ this.parent.style.cursor = "crosshair";
15087
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15088
+ this.parent.addEventListener("pointermove", this.pointermove);
15089
+ this.parent.addEventListener("pointerup", this.pointerup);
15090
+ }
15091
+ deactivate() {
15092
+ if (this.dragId !== void 0) this.dragEnd();
15093
+ this.svg.style.display = "none";
15094
+ this.parent.style.cursor = "";
15095
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15096
+ this.parent.removeEventListener("pointermove", this.pointermove);
15097
+ this.parent.removeEventListener("pointerup", this.pointerup);
15098
+ this.points = [];
15099
+ this.paint();
15100
+ }
15101
+ dist(a, b) {
15102
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
15103
+ }
15104
+ paint() {
15105
+ const all = this.currentPoint ? [...this.points, this.currentPoint] : this.points;
15106
+ this.polygon.setAttribute("points", all.map((p) => `${p.x},${p.y}`).join(" "));
15107
+ }
15108
+ update(e) {
15109
+ this.currentPoint = { x: e.offsetX, y: e.offsetY };
15110
+ const last = this.points[this.points.length - 1];
15111
+ const distance = last ? this.dist(this.currentPoint, last) : 0;
15112
+ const millis = Date.now() - this.lastPointTime;
15113
+ if (this.dragId !== void 0 && (this.points.length === 0 || distance > 20 || millis > 500 && distance > 2 || millis > 200 && distance > 10)) {
15114
+ this.points.push(this.currentPoint);
15115
+ this.lastPointTime = Date.now();
15116
+ }
15117
+ this.paint();
15118
+ }
15119
+ commitSelection(e) {
15120
+ const { maskCanvas: canvas, maskCtx: ctx, parent } = this;
15121
+ if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) {
15122
+ canvas.width = parent.clientWidth;
15123
+ canvas.height = parent.clientHeight;
15124
+ }
15125
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
15126
+ ctx.beginPath();
15127
+ ctx.fillStyle = "#f60";
15128
+ this.points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
15129
+ ctx.closePath();
15130
+ ctx.fill();
15131
+ const op = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15132
+ this.onMaskSelect(op, canvas, ctx);
15133
+ }
15134
+ pointerdown(e) {
15135
+ if (this.dragId !== void 0) return;
15136
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15137
+ e.preventDefault();
15138
+ e.stopPropagation();
15139
+ this.dragId = e.pointerId;
15140
+ this.parent.setPointerCapture(this.dragId);
15141
+ this.update(e);
15142
+ }
15143
+ pointermove(e) {
15144
+ if (this.dragId !== void 0) {
15145
+ e.preventDefault();
15146
+ e.stopPropagation();
15147
+ }
15148
+ this.update(e);
15149
+ }
15150
+ dragEnd() {
15151
+ if (this.dragId !== void 0) {
15152
+ this.parent.releasePointerCapture(this.dragId);
15153
+ this.dragId = void 0;
15154
+ }
15155
+ }
15156
+ pointerup(e) {
15157
+ if (e.pointerId !== this.dragId) return;
15158
+ e.preventDefault();
15159
+ e.stopPropagation();
15160
+ this.commitSelection(e);
15161
+ this.dragEnd();
15162
+ this.points = [];
15163
+ this.paint();
15164
+ }
15165
+ }
15166
+ class PolygonSelection {
15167
+ constructor(parent, maskCanvas, maskCtx, onMaskSelect) {
15168
+ __publicField(this, "parent");
15169
+ __publicField(this, "svg");
15170
+ __publicField(this, "polyline");
15171
+ __publicField(this, "maskCanvas");
15172
+ __publicField(this, "maskCtx");
15173
+ __publicField(this, "onMaskSelect");
15174
+ __publicField(this, "points", []);
15175
+ __publicField(this, "currentPoint", null);
15176
+ this.parent = parent;
15177
+ this.maskCanvas = maskCanvas;
15178
+ this.maskCtx = maskCtx;
15179
+ this.onMaskSelect = onMaskSelect;
15180
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
15181
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
15182
+ parent.appendChild(this.svg);
15183
+ this.polyline = document.createElementNS(this.svg.namespaceURI, "polyline");
15184
+ this.polyline.setAttribute("fill", "rgba(255,102,0,0.15)");
15185
+ this.polyline.setAttribute("stroke", "#f60");
15186
+ this.polyline.setAttribute("stroke-width", "1.5");
15187
+ this.svg.appendChild(this.polyline);
15188
+ this.pointerdown = this.pointerdown.bind(this);
15189
+ this.pointermove = this.pointermove.bind(this);
15190
+ this.pointerup = this.pointerup.bind(this);
15191
+ this.dblclick = this.dblclick.bind(this);
15192
+ }
15193
+ activate() {
15194
+ this.svg.style.display = "block";
15195
+ this.parent.style.cursor = "crosshair";
15196
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15197
+ this.parent.addEventListener("pointermove", this.pointermove);
15198
+ this.parent.addEventListener("pointerup", this.pointerup);
15199
+ this.parent.addEventListener("dblclick", this.dblclick);
15200
+ }
15201
+ deactivate() {
15202
+ this.svg.style.display = "none";
15203
+ this.parent.style.cursor = "";
15204
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15205
+ this.parent.removeEventListener("pointermove", this.pointermove);
15206
+ this.parent.removeEventListener("pointerup", this.pointerup);
15207
+ this.parent.removeEventListener("dblclick", this.dblclick);
15208
+ this.points = [];
15209
+ this.paint();
15210
+ }
15211
+ dist(a, b) {
15212
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
15213
+ }
15214
+ isClosed() {
15215
+ return this.points.length > 1 && !!this.currentPoint && this.dist(this.currentPoint, this.points[0]) < 8;
15216
+ }
15217
+ paint() {
15218
+ const all = [...this.points, this.currentPoint].filter(Boolean);
15219
+ this.polyline.setAttribute("points", all.map((p) => `${p.x},${p.y}`).join(" "));
15220
+ this.polyline.setAttribute("stroke", this.isClosed() ? "#fa6" : "#f60");
15221
+ }
15222
+ commitSelection(e) {
15223
+ const { maskCanvas: canvas, maskCtx: ctx, parent } = this;
15224
+ if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) {
15225
+ canvas.width = parent.clientWidth;
15226
+ canvas.height = parent.clientHeight;
15227
+ }
15228
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
15229
+ ctx.beginPath();
15230
+ ctx.fillStyle = "#f60";
15231
+ this.points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
15232
+ ctx.closePath();
15233
+ ctx.fill();
15234
+ const op = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15235
+ this.onMaskSelect(op, canvas, ctx);
15236
+ this.points = [];
15237
+ this.paint();
15238
+ }
15239
+ pointerdown(e) {
15240
+ if (this.points.length > 0 || (e.pointerType === "mouse" ? e.button === 0 : e.isPrimary)) {
15241
+ e.preventDefault();
15242
+ e.stopPropagation();
15243
+ }
15244
+ }
15245
+ pointermove(e) {
15246
+ this.currentPoint = { x: e.offsetX, y: e.offsetY };
15247
+ if (this.points.length > 0) this.paint();
15248
+ }
15249
+ pointerup(e) {
15250
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15251
+ e.preventDefault();
15252
+ e.stopPropagation();
15253
+ if (this.isClosed()) {
15254
+ this.commitSelection(e);
15255
+ } else if (this.currentPoint && (this.points.length === 0 || this.dist(this.points[this.points.length - 1], this.currentPoint) > 0)) {
15256
+ this.points.push(this.currentPoint);
15257
+ }
15258
+ }
15259
+ dblclick(e) {
15260
+ e.preventDefault();
15261
+ e.stopPropagation();
15262
+ if (this.points.length > 2) {
15263
+ this.commitSelection(e);
15264
+ }
15265
+ }
15266
+ }
15267
+ class BrushSelection {
15268
+ constructor(parent, maskCanvas, maskCtx, onMaskSelect) {
15269
+ __publicField(this, "parent");
15270
+ __publicField(this, "svg");
15271
+ __publicField(this, "circle");
15272
+ __publicField(this, "maskCanvas");
15273
+ __publicField(this, "maskCtx");
15274
+ __publicField(this, "onMaskSelect");
15275
+ __publicField(this, "radius", 40);
15276
+ __publicField(this, "prev", { x: 0, y: 0 });
15277
+ __publicField(this, "dragId");
15278
+ this.parent = parent;
15279
+ this.maskCanvas = maskCanvas;
15280
+ this.maskCtx = maskCtx;
15281
+ this.onMaskSelect = onMaskSelect;
15282
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
15283
+ this.svg.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:10;";
15284
+ parent.appendChild(this.svg);
15285
+ this.circle = document.createElementNS(this.svg.namespaceURI, "circle");
15286
+ this.circle.setAttribute("r", this.radius.toString());
15287
+ this.circle.setAttribute("fill", "none");
15288
+ this.circle.setAttribute("stroke", "#f60");
15289
+ this.circle.setAttribute("stroke-width", "1.5");
15290
+ this.circle.setAttribute("stroke-dasharray", "4 2");
15291
+ this.svg.appendChild(this.circle);
15292
+ this.pointerdown = this.pointerdown.bind(this);
15293
+ this.pointermove = this.pointermove.bind(this);
15294
+ this.pointerup = this.pointerup.bind(this);
15295
+ this.wheel = this.wheel.bind(this);
15296
+ }
15297
+ activate() {
15298
+ this.svg.style.display = "block";
15299
+ this.parent.style.cursor = "none";
15300
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15301
+ this.parent.addEventListener("pointermove", this.pointermove);
15302
+ this.parent.addEventListener("pointerup", this.pointerup);
15303
+ this.parent.addEventListener("wheel", this.wheel);
15304
+ }
15305
+ deactivate() {
15306
+ if (this.dragId !== void 0) this.dragEnd();
15307
+ this.svg.style.display = "none";
15308
+ this.maskCanvas.style.display = "none";
15309
+ this.parent.style.cursor = "";
15310
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15311
+ this.parent.removeEventListener("pointermove", this.pointermove);
15312
+ this.parent.removeEventListener("pointerup", this.pointerup);
15313
+ this.parent.removeEventListener("wheel", this.wheel);
15314
+ }
15315
+ update(e) {
15316
+ const x = e.offsetX, y = e.offsetY;
15317
+ this.circle.setAttribute("cx", x.toString());
15318
+ this.circle.setAttribute("cy", y.toString());
15319
+ if (this.dragId !== void 0) {
15320
+ this.maskCtx.beginPath();
15321
+ this.maskCtx.strokeStyle = "rgba(255,102,0,0.5)";
15322
+ this.maskCtx.lineCap = "round";
15323
+ this.maskCtx.lineWidth = this.radius * 2;
15324
+ this.maskCtx.moveTo(this.prev.x, this.prev.y);
15325
+ this.maskCtx.lineTo(x, y);
15326
+ this.maskCtx.stroke();
15327
+ this.prev.x = x;
15328
+ this.prev.y = y;
15329
+ }
15330
+ }
15331
+ pointerdown(e) {
15332
+ if (this.dragId !== void 0) return;
15333
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15334
+ e.preventDefault();
15335
+ e.stopPropagation();
15336
+ this.dragId = e.pointerId;
15337
+ this.parent.setPointerCapture(this.dragId);
15338
+ const { maskCanvas: canvas, maskCtx: ctx, parent } = this;
15339
+ if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) {
15340
+ canvas.width = parent.clientWidth;
15341
+ canvas.height = parent.clientHeight;
15342
+ }
15343
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
15344
+ canvas.style.display = "block";
15345
+ this.prev.x = e.offsetX;
15346
+ this.prev.y = e.offsetY;
15347
+ this.update(e);
15348
+ }
15349
+ pointermove(e) {
15350
+ if (this.dragId !== void 0) {
15351
+ e.preventDefault();
15352
+ e.stopPropagation();
15353
+ }
15354
+ this.update(e);
15355
+ }
15356
+ dragEnd() {
15357
+ if (this.dragId !== void 0) {
15358
+ this.parent.releasePointerCapture(this.dragId);
15359
+ this.dragId = void 0;
15360
+ }
15361
+ }
15362
+ pointerup(e) {
15363
+ if (e.pointerId !== this.dragId) return;
15364
+ e.preventDefault();
15365
+ e.stopPropagation();
15366
+ this.dragEnd();
15367
+ const op = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15368
+ this.onMaskSelect(op, this.maskCanvas, this.maskCtx);
15369
+ this.maskCanvas.style.display = "none";
15370
+ }
15371
+ wheel(e) {
15372
+ const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
15373
+ if (delta > 0) {
15374
+ this.radius = Math.max(1, this.radius / 1.05);
15375
+ } else {
15376
+ this.radius = Math.min(500, this.radius * 1.05);
15377
+ }
15378
+ this.circle.setAttribute("r", this.radius.toString());
15379
+ e.preventDefault();
15380
+ e.stopPropagation();
15381
+ }
15382
+ }
15383
+ class FloodSelection {
15384
+ constructor(parent, maskCanvas, maskCtx, onMaskSelect, getOffscreenImage) {
15385
+ __publicField(this, "parent");
15386
+ __publicField(this, "maskCanvas");
15387
+ __publicField(this, "maskCtx");
15388
+ __publicField(this, "onMaskSelect");
15389
+ __publicField(this, "getOffscreenImage");
15390
+ __publicField(this, "thresholdEl");
15391
+ __publicField(this, "threshold", 0.2);
15392
+ __publicField(this, "clicked", false);
15393
+ __publicField(this, "imageData", null);
15394
+ this.parent = parent;
15395
+ this.maskCanvas = maskCanvas;
15396
+ this.maskCtx = maskCtx;
15397
+ this.onMaskSelect = onMaskSelect;
15398
+ this.getOffscreenImage = getOffscreenImage;
15399
+ this.thresholdEl = document.createElement("div");
15400
+ this.thresholdEl.style.cssText = "position:absolute;bottom:60px;left:50%;transform:translateX(-50%);background:rgba(20,20,35,0.9);color:#e0e0e0;padding:8px 16px;border-radius:8px;font-size:13px;display:none;z-index:20;gap:8px;align-items:center;";
15401
+ this.thresholdEl.innerHTML = `
15402
+ <label style="margin-right:8px;">阈值:</label>
15403
+ <input type="range" min="0.01" max="0.99" step="0.01" value="${this.threshold}" style="width:120px;vertical-align:middle;">
15404
+ <span style="margin-left:8px;min-width:40px;">${this.threshold.toFixed(2)}</span>
15405
+ `;
15406
+ parent.appendChild(this.thresholdEl);
15407
+ const slider = this.thresholdEl.querySelector("input");
15408
+ const span = this.thresholdEl.querySelector("span");
15409
+ slider.addEventListener("input", () => {
15410
+ this.threshold = parseFloat(slider.value);
15411
+ span.textContent = this.threshold.toFixed(2);
15412
+ });
15413
+ slider.addEventListener("pointerdown", (e) => e.stopPropagation());
15414
+ this.pointerdown = this.pointerdown.bind(this);
15415
+ this.pointermove = this.pointermove.bind(this);
15416
+ this.pointerup = this.pointerup.bind(this);
15417
+ }
15418
+ activate() {
15419
+ this.parent.style.cursor = "crosshair";
15420
+ this.thresholdEl.style.display = "flex";
15421
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15422
+ this.parent.addEventListener("pointermove", this.pointermove);
15423
+ this.parent.addEventListener("pointerup", this.pointerup);
15424
+ }
15425
+ deactivate() {
15426
+ this.parent.style.cursor = "";
15427
+ this.thresholdEl.style.display = "none";
15428
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15429
+ this.parent.removeEventListener("pointermove", this.pointermove);
15430
+ this.parent.removeEventListener("pointerup", this.pointerup);
15431
+ }
15432
+ pointerdown(e) {
15433
+ if (e.pointerType === "mouse" ? e.button === 0 : e.isPrimary) {
15434
+ this.clicked = true;
15435
+ }
15436
+ }
15437
+ pointermove(_e) {
15438
+ this.clicked = false;
15439
+ }
15440
+ pointerup(e) {
15441
+ if (!this.clicked || !(e.pointerType === "mouse" ? e.button === 0 : e.isPrimary)) return;
15442
+ this.clicked = false;
15443
+ const width = this.parent.clientWidth;
15444
+ const height = this.parent.clientHeight;
15445
+ const point = { x: Math.floor(e.offsetX), y: Math.floor(e.offsetY) };
15446
+ const data = this.getOffscreenImage(width, height);
15447
+ if (!data) return;
15448
+ const { maskCanvas: canvas, maskCtx: ctx } = this;
15449
+ if (canvas.width !== width || canvas.height !== height) {
15450
+ canvas.width = width;
15451
+ canvas.height = height;
15452
+ this.imageData = ctx.createImageData(width, height);
15453
+ }
15454
+ if (!this.imageData) this.imageData = ctx.createImageData(width, height);
15455
+ const PIXEL = 4;
15456
+ const startIdx = (point.y * width + point.x) * PIXEL;
15457
+ const pickedAlpha = data[startIdx + 3];
15458
+ const d = this.imageData.data;
15459
+ d.fill(102);
15460
+ const stack = [{ ...point }];
15461
+ const thresholdByte = this.threshold * 255;
15462
+ while (stack.length > 0) {
15463
+ const cur = stack.pop();
15464
+ const idx = (cur.y * width + cur.x) * PIXEL;
15465
+ if (Math.abs(data[idx + 3] - pickedAlpha) < thresholdByte) {
15466
+ d[idx + 0] = 255;
15467
+ d[idx + 2] = 0;
15468
+ d[idx + 3] = 255;
15469
+ if (cur.x > 0 && d[idx - PIXEL + 3] === 102) stack.push({ x: cur.x - 1, y: cur.y });
15470
+ if (cur.x < width - 1 && d[idx + PIXEL + 3] === 102) stack.push({ x: cur.x + 1, y: cur.y });
15471
+ if (cur.y > 0 && d[idx - width * PIXEL + 3] === 102) stack.push({ x: cur.x, y: cur.y - 1 });
15472
+ if (cur.y < height - 1 && d[idx + width * PIXEL + 3] === 102) stack.push({ x: cur.x, y: cur.y + 1 });
15473
+ } else {
15474
+ d[idx + 3] = 0;
15475
+ }
15476
+ }
15477
+ ctx.putImageData(this.imageData, 0, 0);
15478
+ const op = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15479
+ this.onMaskSelect(op, canvas, ctx);
15480
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
15481
+ }
15482
+ }
15483
+ class EyedropperSelection {
15484
+ constructor(parent, onColorMatch) {
15485
+ __publicField(this, "parent");
15486
+ __publicField(this, "onColorMatch");
15487
+ __publicField(this, "thresholdEl");
15488
+ __publicField(this, "threshold", 0.2);
15489
+ __publicField(this, "pointerId", null);
15490
+ this.parent = parent;
15491
+ this.onColorMatch = onColorMatch;
15492
+ this.thresholdEl = document.createElement("div");
15493
+ this.thresholdEl.style.cssText = "position:absolute;bottom:60px;left:50%;transform:translateX(-50%);background:rgba(20,20,35,0.9);color:#e0e0e0;padding:8px 16px;border-radius:8px;font-size:13px;display:none;z-index:20;gap:8px;align-items:center;";
15494
+ this.thresholdEl.innerHTML = `
15495
+ <label style="margin-right:8px;">阈值:</label>
15496
+ <input type="range" min="0" max="1" step="0.01" value="${this.threshold}" style="width:120px;vertical-align:middle;">
15497
+ <span style="margin-left:8px;min-width:40px;">${this.threshold.toFixed(2)}</span>
15498
+ `;
15499
+ parent.appendChild(this.thresholdEl);
15500
+ const slider = this.thresholdEl.querySelector("input");
15501
+ const span = this.thresholdEl.querySelector("span");
15502
+ slider.addEventListener("input", () => {
15503
+ this.threshold = parseFloat(slider.value);
15504
+ span.textContent = this.threshold.toFixed(2);
15505
+ });
15506
+ slider.addEventListener("pointerdown", (e) => e.stopPropagation());
15507
+ this.pointerdown = this.pointerdown.bind(this);
15508
+ this.pointermove = this.pointermove.bind(this);
15509
+ this.pointerup = this.pointerup.bind(this);
15510
+ }
15511
+ activate() {
15512
+ this.parent.style.cursor = "crosshair";
15513
+ this.thresholdEl.style.display = "flex";
15514
+ this.parent.addEventListener("pointerdown", this.pointerdown);
15515
+ this.parent.addEventListener("pointermove", this.pointermove);
15516
+ this.parent.addEventListener("pointerup", this.pointerup);
15517
+ }
15518
+ deactivate() {
15519
+ this.parent.style.cursor = "";
15520
+ this.thresholdEl.style.display = "none";
15521
+ if (this.pointerId !== null) {
15522
+ this.parent.releasePointerCapture(this.pointerId);
15523
+ this.pointerId = null;
15524
+ }
15525
+ this.parent.removeEventListener("pointerdown", this.pointerdown);
15526
+ this.parent.removeEventListener("pointermove", this.pointermove);
15527
+ this.parent.removeEventListener("pointerup", this.pointerup);
15528
+ }
15529
+ pointerdown(e) {
15530
+ if (this.pointerId !== null) return;
15531
+ if (e.pointerType === "mouse" ? e.button !== 0 : !e.isPrimary) return;
15532
+ e.preventDefault();
15533
+ e.stopPropagation();
15534
+ this.pointerId = e.pointerId;
15535
+ this.parent.setPointerCapture(this.pointerId);
15536
+ }
15537
+ pointermove(e) {
15538
+ if (e.pointerId === this.pointerId) {
15539
+ e.preventDefault();
15540
+ e.stopPropagation();
15541
+ }
15542
+ }
15543
+ pointerup(e) {
15544
+ if (e.pointerId !== this.pointerId) return;
15545
+ e.preventDefault();
15546
+ e.stopPropagation();
15547
+ const w = this.parent.clientWidth || 1;
15548
+ const h = this.parent.clientHeight || 1;
15549
+ const op = e.shiftKey ? "add" : e.ctrlKey ? "remove" : "set";
15550
+ this.onColorMatch(op, {
15551
+ x: Math.max(0, Math.min(1, e.offsetX / w)),
15552
+ y: Math.max(0, Math.min(1, e.offsetY / h))
15553
+ }, this.threshold);
15554
+ this.parent.releasePointerCapture(this.pointerId);
15555
+ this.pointerId = null;
15556
+ }
15557
+ }
15558
+ function exportEditedPLY(positions, scales, rotations, colors, opacities, shCoeffs, state) {
15559
+ const totalCount = state.count;
15560
+ let keepCount = 0;
15561
+ for (let i = 0; i < totalCount; i++) {
15562
+ if (!(state.data[i] & State.deleted)) keepCount++;
15563
+ }
15564
+ const hasSH = shCoeffs !== null && shCoeffs.length > 0;
15565
+ const shPerSplat = hasSH ? Math.floor(shCoeffs.length / totalCount) : 0;
15566
+ const shNames = [];
15567
+ if (hasSH) {
15568
+ for (let i = 0; i < shPerSplat; i++) {
15569
+ shNames.push(`f_rest_${i}`);
15570
+ }
15571
+ }
15572
+ let header = "ply\n";
15573
+ header += "format binary_little_endian 1.0\n";
15574
+ header += `element vertex ${keepCount}
15575
+ `;
15576
+ header += "property float x\n";
15577
+ header += "property float y\n";
15578
+ header += "property float z\n";
15579
+ header += "property float nx\n";
15580
+ header += "property float ny\n";
15581
+ header += "property float nz\n";
15582
+ header += "property float f_dc_0\n";
15583
+ header += "property float f_dc_1\n";
15584
+ header += "property float f_dc_2\n";
15585
+ for (const name of shNames) {
15586
+ header += `property float ${name}
15587
+ `;
15588
+ }
15589
+ header += "property float opacity\n";
15590
+ header += "property float scale_0\n";
15591
+ header += "property float scale_1\n";
15592
+ header += "property float scale_2\n";
15593
+ header += "property float rot_0\n";
15594
+ header += "property float rot_1\n";
15595
+ header += "property float rot_2\n";
15596
+ header += "property float rot_3\n";
15597
+ header += "end_header\n";
15598
+ const headerBytes = new TextEncoder().encode(header);
15599
+ const floatsPerSplat = 3 + 3 + 3 + shPerSplat + 1 + 3 + 4;
15600
+ const bytesPerSplat = floatsPerSplat * 4;
15601
+ const totalBytes = headerBytes.byteLength + keepCount * bytesPerSplat;
15602
+ const buffer = new ArrayBuffer(totalBytes);
15603
+ const headerView = new Uint8Array(buffer);
15604
+ headerView.set(headerBytes);
15605
+ const SH_C02 = 0.28209479177387814;
15606
+ const dataView = new DataView(buffer);
15607
+ let offset = headerBytes.byteLength;
15608
+ for (let i = 0; i < totalCount; i++) {
15609
+ if (state.data[i] & State.deleted) continue;
15610
+ const i3 = i * 3;
15611
+ const i4 = i * 4;
15612
+ dataView.setFloat32(offset, positions[i3], true);
15613
+ offset += 4;
15614
+ dataView.setFloat32(offset, positions[i3 + 2], true);
15615
+ offset += 4;
15616
+ dataView.setFloat32(offset, positions[i3 + 1], true);
15617
+ offset += 4;
15618
+ dataView.setFloat32(offset, 0, true);
15619
+ offset += 4;
15620
+ dataView.setFloat32(offset, 0, true);
15621
+ offset += 4;
15622
+ dataView.setFloat32(offset, 0, true);
15623
+ offset += 4;
15624
+ dataView.setFloat32(offset, (colors[i3] - 0.5) / SH_C02, true);
15625
+ offset += 4;
15626
+ dataView.setFloat32(offset, (colors[i3 + 1] - 0.5) / SH_C02, true);
15627
+ offset += 4;
15628
+ dataView.setFloat32(offset, (colors[i3 + 2] - 0.5) / SH_C02, true);
15629
+ offset += 4;
15630
+ if (hasSH) {
15631
+ const shBase = i * shPerSplat;
15632
+ for (let j = 0; j < shPerSplat; j++) {
15633
+ dataView.setFloat32(offset, shCoeffs[shBase + j], true);
15634
+ offset += 4;
15635
+ }
15636
+ }
15637
+ const alpha = opacities[i];
15638
+ const clampedAlpha = Math.max(1e-6, Math.min(1 - 1e-6, alpha));
15639
+ const logitOpacity = Math.log(clampedAlpha / (1 - clampedAlpha));
15640
+ dataView.setFloat32(offset, logitOpacity, true);
15641
+ offset += 4;
15642
+ dataView.setFloat32(offset, Math.log(Math.max(1e-8, scales[i3])), true);
15643
+ offset += 4;
15644
+ dataView.setFloat32(offset, Math.log(Math.max(1e-8, scales[i3 + 2])), true);
15645
+ offset += 4;
15646
+ dataView.setFloat32(offset, Math.log(Math.max(1e-8, scales[i3 + 1])), true);
15647
+ offset += 4;
15648
+ dataView.setFloat32(offset, -rotations[i4], true);
15649
+ offset += 4;
15650
+ dataView.setFloat32(offset, rotations[i4 + 1], true);
15651
+ offset += 4;
15652
+ dataView.setFloat32(offset, rotations[i4 + 3], true);
15653
+ offset += 4;
15654
+ dataView.setFloat32(offset, rotations[i4 + 2], true);
15655
+ offset += 4;
15656
+ }
15657
+ return buffer;
15658
+ }
15659
+ class SplatEditor {
15660
+ constructor(camera, gsRenderer, container, callbacks = {}) {
15661
+ __publicField(this, "camera");
15662
+ __publicField(this, "gsRenderer");
15663
+ __publicField(this, "container");
15664
+ __publicField(this, "splatState");
15665
+ __publicField(this, "editHistory");
15666
+ __publicField(this, "toolManager");
15667
+ __publicField(this, "compactData", null);
15668
+ __publicField(this, "callbacks");
15669
+ __publicField(this, "maskCanvas");
15670
+ __publicField(this, "maskCtx");
15671
+ __publicField(this, "toolOverlay");
15672
+ __publicField(this, "projectedPositions", null);
15673
+ __publicField(this, "projDirty", true);
15674
+ __publicField(this, "_active", false);
15675
+ __publicField(this, "overlayCleanup", []);
15676
+ // ============================================
15677
+ // 键盘快捷键
15678
+ // ============================================
15679
+ __publicField(this, "_keyHandler", null);
15680
+ this.camera = camera;
15681
+ this.gsRenderer = gsRenderer;
15682
+ this.container = container;
15683
+ this.callbacks = callbacks;
15684
+ }
15685
+ get active() {
15686
+ return this._active;
15687
+ }
15688
+ get state() {
15689
+ return this.splatState;
15690
+ }
15691
+ get history() {
15692
+ return this.editHistory;
15693
+ }
15694
+ get tools() {
15695
+ return this.toolManager;
15696
+ }
15697
+ /**
15698
+ * 设置 compact 数据引用(用于导出和颜色匹配)
15699
+ */
15700
+ setCompactData(data) {
15701
+ this.compactData = data;
15702
+ }
15703
+ /**
15704
+ * 进入编辑模式
15705
+ */
15706
+ enter() {
15707
+ if (this._active) return;
15708
+ this._active = true;
15709
+ const count = this.gsRenderer.getSplatCount();
15710
+ this.splatState = new SplatState(count);
15711
+ this.editHistory = new EditHistory(() => {
15712
+ var _a2, _b2;
15713
+ this.onStateChanged();
15714
+ (_b2 = (_a2 = this.callbacks).onHistoryChanged) == null ? void 0 : _b2.call(_a2, this.editHistory.canUndo(), this.editHistory.canRedo());
15715
+ });
15716
+ this.toolOverlay = document.createElement("div");
15717
+ this.toolOverlay.id = "editor-tool-overlay";
15718
+ this.toolOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;display:none;z-index:5;";
15719
+ this.container.appendChild(this.toolOverlay);
15720
+ this.setupOverlayEventForwarding();
15721
+ this.maskCanvas = document.createElement("canvas");
15722
+ this.maskCanvas.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:none;z-index:6;";
15723
+ this.container.appendChild(this.maskCanvas);
15724
+ this.maskCtx = this.maskCanvas.getContext("2d");
15725
+ const containerPos = getComputedStyle(this.container).position;
15726
+ if (containerPos === "static" || containerPos === "") {
15727
+ this.container.style.position = "relative";
15728
+ }
15729
+ this.toolManager = new ToolManager((name) => {
15730
+ var _a2, _b2;
15731
+ this.toolOverlay.style.display = name ? "block" : "none";
15732
+ (_b2 = (_a2 = this.callbacks).onToolChanged) == null ? void 0 : _b2.call(_a2, name);
15733
+ });
15734
+ this.toolManager.register("rect", new RectSelection(
15735
+ this.toolOverlay,
15736
+ (op, rect) => this.selectByRect(op, rect)
15737
+ ));
15738
+ this.toolManager.register("lasso", new LassoSelection(
15739
+ this.toolOverlay,
15740
+ this.maskCanvas,
15741
+ this.maskCtx,
15742
+ (op, canvas, ctx) => this.selectByMask(op, canvas, ctx)
15743
+ ));
15744
+ this.toolManager.register("polygon", new PolygonSelection(
15745
+ this.toolOverlay,
15746
+ this.maskCanvas,
15747
+ this.maskCtx,
15748
+ (op, canvas, ctx) => this.selectByMask(op, canvas, ctx)
15749
+ ));
15750
+ this.toolManager.register("brush", new BrushSelection(
15751
+ this.toolOverlay,
15752
+ this.maskCanvas,
15753
+ this.maskCtx,
15754
+ (op, canvas, ctx) => this.selectByMask(op, canvas, ctx)
15755
+ ));
15756
+ this.toolManager.register("flood", new FloodSelection(
15757
+ this.toolOverlay,
15758
+ this.maskCanvas,
15759
+ this.maskCtx,
15760
+ (op, canvas, ctx) => this.selectByMask(op, canvas, ctx),
15761
+ (_w, _h) => null
15762
+ // offscreen rendering not available yet
15763
+ ));
15764
+ this.toolManager.register("eyedropper", new EyedropperSelection(
15765
+ this.toolOverlay,
15766
+ (op, pt, threshold) => this.selectByColor(op, pt, threshold)
15767
+ ));
15768
+ this.gsRenderer.setEditorState(this.splatState.data);
15769
+ this._keyHandler = this._onKeyDown.bind(this);
15770
+ window.addEventListener("keydown", this._keyHandler);
15771
+ this.projDirty = true;
15772
+ }
15773
+ /**
15774
+ * 退出编辑模式,将编辑结果永久写入渲染数据
15775
+ */
15776
+ exit() {
15777
+ var _a2, _b2;
15778
+ if (!this._active) return;
15779
+ this._active = false;
15780
+ window.removeEventListener("keydown", this._keyHandler);
15781
+ this.toolManager.deactivateAll();
15782
+ this.overlayCleanup.forEach((fn) => fn());
15783
+ this.overlayCleanup = [];
15784
+ (_a2 = this.toolOverlay) == null ? void 0 : _a2.remove();
15785
+ (_b2 = this.maskCanvas) == null ? void 0 : _b2.remove();
15786
+ this.applyEditsToRenderer();
15787
+ this.editHistory.clear();
15788
+ this.gsRenderer.clearEditorState();
15789
+ this.projectedPositions = null;
15790
+ this.compactData = null;
15791
+ }
15792
+ /**
15793
+ * 将编辑结果写回渲染器:从 CompactSplatData 中剔除被标记为 deleted 的 splat,
15794
+ * 用精简数据重建 GPU 缓冲区。
15795
+ */
15796
+ applyEditsToRenderer() {
15797
+ var _a2, _b2;
15798
+ if (!this.compactData || this.splatState.numDeleted === 0) return;
15799
+ const { count, data } = this.splatState;
15800
+ const src = this.compactData;
15801
+ const keepCount = count - this.splatState.numDeleted;
15802
+ if (keepCount <= 0) return;
15803
+ const hasSH = !!src.shCoeffs;
15804
+ const shStride = hasSH ? src.shCoeffs.length / src.count : 0;
15805
+ const positions = new Float32Array(keepCount * 3);
15806
+ const scales = new Float32Array(keepCount * 3);
15807
+ const rotations = new Float32Array(keepCount * 4);
15808
+ const colors = new Float32Array(keepCount * 3);
15809
+ const opacities = new Float32Array(keepCount);
15810
+ const shCoeffs = hasSH ? new Float32Array(keepCount * shStride) : void 0;
15811
+ let dst = 0;
15812
+ for (let i = 0; i < count; i++) {
15813
+ if (data[i] & State.deleted) continue;
15814
+ const i3 = i * 3, d3 = dst * 3;
15815
+ positions[d3] = src.positions[i3];
15816
+ positions[d3 + 1] = src.positions[i3 + 1];
15817
+ positions[d3 + 2] = src.positions[i3 + 2];
15818
+ scales[d3] = src.scales[i3];
15819
+ scales[d3 + 1] = src.scales[i3 + 1];
15820
+ scales[d3 + 2] = src.scales[i3 + 2];
15821
+ const i4 = i * 4, d4 = dst * 4;
15822
+ rotations[d4] = src.rotations[i4];
15823
+ rotations[d4 + 1] = src.rotations[i4 + 1];
15824
+ rotations[d4 + 2] = src.rotations[i4 + 2];
15825
+ rotations[d4 + 3] = src.rotations[i4 + 3];
15826
+ colors[d3] = src.colors[i3];
15827
+ colors[d3 + 1] = src.colors[i3 + 1];
15828
+ colors[d3 + 2] = src.colors[i3 + 2];
15829
+ opacities[dst] = src.opacities[i];
15830
+ if (hasSH) {
15831
+ const srcOff = i * shStride, dstOff = dst * shStride;
15832
+ for (let s = 0; s < shStride; s++) {
15833
+ shCoeffs[dstOff + s] = src.shCoeffs[srcOff + s];
15834
+ }
15835
+ }
15836
+ dst++;
15837
+ }
15838
+ const newData = {
15839
+ count: keepCount,
15840
+ positions,
15841
+ scales,
15842
+ rotations,
15843
+ colors,
15844
+ opacities,
15845
+ ...hasSH ? { shCoeffs } : {}
15846
+ };
15847
+ this.gsRenderer.setCompactData(newData);
15848
+ (_b2 = (_a2 = this.callbacks).onApplyEdits) == null ? void 0 : _b2.call(_a2, newData);
15849
+ }
15850
+ // ============================================
15851
+ // 选择操作
15852
+ // ============================================
15853
+ selectAll() {
15854
+ this.editHistory.add(new SelectAllOp(this.splatState));
15855
+ }
15856
+ selectNone() {
15857
+ this.editHistory.add(new SelectNoneOp(this.splatState));
15858
+ }
15859
+ selectInvert() {
15860
+ this.editHistory.add(new SelectInvertOp(this.splatState));
15861
+ }
15862
+ deleteSelection() {
15863
+ if (this.splatState.numSelected === 0) return;
15864
+ this.editHistory.add(new DeleteSelectionOp(this.splatState));
15865
+ }
15866
+ hideSelection() {
15867
+ if (this.splatState.numSelected === 0) return;
15868
+ this.editHistory.add(new HideSelectionOp(this.splatState));
15869
+ }
15870
+ unhideAll() {
15871
+ this.editHistory.add(new UnhideAllOp(this.splatState));
15872
+ }
15873
+ undo() {
15874
+ this.editHistory.undo();
15875
+ }
15876
+ redo() {
15877
+ this.editHistory.redo();
15878
+ }
15879
+ // ============================================
15880
+ // 导出
15881
+ // ============================================
15882
+ exportPLY() {
15883
+ if (!this.compactData) return null;
15884
+ const { positions, scales, rotations, colors, opacities, shCoeffs } = this.compactData;
15885
+ return exportEditedPLY(positions, scales, rotations, colors, opacities, shCoeffs ?? null, this.splatState);
15886
+ }
15887
+ downloadPLY(filename = "edited.ply") {
15888
+ const buffer = this.exportPLY();
15889
+ if (!buffer) return;
15890
+ const blob = new Blob([buffer], { type: "application/octet-stream" });
15891
+ const url = URL.createObjectURL(blob);
15892
+ const a = document.createElement("a");
15893
+ a.href = url;
15894
+ a.download = filename;
15895
+ a.click();
15896
+ URL.revokeObjectURL(url);
15897
+ }
15898
+ // ============================================
15899
+ // 内部:选择逻辑
15900
+ // ============================================
15901
+ selectByRect(op, rect) {
15902
+ this.ensureProjection();
15903
+ const proj = this.projectedPositions;
15904
+ this.splatState.count;
15905
+ const editOp = new SelectOp(this.splatState, op, (i) => {
15906
+ const bx = i * 3;
15907
+ const sx = proj[bx], sy = proj[bx + 1], sz = proj[bx + 2];
15908
+ if (sz <= 0 || sz >= 1) return false;
15909
+ return sx >= rect.startX && sx <= rect.endX && sy >= rect.startY && sy <= rect.endY;
15910
+ });
15911
+ this.editHistory.add(editOp);
15912
+ }
15913
+ selectByMask(op, canvas, ctx) {
15914
+ this.ensureProjection();
15915
+ const proj = this.projectedPositions;
15916
+ const w = canvas.width;
15917
+ const h = canvas.height;
15918
+ const imageData = ctx.getImageData(0, 0, w, h);
15919
+ const pixels = imageData.data;
15920
+ const editOp = new SelectOp(this.splatState, op, (i) => {
15921
+ const bx = i * 3;
15922
+ const sx = proj[bx], sy = proj[bx + 1], sz = proj[bx + 2];
15923
+ if (sz <= 0 || sz >= 1) return false;
15924
+ const px = Math.floor(sx * w);
15925
+ const py = Math.floor(sy * h);
15926
+ if (px < 0 || px >= w || py < 0 || py >= h) return false;
15927
+ const idx = (py * w + px) * 4;
15928
+ return pixels[idx + 3] > 0;
15929
+ });
15930
+ this.editHistory.add(editOp);
15931
+ }
15932
+ selectByColor(op, _normalizedPoint, threshold) {
15933
+ if (!this.compactData) return;
15934
+ this.ensureProjection();
15935
+ const proj = this.projectedPositions;
15936
+ const { colors } = this.compactData;
15937
+ const w = this.container.clientWidth;
15938
+ const h = this.container.clientHeight;
15939
+ const px = Math.floor(_normalizedPoint.x * w);
15940
+ const py = Math.floor(_normalizedPoint.y * h);
15941
+ let bestIdx = -1;
15942
+ let bestDist = Infinity;
15943
+ for (let i = 0; i < this.splatState.count; i++) {
15944
+ const bx = i * 3;
15945
+ const sz = proj[bx + 2];
15946
+ if (sz <= 0 || sz >= 1) continue;
15947
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) continue;
15948
+ const spx = proj[bx] * w;
15949
+ const spy = proj[bx + 1] * h;
15950
+ const d = (spx - px) ** 2 + (spy - py) ** 2;
15951
+ if (d < bestDist) {
15952
+ bestDist = d;
15953
+ bestIdx = i;
15954
+ }
15955
+ }
15956
+ if (bestIdx < 0) return;
15957
+ const refR = colors[bestIdx * 3];
15958
+ const refG = colors[bestIdx * 3 + 1];
15959
+ const refB = colors[bestIdx * 3 + 2];
15960
+ const editOp = new SelectOp(this.splatState, op, (i) => {
15961
+ if (this.splatState.data[i] & (State.hidden | State.deleted)) return false;
15962
+ const ci = i * 3;
15963
+ const dr = colors[ci] - refR;
15964
+ const dg = colors[ci + 1] - refG;
15965
+ const db = colors[ci + 2] - refB;
15966
+ return Math.sqrt(dr * dr + dg * dg + db * db) < threshold;
15967
+ });
15968
+ this.editHistory.add(editOp);
15969
+ }
15970
+ // ============================================
15971
+ // 投影
15972
+ // ============================================
15973
+ /**
15974
+ * 每帧调用,标记投影需要更新
15975
+ */
15976
+ markProjectionDirty() {
15977
+ this.projDirty = true;
15978
+ }
15979
+ ensureProjection() {
15980
+ const cpuPositions = this.gsRenderer.getCPUPositions();
15981
+ if (!cpuPositions) return;
15982
+ const count = this.splatState.count;
15983
+ if (!this.projectedPositions || this.projectedPositions.length !== count * 3) {
15984
+ this.projectedPositions = new Float32Array(count * 3);
15985
+ }
15986
+ const viewMat = this.camera.viewMatrix;
15987
+ const projMat = this.camera.projectionMatrix;
15988
+ const modelMat = this.gsRenderer.getModelMatrix();
15989
+ const out = this.projectedPositions;
15990
+ for (let i = 0; i < count; i++) {
15991
+ const i3 = i * 3;
15992
+ const x = cpuPositions[i3], y = cpuPositions[i3 + 1], z = cpuPositions[i3 + 2];
15993
+ const wx = modelMat[0] * x + modelMat[4] * y + modelMat[8] * z + modelMat[12];
15994
+ const wy = modelMat[1] * x + modelMat[5] * y + modelMat[9] * z + modelMat[13];
15995
+ const wz = modelMat[2] * x + modelMat[6] * y + modelMat[10] * z + modelMat[14];
15996
+ const vx = viewMat[0] * wx + viewMat[4] * wy + viewMat[8] * wz + viewMat[12];
15997
+ const vy = viewMat[1] * wx + viewMat[5] * wy + viewMat[9] * wz + viewMat[13];
15998
+ const vz = viewMat[2] * wx + viewMat[6] * wy + viewMat[10] * wz + viewMat[14];
15999
+ const vw = viewMat[3] * wx + viewMat[7] * wy + viewMat[11] * wz + viewMat[15];
16000
+ const cx = projMat[0] * vx + projMat[4] * vy + projMat[8] * vz + projMat[12] * vw;
16001
+ const cy = projMat[1] * vx + projMat[5] * vy + projMat[9] * vz + projMat[13] * vw;
16002
+ const cz = projMat[2] * vx + projMat[6] * vy + projMat[10] * vz + projMat[14] * vw;
16003
+ const cw = projMat[3] * vx + projMat[7] * vy + projMat[11] * vz + projMat[15] * vw;
16004
+ if (cw <= 0) {
16005
+ out[i3] = -2;
16006
+ out[i3 + 1] = -2;
16007
+ out[i3 + 2] = -1;
16008
+ continue;
16009
+ }
16010
+ const invW = 1 / cw;
16011
+ out[i3] = (cx * invW + 1) * 0.5;
16012
+ out[i3 + 1] = (1 - cy * invW) * 0.5;
16013
+ out[i3 + 2] = cz * invW;
16014
+ }
16015
+ this.projDirty = false;
16016
+ }
16017
+ // ============================================
16018
+ // 状态变更通知
16019
+ // ============================================
16020
+ onStateChanged() {
16021
+ var _a2, _b2;
16022
+ this.gsRenderer.updateEditorState(this.splatState.data);
16023
+ (_b2 = (_a2 = this.callbacks).onStateChanged) == null ? void 0 : _b2.call(_a2);
16024
+ }
16025
+ // ============================================
16026
+ // 覆盖层事件转发(保持相机控制可用)
16027
+ // ============================================
16028
+ setupOverlayEventForwarding() {
16029
+ const overlay = this.toolOverlay;
16030
+ const canvas = this.container.querySelector("canvas");
16031
+ if (!canvas) return;
16032
+ const addForward = (event, handler, options) => {
16033
+ overlay.addEventListener(event, handler, options);
16034
+ this.overlayCleanup.push(
16035
+ () => overlay.removeEventListener(event, handler, options)
16036
+ );
16037
+ };
16038
+ addForward("wheel", (e) => {
16039
+ canvas.dispatchEvent(new WheelEvent("wheel", e));
16040
+ e.preventDefault();
16041
+ }, { passive: false });
16042
+ addForward("mousedown", (e) => {
16043
+ if (e.button !== 0) {
16044
+ canvas.dispatchEvent(new MouseEvent("mousedown", e));
16045
+ }
16046
+ });
16047
+ addForward("mousemove", (e) => {
16048
+ if (e.buttons & 6) {
16049
+ canvas.dispatchEvent(new MouseEvent("mousemove", e));
16050
+ }
16051
+ });
16052
+ addForward("mouseup", (e) => {
16053
+ if (e.button !== 0) {
16054
+ canvas.dispatchEvent(new MouseEvent("mouseup", e));
16055
+ }
16056
+ });
16057
+ addForward("contextmenu", (e) => {
16058
+ e.preventDefault();
16059
+ });
16060
+ }
16061
+ _onKeyDown(e) {
16062
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
16063
+ if (e.ctrlKey && e.key === "z") {
16064
+ e.preventDefault();
16065
+ if (e.shiftKey) this.redo();
16066
+ else this.undo();
16067
+ } else if (e.ctrlKey && e.key === "y") {
16068
+ e.preventDefault();
16069
+ this.redo();
16070
+ } else if (e.ctrlKey && e.key === "a") {
16071
+ e.preventDefault();
16072
+ this.selectAll();
16073
+ } else if (e.key === "Delete" || e.key === "Backspace") {
16074
+ e.preventDefault();
16075
+ this.deleteSelection();
16076
+ }
16077
+ }
16078
+ }
14213
16079
  class App {
14214
16080
  constructor(canvas) {
14215
16081
  __publicField(this, "canvas");
@@ -14228,6 +16094,8 @@ class App {
14228
16094
  __publicField(this, "animationId", 0);
14229
16095
  // 是否使用移动端渲染器
14230
16096
  __publicField(this, "useMobileRenderer", false);
16097
+ // 最近加载的 CompactSplatData(用于编辑器导出)
16098
+ __publicField(this, "lastCompactData", null);
14231
16099
  // 绑定的事件处理函数
14232
16100
  __publicField(this, "boundOnResize");
14233
16101
  this.canvas = canvas;
@@ -14337,6 +16205,7 @@ class App {
14337
16205
  if (onProgress) onProgress(90, "upload");
14338
16206
  gsRenderer.setCompactData(compactData);
14339
16207
  if (onProgress) onProgress(100, "upload");
16208
+ this.lastCompactData = compactData;
14340
16209
  this.sceneManager.setGSRenderer(gsRenderer);
14341
16210
  this.hotspotManager.setGSRenderer(gsRenderer);
14342
16211
  return compactData.count;
@@ -14352,6 +16221,7 @@ class App {
14352
16221
  if (onProgress) onProgress(90, "upload");
14353
16222
  gsRenderer.setCompactData(compactData);
14354
16223
  if (onProgress) onProgress(100, "upload");
16224
+ this.lastCompactData = compactData;
14355
16225
  this.sceneManager.setGSRenderer(gsRenderer);
14356
16226
  this.hotspotManager.setGSRenderer(gsRenderer);
14357
16227
  return compactData.count;
@@ -14456,6 +16326,7 @@ class App {
14456
16326
  this.useMobileRenderer = false;
14457
16327
  }
14458
16328
  gsRenderer.setCompactData(compactData);
16329
+ this.lastCompactData = compactData;
14459
16330
  this.sceneManager.setGSRenderer(gsRenderer);
14460
16331
  this.hotspotManager.setGSRenderer(gsRenderer);
14461
16332
  if (onProgress) onProgress(100, "upload");
@@ -14720,6 +16591,12 @@ class App {
14720
16591
  isUsingMobileRenderer() {
14721
16592
  return this.useMobileRenderer;
14722
16593
  }
16594
+ getLastCompactData() {
16595
+ return this.lastCompactData;
16596
+ }
16597
+ setLastCompactData(data) {
16598
+ this.lastCompactData = data;
16599
+ }
14723
16600
  // ============================================
14724
16601
  // 热点管理
14725
16602
  // ============================================
@@ -14882,6 +16759,7 @@ export {
14882
16759
  Camera,
14883
16760
  DEFAULT_MATERIAL,
14884
16761
  DEFAULT_OBJ_MATERIAL,
16762
+ EditHistory,
14885
16763
  GLBLoader,
14886
16764
  GSSplatRenderer,
14887
16765
  GSSplatRendererMobile,
@@ -14902,8 +16780,12 @@ export {
14902
16780
  SceneAidsRenderer,
14903
16781
  SceneManager,
14904
16782
  SplatBoundingBoxProvider,
16783
+ SplatEditor,
16784
+ SplatState,
14905
16785
  SplatTransformProxy,
16786
+ State,
14906
16787
  TextureCache,
16788
+ ToolManager,
14907
16789
  TransformGizmo,
14908
16790
  ViewportGizmo,
14909
16791
  calculateTextureDimensions,
@@ -14915,6 +16797,7 @@ export {
14915
16797
  deserializeSOG,
14916
16798
  deserializeSplat,
14917
16799
  destroyCompressedTextures,
16800
+ exportEditedPLY,
14918
16801
  getRecommendedDPR,
14919
16802
  isMobileDevice,
14920
16803
  isWebGPUSupported,