@lingbi/studio 0.8.2 → 0.9.0

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;
@@ -32022,12 +32229,15 @@ class TextureLoadMonitor {
32022
32229
  encounteredError = false;
32023
32230
  finishedScheduling = false;
32024
32231
  manager;
32232
+ onTextureSettled;
32025
32233
  originalTextureLoader;
32026
32234
  pendingRequests = 0;
32235
+ scheduledTextureCount = 0;
32027
32236
  settled = false;
32028
32237
  textures = /* @__PURE__ */ new Set();
32029
- constructor(manager, creator) {
32238
+ constructor(manager, creator, onTextureSettled) {
32030
32239
  this.manager = manager;
32240
+ this.onTextureSettled = onTextureSettled;
32031
32241
  this.completionPromise = new Promise(
32032
32242
  this.captureCompletionResolver.bind(this)
32033
32243
  );
@@ -32038,6 +32248,9 @@ class TextureLoadMonitor {
32038
32248
  this.finishedScheduling = true;
32039
32249
  this.resolveIfSettled();
32040
32250
  }
32251
+ getScheduledTextureCount() {
32252
+ return this.scheduledTextureCount;
32253
+ }
32041
32254
  getTextures() {
32042
32255
  return [...this.textures];
32043
32256
  }
@@ -32065,14 +32278,17 @@ class TextureLoadMonitor {
32065
32278
  handleTextureError() {
32066
32279
  this.encounteredError = true;
32067
32280
  this.pendingRequests -= 1;
32281
+ this.onTextureSettled?.();
32068
32282
  this.resolveIfSettled();
32069
32283
  }
32070
32284
  handleTextureLoad() {
32071
32285
  this.pendingRequests -= 1;
32286
+ this.onTextureSettled?.();
32072
32287
  this.resolveIfSettled();
32073
32288
  }
32074
32289
  loadTexture(url, mapping) {
32075
32290
  this.pendingRequests += 1;
32291
+ this.scheduledTextureCount += 1;
32076
32292
  try {
32077
32293
  const texture = this.originalTextureLoader(
32078
32294
  url,
@@ -32108,15 +32324,16 @@ class ObjLoader {
32108
32324
  constructor(parser = new OBJLoader()) {
32109
32325
  this.parser = parser;
32110
32326
  }
32111
- async loadResources(resources, signal, performanceTrace, role) {
32327
+ async loadResources(resources, signal, performanceTrace, role, progress) {
32112
32328
  return this.loadInternal(
32113
32329
  resources,
32114
32330
  signal,
32115
32331
  performanceTrace ?? ImportPerformanceTrace.createDisabled(),
32116
- role ?? "stone"
32332
+ role ?? "stone",
32333
+ progress
32117
32334
  );
32118
32335
  }
32119
- async loadInternal(resources, signal, performanceTrace, role) {
32336
+ async loadInternal(resources, signal, performanceTrace, role, progress) {
32120
32337
  if (this.hasEmptyRequiredUrl(resources)) {
32121
32338
  return {
32122
32339
  code: ObjImportFailureCode$1.EmptySource,
@@ -32130,6 +32347,7 @@ class ObjLoader {
32130
32347
  const objFetchStage = performanceTrace.startStage("obj-fetch", role);
32131
32348
  const objSource = await this.loadText(resources.objUrl, signal);
32132
32349
  performanceTrace.endStage(objFetchStage, "success");
32350
+ progress?.markModelFetched();
32133
32351
  this.assertNotCancelled(signal);
32134
32352
  const materialScanStage = performanceTrace.startStage("obj-material-scan", role);
32135
32353
  const materialReferences = this.getMaterialLibraryReferences(objSource);
@@ -32149,9 +32367,11 @@ class ObjLoader {
32149
32367
  materialContext = await this.loadMaterialContext(
32150
32368
  materialUrls,
32151
32369
  resources,
32152
- signal
32370
+ signal,
32371
+ progress
32153
32372
  );
32154
32373
  performanceTrace.endStage(materialLoadStage, "success");
32374
+ progress?.markMaterialsResolved(this.getScheduledTextureCount(materialContext));
32155
32375
  } catch {
32156
32376
  performanceTrace.endStage(
32157
32377
  materialLoadStage,
@@ -32175,6 +32395,7 @@ class ObjLoader {
32175
32395
  resources.upAxis
32176
32396
  );
32177
32397
  performanceTrace.endStage(modelCreateStage, "success");
32398
+ progress?.markModelParsed();
32178
32399
  if (!this.hasRenderablePrimitive(group) || !model.hasFiniteBounds()) {
32179
32400
  model.dispose();
32180
32401
  return {
@@ -32335,6 +32556,13 @@ class ObjLoader {
32335
32556
  }
32336
32557
  return [...textures];
32337
32558
  }
32559
+ getScheduledTextureCount(context) {
32560
+ let scheduledTextureCount = 0;
32561
+ for (const monitor of context.monitors) {
32562
+ scheduledTextureCount += monitor.getScheduledTextureCount();
32563
+ }
32564
+ return scheduledTextureCount;
32565
+ }
32338
32566
  getObjectTree(root) {
32339
32567
  const objectTree = [root];
32340
32568
  for (const object of objectTree) {
@@ -32451,11 +32679,11 @@ class ObjLoader {
32451
32679
  isPoints(object) {
32452
32680
  return object instanceof Points;
32453
32681
  }
32454
- async loadMaterialContext(mtlUrls, resources, signal) {
32682
+ async loadMaterialContext(mtlUrls, resources, signal, progress) {
32455
32683
  const libraries = [];
32456
32684
  try {
32457
32685
  for (const mtlUrl of mtlUrls) {
32458
- libraries.push(await this.loadMaterialLibrary(mtlUrl, resources, signal));
32686
+ libraries.push(await this.loadMaterialLibrary(mtlUrl, resources, signal, progress));
32459
32687
  }
32460
32688
  return this.createMaterialContext(libraries);
32461
32689
  } catch (error2) {
@@ -32463,7 +32691,7 @@ class ObjLoader {
32463
32691
  throw error2;
32464
32692
  }
32465
32693
  }
32466
- async loadMaterialLibrary(mtlUrl, resources, signal) {
32694
+ async loadMaterialLibrary(mtlUrl, resources, signal, progress) {
32467
32695
  const materialSource = await this.loadText(mtlUrl, signal);
32468
32696
  this.assertNotCancelled(signal);
32469
32697
  const materialBaseUrl = this.getResourceDirectoryUrl(resources, mtlUrl);
@@ -32474,7 +32702,11 @@ class ObjLoader {
32474
32702
  side: DoubleSide
32475
32703
  });
32476
32704
  const creator = materialLoader.parse(materialSource, materialBaseUrl);
32477
- const monitor = new TextureLoadMonitor(manager, creator);
32705
+ const monitor = new TextureLoadMonitor(
32706
+ manager,
32707
+ creator,
32708
+ progress === void 0 ? void 0 : progress.markTextureSettled.bind(progress)
32709
+ );
32478
32710
  try {
32479
32711
  const pbrMaterialReferences = this.parsePbrMaterialReferences(materialSource);
32480
32712
  this.assertMaterialTextures(resources, materialSource, materialBaseUrl, creator);
@@ -33450,7 +33682,7 @@ class PlyLoader {
33450
33682
  this.parser = parser;
33451
33683
  this.textureLoader = textureLoader;
33452
33684
  }
33453
- async load(resources, signal) {
33685
+ async load(resources, signal, progress) {
33454
33686
  if (signal.aborted) {
33455
33687
  return {
33456
33688
  code: ObjImportFailureCode$1.Cancelled,
@@ -33461,6 +33693,7 @@ class PlyLoader {
33461
33693
  if (!(source instanceof ArrayBuffer)) {
33462
33694
  return source;
33463
33695
  }
33696
+ progress?.markModelFetched();
33464
33697
  let geometry;
33465
33698
  let metadata;
33466
33699
  try {
@@ -33486,7 +33719,14 @@ class PlyLoader {
33486
33719
  success: false
33487
33720
  };
33488
33721
  }
33489
- const modelResult = metadata.hasFaces ? await this.createMeshModel(geometry, metadata.textureReference, resources, signal) : this.createPointsModel(geometry, resources);
33722
+ progress?.markModelParsed();
33723
+ const modelResult = metadata.hasFaces ? await this.createMeshModel(
33724
+ geometry,
33725
+ metadata.textureReference,
33726
+ resources,
33727
+ signal,
33728
+ progress
33729
+ ) : this.createPointsModel(geometry, resources);
33490
33730
  if ("success" in modelResult) {
33491
33731
  return modelResult;
33492
33732
  }
@@ -33514,13 +33754,14 @@ class PlyLoader {
33514
33754
  vertexColors: geometry.hasAttribute("color")
33515
33755
  });
33516
33756
  }
33517
- async createMeshModel(geometry, textureReference, resources, signal) {
33757
+ async createMeshModel(geometry, textureReference, resources, signal, progress) {
33518
33758
  if (!geometry.hasAttribute("normal")) {
33519
33759
  geometry.computeVertexNormals();
33520
33760
  }
33521
33761
  const warnings = [];
33522
33762
  let texture;
33523
33763
  if (textureReference !== void 0 && geometry.hasAttribute("uv")) {
33764
+ progress?.markMaterialsResolved(1);
33524
33765
  const textureResult = await this.textureLoader.load(
33525
33766
  resources.resolveTextureUrl(textureReference),
33526
33767
  signal
@@ -33532,6 +33773,7 @@ class PlyLoader {
33532
33773
  success: false
33533
33774
  };
33534
33775
  }
33776
+ progress?.markTextureSettled();
33535
33777
  if (textureResult.outcome === "succeeded") {
33536
33778
  texture = textureResult.texture;
33537
33779
  } else {
@@ -33617,26 +33859,73 @@ class PlyLoader {
33617
33859
  return value;
33618
33860
  }
33619
33861
  }
33862
+ const OBJ_MODEL_LOAD_PROGRESS_PROFILE = {
33863
+ fetchedPosition: 0.2,
33864
+ materialsPosition: 0.3,
33865
+ parsedPosition: 0.95,
33866
+ textureCeiling: 0.99
33867
+ };
33868
+ const PLY_MODEL_LOAD_PROGRESS_PROFILE = {
33869
+ fetchedPosition: 0.4,
33870
+ materialsPosition: 0.85,
33871
+ parsedPosition: 0.85,
33872
+ textureCeiling: 0.99
33873
+ };
33874
+ class RoleImportProgressReporter {
33875
+ profile;
33876
+ role;
33877
+ textureSettled = 0;
33878
+ textureTotal;
33879
+ tracker;
33880
+ constructor(tracker, role, profile) {
33881
+ this.profile = profile;
33882
+ this.role = role;
33883
+ this.tracker = tracker;
33884
+ }
33885
+ markMaterialsResolved(textureTotal) {
33886
+ this.textureTotal = textureTotal;
33887
+ this.tracker.setRolePosition(this.role, this.profile.materialsPosition);
33888
+ this.reportTexturePosition();
33889
+ }
33890
+ markModelFetched() {
33891
+ this.tracker.setRolePosition(this.role, this.profile.fetchedPosition);
33892
+ }
33893
+ markModelParsed() {
33894
+ this.tracker.setRolePosition(this.role, this.profile.parsedPosition);
33895
+ }
33896
+ markTextureSettled() {
33897
+ this.textureSettled += 1;
33898
+ this.reportTexturePosition();
33899
+ }
33900
+ reportTexturePosition() {
33901
+ const textureTotal = this.textureTotal;
33902
+ if (textureTotal === void 0 || textureTotal <= 0 || this.textureSettled === 0) {
33903
+ return;
33904
+ }
33905
+ const textureSpan = this.profile.textureCeiling - this.profile.parsedPosition;
33906
+ const settledShare = Math.min(this.textureSettled, textureTotal) / textureTotal;
33907
+ this.tracker.setRolePosition(
33908
+ this.role,
33909
+ this.profile.parsedPosition + textureSpan * settledShare
33910
+ );
33911
+ }
33912
+ }
33620
33913
  class SimulationSnapshotFactory {
33621
- create(stone, base) {
33914
+ constructor(scheduler) {
33915
+ this.scheduler = scheduler;
33916
+ }
33917
+ scheduler;
33918
+ // Allocation granularity, not a per-task limit: tasks copy until their time budget expires.
33919
+ static CHUNK_BYTES = 4 * 1024 * 1024;
33920
+ static ATTRIBUTE_CHUNK_BYTES = 256 * 1024;
33921
+ async create(stone, base) {
33622
33922
  const started = performance.now();
33623
33923
  const stoneRoot = this.root(stone);
33624
33924
  const baseRoot = this.root(base);
33625
33925
  stoneRoot.updateMatrixWorld(true);
33626
33926
  if (baseRoot !== stoneRoot) baseRoot.updateMatrixWorld(true);
33627
- const stoneInput = this.collect(stone);
33628
- const baseInput = this.collect(base);
33629
- const bounds = new Box3();
33630
- const point = new Vector3();
33631
- for (let index = 0; index < stoneInput.positions.length; index += 3) {
33632
- bounds.expandByPoint(point.fromArray(stoneInput.positions, index));
33633
- }
33634
- const origin = bounds.getCenter(new Vector3()).toArray();
33635
- for (const input of [stoneInput, baseInput]) {
33636
- for (let index = 0; index < input.positions.length; index++) {
33637
- input.positions[index] = Number(input.positions[index]) - Number(origin[index % 3]);
33638
- }
33639
- }
33927
+ const stoneInput = await this.collect(stone);
33928
+ const baseInput = await this.collect(base);
33640
33929
  return {
33641
33930
  stone: stoneInput,
33642
33931
  base: baseInput,
@@ -33648,54 +33937,126 @@ class SimulationSnapshotFactory {
33648
33937
  while (root.parent !== null) root = root.parent;
33649
33938
  return root;
33650
33939
  }
33651
- collect(root) {
33652
- const positions = [];
33653
- const indices = [];
33654
- const point = new Vector3();
33655
- root.traverse((object) => {
33656
- if (!(object instanceof Mesh)) return;
33940
+ async collect(root) {
33941
+ const result = [];
33942
+ const objects = [root];
33943
+ for (const object of objects) {
33944
+ const pause = this.scheduler.checkpoint();
33945
+ if (pause !== void 0) await pause;
33946
+ objects.push(...object.children);
33947
+ if (!(object instanceof Mesh)) continue;
33657
33948
  const mesh = object;
33658
- if (!mesh.geometry.hasAttribute("position")) {
33659
- throw new Error("The model has no triangular position attribute.");
33660
- }
33661
33949
  const position = mesh.geometry.getAttribute("position");
33662
- if (position.itemSize < 3) {
33950
+ if (!mesh.geometry.hasAttribute("position") || position.itemSize < 3) {
33663
33951
  throw new Error("The model has no triangular position attribute.");
33664
33952
  }
33665
- const determinant = mesh.matrixWorld.determinant();
33666
- if (!Number.isFinite(determinant) || determinant === 0) {
33667
- throw new Error("The model transform is singular or nonfinite.");
33668
- }
33669
- const offset = positions.length / 3;
33670
- for (let index = 0; index < position.count; index++) {
33671
- point.set(position.getX(index), position.getY(index), position.getZ(index)).applyMatrix4(mesh.matrixWorld);
33672
- if (!point.toArray().every(Number.isFinite)) {
33673
- throw new Error("The model contains nonfinite positions.");
33674
- }
33675
- positions.push(point.x, point.y, point.z);
33676
- }
33677
- const sourceIndex = mesh.geometry.index;
33678
- const count = sourceIndex?.count ?? position.count;
33679
- if (count === 0 || count % 3 !== 0) {
33680
- throw new Error("The model has incomplete triangles.");
33681
- }
33682
- for (let index = 0; index < count; index += 3) {
33683
- const a = sourceIndex?.getX(index) ?? index;
33684
- const b = sourceIndex?.getX(index + 1) ?? index + 1;
33685
- const c = sourceIndex?.getX(index + 2) ?? index + 2;
33686
- if ([a, b, c].some(
33687
- (value) => !Number.isSafeInteger(value) || value < 0 || value >= position.count
33688
- )) {
33689
- throw new Error("The model contains an invalid triangle index.");
33690
- }
33691
- indices.push(
33692
- offset + a,
33693
- offset + (determinant < 0 ? c : b),
33694
- offset + (determinant < 0 ? b : c)
33695
- );
33953
+ const matrixWorld = mesh.matrixWorld.toArray();
33954
+ if (!matrixWorld.every(Number.isFinite)) {
33955
+ throw new Error("The model transform is nonfinite.");
33696
33956
  }
33957
+ const positions = await this.copyPositions(position);
33958
+ const index = mesh.geometry.index;
33959
+ const indices = index === null ? null : await this.copyIndices(index);
33960
+ result.push({ positions, indices, vertexCount: position.count, matrixWorld });
33961
+ }
33962
+ return result;
33963
+ }
33964
+ async copyPositions(attribute) {
33965
+ const result = [];
33966
+ const source = attribute.array;
33967
+ const packed = !("isInterleavedBufferAttribute" in attribute) && !attribute.normalized && attribute.itemSize === 3 && (source instanceof Float32Array || source instanceof Float64Array);
33968
+ const vertexChunk = Math.floor(
33969
+ (packed ? SimulationSnapshotFactory.CHUNK_BYTES : SimulationSnapshotFactory.ATTRIBUTE_CHUNK_BYTES) / 24
33970
+ );
33971
+ for (let start = 0; start < attribute.count; start += vertexChunk) {
33972
+ const pause = this.scheduler.checkpoint();
33973
+ if (pause !== void 0) await pause;
33974
+ const end = Math.min(attribute.count, start + vertexChunk);
33975
+ if (packed) {
33976
+ result.push(source.slice(start * 3, end * 3));
33977
+ } else {
33978
+ const values = new Float64Array((end - start) * 3);
33979
+ for (let index = start; index < end; index++) {
33980
+ const offset = (index - start) * 3;
33981
+ values[offset] = attribute.getX(index);
33982
+ values[offset + 1] = attribute.getY(index);
33983
+ values[offset + 2] = attribute.getZ(index);
33984
+ }
33985
+ result.push(values);
33986
+ }
33987
+ }
33988
+ return result;
33989
+ }
33990
+ async copyIndices(attribute) {
33991
+ const result = [];
33992
+ const source = attribute.array;
33993
+ const packed = attribute.itemSize === 1 && !attribute.normalized && (source instanceof Uint16Array || source instanceof Uint32Array);
33994
+ const indexChunk = (packed ? SimulationSnapshotFactory.CHUNK_BYTES : SimulationSnapshotFactory.ATTRIBUTE_CHUNK_BYTES) / 8;
33995
+ for (let start = 0; start < attribute.count; start += indexChunk) {
33996
+ const pause = this.scheduler.checkpoint();
33997
+ if (pause !== void 0) await pause;
33998
+ const end = Math.min(attribute.count, start + indexChunk);
33999
+ if (packed) result.push(source.slice(start, end));
34000
+ else {
34001
+ const values = new Float64Array(end - start);
34002
+ for (let index = start; index < end; index++) {
34003
+ values[index - start] = attribute.getX(index);
34004
+ }
34005
+ result.push(values);
34006
+ }
34007
+ }
34008
+ return result;
34009
+ }
34010
+ }
34011
+ class SimulationTaskScheduler {
34012
+ constructor(signal, started) {
34013
+ this.signal = signal;
34014
+ this.started = started;
34015
+ }
34016
+ signal;
34017
+ started;
34018
+ sliceStarted = performance.now();
34019
+ async waitForPaint() {
34020
+ await this.wait(true);
34021
+ this.checkActive();
34022
+ this.sliceStarted = performance.now();
34023
+ }
34024
+ checkpoint() {
34025
+ this.checkActive();
34026
+ if (performance.now() - this.sliceStarted < 4) return void 0;
34027
+ return this.yieldTask();
34028
+ }
34029
+ async yieldTask() {
34030
+ await this.wait(false);
34031
+ this.checkActive();
34032
+ this.sliceStarted = performance.now();
34033
+ }
34034
+ checkActive() {
34035
+ this.signal.throwIfAborted();
34036
+ if (performance.now() - this.started >= 3e4) {
34037
+ throw new Error("Simulation timed out during snapshot capture.");
34038
+ }
34039
+ }
34040
+ wait(paint) {
34041
+ this.checkActive();
34042
+ return new Promise((resolve) => {
34043
+ let frame;
34044
+ let task;
34045
+ const finish = () => {
34046
+ if (frame !== void 0) cancelAnimationFrame(frame);
34047
+ clearTimeout(task);
34048
+ this.signal.removeEventListener("abort", finish);
34049
+ resolve();
34050
+ };
34051
+ this.signal.addEventListener("abort", finish, { once: true });
34052
+ if (paint) {
34053
+ frame = requestAnimationFrame(() => {
34054
+ clearTimeout(task);
34055
+ task = setTimeout(finish, 0);
34056
+ });
34057
+ }
34058
+ task = setTimeout(finish, paint ? 100 : 0);
33697
34059
  });
33698
- return { positions: Float64Array.from(positions), indices: Uint32Array.from(indices) };
33699
34060
  }
33700
34061
  }
33701
34062
  class StabilityMessages {
@@ -33703,7 +34064,7 @@ class StabilityMessages {
33703
34064
  if (!this.record(value) || !this.token(value["token"]) || !this.record(value["input"]))
33704
34065
  return void 0;
33705
34066
  const input = value["input"];
33706
- if (!this.mesh(input["stone"]) || !this.mesh(input["base"]) || !this.nonnegative(input["captureMilliseconds"]))
34067
+ if (!this.role(input["stone"]) || !this.role(input["base"]) || !this.nonnegative(input["captureMilliseconds"]))
33707
34068
  return void 0;
33708
34069
  return value;
33709
34070
  }
@@ -33736,8 +34097,20 @@ class StabilityMessages {
33736
34097
  }
33737
34098
  return value["outcome"] !== "supported" || Number(value["margin"]) > 1e-6 && Number(value["residual"]) <= 1e-7 && Number(value["contactCount"]) > 0;
33738
34099
  }
34100
+ role(value) {
34101
+ return this.array(value) && value.every((mesh) => this.mesh(mesh));
34102
+ }
34103
+ array(value) {
34104
+ return Array.isArray(value);
34105
+ }
33739
34106
  mesh(value) {
33740
- return this.record(value) && value["positions"] instanceof Float64Array && value["indices"] instanceof Uint32Array;
34107
+ return this.record(value) && Array.isArray(value["positions"]) && value["positions"].every(
34108
+ (chunk) => chunk instanceof Float32Array || chunk instanceof Float64Array
34109
+ ) && (value["indices"] === null || Array.isArray(value["indices"]) && value["indices"].every(
34110
+ (chunk) => chunk instanceof Uint16Array || chunk instanceof Uint32Array || chunk instanceof Float64Array
34111
+ )) && this.nonnegative(value["vertexCount"]) && Number.isSafeInteger(value["vertexCount"]) && Array.isArray(value["matrixWorld"]) && value["matrixWorld"].length === 16 && value["matrixWorld"].every(
34112
+ (entry) => typeof entry === "number" && Number.isFinite(entry)
34113
+ );
33741
34114
  }
33742
34115
  record(value) {
33743
34116
  return typeof value === "object" && value !== null;
@@ -33753,7 +34126,7 @@ class StabilityWorkerFactory {
33753
34126
  create() {
33754
34127
  return new Worker(new URL(
33755
34128
  /* @vite-ignore */
33756
- "" + new URL("assets/StabilityWorkerEntry-CXCNaUyw.js", import.meta.url).href,
34129
+ "" + new URL("assets/StabilityWorkerEntry-L0_8iZmQ.js", import.meta.url).href,
33757
34130
  import.meta.url
33758
34131
  ), {
33759
34132
  type: "module"
@@ -33802,12 +34175,16 @@ class StabilityWorkerClient {
33802
34175
  this.fail("Simulation Worker message could not be read.");
33803
34176
  });
33804
34177
  }
33805
- worker.postMessage({ token, input }, [
33806
- input.stone.positions.buffer,
33807
- input.stone.indices.buffer,
33808
- input.base.positions.buffer,
33809
- input.base.indices.buffer
33810
- ]);
34178
+ const transfer = [];
34179
+ for (const role of [input.stone, input.base]) {
34180
+ for (const mesh of role) {
34181
+ for (const chunk of mesh.positions) transfer.push(chunk.buffer);
34182
+ if (mesh.indices !== null) {
34183
+ for (const chunk of mesh.indices) transfer.push(chunk.buffer);
34184
+ }
34185
+ }
34186
+ }
34187
+ worker.postMessage({ token, input }, transfer);
33811
34188
  } catch {
33812
34189
  this.fail("Simulation Worker could not start.");
33813
34190
  }
@@ -35826,6 +36203,7 @@ class Renderer {
35826
36203
  contextLost = false;
35827
36204
  booleanOperationInProgress = false;
35828
36205
  simulationInProgress = false;
36206
+ simulationCapture;
35829
36207
  simulationToken = 0;
35830
36208
  simulationWorker = new StabilityWorkerClient();
35831
36209
  booleanOperationWorkerClient;
@@ -35837,6 +36215,7 @@ class Renderer {
35837
36215
  fillLight;
35838
36216
  frameHandle;
35839
36217
  gizmoController;
36218
+ groundGrid;
35840
36219
  hemisphereLight;
35841
36220
  pendingFirstFramePerformance;
35842
36221
  importPerformanceTracer = new ImportPerformanceTracer();
@@ -35874,6 +36253,7 @@ class Renderer {
35874
36253
  let contextRestoreListenerAttached = false;
35875
36254
  let controller;
35876
36255
  let gizmoController;
36256
+ let groundGrid;
35877
36257
  let resizeObserver;
35878
36258
  let viewCube;
35879
36259
  this.canvas = canvas;
@@ -35906,6 +36286,9 @@ class Renderer {
35906
36286
  );
35907
36287
  this.gizmoController = gizmoController;
35908
36288
  this.scene.add(gizmoController.getHelper());
36289
+ groundGrid = new GroundGrid();
36290
+ this.groundGrid = groundGrid;
36291
+ groundGrid.attach(this.scene);
35909
36292
  canvasController = new CanvasController(
35910
36293
  canvas,
35911
36294
  viewCube,
@@ -35959,9 +36342,13 @@ class Renderer {
35959
36342
  viewCube?.dispose();
35960
36343
  } finally {
35961
36344
  try {
35962
- controller?.dispose();
36345
+ groundGrid?.dispose();
35963
36346
  } finally {
35964
- renderer.dispose();
36347
+ try {
36348
+ controller?.dispose();
36349
+ } finally {
36350
+ renderer.dispose();
36351
+ }
35965
36352
  }
35966
36353
  }
35967
36354
  }
@@ -36008,25 +36395,29 @@ class Renderer {
36008
36395
  this.viewCube.dispose();
36009
36396
  } finally {
36010
36397
  try {
36011
- this.releaseActiveModels();
36398
+ this.groundGrid.dispose();
36012
36399
  } finally {
36013
36400
  try {
36014
- this.resizeObserver.disconnect();
36401
+ this.releaseActiveModels();
36015
36402
  } finally {
36016
36403
  try {
36017
- this.canvas.removeEventListener(
36018
- "webglcontextrestored",
36019
- this.contextRestoreListener
36020
- );
36404
+ this.resizeObserver.disconnect();
36021
36405
  } finally {
36022
36406
  try {
36023
36407
  this.canvas.removeEventListener(
36024
- "webglcontextlost",
36025
- this.contextLossListener
36408
+ "webglcontextrestored",
36409
+ this.contextRestoreListener
36026
36410
  );
36027
36411
  } finally {
36028
- this.renderer.forceContextLoss();
36029
- this.renderer.dispose();
36412
+ try {
36413
+ this.canvas.removeEventListener(
36414
+ "webglcontextlost",
36415
+ this.contextLossListener
36416
+ );
36417
+ } finally {
36418
+ this.renderer.forceContextLoss();
36419
+ this.renderer.dispose();
36420
+ }
36030
36421
  }
36031
36422
  }
36032
36423
  }
@@ -36064,7 +36455,10 @@ class Renderer {
36064
36455
  stone: new ModelResourceList(source.stone.fileUrls, source.stone.upAxis)
36065
36456
  };
36066
36457
  return this.completeSceneImportFromResources(
36067
- this.startImportSession(performanceTrace),
36458
+ this.startImportSession(
36459
+ performanceTrace,
36460
+ this.createImportProgressTracker(resources)
36461
+ ),
36068
36462
  resources
36069
36463
  );
36070
36464
  } catch {
@@ -36106,7 +36500,10 @@ class Renderer {
36106
36500
  stone: new ModelResourceList(source.stone.files, source.stone.upAxis)
36107
36501
  };
36108
36502
  return this.completeSceneImportFromResources(
36109
- this.startImportSession(performanceTrace),
36503
+ this.startImportSession(
36504
+ performanceTrace,
36505
+ this.createImportProgressTracker(resources)
36506
+ ),
36110
36507
  resources
36111
36508
  );
36112
36509
  } catch {
@@ -36153,6 +36550,7 @@ class Renderer {
36153
36550
  available: this.activeStoneModel !== void 0,
36154
36551
  contextLost: this.contextLost,
36155
36552
  importInProgress: this.activeImport !== void 0,
36553
+ ...this.activeImport === void 0 ? {} : { importProgress: this.activeImport.progressTracker.getPercent() },
36156
36554
  interactionInProgress,
36157
36555
  revision: this.sceneRevision,
36158
36556
  resourceRevision: this.sceneResourceRevision
@@ -36359,6 +36757,8 @@ class Renderer {
36359
36757
  const started = performance.now();
36360
36758
  const token = ++this.simulationToken;
36361
36759
  const revision = this.sceneRevision;
36760
+ const capture = new AbortController();
36761
+ this.simulationCapture = capture;
36362
36762
  this.simulationInProgress = true;
36363
36763
  try {
36364
36764
  this.gizmoController.end();
@@ -36387,7 +36787,13 @@ class Renderer {
36387
36787
  console.info("[stone-stability]", response2);
36388
36788
  return response2;
36389
36789
  }
36390
- const input = new SimulationSnapshotFactory().create(stone.object, base.object);
36790
+ const scheduler = new SimulationTaskScheduler(capture.signal, started);
36791
+ await scheduler.waitForPaint();
36792
+ const input = await new SimulationSnapshotFactory(scheduler).create(
36793
+ stone.object,
36794
+ base.object
36795
+ );
36796
+ capture.signal.throwIfAborted();
36391
36797
  const response = await this.simulationWorker.run(input, started);
36392
36798
  if (token !== this.simulationToken || revision !== this.sceneRevision || !this.isDualModelSceneReady()) {
36393
36799
  return { status: "cancelled" };
@@ -36395,11 +36801,13 @@ class Renderer {
36395
36801
  console.info("[stone-stability]", response);
36396
36802
  return response;
36397
36803
  } catch (error2) {
36804
+ if (capture.signal.aborted) return { status: "cancelled" };
36398
36805
  const reason = error2 instanceof Error ? error2.message : "Simulation failed.";
36399
36806
  console.info("[stone-stability]", { status: "failed", reason });
36400
36807
  return { status: "failed", reason };
36401
36808
  } finally {
36402
36809
  if (token === this.simulationToken) {
36810
+ this.simulationCapture = void 0;
36403
36811
  this.simulationInProgress = false;
36404
36812
  this.notifyWorkspaceState();
36405
36813
  }
@@ -36407,6 +36815,8 @@ class Renderer {
36407
36815
  }
36408
36816
  cancelSimulation() {
36409
36817
  const wasActive = this.simulationInProgress;
36818
+ this.simulationCapture?.abort();
36819
+ this.simulationCapture = void 0;
36410
36820
  this.simulationToken++;
36411
36821
  this.simulationInProgress = false;
36412
36822
  this.simulationWorker.cancel();
@@ -36603,12 +37013,22 @@ class Renderer {
36603
37013
  let result;
36604
37014
  try {
36605
37015
  try {
37016
+ const progress = new RoleImportProgressReporter(
37017
+ session.progressTracker,
37018
+ role,
37019
+ source.resources instanceof ObjResourceList ? OBJ_MODEL_LOAD_PROGRESS_PROFILE : PLY_MODEL_LOAD_PROGRESS_PROFILE
37020
+ );
36606
37021
  result = source.resources instanceof ObjResourceList ? await this.modelLoader.loadResources(
36607
37022
  source.resources,
36608
37023
  session.controller.signal,
36609
37024
  session.performanceTrace,
36610
- role
36611
- ) : await this.plyLoader.load(source.resources, session.controller.signal);
37025
+ role,
37026
+ progress
37027
+ ) : await this.plyLoader.load(
37028
+ source.resources,
37029
+ session.controller.signal,
37030
+ progress
37031
+ );
36612
37032
  } catch {
36613
37033
  return this.completeUnexpectedImportFailure(session);
36614
37034
  }
@@ -36653,6 +37073,7 @@ class Renderer {
36653
37073
  };
36654
37074
  }
36655
37075
  loadStatus = "success";
37076
+ session.progressTracker.completeRole(role);
36656
37077
  return {
36657
37078
  model: result.model,
36658
37079
  warnings: result.warnings ?? []
@@ -36740,6 +37161,7 @@ class Renderer {
36740
37161
  this.booleanEditSessionActive = false;
36741
37162
  this.activeBooleanOperationSessionToken = void 0;
36742
37163
  this.abortEditingState();
37164
+ this.refreshGroundGrid([stone, candidate]);
36743
37165
  } catch {
36744
37166
  this.activeBaseModel = previousBase;
36745
37167
  (candidate.transformRoot ?? candidate.object).removeFromParent();
@@ -37056,6 +37478,7 @@ class Renderer {
37056
37478
  ...this.collectPresentModels(this.activeStoneModel, this.activeBaseModel)
37057
37479
  ];
37058
37480
  this.gizmoController.setActiveScene({ base: void 0, stone: void 0 });
37481
+ this.groundGrid.hide();
37059
37482
  this.transformedSceneBoundsDirty = false;
37060
37483
  this.activeStoneModel = void 0;
37061
37484
  this.activeBaseModel = void 0;
@@ -37104,6 +37527,18 @@ class Renderer {
37104
37527
  this.gizmoController.setModelVisibility("stone", this.modelVisibility.stoneVisible);
37105
37528
  this.gizmoController.setModelVisibility("base", this.modelVisibility.baseVisible);
37106
37529
  }
37530
+ /**
37531
+ * Retargets the ground grid from the present models. The grid anchors to
37532
+ * models that are merely hidden as well, so toggling base visibility never
37533
+ * moves the ground plane; only a committed paired scene shows it.
37534
+ */
37535
+ refreshGroundGrid(models) {
37536
+ if (models.length < 2) {
37537
+ this.groundGrid.hide();
37538
+ return;
37539
+ }
37540
+ this.groundGrid.updateFromBounds(this.mergeBounds(models));
37541
+ }
37107
37542
  applyModelVisibility() {
37108
37543
  const stoneRoot = this.activeStoneModel?.transformRoot ?? this.activeStoneModel?.object;
37109
37544
  const baseRoot = this.activeBaseModel?.transformRoot ?? this.activeBaseModel?.object;
@@ -37152,6 +37587,7 @@ class Renderer {
37152
37587
  }
37153
37588
  renderScene(includeViewCube = true) {
37154
37589
  this.syncTransformedSceneBounds();
37590
+ this.syncGroundGridForCamera();
37155
37591
  this.gizmoController.updateForRender();
37156
37592
  if (includeViewCube) {
37157
37593
  this.viewCube.update(this.camera);
@@ -37161,6 +37597,17 @@ class Renderer {
37161
37597
  this.renderViewCube();
37162
37598
  }
37163
37599
  }
37600
+ /**
37601
+ * Keeps the ground grid covering most of the visible area as the camera
37602
+ * zooms. Runs per rendered frame; updating only touches uniforms and the
37603
+ * mesh transform, so it stays allocation-free.
37604
+ */
37605
+ syncGroundGridForCamera() {
37606
+ const frustumHeight = (this.camera.top - this.camera.bottom) / this.camera.zoom;
37607
+ const frustumWidth = (this.camera.right - this.camera.left) / (this.camera.top - this.camera.bottom) * frustumHeight;
37608
+ const halfDiagonal = Math.sqrt(frustumHeight * frustumHeight + frustumWidth * frustumWidth) / 2;
37609
+ this.groundGrid.updateForViewport(halfDiagonal);
37610
+ }
37164
37611
  renderViewCube() {
37165
37612
  const viewport = this.viewCube.getViewport();
37166
37613
  const previousAutoClear = this.renderer.autoClear;
@@ -37199,6 +37646,9 @@ class Renderer {
37199
37646
  }
37200
37647
  }
37201
37648
  this.controller.syncTransformedBounds(this.mergeBounds(models));
37649
+ this.refreshGroundGrid(
37650
+ this.collectPresentModels(this.activeStoneModel, this.activeBaseModel)
37651
+ );
37202
37652
  }
37203
37653
  /**
37204
37654
  * Atomically replaces the committed active scene. New model references are
@@ -37273,6 +37723,7 @@ class Renderer {
37273
37723
  };
37274
37724
  this.gizmoController.setActiveScene(activeScene);
37275
37725
  this.applyGizmoVisibility();
37726
+ this.refreshGroundGrid(candidateModels);
37276
37727
  this.sceneRevision += 1;
37277
37728
  this.sceneResourceRevision = this.sceneRevision;
37278
37729
  this.pendingFirstFramePerformance = {
@@ -37282,6 +37733,7 @@ class Renderer {
37282
37733
  };
37283
37734
  session.performanceTrace.markStage("import-promise-resolved", "scene");
37284
37735
  session.performanceTrace.endStage(sceneCommitStage, "success");
37736
+ session.progressTracker.completeScene();
37285
37737
  this.finishImportSession(session);
37286
37738
  this.releasePair(previousStone, previousBase);
37287
37739
  this.requestRender();
@@ -37405,6 +37857,13 @@ class Renderer {
37405
37857
  this.notifyWorkspaceState();
37406
37858
  }
37407
37859
  }
37860
+ handleImportProgressChanged(_source) {
37861
+ this.notifyWorkspaceState();
37862
+ }
37863
+ createImportProgressTracker(resources) {
37864
+ const roles = resources.base === void 0 ? ["stone"] : ["stone", "base"];
37865
+ return new ImportProgressTracker(roles, this.handleImportProgressChanged.bind(this));
37866
+ }
37408
37867
  notifyWorkspaceState() {
37409
37868
  this.workspaceStateListener?.(this.getWorkspaceState());
37410
37869
  }
@@ -37453,6 +37912,7 @@ class Renderer {
37453
37912
  this.renderer.setSize(width, height, false);
37454
37913
  this.controller.resize(width, height);
37455
37914
  this.viewCube.resize(width, height, pixelRatio);
37915
+ this.groundGrid.setPixelRatio(pixelRatio);
37456
37916
  this.requestRender();
37457
37917
  }
37458
37918
  invalidateActiveImport(code) {
@@ -37498,7 +37958,7 @@ class Renderer {
37498
37958
  this.notifyWorkspaceState();
37499
37959
  }
37500
37960
  }
37501
- startImportSession(performanceTrace) {
37961
+ startImportSession(performanceTrace, progressTracker) {
37502
37962
  this.cancelSimulation();
37503
37963
  this.cancelBooleanOperation();
37504
37964
  this.cancelPendingFirstFramePerformance("cancelled");
@@ -37511,7 +37971,8 @@ class Renderer {
37511
37971
  const session = {
37512
37972
  controller: new AbortController(),
37513
37973
  invalidationCode: void 0,
37514
- performanceTrace
37974
+ performanceTrace,
37975
+ progressTracker
37515
37976
  };
37516
37977
  performanceTrace.markStage("import-session-start", "scene");
37517
37978
  this.activeImport = session;
@@ -37752,6 +38213,8 @@ class ArchiveImportWorkerMessageParser {
37752
38213
  return this.parseEntryStart(record, taskId);
37753
38214
  case "error":
37754
38215
  return typeof record["message"] === "string" ? { message: record["message"], taskId, type: "error" } : void 0;
38216
+ case "progress":
38217
+ return this.parseProgress(record, taskId);
37755
38218
  default:
37756
38219
  return void 0;
37757
38220
  }
@@ -37769,6 +38232,17 @@ class ArchiveImportWorkerMessageParser {
37769
38232
  isPositiveSafeInteger(value) {
37770
38233
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
37771
38234
  }
38235
+ isNonNegativeSafeInteger(value) {
38236
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
38237
+ }
38238
+ parseProgress(record, taskId) {
38239
+ const bytesLoaded = record["bytesLoaded"];
38240
+ const bytesTotal = record["bytesTotal"];
38241
+ if (!this.isNonNegativeSafeInteger(bytesLoaded) || !this.isNonNegativeSafeInteger(bytesTotal)) {
38242
+ return void 0;
38243
+ }
38244
+ return { bytesLoaded, bytesTotal, taskId, type: "progress" };
38245
+ }
37772
38246
  isRecord(value) {
37773
38247
  return typeof value === "object" && value !== null && !Array.isArray(value);
37774
38248
  }
@@ -37828,7 +38302,7 @@ class BrowserArchiveImportWorkerEndpoint {
37828
38302
  constructor() {
37829
38303
  this.worker = new Worker(new URL(
37830
38304
  /* @vite-ignore */
37831
- "" + new URL("assets/ArchiveImportWorkerEntry-Dh4RZo22.js", import.meta.url).href,
38305
+ "" + new URL("assets/ArchiveImportWorkerEntry-wkxjE56k.js", import.meta.url).href,
37832
38306
  import.meta.url
37833
38307
  ), {
37834
38308
  type: "module"
@@ -37868,9 +38342,11 @@ class ArchiveImportWorkerClient {
37868
38342
  jobValidator = new ArchiveImportJobValidator();
37869
38343
  lastStartedTaskId = 0;
37870
38344
  messageParser = new ArchiveImportWorkerMessageParser();
38345
+ onProgress;
37871
38346
  workerFactory;
37872
- constructor(workerFactory = new BrowserArchiveImportWorkerFactory()) {
38347
+ constructor(workerFactory = new BrowserArchiveImportWorkerFactory(), onProgress) {
37873
38348
  this.workerFactory = workerFactory;
38349
+ this.onProgress = onProgress;
37874
38350
  }
37875
38351
  cancel(taskId) {
37876
38352
  const task = this.activeTask;
@@ -37945,6 +38421,10 @@ class ArchiveImportWorkerClient {
37945
38421
  outbound.message
37946
38422
  )
37947
38423
  );
38424
+ return;
38425
+ case "progress":
38426
+ this.handleProgress(outbound);
38427
+ return;
37948
38428
  }
37949
38429
  } catch (error2) {
37950
38430
  this.rejectTask(task, this.toError(error2));
@@ -38014,6 +38494,10 @@ class ArchiveImportWorkerClient {
38014
38494
  type: "acknowledge-chunk"
38015
38495
  });
38016
38496
  }
38497
+ handleProgress(message) {
38498
+ const percent = message.bytesTotal <= 0 ? 0 : Math.min(100, Math.floor(message.bytesLoaded / message.bytesTotal * 100));
38499
+ this.onProgress?.(percent);
38500
+ }
38017
38501
  rejectTask(task, error2) {
38018
38502
  if (this.activeTask !== task) {
38019
38503
  return;
@@ -38195,6 +38679,26 @@ var ImportPerformanceMode = /* @__PURE__ */ ((ImportPerformanceMode2) => {
38195
38679
  ImportPerformanceMode2["Verbose"] = "verbose";
38196
38680
  return ImportPerformanceMode2;
38197
38681
  })(ImportPerformanceMode || {});
38682
+ const ARCHIVE_SEGMENT_PERCENT = 20;
38683
+ class ImportProgressCompositor {
38684
+ compose(input) {
38685
+ const archiveProgress = this.clampPercent(input.archiveProgress);
38686
+ const rendererProgress = this.clampPercent(input.rendererProgress);
38687
+ if (archiveProgress === void 0) {
38688
+ return rendererProgress;
38689
+ }
38690
+ if (rendererProgress === void 0) {
38691
+ return Math.floor(archiveProgress / 100 * ARCHIVE_SEGMENT_PERCENT);
38692
+ }
38693
+ return ARCHIVE_SEGMENT_PERCENT + Math.floor(rendererProgress / 100 * (100 - ARCHIVE_SEGMENT_PERCENT));
38694
+ }
38695
+ clampPercent(percent) {
38696
+ if (percent === void 0) {
38697
+ return void 0;
38698
+ }
38699
+ return Math.max(0, Math.min(100, percent));
38700
+ }
38701
+ }
38198
38702
  class DownloadSaveSink {
38199
38703
  aborted = false;
38200
38704
  blobChunks = [];
@@ -39914,7 +40418,7 @@ var StoneBasePoseMode = /* @__PURE__ */ ((StoneBasePoseMode2) => {
39914
40418
  StoneBasePoseMode2["PreservePose"] = "preserve-pose";
39915
40419
  return StoneBasePoseMode2;
39916
40420
  })(StoneBasePoseMode || {});
39917
- 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";
40421
+ 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";
39918
40422
  class StudioWorkspace {
39919
40423
  canvas;
39920
40424
  static SVG_NAMESPACE = "http://www.w3.org/2000/svg";
@@ -39925,7 +40429,12 @@ class StudioWorkspace {
39925
40429
  booleanButtonElement;
39926
40430
  simulationButtonElement;
39927
40431
  booleanOperationOverlayElement;
40432
+ booleanOperationSpinnerElement;
39928
40433
  booleanOperationStatusElement;
40434
+ importPercentElement;
40435
+ importProgressElement;
40436
+ importProgressFillElement;
40437
+ importProgressRowElement;
39929
40438
  booleanEditConfirmationElement;
39930
40439
  bottomToolbarElement;
39931
40440
  downloadButtonElement;
@@ -39957,6 +40466,10 @@ class StudioWorkspace {
39957
40466
  const booleanOperationOverlayElement = ownerDocument.createElement("div");
39958
40467
  const booleanOperationSpinnerElement = ownerDocument.createElement("div");
39959
40468
  const booleanOperationStatusElement = ownerDocument.createElement("div");
40469
+ const importPercentElement = ownerDocument.createElement("div");
40470
+ const importProgressElement = ownerDocument.createElement("div");
40471
+ const importProgressFillElement = ownerDocument.createElement("div");
40472
+ const importProgressRowElement = ownerDocument.createElement("div");
39960
40473
  const booleanEditConfirmationElement = ownerDocument.createElement("div");
39961
40474
  const booleanEditConfirmationTextElement = ownerDocument.createElement("p");
39962
40475
  const booleanEditConfirmationCancelButtonElement = ownerDocument.createElement("button");
@@ -40006,10 +40519,24 @@ class StudioWorkspace {
40006
40519
  booleanOperationOverlayElement.hidden = true;
40007
40520
  booleanOperationSpinnerElement.className = "lingbi-studio-boolean-operation-spinner";
40008
40521
  booleanOperationSpinnerElement.setAttribute("aria-hidden", "true");
40522
+ importProgressRowElement.className = "lingbi-studio-import-row";
40523
+ importProgressRowElement.hidden = true;
40524
+ importPercentElement.className = "lingbi-studio-import-percent";
40525
+ importPercentElement.textContent = "0%";
40526
+ importProgressElement.className = "lingbi-studio-import-progress";
40527
+ importProgressElement.setAttribute("aria-label", "模型加载进度");
40528
+ importProgressElement.setAttribute("aria-valuemax", "100");
40529
+ importProgressElement.setAttribute("aria-valuemin", "0");
40530
+ importProgressElement.setAttribute("aria-valuenow", "0");
40531
+ importProgressElement.setAttribute("role", "progressbar");
40532
+ importProgressFillElement.className = "lingbi-studio-import-progress-fill";
40533
+ importProgressElement.append(importProgressFillElement);
40534
+ importProgressRowElement.append(importProgressElement, importPercentElement);
40009
40535
  booleanOperationStatusElement.className = "lingbi-studio-boolean-operation-status";
40010
40536
  booleanOperationStatusElement.textContent = "布尔计算中";
40011
40537
  booleanOperationOverlayElement.append(
40012
40538
  booleanOperationSpinnerElement,
40539
+ importProgressRowElement,
40013
40540
  booleanOperationStatusElement
40014
40541
  );
40015
40542
  booleanEditConfirmationElement.className = "lingbi-studio-boolean-edit-confirmation";
@@ -40137,7 +40664,12 @@ class StudioWorkspace {
40137
40664
  this.booleanButtonElement = booleanButtonElement;
40138
40665
  this.simulationButtonElement = simulationButtonElement;
40139
40666
  this.booleanOperationOverlayElement = booleanOperationOverlayElement;
40667
+ this.booleanOperationSpinnerElement = booleanOperationSpinnerElement;
40140
40668
  this.booleanOperationStatusElement = booleanOperationStatusElement;
40669
+ this.importPercentElement = importPercentElement;
40670
+ this.importProgressElement = importProgressElement;
40671
+ this.importProgressFillElement = importProgressFillElement;
40672
+ this.importProgressRowElement = importProgressRowElement;
40141
40673
  this.booleanEditConfirmationElement = booleanEditConfirmationElement;
40142
40674
  this.bottomToolbarElement = bottomToolbarElement;
40143
40675
  this.downloadButtonElement = downloadButtonElement;
@@ -40260,11 +40792,14 @@ class StudioWorkspace {
40260
40792
  }
40261
40793
  /** Applies Application's combined renderer and download state. */
40262
40794
  setToolbarState(state) {
40795
+ const importInProgress = state.importInProgress ?? false;
40796
+ const importProgress = Math.max(0, Math.min(100, Math.floor(state.importProgress ?? 0)));
40263
40797
  const booleanOperationInProgress = state.booleanInProgress ?? false;
40264
40798
  const downloadInProgress = state.downloadInProgress ?? false;
40265
40799
  const inventoryInProgress = state.inventoryInProgress ?? false;
40266
40800
  const simulationInProgress = state.simulationInProgress ?? false;
40267
- const operationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress || simulationInProgress;
40801
+ const otherOperationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress || simulationInProgress;
40802
+ const operationInProgress = importInProgress || otherOperationInProgress;
40268
40803
  const confirmationVisible = state.booleanEditConfirmationVisible ?? false;
40269
40804
  const modelVisibility = state.modelVisibility;
40270
40805
  this.currentEditActive = state.editActive;
@@ -40288,7 +40823,15 @@ class StudioWorkspace {
40288
40823
  this.booleanButtonElement.setAttribute("aria-busy", String(booleanOperationInProgress));
40289
40824
  this.booleanOperationOverlayElement.hidden = !operationInProgress;
40290
40825
  this.booleanOperationOverlayElement.setAttribute("aria-busy", String(operationInProgress));
40291
- this.booleanOperationStatusElement.textContent = simulationInProgress ? "仿真计算中" : booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
40826
+ this.booleanOperationSpinnerElement.hidden = importInProgress;
40827
+ this.importProgressRowElement.hidden = !importInProgress;
40828
+ if (importInProgress) {
40829
+ const percentText = `${String(importProgress)}%`;
40830
+ this.importProgressElement.setAttribute("aria-valuenow", String(importProgress));
40831
+ this.importProgressFillElement.style.width = percentText;
40832
+ this.importPercentElement.textContent = percentText;
40833
+ }
40834
+ this.booleanOperationStatusElement.textContent = importInProgress ? "模型加载中" : simulationInProgress ? "仿真计算中" : booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
40292
40835
  this.canvas.toggleAttribute("inert", operationInProgress || confirmationVisible);
40293
40836
  this.bottomToolbarElement.toggleAttribute(
40294
40837
  "inert",
@@ -40476,6 +41019,7 @@ class StudioWorkspace {
40476
41019
  class Application {
40477
41020
  archiveImportController;
40478
41021
  archiveImporting = false;
41022
+ archiveImportProgress;
40479
41023
  booleanEditConfirmationVisible = false;
40480
41024
  disposed = false;
40481
41025
  downloadController;
@@ -40488,6 +41032,7 @@ class Application {
40488
41032
  editedArchiveResourceContext;
40489
41033
  importGeneration = 0;
40490
41034
  importPerformanceMode = ImportPerformanceMode.Off;
41035
+ importProgressCompositor = new ImportProgressCompositor();
40491
41036
  inventoryConfirmationController;
40492
41037
  inventoryConfirming = false;
40493
41038
  inventoryError;
@@ -40545,7 +41090,10 @@ class Application {
40545
41090
  this.handleInventorySuccess.bind(this),
40546
41091
  this.handleInventoryStateChanged.bind(this)
40547
41092
  );
40548
- this.archiveImportController = new ArchiveImportWorkerClient();
41093
+ this.archiveImportController = new ArchiveImportWorkerClient(
41094
+ void 0,
41095
+ this.handleArchiveImportProgress.bind(this)
41096
+ );
40549
41097
  this.handleRendererWorkspaceStateChanged(renderer.getWorkspaceState());
40550
41098
  } catch (error2) {
40551
41099
  this.workspace = void 0;
@@ -40631,12 +41179,14 @@ class Application {
40631
41179
  }
40632
41180
  this.nextArchiveTaskId += 1;
40633
41181
  this.archiveImporting = true;
41182
+ this.archiveImportProgress = 0;
40634
41183
  this.downloadError = void 0;
40635
41184
  this.downloadSuccess = void 0;
40636
41185
  this.publishWorkspaceState();
40637
41186
  const controller = this.archiveImportController;
40638
41187
  if (controller === void 0) {
40639
41188
  this.archiveImporting = false;
41189
+ this.archiveImportProgress = void 0;
40640
41190
  this.publishWorkspaceState();
40641
41191
  return Promise.resolve({
40642
41192
  code: ObjImportFailureCode.Unavailable,
@@ -40694,6 +41244,7 @@ class Application {
40694
41244
  cancelArchiveImport() {
40695
41245
  this.archiveImportController?.cancel();
40696
41246
  this.archiveImporting = false;
41247
+ this.archiveImportProgress = void 0;
40697
41248
  this.invalidateImportGeneration();
40698
41249
  }
40699
41250
  async completeModelResourceSceneImport(source, candidate, generation) {
@@ -40764,6 +41315,7 @@ class Application {
40764
41315
  } finally {
40765
41316
  if (this.isCurrentImportGeneration(generation)) {
40766
41317
  this.archiveImporting = false;
41318
+ this.archiveImportProgress = void 0;
40767
41319
  this.publishWorkspaceState();
40768
41320
  }
40769
41321
  }
@@ -40771,6 +41323,7 @@ class Application {
40771
41323
  completeEditedArchiveImportFailure(generation, error2) {
40772
41324
  if (this.isCurrentImportGeneration(generation)) {
40773
41325
  this.archiveImporting = false;
41326
+ this.archiveImportProgress = void 0;
40774
41327
  this.publishWorkspaceState();
40775
41328
  }
40776
41329
  return Promise.resolve(this.mapEditedArchiveError(error2));
@@ -41141,6 +41694,13 @@ class Application {
41141
41694
  }
41142
41695
  this.clearBooleanEditConfirmation();
41143
41696
  }
41697
+ handleArchiveImportProgress(percent) {
41698
+ if (this.disposed || !this.archiveImporting) {
41699
+ return;
41700
+ }
41701
+ this.archiveImportProgress = percent;
41702
+ this.publishWorkspaceState();
41703
+ }
41144
41704
  handleRendererWorkspaceStateChanged(state) {
41145
41705
  if (this.disposed) {
41146
41706
  return;
@@ -41258,6 +41818,10 @@ class Application {
41258
41818
  if (workspace === void 0 || state === void 0) {
41259
41819
  return;
41260
41820
  }
41821
+ const importProgress = this.importProgressCompositor.compose({
41822
+ archiveProgress: this.archiveImportProgress,
41823
+ rendererProgress: state.scene.importProgress
41824
+ });
41261
41825
  const toolbarState = {
41262
41826
  simulationEnabled: state.simulation?.enabled === true && !this.downloading && !this.inventorying && !this.inventoryConfirming && !this.archiveImporting && !this.booleanEditConfirmationVisible,
41263
41827
  simulationInProgress: state.simulation?.inProgress ?? false,
@@ -41275,6 +41839,8 @@ class Application {
41275
41839
  editEnabled: state.edit.enabled && !this.downloading && !this.inventorying && !this.archiveImporting && !this.booleanEditConfirmationVisible,
41276
41840
  editVisible: state.edit.visible,
41277
41841
  ...this.editStatusMessage === void 0 ? {} : { editStatusMessage: this.editStatusMessage },
41842
+ importInProgress: state.scene.importInProgress || this.archiveImporting,
41843
+ ...importProgress === void 0 ? {} : { importProgress },
41278
41844
  inventoryEnabled: this.canInventory(state),
41279
41845
  ...this.inventoryError === void 0 ? {} : { inventoryError: this.inventoryError },
41280
41846
  inventoryInProgress: this.inventorying,
@@ -41320,9 +41886,11 @@ class Application {
41320
41886
  state.scene.available,
41321
41887
  state.scene.contextLost,
41322
41888
  state.scene.importInProgress,
41889
+ state.scene.importProgress ?? null,
41323
41890
  state.scene.revision,
41324
41891
  state.scene.resourceRevision ?? null,
41325
41892
  this.archiveImporting,
41893
+ this.archiveImportProgress ?? null,
41326
41894
  this.downloading,
41327
41895
  this.inventoryConfirming,
41328
41896
  this.inventorying,
@@ -41335,7 +41903,7 @@ class Application {
41335
41903
  ]);
41336
41904
  }
41337
41905
  isSameToolbarState(state, previousState) {
41338
- 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);
41906
+ 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);
41339
41907
  }
41340
41908
  isSameModelVisibility(state, previousState) {
41341
41909
  return state?.baseVisible === previousState?.baseVisible && state?.enabled === previousState?.enabled && state?.stoneVisible === previousState?.stoneVisible;