@lingbi/studio 0.8.3 → 0.9.1

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/index.js CHANGED
@@ -29202,7 +29202,8 @@ const MINIMUM_FRUSTUM_HEIGHT = 1e-4;
29202
29202
  const MINIMUM_NEAR = 0.01;
29203
29203
  const MINIMUM_ZOOM = 0.01;
29204
29204
  const MAXIMUM_ZOOM = 100;
29205
- const CLIP_MARGIN_RATIO = 0.05;
29205
+ const CLIP_MARGIN_RATIO = 0.5;
29206
+ const VIEW_MARGIN_RATIO = 1.3;
29206
29207
  const WHEEL_ZOOM_SPEED = 15e-4;
29207
29208
  var CameraNavigationMode = /* @__PURE__ */ ((CameraNavigationMode2) => {
29208
29209
  CameraNavigationMode2[CameraNavigationMode2["Rotate"] = 0] = "Rotate";
@@ -29716,7 +29717,7 @@ class CameraController {
29716
29717
  );
29717
29718
  const frustumHeight = this.getFullFrustumHeight(safeFrustumHeight);
29718
29719
  const depthRange = maximumForward - minimumForward;
29719
- const clipMargin = Math.max(MINIMUM_CLIP_MARGIN, depthRange * CLIP_MARGIN_RATIO);
29720
+ const clipMargin = this.createClipMargin(depthRange, frustumHeight);
29720
29721
  const distance = Math.max(
29721
29722
  this.initialCameraDistance,
29722
29723
  -minimumForward + clipMargin * 2,
@@ -29921,7 +29922,10 @@ class CameraController {
29921
29922
  maximumDepth = Math.max(maximumDepth, depth);
29922
29923
  }
29923
29924
  const depthRange = maximumDepth - minimumDepth;
29924
- const clipMargin = Math.max(MINIMUM_CLIP_MARGIN, depthRange * CLIP_MARGIN_RATIO);
29925
+ const clipMargin = this.createClipMargin(
29926
+ depthRange,
29927
+ (this.camera.top - this.camera.bottom) / this.camera.zoom
29928
+ );
29925
29929
  const minimumVisibleDepth = MINIMUM_NEAR + clipMargin;
29926
29930
  if (minimumDepth >= minimumVisibleDepth) {
29927
29931
  return;
@@ -29944,7 +29948,10 @@ class CameraController {
29944
29948
  maximumDepth = Math.max(maximumDepth, depth);
29945
29949
  }
29946
29950
  const depthRange = maximumDepth - minimumDepth;
29947
- const clipMargin = Math.max(MINIMUM_CLIP_MARGIN, depthRange * CLIP_MARGIN_RATIO);
29951
+ const clipMargin = this.createClipMargin(
29952
+ depthRange,
29953
+ (this.camera.top - this.camera.bottom) / this.camera.zoom
29954
+ );
29948
29955
  this.camera.near = Math.max(MINIMUM_NEAR, minimumDepth - clipMargin);
29949
29956
  this.camera.far = Math.max(
29950
29957
  this.camera.near + MINIMUM_CLIP_RANGE,
@@ -29984,6 +29991,19 @@ class CameraController {
29984
29991
  getSafeAspect() {
29985
29992
  return this.viewportWidth / this.getSafeViewportHeight();
29986
29993
  }
29994
+ /**
29995
+ * Orthographic depth is linear, so generous margins cost nothing. The
29996
+ * model ratio covers scene depth, and the viewport ratio keeps the ground
29997
+ * grid inside the clip window as the camera zooms out.
29998
+ */
29999
+ createClipMargin(depthRange, frustumHeight) {
30000
+ const aspect2 = this.getSafeAspect();
30001
+ const halfDiagonal = frustumHeight / 2 * Math.sqrt(1 + aspect2 * aspect2);
30002
+ return Math.max(
30003
+ MINIMUM_CLIP_MARGIN,
30004
+ Math.max(depthRange * CLIP_MARGIN_RATIO, halfDiagonal * VIEW_MARGIN_RATIO)
30005
+ );
30006
+ }
29987
30007
  getSafeViewportHeight() {
29988
30008
  const maximumInset = Math.max(0, this.viewportHeight - 1);
29989
30009
  const effectiveInset = Math.min(this.viewportBottomInset, maximumInset);
@@ -30545,6 +30565,148 @@ class CanvasController {
30545
30565
  }
30546
30566
  }
30547
30567
  }
30568
+ const CENTER_LINE_COLOR = 10132122;
30569
+ const CENTER_LINE_EXTRA_WIDTH = "2.0";
30570
+ const FADE_END_RATIO = 1.4;
30571
+ const FADE_START_RATIO = 0.8;
30572
+ const GRID_DIVISIONS = 24;
30573
+ const GRID_LINE_COLOR = 9474192;
30574
+ const GRID_LINE_OPACITY = 1;
30575
+ const GRID_LINE_WIDTH_PIXELS = 2;
30576
+ const GRID_SEPARATION_RATIO = 2e-3;
30577
+ const MINIMUM_ALPHA = "0.01";
30578
+ const QUAD_COVERAGE = 2.1;
30579
+ const VIEW_RATIO = 0.6;
30580
+ const VERTEX_SHADER = (
30581
+ /* glsl */
30582
+ `
30583
+ varying vec2 vUv;
30584
+
30585
+ void main() {
30586
+ vUv = uv;
30587
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
30588
+ }
30589
+ `
30590
+ );
30591
+ const FRAGMENT_SHADER = (
30592
+ /* glsl */
30593
+ `
30594
+ uniform vec3 uCenterColor;
30595
+ uniform float uDivisionCount;
30596
+ uniform float uFadeEnd;
30597
+ uniform float uFadeStart;
30598
+ uniform vec3 uGridColor;
30599
+ uniform float uGridSize;
30600
+ uniform float uLineOpacity;
30601
+ uniform float uLineWidth;
30602
+ varying vec2 vUv;
30603
+
30604
+ void main() {
30605
+ vec2 coord = (vUv - 0.5) * uGridSize;
30606
+ float cellSize = uGridSize / uDivisionCount;
30607
+ vec2 cellCoord = coord / cellSize;
30608
+ vec2 gridDistance = abs(fract(cellCoord - 0.5) - 0.5) / (fwidth(cellCoord) * uLineWidth);
30609
+ float gridLine = 1.0 - min(min(gridDistance.x, gridDistance.y), 1.0);
30610
+ vec2 centerDistance = abs(coord) / (fwidth(coord) * uLineWidth * ${CENTER_LINE_EXTRA_WIDTH});
30611
+ float centerLine = 1.0 - min(min(centerDistance.x, centerDistance.y), 1.0);
30612
+ float lineStrength = max(gridLine, centerLine);
30613
+ vec3 lineColor = mix(uGridColor, uCenterColor, centerLine);
30614
+ float radial = length(coord);
30615
+ float alpha = lineStrength * (1.0 - smoothstep(uFadeStart, uFadeEnd, radial)) * uLineOpacity;
30616
+
30617
+ if (alpha < ${MINIMUM_ALPHA}) {
30618
+ discard;
30619
+ }
30620
+
30621
+ gl_FragColor = vec4(lineColor, alpha);
30622
+ }
30623
+ `
30624
+ );
30625
+ class GroundGrid {
30626
+ material;
30627
+ mesh;
30628
+ anchorCenterX = 0;
30629
+ anchorCenterY = 0;
30630
+ anchorMinZ = 0;
30631
+ anchorSpan = 1;
30632
+ gridUniforms = {
30633
+ uCenterColor: { value: new Color(CENTER_LINE_COLOR) },
30634
+ uDivisionCount: { value: GRID_DIVISIONS },
30635
+ uFadeEnd: { value: FADE_END_RATIO },
30636
+ uFadeStart: { value: FADE_START_RATIO },
30637
+ uGridColor: { value: new Color(GRID_LINE_COLOR) },
30638
+ uGridSize: { value: 1 },
30639
+ uLineOpacity: { value: GRID_LINE_OPACITY },
30640
+ uLineWidth: { value: 1 / GRID_LINE_WIDTH_PIXELS }
30641
+ };
30642
+ constructor() {
30643
+ this.material = new ShaderMaterial({
30644
+ depthWrite: false,
30645
+ fragmentShader: FRAGMENT_SHADER,
30646
+ side: DoubleSide,
30647
+ transparent: true,
30648
+ uniforms: this.gridUniforms,
30649
+ vertexShader: VERTEX_SHADER
30650
+ });
30651
+ this.mesh = new Mesh(new PlaneGeometry(1, 1), this.material);
30652
+ this.mesh.visible = false;
30653
+ }
30654
+ getHelper() {
30655
+ return this.mesh;
30656
+ }
30657
+ attach(scene) {
30658
+ scene.add(this.mesh);
30659
+ }
30660
+ updateFromBounds(bounds) {
30661
+ this.anchorCenterX = (bounds.min.x + bounds.max.x) / 2;
30662
+ this.anchorCenterY = (bounds.min.y + bounds.max.y) / 2;
30663
+ this.anchorMinZ = bounds.min.z;
30664
+ this.anchorSpan = Math.max(bounds.max.x - bounds.min.x, bounds.max.y - bounds.min.y);
30665
+ this.applyLayout(this.anchorSpan * FADE_END_RATIO);
30666
+ this.mesh.visible = true;
30667
+ }
30668
+ /**
30669
+ * Grows the grid when the viewport zooms out beyond the model-anchored
30670
+ * radius, so the ground keeps covering most of the visible area instead
30671
+ * of shrinking into a patch around the model.
30672
+ */
30673
+ updateForViewport(halfDiagonal) {
30674
+ if (!this.mesh.visible) {
30675
+ return;
30676
+ }
30677
+ const viewportRadius = halfDiagonal * VIEW_RATIO;
30678
+ const fadeRadius = Math.max(this.anchorSpan * FADE_END_RATIO, viewportRadius);
30679
+ this.applyLayout(fadeRadius);
30680
+ }
30681
+ applyLayout(fadeRadius) {
30682
+ const size = fadeRadius * QUAD_COVERAGE;
30683
+ this.gridUniforms.uGridSize.value = size;
30684
+ this.gridUniforms.uFadeStart.value = fadeRadius * (FADE_START_RATIO / FADE_END_RATIO);
30685
+ this.gridUniforms.uFadeEnd.value = fadeRadius;
30686
+ this.mesh.scale.set(size, size, 1);
30687
+ this.mesh.position.set(
30688
+ this.anchorCenterX,
30689
+ this.anchorCenterY,
30690
+ this.anchorMinZ - this.anchorSpan * GRID_SEPARATION_RATIO
30691
+ );
30692
+ }
30693
+ /**
30694
+ * Keeps the drawn line width at the intended CSS pixel size on high-DPI
30695
+ * outputs: fwidth-based lines live in framebuffer pixels, so the inverse
30696
+ * width must scale with the device pixel ratio.
30697
+ */
30698
+ setPixelRatio(pixelRatio) {
30699
+ this.gridUniforms.uLineWidth.value = 1 / (GRID_LINE_WIDTH_PIXELS * pixelRatio);
30700
+ }
30701
+ hide() {
30702
+ this.mesh.visible = false;
30703
+ }
30704
+ dispose() {
30705
+ this.mesh.removeFromParent();
30706
+ this.mesh.geometry.dispose();
30707
+ this.material.dispose();
30708
+ }
30709
+ }
30548
30710
  class ConsoleImportPerformanceLogSink {
30549
30711
  categoryByStageName = {
30550
30712
  "boolean-source-snapshot": "scene",
@@ -30840,6 +31002,51 @@ class ImportPerformanceTracer {
30840
31002
  return trace;
30841
31003
  }
30842
31004
  }
31005
+ const COMMITTED_PERCENT = 100;
31006
+ const ROLE_SPAN_TOTAL = 0.96;
31007
+ class ImportProgressTracker {
31008
+ listener;
31009
+ percent = 0;
31010
+ rolePositions = /* @__PURE__ */ new Map();
31011
+ roleSpans;
31012
+ constructor(roles, listener) {
31013
+ this.listener = listener;
31014
+ const roleSpans = /* @__PURE__ */ new Map();
31015
+ const spanPerRole = ROLE_SPAN_TOTAL / roles.length;
31016
+ for (const [roleIndex, role] of roles.entries()) {
31017
+ roleSpans.set(role, { span: spanPerRole, start: spanPerRole * roleIndex });
31018
+ }
31019
+ this.roleSpans = roleSpans;
31020
+ }
31021
+ completeRole(role) {
31022
+ this.setRolePosition(role, 1);
31023
+ }
31024
+ completeScene() {
31025
+ this.setPercent(COMMITTED_PERCENT);
31026
+ }
31027
+ getPercent() {
31028
+ return this.percent;
31029
+ }
31030
+ setRolePosition(role, position) {
31031
+ const roleSpan = this.roleSpans.get(role);
31032
+ if (roleSpan === void 0) {
31033
+ return;
31034
+ }
31035
+ const nextPosition = Math.max(this.rolePositions.get(role) ?? 0, Math.min(1, position));
31036
+ this.rolePositions.set(role, nextPosition);
31037
+ this.setPercent(
31038
+ Math.floor((roleSpan.start + roleSpan.span * nextPosition) * COMMITTED_PERCENT)
31039
+ );
31040
+ }
31041
+ setPercent(nextPercent) {
31042
+ const percent = Math.max(this.percent, Math.min(COMMITTED_PERCENT, nextPercent));
31043
+ if (percent === this.percent) {
31044
+ return;
31045
+ }
31046
+ this.percent = percent;
31047
+ this.listener?.(this);
31048
+ }
31049
+ }
30843
31050
  const LOCAL_RESOURCE_BASE_URL = "https://renderer-local.invalid/";
30844
31051
  class ObjResourceList {
30845
31052
  mtlUrls;
@@ -32014,6 +32221,85 @@ class OBJLoader extends Loader {
32014
32221
  return container;
32015
32222
  }
32016
32223
  }
32224
+ class StreamingResourceReader {
32225
+ async readText(url, signal, progress) {
32226
+ const response = await this.fetchOk(url, signal);
32227
+ const streamedBody = this.getStreamedBody(response, progress);
32228
+ if (streamedBody === null) {
32229
+ return response.text();
32230
+ }
32231
+ return new TextDecoder().decode(
32232
+ await this.readStreamingBody(
32233
+ streamedBody.stream,
32234
+ streamedBody.contentLength,
32235
+ streamedBody.progress
32236
+ )
32237
+ );
32238
+ }
32239
+ async readArrayBuffer(url, signal, progress) {
32240
+ const response = await this.fetchOk(url, signal);
32241
+ const streamedBody = this.getStreamedBody(response, progress);
32242
+ if (streamedBody === null) {
32243
+ return response.arrayBuffer();
32244
+ }
32245
+ const bytes = await this.readStreamingBody(
32246
+ streamedBody.stream,
32247
+ streamedBody.contentLength,
32248
+ streamedBody.progress
32249
+ );
32250
+ return bytes.buffer;
32251
+ }
32252
+ async fetchOk(url, signal) {
32253
+ const response = await fetch(url, { signal });
32254
+ if (!response.ok) {
32255
+ throw new Error(`Unable to load resource: ${url}`);
32256
+ }
32257
+ return response;
32258
+ }
32259
+ getStreamedBody(response, progress) {
32260
+ if (progress === void 0) {
32261
+ return null;
32262
+ }
32263
+ const contentLength = this.parseContentLength(response);
32264
+ if (contentLength === void 0) {
32265
+ return null;
32266
+ }
32267
+ const stream = response.body;
32268
+ return stream === null ? null : { contentLength, progress, stream };
32269
+ }
32270
+ async readStreamingBody(stream, contentLength, progress) {
32271
+ const reader = stream.getReader();
32272
+ const chunks = [];
32273
+ let loaded = 0;
32274
+ for (; ; ) {
32275
+ const next = await reader.read();
32276
+ if (next.done) {
32277
+ break;
32278
+ }
32279
+ chunks.push(next.value);
32280
+ loaded += next.value.byteLength;
32281
+ progress(Math.min(1, loaded / contentLength));
32282
+ }
32283
+ return this.mergeChunks(chunks, loaded);
32284
+ }
32285
+ mergeChunks(chunks, total) {
32286
+ const merged = new Uint8Array(total);
32287
+ let offset = 0;
32288
+ for (const chunk of chunks) {
32289
+ merged.set(chunk, offset);
32290
+ offset += chunk.byteLength;
32291
+ }
32292
+ return merged;
32293
+ }
32294
+ parseContentLength(response) {
32295
+ const raw = response.headers.get("Content-Length");
32296
+ if (raw === null) {
32297
+ return void 0;
32298
+ }
32299
+ const parsed = Number(raw);
32300
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
32301
+ }
32302
+ }
32017
32303
  const DEFAULT_MODEL_COLOR$1 = 10265519;
32018
32304
  const DEFAULT_POINT_SIZE$1 = 3;
32019
32305
  class TextureLoadMonitor {
@@ -32022,12 +32308,15 @@ class TextureLoadMonitor {
32022
32308
  encounteredError = false;
32023
32309
  finishedScheduling = false;
32024
32310
  manager;
32311
+ onTextureSettled;
32025
32312
  originalTextureLoader;
32026
32313
  pendingRequests = 0;
32314
+ scheduledTextureCount = 0;
32027
32315
  settled = false;
32028
32316
  textures = /* @__PURE__ */ new Set();
32029
- constructor(manager, creator) {
32317
+ constructor(manager, creator, onTextureSettled) {
32030
32318
  this.manager = manager;
32319
+ this.onTextureSettled = onTextureSettled;
32031
32320
  this.completionPromise = new Promise(
32032
32321
  this.captureCompletionResolver.bind(this)
32033
32322
  );
@@ -32038,6 +32327,9 @@ class TextureLoadMonitor {
32038
32327
  this.finishedScheduling = true;
32039
32328
  this.resolveIfSettled();
32040
32329
  }
32330
+ getScheduledTextureCount() {
32331
+ return this.scheduledTextureCount;
32332
+ }
32041
32333
  getTextures() {
32042
32334
  return [...this.textures];
32043
32335
  }
@@ -32065,14 +32357,17 @@ class TextureLoadMonitor {
32065
32357
  handleTextureError() {
32066
32358
  this.encounteredError = true;
32067
32359
  this.pendingRequests -= 1;
32360
+ this.onTextureSettled?.();
32068
32361
  this.resolveIfSettled();
32069
32362
  }
32070
32363
  handleTextureLoad() {
32071
32364
  this.pendingRequests -= 1;
32365
+ this.onTextureSettled?.();
32072
32366
  this.resolveIfSettled();
32073
32367
  }
32074
32368
  loadTexture(url, mapping) {
32075
32369
  this.pendingRequests += 1;
32370
+ this.scheduledTextureCount += 1;
32076
32371
  try {
32077
32372
  const texture = this.originalTextureLoader(
32078
32373
  url,
@@ -32105,18 +32400,20 @@ class TextureLoadMonitor {
32105
32400
  }
32106
32401
  class ObjLoader {
32107
32402
  parser;
32403
+ resourceReader = new StreamingResourceReader();
32108
32404
  constructor(parser = new OBJLoader()) {
32109
32405
  this.parser = parser;
32110
32406
  }
32111
- async loadResources(resources, signal, performanceTrace, role) {
32407
+ async loadResources(resources, signal, performanceTrace, role, progress) {
32112
32408
  return this.loadInternal(
32113
32409
  resources,
32114
32410
  signal,
32115
32411
  performanceTrace ?? ImportPerformanceTrace.createDisabled(),
32116
- role ?? "stone"
32412
+ role ?? "stone",
32413
+ progress
32117
32414
  );
32118
32415
  }
32119
- async loadInternal(resources, signal, performanceTrace, role) {
32416
+ async loadInternal(resources, signal, performanceTrace, role, progress) {
32120
32417
  if (this.hasEmptyRequiredUrl(resources)) {
32121
32418
  return {
32122
32419
  code: ObjImportFailureCode$1.EmptySource,
@@ -32128,8 +32425,9 @@ class ObjLoader {
32128
32425
  let warnings = [];
32129
32426
  try {
32130
32427
  const objFetchStage = performanceTrace.startStage("obj-fetch", role);
32131
- const objSource = await this.loadText(resources.objUrl, signal);
32428
+ const objSource = await this.loadText(resources.objUrl, signal, progress);
32132
32429
  performanceTrace.endStage(objFetchStage, "success");
32430
+ progress?.markModelFetched();
32133
32431
  this.assertNotCancelled(signal);
32134
32432
  const materialScanStage = performanceTrace.startStage("obj-material-scan", role);
32135
32433
  const materialReferences = this.getMaterialLibraryReferences(objSource);
@@ -32149,9 +32447,11 @@ class ObjLoader {
32149
32447
  materialContext = await this.loadMaterialContext(
32150
32448
  materialUrls,
32151
32449
  resources,
32152
- signal
32450
+ signal,
32451
+ progress
32153
32452
  );
32154
32453
  performanceTrace.endStage(materialLoadStage, "success");
32454
+ progress?.markMaterialsResolved(this.getScheduledTextureCount(materialContext));
32155
32455
  } catch {
32156
32456
  performanceTrace.endStage(
32157
32457
  materialLoadStage,
@@ -32175,6 +32475,7 @@ class ObjLoader {
32175
32475
  resources.upAxis
32176
32476
  );
32177
32477
  performanceTrace.endStage(modelCreateStage, "success");
32478
+ progress?.markModelParsed();
32178
32479
  if (!this.hasRenderablePrimitive(group) || !model.hasFiniteBounds()) {
32179
32480
  model.dispose();
32180
32481
  return {
@@ -32335,6 +32636,13 @@ class ObjLoader {
32335
32636
  }
32336
32637
  return [...textures];
32337
32638
  }
32639
+ getScheduledTextureCount(context) {
32640
+ let scheduledTextureCount = 0;
32641
+ for (const monitor of context.monitors) {
32642
+ scheduledTextureCount += monitor.getScheduledTextureCount();
32643
+ }
32644
+ return scheduledTextureCount;
32645
+ }
32338
32646
  getObjectTree(root) {
32339
32647
  const objectTree = [root];
32340
32648
  for (const object of objectTree) {
@@ -32451,11 +32759,11 @@ class ObjLoader {
32451
32759
  isPoints(object) {
32452
32760
  return object instanceof Points;
32453
32761
  }
32454
- async loadMaterialContext(mtlUrls, resources, signal) {
32762
+ async loadMaterialContext(mtlUrls, resources, signal, progress) {
32455
32763
  const libraries = [];
32456
32764
  try {
32457
32765
  for (const mtlUrl of mtlUrls) {
32458
- libraries.push(await this.loadMaterialLibrary(mtlUrl, resources, signal));
32766
+ libraries.push(await this.loadMaterialLibrary(mtlUrl, resources, signal, progress));
32459
32767
  }
32460
32768
  return this.createMaterialContext(libraries);
32461
32769
  } catch (error2) {
@@ -32463,7 +32771,7 @@ class ObjLoader {
32463
32771
  throw error2;
32464
32772
  }
32465
32773
  }
32466
- async loadMaterialLibrary(mtlUrl, resources, signal) {
32774
+ async loadMaterialLibrary(mtlUrl, resources, signal, progress) {
32467
32775
  const materialSource = await this.loadText(mtlUrl, signal);
32468
32776
  this.assertNotCancelled(signal);
32469
32777
  const materialBaseUrl = this.getResourceDirectoryUrl(resources, mtlUrl);
@@ -32474,7 +32782,11 @@ class ObjLoader {
32474
32782
  side: DoubleSide
32475
32783
  });
32476
32784
  const creator = materialLoader.parse(materialSource, materialBaseUrl);
32477
- const monitor = new TextureLoadMonitor(manager, creator);
32785
+ const monitor = new TextureLoadMonitor(
32786
+ manager,
32787
+ creator,
32788
+ progress === void 0 ? void 0 : progress.markTextureSettled.bind(progress)
32789
+ );
32478
32790
  try {
32479
32791
  const pbrMaterialReferences = this.parsePbrMaterialReferences(materialSource);
32480
32792
  this.assertMaterialTextures(resources, materialSource, materialBaseUrl, creator);
@@ -32545,14 +32857,12 @@ class ObjLoader {
32545
32857
  }
32546
32858
  return outcome;
32547
32859
  }
32548
- async loadText(url, signal) {
32549
- const response = await fetch(url, {
32550
- signal
32551
- });
32552
- if (!response.ok) {
32553
- throw new Error(`Unable to load OBJ resource: ${url}`);
32554
- }
32555
- return response.text();
32860
+ async loadText(url, signal, progress) {
32861
+ return this.resourceReader.readText(
32862
+ url,
32863
+ signal,
32864
+ progress === void 0 ? void 0 : progress.markModelFetchProgress.bind(progress)
32865
+ );
32556
32866
  }
32557
32867
  getImportPerformanceFailureStatus(signal) {
32558
32868
  return signal.aborted ? "cancelled" : "failure";
@@ -33445,22 +33755,24 @@ const DEFAULT_POINT_SIZE = 3;
33445
33755
  const MAX_HEADER_BYTES = 65536;
33446
33756
  class PlyLoader {
33447
33757
  parser;
33758
+ resourceReader = new StreamingResourceReader();
33448
33759
  textureLoader;
33449
33760
  constructor(parser = new PLYLoader(), textureLoader = new PlyTextureLoader()) {
33450
33761
  this.parser = parser;
33451
33762
  this.textureLoader = textureLoader;
33452
33763
  }
33453
- async load(resources, signal) {
33764
+ async load(resources, signal, progress) {
33454
33765
  if (signal.aborted) {
33455
33766
  return {
33456
33767
  code: ObjImportFailureCode$1.Cancelled,
33457
33768
  success: false
33458
33769
  };
33459
33770
  }
33460
- const source = await this.fetchSource(resources.plyUrl, signal);
33771
+ const source = await this.fetchSource(resources.plyUrl, signal, progress);
33461
33772
  if (!(source instanceof ArrayBuffer)) {
33462
33773
  return source;
33463
33774
  }
33775
+ progress?.markModelFetched();
33464
33776
  let geometry;
33465
33777
  let metadata;
33466
33778
  try {
@@ -33486,7 +33798,14 @@ class PlyLoader {
33486
33798
  success: false
33487
33799
  };
33488
33800
  }
33489
- const modelResult = metadata.hasFaces ? await this.createMeshModel(geometry, metadata.textureReference, resources, signal) : this.createPointsModel(geometry, resources);
33801
+ progress?.markModelParsed();
33802
+ const modelResult = metadata.hasFaces ? await this.createMeshModel(
33803
+ geometry,
33804
+ metadata.textureReference,
33805
+ resources,
33806
+ signal,
33807
+ progress
33808
+ ) : this.createPointsModel(geometry, resources);
33490
33809
  if ("success" in modelResult) {
33491
33810
  return modelResult;
33492
33811
  }
@@ -33514,13 +33833,14 @@ class PlyLoader {
33514
33833
  vertexColors: geometry.hasAttribute("color")
33515
33834
  });
33516
33835
  }
33517
- async createMeshModel(geometry, textureReference, resources, signal) {
33836
+ async createMeshModel(geometry, textureReference, resources, signal, progress) {
33518
33837
  if (!geometry.hasAttribute("normal")) {
33519
33838
  geometry.computeVertexNormals();
33520
33839
  }
33521
33840
  const warnings = [];
33522
33841
  let texture;
33523
33842
  if (textureReference !== void 0 && geometry.hasAttribute("uv")) {
33843
+ progress?.markMaterialsResolved(1);
33524
33844
  const textureResult = await this.textureLoader.load(
33525
33845
  resources.resolveTextureUrl(textureReference),
33526
33846
  signal
@@ -33532,6 +33852,7 @@ class PlyLoader {
33532
33852
  success: false
33533
33853
  };
33534
33854
  }
33855
+ progress?.markTextureSettled();
33535
33856
  if (textureResult.outcome === "succeeded") {
33536
33857
  texture = textureResult.texture;
33537
33858
  } else {
@@ -33573,16 +33894,13 @@ class PlyLoader {
33573
33894
  warnings: []
33574
33895
  };
33575
33896
  }
33576
- async fetchSource(url, signal) {
33897
+ async fetchSource(url, signal, progress) {
33577
33898
  try {
33578
- const response = await fetch(url, { signal });
33579
- if (!response.ok) {
33580
- return {
33581
- code: ObjImportFailureCode$1.InvalidSource,
33582
- success: false
33583
- };
33584
- }
33585
- return await response.arrayBuffer();
33899
+ return await this.resourceReader.readArrayBuffer(
33900
+ url,
33901
+ signal,
33902
+ progress === void 0 ? void 0 : progress.markModelFetchProgress.bind(progress)
33903
+ );
33586
33904
  } catch {
33587
33905
  return {
33588
33906
  code: signal.aborted ? ObjImportFailureCode$1.Cancelled : ObjImportFailureCode$1.InvalidSource,
@@ -33617,6 +33935,66 @@ class PlyLoader {
33617
33935
  return value;
33618
33936
  }
33619
33937
  }
33938
+ const OBJ_MODEL_LOAD_PROGRESS_PROFILE = {
33939
+ fetchedPosition: 0.2,
33940
+ materialsPosition: 0.3,
33941
+ parsedPosition: 0.95,
33942
+ textureCeiling: 0.99
33943
+ };
33944
+ const PLY_MODEL_LOAD_PROGRESS_PROFILE = {
33945
+ fetchedPosition: 0.4,
33946
+ materialsPosition: 0.85,
33947
+ parsedPosition: 0.85,
33948
+ textureCeiling: 0.99
33949
+ };
33950
+ class RoleImportProgressReporter {
33951
+ profile;
33952
+ role;
33953
+ textureSettled = 0;
33954
+ textureTotal;
33955
+ tracker;
33956
+ constructor(tracker, role, profile) {
33957
+ this.profile = profile;
33958
+ this.role = role;
33959
+ this.tracker = tracker;
33960
+ }
33961
+ markMaterialsResolved(textureTotal) {
33962
+ this.textureTotal = textureTotal;
33963
+ this.tracker.setRolePosition(this.role, this.profile.materialsPosition);
33964
+ this.reportTexturePosition();
33965
+ }
33966
+ markModelFetchProgress(fraction) {
33967
+ this.tracker.setRolePosition(
33968
+ this.role,
33969
+ this.profile.fetchedPosition * this.clampFraction(fraction)
33970
+ );
33971
+ }
33972
+ markModelFetched() {
33973
+ this.tracker.setRolePosition(this.role, this.profile.fetchedPosition);
33974
+ }
33975
+ markModelParsed() {
33976
+ this.tracker.setRolePosition(this.role, this.profile.parsedPosition);
33977
+ }
33978
+ markTextureSettled() {
33979
+ this.textureSettled += 1;
33980
+ this.reportTexturePosition();
33981
+ }
33982
+ reportTexturePosition() {
33983
+ const textureTotal = this.textureTotal;
33984
+ if (textureTotal === void 0 || textureTotal <= 0 || this.textureSettled === 0) {
33985
+ return;
33986
+ }
33987
+ const textureSpan = this.profile.textureCeiling - this.profile.parsedPosition;
33988
+ const settledShare = Math.min(this.textureSettled, textureTotal) / textureTotal;
33989
+ this.tracker.setRolePosition(
33990
+ this.role,
33991
+ this.profile.parsedPosition + textureSpan * settledShare
33992
+ );
33993
+ }
33994
+ clampFraction(fraction) {
33995
+ return Math.max(0, Math.min(1, fraction));
33996
+ }
33997
+ }
33620
33998
  class SimulationSnapshotFactory {
33621
33999
  constructor(scheduler) {
33622
34000
  this.scheduler = scheduler;
@@ -35922,6 +36300,7 @@ class Renderer {
35922
36300
  fillLight;
35923
36301
  frameHandle;
35924
36302
  gizmoController;
36303
+ groundGrid;
35925
36304
  hemisphereLight;
35926
36305
  pendingFirstFramePerformance;
35927
36306
  importPerformanceTracer = new ImportPerformanceTracer();
@@ -35959,6 +36338,7 @@ class Renderer {
35959
36338
  let contextRestoreListenerAttached = false;
35960
36339
  let controller;
35961
36340
  let gizmoController;
36341
+ let groundGrid;
35962
36342
  let resizeObserver;
35963
36343
  let viewCube;
35964
36344
  this.canvas = canvas;
@@ -35991,6 +36371,9 @@ class Renderer {
35991
36371
  );
35992
36372
  this.gizmoController = gizmoController;
35993
36373
  this.scene.add(gizmoController.getHelper());
36374
+ groundGrid = new GroundGrid();
36375
+ this.groundGrid = groundGrid;
36376
+ groundGrid.attach(this.scene);
35994
36377
  canvasController = new CanvasController(
35995
36378
  canvas,
35996
36379
  viewCube,
@@ -36044,9 +36427,13 @@ class Renderer {
36044
36427
  viewCube?.dispose();
36045
36428
  } finally {
36046
36429
  try {
36047
- controller?.dispose();
36430
+ groundGrid?.dispose();
36048
36431
  } finally {
36049
- renderer.dispose();
36432
+ try {
36433
+ controller?.dispose();
36434
+ } finally {
36435
+ renderer.dispose();
36436
+ }
36050
36437
  }
36051
36438
  }
36052
36439
  }
@@ -36093,25 +36480,29 @@ class Renderer {
36093
36480
  this.viewCube.dispose();
36094
36481
  } finally {
36095
36482
  try {
36096
- this.releaseActiveModels();
36483
+ this.groundGrid.dispose();
36097
36484
  } finally {
36098
36485
  try {
36099
- this.resizeObserver.disconnect();
36486
+ this.releaseActiveModels();
36100
36487
  } finally {
36101
36488
  try {
36102
- this.canvas.removeEventListener(
36103
- "webglcontextrestored",
36104
- this.contextRestoreListener
36105
- );
36489
+ this.resizeObserver.disconnect();
36106
36490
  } finally {
36107
36491
  try {
36108
36492
  this.canvas.removeEventListener(
36109
- "webglcontextlost",
36110
- this.contextLossListener
36493
+ "webglcontextrestored",
36494
+ this.contextRestoreListener
36111
36495
  );
36112
36496
  } finally {
36113
- this.renderer.forceContextLoss();
36114
- this.renderer.dispose();
36497
+ try {
36498
+ this.canvas.removeEventListener(
36499
+ "webglcontextlost",
36500
+ this.contextLossListener
36501
+ );
36502
+ } finally {
36503
+ this.renderer.forceContextLoss();
36504
+ this.renderer.dispose();
36505
+ }
36115
36506
  }
36116
36507
  }
36117
36508
  }
@@ -36149,7 +36540,10 @@ class Renderer {
36149
36540
  stone: new ModelResourceList(source.stone.fileUrls, source.stone.upAxis)
36150
36541
  };
36151
36542
  return this.completeSceneImportFromResources(
36152
- this.startImportSession(performanceTrace),
36543
+ this.startImportSession(
36544
+ performanceTrace,
36545
+ this.createImportProgressTracker(resources)
36546
+ ),
36153
36547
  resources
36154
36548
  );
36155
36549
  } catch {
@@ -36191,7 +36585,10 @@ class Renderer {
36191
36585
  stone: new ModelResourceList(source.stone.files, source.stone.upAxis)
36192
36586
  };
36193
36587
  return this.completeSceneImportFromResources(
36194
- this.startImportSession(performanceTrace),
36588
+ this.startImportSession(
36589
+ performanceTrace,
36590
+ this.createImportProgressTracker(resources)
36591
+ ),
36195
36592
  resources
36196
36593
  );
36197
36594
  } catch {
@@ -36238,6 +36635,7 @@ class Renderer {
36238
36635
  available: this.activeStoneModel !== void 0,
36239
36636
  contextLost: this.contextLost,
36240
36637
  importInProgress: this.activeImport !== void 0,
36638
+ ...this.activeImport === void 0 ? {} : { importProgress: this.activeImport.progressTracker.getPercent() },
36241
36639
  interactionInProgress,
36242
36640
  revision: this.sceneRevision,
36243
36641
  resourceRevision: this.sceneResourceRevision
@@ -36700,12 +37098,22 @@ class Renderer {
36700
37098
  let result;
36701
37099
  try {
36702
37100
  try {
37101
+ const progress = new RoleImportProgressReporter(
37102
+ session.progressTracker,
37103
+ role,
37104
+ source.resources instanceof ObjResourceList ? OBJ_MODEL_LOAD_PROGRESS_PROFILE : PLY_MODEL_LOAD_PROGRESS_PROFILE
37105
+ );
36703
37106
  result = source.resources instanceof ObjResourceList ? await this.modelLoader.loadResources(
36704
37107
  source.resources,
36705
37108
  session.controller.signal,
36706
37109
  session.performanceTrace,
36707
- role
36708
- ) : await this.plyLoader.load(source.resources, session.controller.signal);
37110
+ role,
37111
+ progress
37112
+ ) : await this.plyLoader.load(
37113
+ source.resources,
37114
+ session.controller.signal,
37115
+ progress
37116
+ );
36709
37117
  } catch {
36710
37118
  return this.completeUnexpectedImportFailure(session);
36711
37119
  }
@@ -36750,6 +37158,7 @@ class Renderer {
36750
37158
  };
36751
37159
  }
36752
37160
  loadStatus = "success";
37161
+ session.progressTracker.completeRole(role);
36753
37162
  return {
36754
37163
  model: result.model,
36755
37164
  warnings: result.warnings ?? []
@@ -36837,6 +37246,7 @@ class Renderer {
36837
37246
  this.booleanEditSessionActive = false;
36838
37247
  this.activeBooleanOperationSessionToken = void 0;
36839
37248
  this.abortEditingState();
37249
+ this.refreshGroundGrid([stone, candidate]);
36840
37250
  } catch {
36841
37251
  this.activeBaseModel = previousBase;
36842
37252
  (candidate.transformRoot ?? candidate.object).removeFromParent();
@@ -37153,6 +37563,7 @@ class Renderer {
37153
37563
  ...this.collectPresentModels(this.activeStoneModel, this.activeBaseModel)
37154
37564
  ];
37155
37565
  this.gizmoController.setActiveScene({ base: void 0, stone: void 0 });
37566
+ this.groundGrid.hide();
37156
37567
  this.transformedSceneBoundsDirty = false;
37157
37568
  this.activeStoneModel = void 0;
37158
37569
  this.activeBaseModel = void 0;
@@ -37201,6 +37612,18 @@ class Renderer {
37201
37612
  this.gizmoController.setModelVisibility("stone", this.modelVisibility.stoneVisible);
37202
37613
  this.gizmoController.setModelVisibility("base", this.modelVisibility.baseVisible);
37203
37614
  }
37615
+ /**
37616
+ * Retargets the ground grid from the present models. The grid anchors to
37617
+ * models that are merely hidden as well, so toggling base visibility never
37618
+ * moves the ground plane; only a committed paired scene shows it.
37619
+ */
37620
+ refreshGroundGrid(models) {
37621
+ if (models.length < 2) {
37622
+ this.groundGrid.hide();
37623
+ return;
37624
+ }
37625
+ this.groundGrid.updateFromBounds(this.mergeBounds(models));
37626
+ }
37204
37627
  applyModelVisibility() {
37205
37628
  const stoneRoot = this.activeStoneModel?.transformRoot ?? this.activeStoneModel?.object;
37206
37629
  const baseRoot = this.activeBaseModel?.transformRoot ?? this.activeBaseModel?.object;
@@ -37249,6 +37672,7 @@ class Renderer {
37249
37672
  }
37250
37673
  renderScene(includeViewCube = true) {
37251
37674
  this.syncTransformedSceneBounds();
37675
+ this.syncGroundGridForCamera();
37252
37676
  this.gizmoController.updateForRender();
37253
37677
  if (includeViewCube) {
37254
37678
  this.viewCube.update(this.camera);
@@ -37258,6 +37682,17 @@ class Renderer {
37258
37682
  this.renderViewCube();
37259
37683
  }
37260
37684
  }
37685
+ /**
37686
+ * Keeps the ground grid covering most of the visible area as the camera
37687
+ * zooms. Runs per rendered frame; updating only touches uniforms and the
37688
+ * mesh transform, so it stays allocation-free.
37689
+ */
37690
+ syncGroundGridForCamera() {
37691
+ const frustumHeight = (this.camera.top - this.camera.bottom) / this.camera.zoom;
37692
+ const frustumWidth = (this.camera.right - this.camera.left) / (this.camera.top - this.camera.bottom) * frustumHeight;
37693
+ const halfDiagonal = Math.sqrt(frustumHeight * frustumHeight + frustumWidth * frustumWidth) / 2;
37694
+ this.groundGrid.updateForViewport(halfDiagonal);
37695
+ }
37261
37696
  renderViewCube() {
37262
37697
  const viewport = this.viewCube.getViewport();
37263
37698
  const previousAutoClear = this.renderer.autoClear;
@@ -37296,6 +37731,9 @@ class Renderer {
37296
37731
  }
37297
37732
  }
37298
37733
  this.controller.syncTransformedBounds(this.mergeBounds(models));
37734
+ this.refreshGroundGrid(
37735
+ this.collectPresentModels(this.activeStoneModel, this.activeBaseModel)
37736
+ );
37299
37737
  }
37300
37738
  /**
37301
37739
  * Atomically replaces the committed active scene. New model references are
@@ -37370,6 +37808,7 @@ class Renderer {
37370
37808
  };
37371
37809
  this.gizmoController.setActiveScene(activeScene);
37372
37810
  this.applyGizmoVisibility();
37811
+ this.refreshGroundGrid(candidateModels);
37373
37812
  this.sceneRevision += 1;
37374
37813
  this.sceneResourceRevision = this.sceneRevision;
37375
37814
  this.pendingFirstFramePerformance = {
@@ -37379,6 +37818,7 @@ class Renderer {
37379
37818
  };
37380
37819
  session.performanceTrace.markStage("import-promise-resolved", "scene");
37381
37820
  session.performanceTrace.endStage(sceneCommitStage, "success");
37821
+ session.progressTracker.completeScene();
37382
37822
  this.finishImportSession(session);
37383
37823
  this.releasePair(previousStone, previousBase);
37384
37824
  this.requestRender();
@@ -37502,6 +37942,13 @@ class Renderer {
37502
37942
  this.notifyWorkspaceState();
37503
37943
  }
37504
37944
  }
37945
+ handleImportProgressChanged(_source) {
37946
+ this.notifyWorkspaceState();
37947
+ }
37948
+ createImportProgressTracker(resources) {
37949
+ const roles = resources.base === void 0 ? ["stone"] : ["stone", "base"];
37950
+ return new ImportProgressTracker(roles, this.handleImportProgressChanged.bind(this));
37951
+ }
37505
37952
  notifyWorkspaceState() {
37506
37953
  this.workspaceStateListener?.(this.getWorkspaceState());
37507
37954
  }
@@ -37550,6 +37997,7 @@ class Renderer {
37550
37997
  this.renderer.setSize(width, height, false);
37551
37998
  this.controller.resize(width, height);
37552
37999
  this.viewCube.resize(width, height, pixelRatio);
38000
+ this.groundGrid.setPixelRatio(pixelRatio);
37553
38001
  this.requestRender();
37554
38002
  }
37555
38003
  invalidateActiveImport(code) {
@@ -37595,7 +38043,7 @@ class Renderer {
37595
38043
  this.notifyWorkspaceState();
37596
38044
  }
37597
38045
  }
37598
- startImportSession(performanceTrace) {
38046
+ startImportSession(performanceTrace, progressTracker) {
37599
38047
  this.cancelSimulation();
37600
38048
  this.cancelBooleanOperation();
37601
38049
  this.cancelPendingFirstFramePerformance("cancelled");
@@ -37608,7 +38056,8 @@ class Renderer {
37608
38056
  const session = {
37609
38057
  controller: new AbortController(),
37610
38058
  invalidationCode: void 0,
37611
- performanceTrace
38059
+ performanceTrace,
38060
+ progressTracker
37612
38061
  };
37613
38062
  performanceTrace.markStage("import-session-start", "scene");
37614
38063
  this.activeImport = session;
@@ -37849,6 +38298,8 @@ class ArchiveImportWorkerMessageParser {
37849
38298
  return this.parseEntryStart(record, taskId);
37850
38299
  case "error":
37851
38300
  return typeof record["message"] === "string" ? { message: record["message"], taskId, type: "error" } : void 0;
38301
+ case "progress":
38302
+ return this.parseProgress(record, taskId);
37852
38303
  default:
37853
38304
  return void 0;
37854
38305
  }
@@ -37866,6 +38317,17 @@ class ArchiveImportWorkerMessageParser {
37866
38317
  isPositiveSafeInteger(value) {
37867
38318
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
37868
38319
  }
38320
+ isNonNegativeSafeInteger(value) {
38321
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
38322
+ }
38323
+ parseProgress(record, taskId) {
38324
+ const bytesLoaded = record["bytesLoaded"];
38325
+ const bytesTotal = record["bytesTotal"];
38326
+ if (!this.isNonNegativeSafeInteger(bytesLoaded) || !this.isNonNegativeSafeInteger(bytesTotal)) {
38327
+ return void 0;
38328
+ }
38329
+ return { bytesLoaded, bytesTotal, taskId, type: "progress" };
38330
+ }
37869
38331
  isRecord(value) {
37870
38332
  return typeof value === "object" && value !== null && !Array.isArray(value);
37871
38333
  }
@@ -37925,7 +38387,7 @@ class BrowserArchiveImportWorkerEndpoint {
37925
38387
  constructor() {
37926
38388
  this.worker = new Worker(new URL(
37927
38389
  /* @vite-ignore */
37928
- "" + new URL("assets/ArchiveImportWorkerEntry-Dh4RZo22.js", import.meta.url).href,
38390
+ "" + new URL("assets/ArchiveImportWorkerEntry-wkxjE56k.js", import.meta.url).href,
37929
38391
  import.meta.url
37930
38392
  ), {
37931
38393
  type: "module"
@@ -37965,9 +38427,11 @@ class ArchiveImportWorkerClient {
37965
38427
  jobValidator = new ArchiveImportJobValidator();
37966
38428
  lastStartedTaskId = 0;
37967
38429
  messageParser = new ArchiveImportWorkerMessageParser();
38430
+ onProgress;
37968
38431
  workerFactory;
37969
- constructor(workerFactory = new BrowserArchiveImportWorkerFactory()) {
38432
+ constructor(workerFactory = new BrowserArchiveImportWorkerFactory(), onProgress) {
37970
38433
  this.workerFactory = workerFactory;
38434
+ this.onProgress = onProgress;
37971
38435
  }
37972
38436
  cancel(taskId) {
37973
38437
  const task = this.activeTask;
@@ -38042,6 +38506,10 @@ class ArchiveImportWorkerClient {
38042
38506
  outbound.message
38043
38507
  )
38044
38508
  );
38509
+ return;
38510
+ case "progress":
38511
+ this.handleProgress(outbound);
38512
+ return;
38045
38513
  }
38046
38514
  } catch (error2) {
38047
38515
  this.rejectTask(task, this.toError(error2));
@@ -38111,6 +38579,10 @@ class ArchiveImportWorkerClient {
38111
38579
  type: "acknowledge-chunk"
38112
38580
  });
38113
38581
  }
38582
+ handleProgress(message) {
38583
+ const percent = message.bytesTotal <= 0 ? 0 : Math.min(100, Math.floor(message.bytesLoaded / message.bytesTotal * 100));
38584
+ this.onProgress?.(percent);
38585
+ }
38114
38586
  rejectTask(task, error2) {
38115
38587
  if (this.activeTask !== task) {
38116
38588
  return;
@@ -38292,6 +38764,26 @@ var ImportPerformanceMode = /* @__PURE__ */ ((ImportPerformanceMode2) => {
38292
38764
  ImportPerformanceMode2["Verbose"] = "verbose";
38293
38765
  return ImportPerformanceMode2;
38294
38766
  })(ImportPerformanceMode || {});
38767
+ const ARCHIVE_SEGMENT_PERCENT = 20;
38768
+ class ImportProgressCompositor {
38769
+ compose(input) {
38770
+ const archiveProgress = this.clampPercent(input.archiveProgress);
38771
+ const rendererProgress = this.clampPercent(input.rendererProgress);
38772
+ if (archiveProgress === void 0) {
38773
+ return rendererProgress;
38774
+ }
38775
+ if (rendererProgress === void 0) {
38776
+ return Math.floor(archiveProgress / 100 * ARCHIVE_SEGMENT_PERCENT);
38777
+ }
38778
+ return ARCHIVE_SEGMENT_PERCENT + Math.floor(rendererProgress / 100 * (100 - ARCHIVE_SEGMENT_PERCENT));
38779
+ }
38780
+ clampPercent(percent) {
38781
+ if (percent === void 0) {
38782
+ return void 0;
38783
+ }
38784
+ return Math.max(0, Math.min(100, percent));
38785
+ }
38786
+ }
38295
38787
  class DownloadSaveSink {
38296
38788
  aborted = false;
38297
38789
  blobChunks = [];
@@ -40011,7 +40503,7 @@ var StoneBasePoseMode = /* @__PURE__ */ ((StoneBasePoseMode2) => {
40011
40503
  StoneBasePoseMode2["PreservePose"] = "preserve-pose";
40012
40504
  return StoneBasePoseMode2;
40013
40505
  })(StoneBasePoseMode || {});
40014
- const studioWorkspaceStyles = ".lingbi-studio-workspace {\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.lingbi-studio-viewport {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.lingbi-studio-viewport > canvas {\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n background: #3d3f41;\n}\n\n.lingbi-studio-boolean-operation-overlay {\n position: absolute;\n z-index: 3;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n background: rgba(17, 17, 17, 0.32);\n color: #ffffff;\n cursor: wait;\n}\n\n.lingbi-studio-boolean-operation-overlay[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-edit-confirmation {\n position: absolute;\n z-index: 4;\n top: 50%;\n left: 50%;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n width: min(420px, calc(100% - 32px));\n padding: 24px;\n border-radius: 8px;\n background: #ffffff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.22);\n transform: translate(-50%, -50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-boolean-edit-confirmation[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-edit-confirmation-text {\n grid-column: 1 / -1;\n margin: 0 0 8px;\n color: #171717;\n font-size: 16px;\n font-weight: 500;\n line-height: 24px;\n text-align: center;\n}\n\n.lingbi-studio-boolean-edit-confirmation-cancel,\n.lingbi-studio-boolean-edit-confirmation-continue {\n height: 40px;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n font: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 24px;\n cursor: pointer;\n}\n\n.lingbi-studio-boolean-edit-confirmation-cancel {\n background: #ffffff;\n color: #171717;\n}\n\n.lingbi-studio-boolean-edit-confirmation-continue {\n border-color: #f27c38;\n background: #f27c38;\n color: #ffffff;\n}\n\n.lingbi-studio-boolean-operation-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid rgba(255, 105, 0, 0.28);\n border-top-color: #ff6900;\n border-radius: 50%;\n animation: lingbi-studio-boolean-operation-spin 0.8s linear infinite;\n box-sizing: border-box;\n}\n\n.lingbi-studio-boolean-operation-status {\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n letter-spacing: 0;\n}\n\n.lingbi-studio-bottom-toolbar {\n position: absolute;\n z-index: 1;\n bottom: 36px;\n left: 50%;\n display: grid;\n align-items: center;\n grid-template-columns: minmax(0, 1fr);\n width: min(120px, calc(100% - 32px));\n height: 64px;\n padding: 8px 12px;\n transform: translateX(-50%);\n box-sizing: border-box;\n border-radius: 8px;\n background: #f4f4f4;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);\n}\n\n.lingbi-studio-bottom-toolbar.has-edit-control {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n width: min(480px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-inventory-control {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n width: min(240px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-edit-control.has-inventory-control {\n grid-template-columns: repeat(5, minmax(0, 1fr));\n width: min(600px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-inventory-control {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n width: min(360px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-edit-control {\n grid-template-columns: repeat(5, minmax(0, 1fr));\n width: min(600px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-edit-control.has-inventory-control {\n grid-template-columns: repeat(6, minmax(0, 1fr));\n width: min(720px, calc(100% - 32px));\n}\n\n.lingbi-studio-model-visibility {\n position: absolute;\n z-index: 1;\n bottom: 108px;\n left: 50%;\n display: grid;\n gap: 8px;\n width: 144px;\n padding: 12px;\n border-radius: 8px;\n background: #f4f4f4;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);\n transform: translateX(-240px);\n box-sizing: border-box;\n}\n\n.lingbi-studio-model-visibility[hidden] {\n display: none;\n}\n\n.lingbi-studio-model-visibility-title {\n color: #171717;\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.lingbi-studio-model-visibility-row {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n color: #171717;\n font-size: 14px;\n line-height: 20px;\n}\n\n.lingbi-studio-model-visibility-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n appearance: none;\n width: 20px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n box-shadow: none;\n color: #171717;\n cursor: not-allowed;\n font: inherit;\n line-height: 0;\n outline: none;\n box-sizing: border-box;\n}\n\n.lingbi-studio-model-visibility-icon {\n display: block;\n width: 18px;\n height: 18px;\n flex: 0 0 auto;\n pointer-events: none;\n}\n\n.lingbi-studio-model-visibility-button:not(:disabled) {\n cursor: pointer;\n}\n\n.lingbi-studio-model-visibility-button:disabled {\n color: #8c8c8c;\n}\n\n.lingbi-studio-model-button.is-active,\n.lingbi-studio-model-button.is-active:disabled {\n color: #ff6900;\n font-weight: 600;\n}\n\n.lingbi-studio-boolean-button,\n.lingbi-studio-simulation-button,\n.lingbi-studio-download-button,\n.lingbi-studio-inventory-button,\n.lingbi-studio-edit-button,\n.lingbi-studio-model-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n min-width: 0;\n height: 48px;\n padding: 0 8px;\n border: 0;\n border-radius: 4px;\n background: transparent;\n color: #171717;\n cursor: not-allowed;\n font: inherit;\n font-size: 18px;\n font-weight: 500;\n line-height: 24px;\n letter-spacing: 0;\n opacity: 1;\n box-sizing: border-box;\n white-space: nowrap;\n transition:\n background-color 140ms ease,\n color 140ms ease;\n}\n\n.lingbi-studio-operation-button-wrapper {\n display: block;\n width: 100%;\n min-width: 0;\n height: 48px;\n box-sizing: border-box;\n}\n\n.lingbi-studio-operation-button-wrapper > button:disabled {\n pointer-events: none;\n}\n\n.lingbi-studio-boolean-button[hidden],\n.lingbi-studio-simulation-button[hidden],\n.lingbi-studio-download-button[hidden],\n.lingbi-studio-inventory-button[hidden],\n.lingbi-studio-edit-button[hidden],\n.lingbi-studio-model-button[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-button:not(:disabled),\n.lingbi-studio-simulation-button:not(:disabled),\n.lingbi-studio-download-button:not(:disabled),\n.lingbi-studio-inventory-button:not(:disabled),\n.lingbi-studio-edit-button:not(:disabled),\n.lingbi-studio-model-button:not(:disabled) {\n cursor: pointer;\n}\n\n.lingbi-studio-boolean-button:disabled,\n.lingbi-studio-simulation-button:disabled,\n.lingbi-studio-download-button:disabled,\n.lingbi-studio-inventory-button:disabled,\n.lingbi-studio-edit-button:disabled,\n.lingbi-studio-model-button:disabled,\n.lingbi-studio-model-visibility-button:disabled {\n background: transparent;\n color: #8c8c8c;\n opacity: 1;\n}\n\n.lingbi-studio-edit-button.is-active,\n.lingbi-studio-edit-button.is-active:disabled,\n.lingbi-studio-model-button.is-active,\n.lingbi-studio-model-button.is-active:disabled {\n color: #ff6900;\n font-weight: 600;\n}\n\n.lingbi-studio-download-status {\n position: absolute;\n z-index: 2;\n top: 24px;\n left: 50%;\n max-width: calc(100% - 24px);\n padding: 6px 10px;\n border: 1px solid #f4b5b0;\n border-radius: 4px;\n background: #fff1f0;\n color: #b42318;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: center;\n transform: translateX(-50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-download-status[hidden] {\n display: none;\n}\n\n.lingbi-studio-download-success {\n position: absolute;\n z-index: 2;\n top: 24px;\n left: 50%;\n max-width: calc(100% - 24px);\n padding: 8px 16px;\n border: 1px solid #86d39a;\n border-radius: 6px;\n background: #f0fff4;\n color: #176b32;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n text-align: center;\n transform: translateX(-50%);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);\n box-sizing: border-box;\n}\n\n.lingbi-studio-download-success[hidden] {\n display: none;\n}\n\n.lingbi-studio-edit-status {\n position: absolute;\n z-index: 2;\n bottom: 112px;\n left: 50%;\n display: flex;\n align-items: center;\n gap: 12px;\n max-width: calc(100% - 24px);\n padding: 10px 14px;\n border: 0;\n border-radius: 8px;\n background: #e9e9e9;\n color: #171717;\n transform: translateX(-50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-edit-status[hidden] {\n display: none;\n}\n\n.lingbi-studio-edit-status-text {\n display: inline-block;\n min-width: 0;\n font-size: 20px;\n line-height: 28px;\n letter-spacing: 0;\n}\n\n.lingbi-studio-exit-button {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n height: 40px;\n padding: 0 18px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: #f27c38;\n color: #ffffff;\n cursor: pointer;\n font: inherit;\n font-size: 18px;\n font-weight: 600;\n line-height: 24px;\n letter-spacing: 0;\n transition:\n background-color 140ms ease,\n color 140ms ease;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .lingbi-studio-boolean-button:not(:disabled):hover,\n .lingbi-studio-simulation-button:not(:disabled):hover,\n .lingbi-studio-download-button:not(:disabled):hover,\n .lingbi-studio-inventory-button:not(:disabled):hover,\n .lingbi-studio-edit-button:not(:disabled):hover,\n .lingbi-studio-model-button:not(:disabled):hover {\n background: #e8e8e8;\n }\n\n .lingbi-studio-simulation-button:not(:disabled):hover,\n .lingbi-studio-download-button:not(:disabled):hover,\n .lingbi-studio-inventory-button:not(:disabled):hover,\n .lingbi-studio-edit-button:not(:disabled):hover,\n .lingbi-studio-model-button:not(:disabled):hover {\n color: #f06400;\n }\n\n .lingbi-studio-model-visibility-button:not(:disabled):hover {\n background: transparent;\n }\n\n .lingbi-studio-exit-button:hover {\n background: #ff8a1f;\n }\n\n .lingbi-studio-boolean-edit-confirmation-cancel:hover {\n background: #f5f5f5;\n }\n\n .lingbi-studio-boolean-edit-confirmation-continue:hover {\n background: #ff8a1f;\n }\n}\n\n.lingbi-studio-boolean-button:not(:disabled):active,\n.lingbi-studio-download-button:not(:disabled):active,\n.lingbi-studio-inventory-button:not(:disabled):active,\n.lingbi-studio-edit-button:not(:disabled):active,\n.lingbi-studio-model-button:not(:disabled):active,\n.lingbi-studio-model-visibility-button:not(:disabled):active {\n background: transparent;\n}\n\n.lingbi-studio-exit-button:active {\n background: #e66d00;\n}\n\n@keyframes lingbi-studio-boolean-operation-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lingbi-studio-boolean-button,\n .lingbi-studio-simulation-button,\n .lingbi-studio-download-button,\n .lingbi-studio-inventory-button,\n .lingbi-studio-edit-button,\n .lingbi-studio-model-button,\n .lingbi-studio-exit-button {\n transition: none;\n }\n\n .lingbi-studio-boolean-operation-spinner {\n animation: none;\n }\n}\n\n@media (max-width: 420px) {\n .lingbi-studio-bottom-toolbar.has-edit-control {\n width: calc(100% - 16px);\n padding: 8px 4px;\n }\n\n .lingbi-studio-bottom-toolbar.has-model-control.has-edit-control {\n width: calc(100% - 16px);\n }\n\n .lingbi-studio-bottom-toolbar.has-model-control.has-edit-control.has-inventory-control {\n width: calc(100% - 16px);\n }\n\n .lingbi-studio-model-visibility {\n left: 8px;\n transform: none;\n }\n\n .lingbi-studio-boolean-button,\n .lingbi-studio-simulation-button,\n .lingbi-studio-download-button,\n .lingbi-studio-inventory-button,\n .lingbi-studio-edit-button,\n .lingbi-studio-model-button {\n padding: 0 2px;\n font-size: 16px;\n }\n\n .lingbi-studio-edit-status-text {\n font-size: 16px;\n line-height: 24px;\n }\n\n .lingbi-studio-exit-button {\n height: 36px;\n padding: 0 14px;\n font-size: 16px;\n }\n}\n\n.lingbi-studio-viewport > canvas:focus-visible,\n.lingbi-studio-boolean-button:focus-visible,\n.lingbi-studio-simulation-button:focus-visible,\n.lingbi-studio-download-button:focus-visible,\n.lingbi-studio-inventory-button:focus-visible,\n.lingbi-studio-edit-button:focus-visible,\n.lingbi-studio-model-button:focus-visible,\n.lingbi-studio-model-visibility-button:focus-visible,\n.lingbi-studio-exit-button:focus-visible,\n.lingbi-studio-boolean-edit-confirmation-cancel:focus-visible,\n.lingbi-studio-boolean-edit-confirmation-continue:focus-visible {\n outline: 2px solid #7cc0ff;\n outline-offset: 2px;\n}\n";
40506
+ const studioWorkspaceStyles = ".lingbi-studio-workspace {\n position: relative;\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.lingbi-studio-viewport {\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n\n.lingbi-studio-viewport > canvas {\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n background: #3d3f41;\n}\n\n.lingbi-studio-boolean-operation-overlay {\n position: absolute;\n z-index: 3;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n background: rgba(17, 17, 17, 0.32);\n color: #ffffff;\n cursor: wait;\n}\n\n.lingbi-studio-boolean-operation-overlay[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-edit-confirmation {\n position: absolute;\n z-index: 4;\n top: 50%;\n left: 50%;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n width: min(420px, calc(100% - 32px));\n padding: 24px;\n border-radius: 8px;\n background: #ffffff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.22);\n transform: translate(-50%, -50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-boolean-edit-confirmation[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-edit-confirmation-text {\n grid-column: 1 / -1;\n margin: 0 0 8px;\n color: #171717;\n font-size: 16px;\n font-weight: 500;\n line-height: 24px;\n text-align: center;\n}\n\n.lingbi-studio-boolean-edit-confirmation-cancel,\n.lingbi-studio-boolean-edit-confirmation-continue {\n height: 40px;\n border: 1px solid #d9d9d9;\n border-radius: 4px;\n font: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 24px;\n cursor: pointer;\n}\n\n.lingbi-studio-boolean-edit-confirmation-cancel {\n background: #ffffff;\n color: #171717;\n}\n\n.lingbi-studio-boolean-edit-confirmation-continue {\n border-color: #f27c38;\n background: #f27c38;\n color: #ffffff;\n}\n\n.lingbi-studio-boolean-operation-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid rgba(255, 105, 0, 0.28);\n border-top-color: #ff6900;\n border-radius: 50%;\n animation: lingbi-studio-boolean-operation-spin 0.8s linear infinite;\n box-sizing: border-box;\n}\n\n.lingbi-studio-boolean-operation-status {\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n letter-spacing: 0;\n}\n\n.lingbi-studio-import-row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.lingbi-studio-import-row[hidden] {\n display: none;\n}\n\n.lingbi-studio-import-progress {\n display: block;\n width: 160px;\n height: 4px;\n overflow: hidden;\n border-radius: 999px;\n background: rgba(255, 105, 0, 0.28);\n box-sizing: border-box;\n}\n\n.lingbi-studio-import-progress[hidden] {\n display: none;\n}\n\n.lingbi-studio-import-progress-fill {\n width: 0;\n height: 100%;\n border-radius: 999px;\n background: #ff6900;\n}\n\n.lingbi-studio-import-percent {\n min-width: 36px;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n text-align: left;\n}\n\n.lingbi-studio-bottom-toolbar {\n position: absolute;\n z-index: 1;\n bottom: 36px;\n left: 50%;\n display: grid;\n align-items: center;\n grid-template-columns: minmax(0, 1fr);\n width: min(120px, calc(100% - 32px));\n height: 64px;\n padding: 8px 12px;\n transform: translateX(-50%);\n box-sizing: border-box;\n border-radius: 8px;\n background: #f4f4f4;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);\n}\n\n.lingbi-studio-bottom-toolbar.has-edit-control {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n width: min(480px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-inventory-control {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n width: min(240px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-edit-control.has-inventory-control {\n grid-template-columns: repeat(5, minmax(0, 1fr));\n width: min(600px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-inventory-control {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n width: min(360px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-edit-control {\n grid-template-columns: repeat(5, minmax(0, 1fr));\n width: min(600px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.has-edit-control.has-inventory-control {\n grid-template-columns: repeat(6, minmax(0, 1fr));\n width: min(720px, calc(100% - 32px));\n}\n\n.lingbi-studio-model-visibility {\n position: absolute;\n z-index: 1;\n bottom: 108px;\n left: 50%;\n display: grid;\n gap: 8px;\n width: 144px;\n padding: 12px;\n border-radius: 8px;\n background: #f4f4f4;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.14);\n transform: translateX(-240px);\n box-sizing: border-box;\n}\n\n.lingbi-studio-model-visibility[hidden] {\n display: none;\n}\n\n.lingbi-studio-model-visibility-title {\n color: #171717;\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.lingbi-studio-model-visibility-row {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 8px;\n align-items: center;\n color: #171717;\n font-size: 14px;\n line-height: 20px;\n}\n\n.lingbi-studio-model-visibility-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n appearance: none;\n width: 20px;\n height: 28px;\n padding: 0;\n border: 0;\n border-radius: 0;\n background: transparent;\n box-shadow: none;\n color: #171717;\n cursor: not-allowed;\n font: inherit;\n line-height: 0;\n outline: none;\n box-sizing: border-box;\n}\n\n.lingbi-studio-model-visibility-icon {\n display: block;\n width: 18px;\n height: 18px;\n flex: 0 0 auto;\n pointer-events: none;\n}\n\n.lingbi-studio-model-visibility-button:not(:disabled) {\n cursor: pointer;\n}\n\n.lingbi-studio-model-visibility-button:disabled {\n color: #8c8c8c;\n}\n\n.lingbi-studio-model-button.is-active,\n.lingbi-studio-model-button.is-active:disabled {\n color: #ff6900;\n font-weight: 600;\n}\n\n.lingbi-studio-boolean-button,\n.lingbi-studio-simulation-button,\n.lingbi-studio-download-button,\n.lingbi-studio-inventory-button,\n.lingbi-studio-edit-button,\n.lingbi-studio-model-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n min-width: 0;\n height: 48px;\n padding: 0 8px;\n border: 0;\n border-radius: 4px;\n background: transparent;\n color: #171717;\n cursor: not-allowed;\n font: inherit;\n font-size: 18px;\n font-weight: 500;\n line-height: 24px;\n letter-spacing: 0;\n opacity: 1;\n box-sizing: border-box;\n white-space: nowrap;\n transition:\n background-color 140ms ease,\n color 140ms ease;\n}\n\n.lingbi-studio-operation-button-wrapper {\n display: block;\n width: 100%;\n min-width: 0;\n height: 48px;\n box-sizing: border-box;\n}\n\n.lingbi-studio-operation-button-wrapper > button:disabled {\n pointer-events: none;\n}\n\n.lingbi-studio-boolean-button[hidden],\n.lingbi-studio-simulation-button[hidden],\n.lingbi-studio-download-button[hidden],\n.lingbi-studio-inventory-button[hidden],\n.lingbi-studio-edit-button[hidden],\n.lingbi-studio-model-button[hidden] {\n display: none;\n}\n\n.lingbi-studio-boolean-button:not(:disabled),\n.lingbi-studio-simulation-button:not(:disabled),\n.lingbi-studio-download-button:not(:disabled),\n.lingbi-studio-inventory-button:not(:disabled),\n.lingbi-studio-edit-button:not(:disabled),\n.lingbi-studio-model-button:not(:disabled) {\n cursor: pointer;\n}\n\n.lingbi-studio-boolean-button:disabled,\n.lingbi-studio-simulation-button:disabled,\n.lingbi-studio-download-button:disabled,\n.lingbi-studio-inventory-button:disabled,\n.lingbi-studio-edit-button:disabled,\n.lingbi-studio-model-button:disabled,\n.lingbi-studio-model-visibility-button:disabled {\n background: transparent;\n color: #8c8c8c;\n opacity: 1;\n}\n\n.lingbi-studio-edit-button.is-active,\n.lingbi-studio-edit-button.is-active:disabled,\n.lingbi-studio-model-button.is-active,\n.lingbi-studio-model-button.is-active:disabled {\n color: #ff6900;\n font-weight: 600;\n}\n\n.lingbi-studio-download-status {\n position: absolute;\n z-index: 2;\n top: 24px;\n left: 50%;\n max-width: calc(100% - 24px);\n padding: 6px 10px;\n border: 1px solid #f4b5b0;\n border-radius: 4px;\n background: #fff1f0;\n color: #b42318;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n text-align: center;\n transform: translateX(-50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-download-status[hidden] {\n display: none;\n}\n\n.lingbi-studio-download-success {\n position: absolute;\n z-index: 2;\n top: 24px;\n left: 50%;\n max-width: calc(100% - 24px);\n padding: 8px 16px;\n border: 1px solid #86d39a;\n border-radius: 6px;\n background: #f0fff4;\n color: #176b32;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n text-align: center;\n transform: translateX(-50%);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);\n box-sizing: border-box;\n}\n\n.lingbi-studio-download-success[hidden] {\n display: none;\n}\n\n.lingbi-studio-edit-status {\n position: absolute;\n z-index: 2;\n bottom: 112px;\n left: 50%;\n display: flex;\n align-items: center;\n gap: 12px;\n max-width: calc(100% - 24px);\n padding: 10px 14px;\n border: 0;\n border-radius: 8px;\n background: #e9e9e9;\n color: #171717;\n transform: translateX(-50%);\n box-sizing: border-box;\n}\n\n.lingbi-studio-edit-status[hidden] {\n display: none;\n}\n\n.lingbi-studio-edit-status-text {\n display: inline-block;\n min-width: 0;\n font-size: 20px;\n line-height: 28px;\n letter-spacing: 0;\n}\n\n.lingbi-studio-exit-button {\n display: inline-flex;\n flex: 0 0 auto;\n align-items: center;\n justify-content: center;\n height: 40px;\n padding: 0 18px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: #f27c38;\n color: #ffffff;\n cursor: pointer;\n font: inherit;\n font-size: 18px;\n font-weight: 600;\n line-height: 24px;\n letter-spacing: 0;\n transition:\n background-color 140ms ease,\n color 140ms ease;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .lingbi-studio-boolean-button:not(:disabled):hover,\n .lingbi-studio-simulation-button:not(:disabled):hover,\n .lingbi-studio-download-button:not(:disabled):hover,\n .lingbi-studio-inventory-button:not(:disabled):hover,\n .lingbi-studio-edit-button:not(:disabled):hover,\n .lingbi-studio-model-button:not(:disabled):hover {\n background: #e8e8e8;\n }\n\n .lingbi-studio-simulation-button:not(:disabled):hover,\n .lingbi-studio-download-button:not(:disabled):hover,\n .lingbi-studio-inventory-button:not(:disabled):hover,\n .lingbi-studio-edit-button:not(:disabled):hover,\n .lingbi-studio-model-button:not(:disabled):hover {\n color: #f06400;\n }\n\n .lingbi-studio-model-visibility-button:not(:disabled):hover {\n background: transparent;\n }\n\n .lingbi-studio-exit-button:hover {\n background: #ff8a1f;\n }\n\n .lingbi-studio-boolean-edit-confirmation-cancel:hover {\n background: #f5f5f5;\n }\n\n .lingbi-studio-boolean-edit-confirmation-continue:hover {\n background: #ff8a1f;\n }\n}\n\n.lingbi-studio-boolean-button:not(:disabled):active,\n.lingbi-studio-download-button:not(:disabled):active,\n.lingbi-studio-inventory-button:not(:disabled):active,\n.lingbi-studio-edit-button:not(:disabled):active,\n.lingbi-studio-model-button:not(:disabled):active,\n.lingbi-studio-model-visibility-button:not(:disabled):active {\n background: transparent;\n}\n\n.lingbi-studio-exit-button:active {\n background: #e66d00;\n}\n\n@keyframes lingbi-studio-boolean-operation-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lingbi-studio-boolean-button,\n .lingbi-studio-simulation-button,\n .lingbi-studio-download-button,\n .lingbi-studio-inventory-button,\n .lingbi-studio-edit-button,\n .lingbi-studio-model-button,\n .lingbi-studio-exit-button {\n transition: none;\n }\n\n .lingbi-studio-boolean-operation-spinner {\n animation: none;\n }\n}\n\n@media (max-width: 420px) {\n .lingbi-studio-bottom-toolbar.has-edit-control {\n width: calc(100% - 16px);\n padding: 8px 4px;\n }\n\n .lingbi-studio-bottom-toolbar.has-model-control.has-edit-control {\n width: calc(100% - 16px);\n }\n\n .lingbi-studio-bottom-toolbar.has-model-control.has-edit-control.has-inventory-control {\n width: calc(100% - 16px);\n }\n\n .lingbi-studio-model-visibility {\n left: 8px;\n transform: none;\n }\n\n .lingbi-studio-boolean-button,\n .lingbi-studio-simulation-button,\n .lingbi-studio-download-button,\n .lingbi-studio-inventory-button,\n .lingbi-studio-edit-button,\n .lingbi-studio-model-button {\n padding: 0 2px;\n font-size: 16px;\n }\n\n .lingbi-studio-edit-status-text {\n font-size: 16px;\n line-height: 24px;\n }\n\n .lingbi-studio-exit-button {\n height: 36px;\n padding: 0 14px;\n font-size: 16px;\n }\n}\n\n.lingbi-studio-viewport > canvas:focus-visible,\n.lingbi-studio-boolean-button:focus-visible,\n.lingbi-studio-simulation-button:focus-visible,\n.lingbi-studio-download-button:focus-visible,\n.lingbi-studio-inventory-button:focus-visible,\n.lingbi-studio-edit-button:focus-visible,\n.lingbi-studio-model-button:focus-visible,\n.lingbi-studio-model-visibility-button:focus-visible,\n.lingbi-studio-exit-button:focus-visible,\n.lingbi-studio-boolean-edit-confirmation-cancel:focus-visible,\n.lingbi-studio-boolean-edit-confirmation-continue:focus-visible {\n outline: 2px solid #7cc0ff;\n outline-offset: 2px;\n}\n";
40015
40507
  class StudioWorkspace {
40016
40508
  canvas;
40017
40509
  static SVG_NAMESPACE = "http://www.w3.org/2000/svg";
@@ -40022,7 +40514,12 @@ class StudioWorkspace {
40022
40514
  booleanButtonElement;
40023
40515
  simulationButtonElement;
40024
40516
  booleanOperationOverlayElement;
40517
+ booleanOperationSpinnerElement;
40025
40518
  booleanOperationStatusElement;
40519
+ importPercentElement;
40520
+ importProgressElement;
40521
+ importProgressFillElement;
40522
+ importProgressRowElement;
40026
40523
  booleanEditConfirmationElement;
40027
40524
  bottomToolbarElement;
40028
40525
  downloadButtonElement;
@@ -40054,6 +40551,10 @@ class StudioWorkspace {
40054
40551
  const booleanOperationOverlayElement = ownerDocument.createElement("div");
40055
40552
  const booleanOperationSpinnerElement = ownerDocument.createElement("div");
40056
40553
  const booleanOperationStatusElement = ownerDocument.createElement("div");
40554
+ const importPercentElement = ownerDocument.createElement("div");
40555
+ const importProgressElement = ownerDocument.createElement("div");
40556
+ const importProgressFillElement = ownerDocument.createElement("div");
40557
+ const importProgressRowElement = ownerDocument.createElement("div");
40057
40558
  const booleanEditConfirmationElement = ownerDocument.createElement("div");
40058
40559
  const booleanEditConfirmationTextElement = ownerDocument.createElement("p");
40059
40560
  const booleanEditConfirmationCancelButtonElement = ownerDocument.createElement("button");
@@ -40103,10 +40604,24 @@ class StudioWorkspace {
40103
40604
  booleanOperationOverlayElement.hidden = true;
40104
40605
  booleanOperationSpinnerElement.className = "lingbi-studio-boolean-operation-spinner";
40105
40606
  booleanOperationSpinnerElement.setAttribute("aria-hidden", "true");
40607
+ importProgressRowElement.className = "lingbi-studio-import-row";
40608
+ importProgressRowElement.hidden = true;
40609
+ importPercentElement.className = "lingbi-studio-import-percent";
40610
+ importPercentElement.textContent = "0%";
40611
+ importProgressElement.className = "lingbi-studio-import-progress";
40612
+ importProgressElement.setAttribute("aria-label", "模型加载进度");
40613
+ importProgressElement.setAttribute("aria-valuemax", "100");
40614
+ importProgressElement.setAttribute("aria-valuemin", "0");
40615
+ importProgressElement.setAttribute("aria-valuenow", "0");
40616
+ importProgressElement.setAttribute("role", "progressbar");
40617
+ importProgressFillElement.className = "lingbi-studio-import-progress-fill";
40618
+ importProgressElement.append(importProgressFillElement);
40619
+ importProgressRowElement.append(importProgressElement, importPercentElement);
40106
40620
  booleanOperationStatusElement.className = "lingbi-studio-boolean-operation-status";
40107
40621
  booleanOperationStatusElement.textContent = "布尔计算中";
40108
40622
  booleanOperationOverlayElement.append(
40109
40623
  booleanOperationSpinnerElement,
40624
+ importProgressRowElement,
40110
40625
  booleanOperationStatusElement
40111
40626
  );
40112
40627
  booleanEditConfirmationElement.className = "lingbi-studio-boolean-edit-confirmation";
@@ -40234,7 +40749,12 @@ class StudioWorkspace {
40234
40749
  this.booleanButtonElement = booleanButtonElement;
40235
40750
  this.simulationButtonElement = simulationButtonElement;
40236
40751
  this.booleanOperationOverlayElement = booleanOperationOverlayElement;
40752
+ this.booleanOperationSpinnerElement = booleanOperationSpinnerElement;
40237
40753
  this.booleanOperationStatusElement = booleanOperationStatusElement;
40754
+ this.importPercentElement = importPercentElement;
40755
+ this.importProgressElement = importProgressElement;
40756
+ this.importProgressFillElement = importProgressFillElement;
40757
+ this.importProgressRowElement = importProgressRowElement;
40238
40758
  this.booleanEditConfirmationElement = booleanEditConfirmationElement;
40239
40759
  this.bottomToolbarElement = bottomToolbarElement;
40240
40760
  this.downloadButtonElement = downloadButtonElement;
@@ -40357,11 +40877,14 @@ class StudioWorkspace {
40357
40877
  }
40358
40878
  /** Applies Application's combined renderer and download state. */
40359
40879
  setToolbarState(state) {
40880
+ const importInProgress = state.importInProgress ?? false;
40881
+ const importProgress = Math.max(0, Math.min(100, Math.floor(state.importProgress ?? 0)));
40360
40882
  const booleanOperationInProgress = state.booleanInProgress ?? false;
40361
40883
  const downloadInProgress = state.downloadInProgress ?? false;
40362
40884
  const inventoryInProgress = state.inventoryInProgress ?? false;
40363
40885
  const simulationInProgress = state.simulationInProgress ?? false;
40364
- const operationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress || simulationInProgress;
40886
+ const otherOperationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress || simulationInProgress;
40887
+ const operationInProgress = importInProgress || otherOperationInProgress;
40365
40888
  const confirmationVisible = state.booleanEditConfirmationVisible ?? false;
40366
40889
  const modelVisibility = state.modelVisibility;
40367
40890
  this.currentEditActive = state.editActive;
@@ -40385,7 +40908,15 @@ class StudioWorkspace {
40385
40908
  this.booleanButtonElement.setAttribute("aria-busy", String(booleanOperationInProgress));
40386
40909
  this.booleanOperationOverlayElement.hidden = !operationInProgress;
40387
40910
  this.booleanOperationOverlayElement.setAttribute("aria-busy", String(operationInProgress));
40388
- this.booleanOperationStatusElement.textContent = simulationInProgress ? "仿真计算中" : booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
40911
+ this.booleanOperationSpinnerElement.hidden = importInProgress;
40912
+ this.importProgressRowElement.hidden = !importInProgress;
40913
+ if (importInProgress) {
40914
+ const percentText = `${String(importProgress)}%`;
40915
+ this.importProgressElement.setAttribute("aria-valuenow", String(importProgress));
40916
+ this.importProgressFillElement.style.width = percentText;
40917
+ this.importPercentElement.textContent = percentText;
40918
+ }
40919
+ this.booleanOperationStatusElement.textContent = importInProgress ? "模型加载中" : simulationInProgress ? "仿真计算中" : booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
40389
40920
  this.canvas.toggleAttribute("inert", operationInProgress || confirmationVisible);
40390
40921
  this.bottomToolbarElement.toggleAttribute(
40391
40922
  "inert",
@@ -40573,6 +41104,7 @@ class StudioWorkspace {
40573
41104
  class Application {
40574
41105
  archiveImportController;
40575
41106
  archiveImporting = false;
41107
+ archiveImportProgress;
40576
41108
  booleanEditConfirmationVisible = false;
40577
41109
  disposed = false;
40578
41110
  downloadController;
@@ -40585,6 +41117,7 @@ class Application {
40585
41117
  editedArchiveResourceContext;
40586
41118
  importGeneration = 0;
40587
41119
  importPerformanceMode = ImportPerformanceMode.Off;
41120
+ importProgressCompositor = new ImportProgressCompositor();
40588
41121
  inventoryConfirmationController;
40589
41122
  inventoryConfirming = false;
40590
41123
  inventoryError;
@@ -40642,7 +41175,10 @@ class Application {
40642
41175
  this.handleInventorySuccess.bind(this),
40643
41176
  this.handleInventoryStateChanged.bind(this)
40644
41177
  );
40645
- this.archiveImportController = new ArchiveImportWorkerClient();
41178
+ this.archiveImportController = new ArchiveImportWorkerClient(
41179
+ void 0,
41180
+ this.handleArchiveImportProgress.bind(this)
41181
+ );
40646
41182
  this.handleRendererWorkspaceStateChanged(renderer.getWorkspaceState());
40647
41183
  } catch (error2) {
40648
41184
  this.workspace = void 0;
@@ -40728,12 +41264,14 @@ class Application {
40728
41264
  }
40729
41265
  this.nextArchiveTaskId += 1;
40730
41266
  this.archiveImporting = true;
41267
+ this.archiveImportProgress = 0;
40731
41268
  this.downloadError = void 0;
40732
41269
  this.downloadSuccess = void 0;
40733
41270
  this.publishWorkspaceState();
40734
41271
  const controller = this.archiveImportController;
40735
41272
  if (controller === void 0) {
40736
41273
  this.archiveImporting = false;
41274
+ this.archiveImportProgress = void 0;
40737
41275
  this.publishWorkspaceState();
40738
41276
  return Promise.resolve({
40739
41277
  code: ObjImportFailureCode.Unavailable,
@@ -40791,6 +41329,7 @@ class Application {
40791
41329
  cancelArchiveImport() {
40792
41330
  this.archiveImportController?.cancel();
40793
41331
  this.archiveImporting = false;
41332
+ this.archiveImportProgress = void 0;
40794
41333
  this.invalidateImportGeneration();
40795
41334
  }
40796
41335
  async completeModelResourceSceneImport(source, candidate, generation) {
@@ -40861,6 +41400,7 @@ class Application {
40861
41400
  } finally {
40862
41401
  if (this.isCurrentImportGeneration(generation)) {
40863
41402
  this.archiveImporting = false;
41403
+ this.archiveImportProgress = void 0;
40864
41404
  this.publishWorkspaceState();
40865
41405
  }
40866
41406
  }
@@ -40868,6 +41408,7 @@ class Application {
40868
41408
  completeEditedArchiveImportFailure(generation, error2) {
40869
41409
  if (this.isCurrentImportGeneration(generation)) {
40870
41410
  this.archiveImporting = false;
41411
+ this.archiveImportProgress = void 0;
40871
41412
  this.publishWorkspaceState();
40872
41413
  }
40873
41414
  return Promise.resolve(this.mapEditedArchiveError(error2));
@@ -41238,6 +41779,13 @@ class Application {
41238
41779
  }
41239
41780
  this.clearBooleanEditConfirmation();
41240
41781
  }
41782
+ handleArchiveImportProgress(percent) {
41783
+ if (this.disposed || !this.archiveImporting) {
41784
+ return;
41785
+ }
41786
+ this.archiveImportProgress = percent;
41787
+ this.publishWorkspaceState();
41788
+ }
41241
41789
  handleRendererWorkspaceStateChanged(state) {
41242
41790
  if (this.disposed) {
41243
41791
  return;
@@ -41355,6 +41903,10 @@ class Application {
41355
41903
  if (workspace === void 0 || state === void 0) {
41356
41904
  return;
41357
41905
  }
41906
+ const importProgress = this.importProgressCompositor.compose({
41907
+ archiveProgress: this.archiveImportProgress,
41908
+ rendererProgress: state.scene.importProgress
41909
+ });
41358
41910
  const toolbarState = {
41359
41911
  simulationEnabled: state.simulation?.enabled === true && !this.downloading && !this.inventorying && !this.inventoryConfirming && !this.archiveImporting && !this.booleanEditConfirmationVisible,
41360
41912
  simulationInProgress: state.simulation?.inProgress ?? false,
@@ -41372,6 +41924,8 @@ class Application {
41372
41924
  editEnabled: state.edit.enabled && !this.downloading && !this.inventorying && !this.archiveImporting && !this.booleanEditConfirmationVisible,
41373
41925
  editVisible: state.edit.visible,
41374
41926
  ...this.editStatusMessage === void 0 ? {} : { editStatusMessage: this.editStatusMessage },
41927
+ importInProgress: state.scene.importInProgress || this.archiveImporting,
41928
+ ...importProgress === void 0 ? {} : { importProgress },
41375
41929
  inventoryEnabled: this.canInventory(state),
41376
41930
  ...this.inventoryError === void 0 ? {} : { inventoryError: this.inventoryError },
41377
41931
  inventoryInProgress: this.inventorying,
@@ -41417,9 +41971,11 @@ class Application {
41417
41971
  state.scene.available,
41418
41972
  state.scene.contextLost,
41419
41973
  state.scene.importInProgress,
41974
+ state.scene.importProgress ?? null,
41420
41975
  state.scene.revision,
41421
41976
  state.scene.resourceRevision ?? null,
41422
41977
  this.archiveImporting,
41978
+ this.archiveImportProgress ?? null,
41423
41979
  this.downloading,
41424
41980
  this.inventoryConfirming,
41425
41981
  this.inventorying,
@@ -41432,7 +41988,7 @@ class Application {
41432
41988
  ]);
41433
41989
  }
41434
41990
  isSameToolbarState(state, previousState) {
41435
- return previousState !== void 0 && state.simulationEnabled === previousState.simulationEnabled && state.simulationInProgress === previousState.simulationInProgress && state.simulationOutcome === previousState.simulationOutcome && state.booleanEnabled === previousState.booleanEnabled && state.booleanInProgress === previousState.booleanInProgress && state.booleanReediting === previousState.booleanReediting && state.booleanResultInvalidated === previousState.booleanResultInvalidated && state.booleanEditConfirmationVisible === previousState.booleanEditConfirmationVisible && state.downloadEnabled === previousState.downloadEnabled && state.downloadError === previousState.downloadError && state.downloadInProgress === previousState.downloadInProgress && state.downloadSuccess === previousState.downloadSuccess && state.editActive === previousState.editActive && state.editEnabled === previousState.editEnabled && state.editVisible === previousState.editVisible && state.editStatusMessage === previousState.editStatusMessage && state.inventoryEnabled === previousState.inventoryEnabled && state.inventoryError === previousState.inventoryError && state.inventoryInProgress === previousState.inventoryInProgress && state.inventoryVisible === previousState.inventoryVisible && this.isSameModelVisibility(state.modelVisibility, previousState.modelVisibility);
41991
+ return previousState !== void 0 && state.simulationEnabled === previousState.simulationEnabled && state.simulationInProgress === previousState.simulationInProgress && state.simulationOutcome === previousState.simulationOutcome && state.booleanEnabled === previousState.booleanEnabled && state.booleanInProgress === previousState.booleanInProgress && state.booleanReediting === previousState.booleanReediting && state.booleanResultInvalidated === previousState.booleanResultInvalidated && state.booleanEditConfirmationVisible === previousState.booleanEditConfirmationVisible && state.downloadEnabled === previousState.downloadEnabled && state.downloadError === previousState.downloadError && state.downloadInProgress === previousState.downloadInProgress && state.downloadSuccess === previousState.downloadSuccess && state.editActive === previousState.editActive && state.editEnabled === previousState.editEnabled && state.editVisible === previousState.editVisible && state.editStatusMessage === previousState.editStatusMessage && state.importInProgress === previousState.importInProgress && state.importProgress === previousState.importProgress && state.inventoryEnabled === previousState.inventoryEnabled && state.inventoryError === previousState.inventoryError && state.inventoryInProgress === previousState.inventoryInProgress && state.inventoryVisible === previousState.inventoryVisible && this.isSameModelVisibility(state.modelVisibility, previousState.modelVisibility);
41436
41992
  }
41437
41993
  isSameModelVisibility(state, previousState) {
41438
41994
  return state?.baseVisible === previousState?.baseVisible && state?.enabled === previousState?.enabled && state?.stoneVisible === previousState?.stoneVisible;