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