@lingbi/studio 0.7.0 → 0.8.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
@@ -33617,6 +33617,251 @@ class PlyLoader {
33617
33617
  return value;
33618
33618
  }
33619
33619
  }
33620
+ class SimulationSnapshotFactory {
33621
+ create(stone, base) {
33622
+ const started = performance.now();
33623
+ const stoneRoot = this.root(stone);
33624
+ const baseRoot = this.root(base);
33625
+ stoneRoot.updateMatrixWorld(true);
33626
+ 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
+ }
33640
+ return {
33641
+ stone: stoneInput,
33642
+ base: baseInput,
33643
+ captureMilliseconds: performance.now() - started
33644
+ };
33645
+ }
33646
+ root(object) {
33647
+ let root = object;
33648
+ while (root.parent !== null) root = root.parent;
33649
+ return root;
33650
+ }
33651
+ collect(root) {
33652
+ const positions = [];
33653
+ const indices = [];
33654
+ const point = new Vector3();
33655
+ root.traverse((object) => {
33656
+ if (!(object instanceof Mesh)) return;
33657
+ const mesh = object;
33658
+ if (!mesh.geometry.hasAttribute("position")) {
33659
+ throw new Error("The model has no triangular position attribute.");
33660
+ }
33661
+ const position = mesh.geometry.getAttribute("position");
33662
+ if (position.itemSize < 3) {
33663
+ throw new Error("The model has no triangular position attribute.");
33664
+ }
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
+ );
33696
+ }
33697
+ });
33698
+ return { positions: Float64Array.from(positions), indices: Uint32Array.from(indices) };
33699
+ }
33700
+ }
33701
+ class StabilityMessages {
33702
+ request(value) {
33703
+ if (!this.record(value) || !this.token(value["token"]) || !this.record(value["input"]))
33704
+ return void 0;
33705
+ const input = value["input"];
33706
+ if (!this.mesh(input["stone"]) || !this.mesh(input["base"]) || !this.nonnegative(input["captureMilliseconds"]))
33707
+ return void 0;
33708
+ return value;
33709
+ }
33710
+ reply(value) {
33711
+ if (!this.record(value) || !this.token(value["token"]) || !this.record(value["response"]))
33712
+ return void 0;
33713
+ const response = value["response"];
33714
+ if (response["status"] === "failed" && typeof response["reason"] === "string")
33715
+ return value;
33716
+ if (response["status"] === "completed" && this.result(response["result"]))
33717
+ return value;
33718
+ return void 0;
33719
+ }
33720
+ result(value) {
33721
+ if (!this.record(value) || !["supported", "noSupport", "unbalanced", "marginal"].includes(String(value["outcome"])))
33722
+ return false;
33723
+ for (const key of [
33724
+ "margin",
33725
+ "residual",
33726
+ "contactCount",
33727
+ "candidateCount",
33728
+ "triangleCount",
33729
+ "initializationMilliseconds",
33730
+ "geometryMilliseconds",
33731
+ "solveMilliseconds",
33732
+ "captureMilliseconds",
33733
+ "totalMilliseconds"
33734
+ ]) {
33735
+ if (!this.nonnegative(value[key])) return false;
33736
+ }
33737
+ return value["outcome"] !== "supported" || Number(value["margin"]) > 1e-6 && Number(value["residual"]) <= 1e-7 && Number(value["contactCount"]) > 0;
33738
+ }
33739
+ mesh(value) {
33740
+ return this.record(value) && value["positions"] instanceof Float64Array && value["indices"] instanceof Uint32Array;
33741
+ }
33742
+ record(value) {
33743
+ return typeof value === "object" && value !== null;
33744
+ }
33745
+ token(value) {
33746
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
33747
+ }
33748
+ nonnegative(value) {
33749
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
33750
+ }
33751
+ }
33752
+ class StabilityWorkerFactory {
33753
+ create() {
33754
+ return new Worker(new URL(
33755
+ /* @vite-ignore */
33756
+ "" + new URL("assets/StabilityWorkerEntry-CXCNaUyw.js", import.meta.url).href,
33757
+ import.meta.url
33758
+ ), {
33759
+ type: "module"
33760
+ });
33761
+ }
33762
+ }
33763
+ class StabilityWorkerClient {
33764
+ constructor(factory = new StabilityWorkerFactory()) {
33765
+ this.factory = factory;
33766
+ }
33767
+ factory;
33768
+ worker;
33769
+ pending;
33770
+ token = 0;
33771
+ disposed = false;
33772
+ messages = new StabilityMessages();
33773
+ run(input, started) {
33774
+ if (this.disposed || this.pending !== void 0)
33775
+ return Promise.resolve({ status: "cancelled" });
33776
+ const remaining = 3e4 - (performance.now() - started);
33777
+ if (remaining <= 0)
33778
+ return Promise.resolve({
33779
+ status: "failed",
33780
+ reason: "Simulation timed out during snapshot capture."
33781
+ });
33782
+ return new Promise((resolve) => {
33783
+ const token = ++this.token;
33784
+ const timer = setTimeout(() => {
33785
+ this.fail("Simulation timed out.");
33786
+ }, remaining);
33787
+ this.pending = { token, started, resolve, timer };
33788
+ try {
33789
+ let worker = this.worker;
33790
+ if (worker === void 0) {
33791
+ worker = this.factory.create();
33792
+ this.worker = worker;
33793
+ worker.addEventListener("message", (event) => {
33794
+ if (this.worker !== worker) return;
33795
+ this.message(event.data);
33796
+ });
33797
+ worker.addEventListener("error", () => {
33798
+ if (this.worker === worker) this.fail("Simulation Worker failed.");
33799
+ });
33800
+ worker.addEventListener("messageerror", () => {
33801
+ if (this.worker === worker)
33802
+ this.fail("Simulation Worker message could not be read.");
33803
+ });
33804
+ }
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
+ ]);
33811
+ } catch {
33812
+ this.fail("Simulation Worker could not start.");
33813
+ }
33814
+ });
33815
+ }
33816
+ cancel() {
33817
+ this.terminate();
33818
+ this.finish({ status: "cancelled" });
33819
+ }
33820
+ dispose() {
33821
+ this.disposed = true;
33822
+ this.cancel();
33823
+ }
33824
+ message(value) {
33825
+ const reply = this.messages.reply(value);
33826
+ if (reply === void 0) {
33827
+ this.fail("Invalid simulation Worker response.");
33828
+ return;
33829
+ }
33830
+ const pending = this.pending;
33831
+ if (reply.token !== pending?.token) return;
33832
+ if (performance.now() - pending.started >= 3e4) {
33833
+ this.fail("Simulation timed out.");
33834
+ return;
33835
+ }
33836
+ if (reply.response.status === "failed") {
33837
+ this.fail(reply.response.reason);
33838
+ return;
33839
+ }
33840
+ this.finish({
33841
+ status: "completed",
33842
+ result: {
33843
+ ...reply.response.result,
33844
+ totalMilliseconds: performance.now() - pending.started
33845
+ }
33846
+ });
33847
+ }
33848
+ fail(reason) {
33849
+ this.terminate();
33850
+ this.finish({ status: "failed", reason });
33851
+ }
33852
+ finish(response) {
33853
+ const pending = this.pending;
33854
+ this.pending = void 0;
33855
+ if (pending === void 0) return;
33856
+ clearTimeout(pending.timer);
33857
+ pending.resolve(response);
33858
+ }
33859
+ terminate() {
33860
+ const worker = this.worker;
33861
+ this.worker = void 0;
33862
+ worker?.terminate();
33863
+ }
33864
+ }
33620
33865
  var StoneBasePoseMode$1 = /* @__PURE__ */ ((StoneBasePoseMode2) => {
33621
33866
  StoneBasePoseMode2["AutoCorrect"] = "auto-correct";
33622
33867
  StoneBasePoseMode2["PreservePose"] = "preserve-pose";
@@ -35580,6 +35825,9 @@ class Renderer {
35580
35825
  contextRestoreListener;
35581
35826
  contextLost = false;
35582
35827
  booleanOperationInProgress = false;
35828
+ simulationInProgress = false;
35829
+ simulationToken = 0;
35830
+ simulationWorker = new StabilityWorkerClient();
35583
35831
  booleanOperationWorkerClient;
35584
35832
  controller;
35585
35833
  directionalLight;
@@ -35736,6 +35984,8 @@ class Renderer {
35736
35984
  }
35737
35985
  this.cancelBooleanOperation();
35738
35986
  this.booleanOperationWorkerClient.dispose();
35987
+ this.cancelSimulation();
35988
+ this.simulationWorker.dispose();
35739
35989
  this.disposed = true;
35740
35990
  this.invalidateActiveImport(ObjImportFailureCode$1.Cancelled);
35741
35991
  this.cancelPendingFirstFramePerformance("cancelled");
@@ -35876,6 +36126,10 @@ class Renderer {
35876
36126
  const interactionInProgress = this.canvasController.isInteractionActive();
35877
36127
  const dualModelReady = this.isDualModelSceneReady();
35878
36128
  return {
36129
+ simulation: {
36130
+ enabled: dualModelReady && !this.booleanOperationInProgress && !this.simulationInProgress,
36131
+ inProgress: this.simulationInProgress
36132
+ },
35879
36133
  booleanOperation: {
35880
36134
  enabled: this.isBooleanOperationReady(),
35881
36135
  inProgress: this.booleanOperationInProgress,
@@ -35885,7 +36139,7 @@ class Renderer {
35885
36139
  },
35886
36140
  edit: {
35887
36141
  active: this.editingActive,
35888
- enabled: dualModelReady && !interactionInProgress,
36142
+ enabled: dualModelReady && !interactionInProgress && !this.simulationInProgress,
35889
36143
  visible: editVisible
35890
36144
  },
35891
36145
  ...dualModelReady ? {
@@ -35995,6 +36249,7 @@ class Renderer {
35995
36249
  */
35996
36250
  toggleEditMode() {
35997
36251
  this.assertNotDisposed();
36252
+ if (this.simulationInProgress) return;
35998
36253
  if (this.editingActive) {
35999
36254
  this.exitEditMode();
36000
36255
  return;
@@ -36011,7 +36266,7 @@ class Renderer {
36011
36266
  this.assertNotDisposed();
36012
36267
  const history = this.booleanEditHistory;
36013
36268
  const sourceSnapshot = this.booleanEditSourceSnapshot;
36014
- if (history === void 0 || sourceSnapshot === void 0 || this.editingActive || this.booleanOperationInProgress || !this.isDualModelSceneReady()) {
36269
+ if (history === void 0 || sourceSnapshot === void 0 || this.editingActive || this.booleanOperationInProgress || this.simulationInProgress || !this.isDualModelSceneReady()) {
36015
36270
  return false;
36016
36271
  }
36017
36272
  let restoredStone;
@@ -36080,7 +36335,7 @@ class Renderer {
36080
36335
  /** Leaves edit mode and guarantees that the selected gizmo is detached. */
36081
36336
  exitEditMode() {
36082
36337
  this.assertNotDisposed();
36083
- if (!this.editingActive) {
36338
+ if (!this.editingActive || this.simulationInProgress) {
36084
36339
  return;
36085
36340
  }
36086
36341
  const wasReediting = this.booleanEditSessionActive;
@@ -36096,6 +36351,72 @@ class Renderer {
36096
36351
  this.abortEditingState();
36097
36352
  this.notifyWorkspaceState();
36098
36353
  }
36354
+ /** Internal Studio bridge; snapshots the current placement without changing Boolean history. */
36355
+ async requestSimulation() {
36356
+ if (!this.isDualModelSceneReady() || this.booleanOperationInProgress || this.simulationInProgress) {
36357
+ return { status: "cancelled" };
36358
+ }
36359
+ const started = performance.now();
36360
+ const token = ++this.simulationToken;
36361
+ const revision = this.sceneRevision;
36362
+ this.simulationInProgress = true;
36363
+ try {
36364
+ this.gizmoController.end();
36365
+ this.canvasController.cancelInteraction(true, "pointercancel");
36366
+ this.notifyWorkspaceState();
36367
+ const stone = this.activeStoneModel;
36368
+ const base = this.activeBaseModel;
36369
+ if (stone === void 0 || base === void 0) return { status: "cancelled" };
36370
+ if (this.areModelsDefinitelySeparated(stone, base)) {
36371
+ const response2 = {
36372
+ status: "completed",
36373
+ result: {
36374
+ outcome: "noSupport",
36375
+ margin: 0,
36376
+ residual: 0,
36377
+ contactCount: 0,
36378
+ candidateCount: 0,
36379
+ triangleCount: 0,
36380
+ initializationMilliseconds: 0,
36381
+ geometryMilliseconds: performance.now() - started,
36382
+ solveMilliseconds: 0,
36383
+ captureMilliseconds: 0,
36384
+ totalMilliseconds: performance.now() - started
36385
+ }
36386
+ };
36387
+ console.info("[stone-stability]", response2);
36388
+ return response2;
36389
+ }
36390
+ const input = new SimulationSnapshotFactory().create(stone.object, base.object);
36391
+ const response = await this.simulationWorker.run(input, started);
36392
+ if (token !== this.simulationToken || revision !== this.sceneRevision || !this.isDualModelSceneReady()) {
36393
+ return { status: "cancelled" };
36394
+ }
36395
+ console.info("[stone-stability]", response);
36396
+ return response;
36397
+ } catch (error2) {
36398
+ const reason = error2 instanceof Error ? error2.message : "Simulation failed.";
36399
+ console.info("[stone-stability]", { status: "failed", reason });
36400
+ return { status: "failed", reason };
36401
+ } finally {
36402
+ if (token === this.simulationToken) {
36403
+ this.simulationInProgress = false;
36404
+ this.notifyWorkspaceState();
36405
+ }
36406
+ }
36407
+ }
36408
+ cancelSimulation() {
36409
+ const wasActive = this.simulationInProgress;
36410
+ this.simulationToken++;
36411
+ this.simulationInProgress = false;
36412
+ this.simulationWorker.cancel();
36413
+ if (wasActive && !this.disposed) this.notifyWorkspaceState();
36414
+ }
36415
+ areModelsDefinitelySeparated(stone, base) {
36416
+ const stoneBounds = stone.bounds;
36417
+ const baseBounds = base.bounds;
36418
+ return stoneBounds.max.x < baseBounds.min.x || baseBounds.max.x < stoneBounds.min.x || stoneBounds.max.y < baseBounds.min.y || baseBounds.max.y < stoneBounds.min.y || stoneBounds.max.z < baseBounds.min.z || baseBounds.max.z < stoneBounds.min.z;
36419
+ }
36099
36420
  /** Starts the fixed base-minus-stone operation for the Studio Boolean button. */
36100
36421
  requestBooleanOperation() {
36101
36422
  this.assertNotDisposed();
@@ -36675,6 +36996,7 @@ class Renderer {
36675
36996
  return;
36676
36997
  }
36677
36998
  this.contextLost = true;
36999
+ this.cancelSimulation();
36678
37000
  this.cancelBooleanOperation();
36679
37001
  this.clearBooleanHistory();
36680
37002
  this.booleanResultInvalidated = true;
@@ -36721,10 +37043,10 @@ class Renderer {
36721
37043
  return !this.disposed && !this.contextLost && this.activeImport === void 0 && this.activeStoneModel !== void 0 && this.activeBaseModel !== void 0;
36722
37044
  }
36723
37045
  isBooleanOperationReady() {
36724
- return this.isDualModelSceneReady() && this.editingActive && !this.canvasController.isInteractionActive() && !this.booleanOperationInProgress && this.modelVisibility.stoneVisible && this.modelVisibility.baseVisible;
37046
+ return this.isDualModelSceneReady() && this.editingActive && !this.canvasController.isInteractionActive() && !this.booleanOperationInProgress && !this.simulationInProgress && this.modelVisibility.stoneVisible && this.modelVisibility.baseVisible;
36725
37047
  }
36726
37048
  isModelVisibilityEnabled() {
36727
- return this.isDualModelSceneReady() && !this.canvasController.isInteractionActive() && !this.booleanOperationInProgress;
37049
+ return this.isDualModelSceneReady() && !this.canvasController.isInteractionActive() && !this.booleanOperationInProgress && !this.simulationInProgress;
36728
37050
  }
36729
37051
  isCurrentBooleanOperation(revision, operationToken) {
36730
37052
  return this.activeBooleanOperationRevision === revision && this.activeBooleanOperationToken === operationToken && this.booleanOperationInProgress && this.sceneRevision === revision && this.editingActive && this.activeBooleanOperationBaseModel === this.activeBaseModel && this.activeBooleanOperationStoneModel === this.activeStoneModel && this.isDualModelSceneReady() && (this.booleanEditSessionActive ? this.activeBooleanOperationSessionToken === this.booleanEditSessionToken : this.activeBooleanOperationSessionToken === void 0);
@@ -37177,6 +37499,7 @@ class Renderer {
37177
37499
  }
37178
37500
  }
37179
37501
  startImportSession(performanceTrace) {
37502
+ this.cancelSimulation();
37180
37503
  this.cancelBooleanOperation();
37181
37504
  this.cancelPendingFirstFramePerformance("cancelled");
37182
37505
  this.invalidateActiveImport(ObjImportFailureCode$1.Cancelled);
@@ -39591,7 +39914,7 @@ var StoneBasePoseMode = /* @__PURE__ */ ((StoneBasePoseMode2) => {
39591
39914
  StoneBasePoseMode2["PreservePose"] = "preserve-pose";
39592
39915
  return StoneBasePoseMode2;
39593
39916
  })(StoneBasePoseMode || {});
39594
- 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(3, minmax(0, 1fr));\n width: min(360px, 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(4, minmax(0, 1fr));\n width: min(480px, 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(4, minmax(0, 1fr));\n width: min(480px, calc(100% - 32px));\n}\n\n.lingbi-studio-bottom-toolbar.has-model-control.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-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-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-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-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-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-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-edit-button:not(:disabled):hover {\n color: #f06400;\n }\n\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-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-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-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";
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-edit-button:not(:disabled):hover {\n color: #f06400;\n }\n\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";
39595
39918
  class StudioWorkspace {
39596
39919
  canvas;
39597
39920
  static SVG_NAMESPACE = "http://www.w3.org/2000/svg";
@@ -39600,6 +39923,7 @@ class StudioWorkspace {
39600
39923
  static BOTTOM_TOOLBAR_VIEWPORT_GAP = 8;
39601
39924
  static BOTTOM_TOOLBAR_VIEWPORT_BOTTOM_INSET = StudioWorkspace.BOTTOM_TOOLBAR_BOTTOM_OFFSET + StudioWorkspace.BOTTOM_TOOLBAR_HEIGHT + StudioWorkspace.BOTTOM_TOOLBAR_VIEWPORT_GAP;
39602
39925
  booleanButtonElement;
39926
+ simulationButtonElement;
39603
39927
  booleanOperationOverlayElement;
39604
39928
  booleanOperationStatusElement;
39605
39929
  booleanEditConfirmationElement;
@@ -39622,13 +39946,14 @@ class StudioWorkspace {
39622
39946
  styleElement;
39623
39947
  currentEditActive = false;
39624
39948
  modelPanelVisible = false;
39625
- constructor(container, onEditRequested, onEditExited, onDownloadRequested, onBooleanRequested, onBooleanEditConfirmed, onBooleanEditCancelled, onModelVisibilityRequested, onInventoryRequested, onEditOperationBlocked) {
39949
+ constructor(container, onEditRequested, onEditExited, onDownloadRequested, onBooleanRequested, onBooleanEditConfirmed, onBooleanEditCancelled, onModelVisibilityRequested, onInventoryRequested, onEditOperationBlocked, onSimulationRequested) {
39626
39950
  const ownerDocument = container.ownerDocument;
39627
39951
  const rootElement = ownerDocument.createElement("section");
39628
39952
  const viewportElement = ownerDocument.createElement("div");
39629
39953
  const canvas = ownerDocument.createElement("canvas");
39630
39954
  const bottomToolbarElement = ownerDocument.createElement("div");
39631
39955
  const booleanButtonElement = ownerDocument.createElement("button");
39956
+ const simulationButtonElement = ownerDocument.createElement("button");
39632
39957
  const booleanOperationOverlayElement = ownerDocument.createElement("div");
39633
39958
  const booleanOperationSpinnerElement = ownerDocument.createElement("div");
39634
39959
  const booleanOperationStatusElement = ownerDocument.createElement("div");
@@ -39670,6 +39995,11 @@ class StudioWorkspace {
39670
39995
  booleanButtonElement.hidden = true;
39671
39996
  booleanButtonElement.type = "button";
39672
39997
  booleanButtonElement.textContent = "布尔";
39998
+ simulationButtonElement.className = "lingbi-studio-simulation-button";
39999
+ simulationButtonElement.disabled = true;
40000
+ simulationButtonElement.hidden = true;
40001
+ simulationButtonElement.type = "button";
40002
+ simulationButtonElement.textContent = "仿真";
39673
40003
  booleanOperationOverlayElement.className = "lingbi-studio-boolean-operation-overlay";
39674
40004
  booleanOperationOverlayElement.setAttribute("aria-live", "polite");
39675
40005
  booleanOperationOverlayElement.setAttribute("role", "status");
@@ -39734,6 +40064,7 @@ class StudioWorkspace {
39734
40064
  modelButtonElement,
39735
40065
  editButtonElement,
39736
40066
  booleanButtonElement,
40067
+ simulationButtonElement,
39737
40068
  downloadButtonWrapperElement
39738
40069
  );
39739
40070
  downloadStatusElement.className = "lingbi-studio-download-status";
@@ -39804,6 +40135,7 @@ class StudioWorkspace {
39804
40135
  }
39805
40136
  this.canvas = canvas;
39806
40137
  this.booleanButtonElement = booleanButtonElement;
40138
+ this.simulationButtonElement = simulationButtonElement;
39807
40139
  this.booleanOperationOverlayElement = booleanOperationOverlayElement;
39808
40140
  this.booleanOperationStatusElement = booleanOperationStatusElement;
39809
40141
  this.booleanEditConfirmationElement = booleanEditConfirmationElement;
@@ -39831,6 +40163,13 @@ class StudioWorkspace {
39831
40163
  this.wrapClick(onEditRequested),
39832
40164
  eventListenerOptions
39833
40165
  );
40166
+ if (onSimulationRequested !== void 0) {
40167
+ simulationButtonElement.addEventListener(
40168
+ "click",
40169
+ this.wrapClick(onSimulationRequested),
40170
+ eventListenerOptions
40171
+ );
40172
+ }
39834
40173
  if (onBooleanRequested !== void 0) {
39835
40174
  booleanButtonElement.addEventListener(
39836
40175
  "click",
@@ -39924,7 +40263,8 @@ class StudioWorkspace {
39924
40263
  const booleanOperationInProgress = state.booleanInProgress ?? false;
39925
40264
  const downloadInProgress = state.downloadInProgress ?? false;
39926
40265
  const inventoryInProgress = state.inventoryInProgress ?? false;
39927
- const operationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress;
40266
+ const simulationInProgress = state.simulationInProgress ?? false;
40267
+ const operationInProgress = booleanOperationInProgress || downloadInProgress || inventoryInProgress || simulationInProgress;
39928
40268
  const confirmationVisible = state.booleanEditConfirmationVisible ?? false;
39929
40269
  const modelVisibility = state.modelVisibility;
39930
40270
  this.currentEditActive = state.editActive;
@@ -39942,10 +40282,13 @@ class StudioWorkspace {
39942
40282
  state.inventoryVisible === true
39943
40283
  );
39944
40284
  this.booleanButtonElement.disabled = !state.booleanEnabled;
40285
+ this.simulationButtonElement.disabled = state.simulationEnabled !== true;
40286
+ this.simulationButtonElement.hidden = !state.editVisible;
40287
+ this.simulationButtonElement.setAttribute("aria-busy", String(simulationInProgress));
39945
40288
  this.booleanButtonElement.setAttribute("aria-busy", String(booleanOperationInProgress));
39946
40289
  this.booleanOperationOverlayElement.hidden = !operationInProgress;
39947
40290
  this.booleanOperationOverlayElement.setAttribute("aria-busy", String(operationInProgress));
39948
- this.booleanOperationStatusElement.textContent = booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
40291
+ this.booleanOperationStatusElement.textContent = simulationInProgress ? "仿真计算中" : booleanOperationInProgress ? "布尔计算中" : inventoryInProgress ? "入库处理中" : "下载处理中";
39949
40292
  this.canvas.toggleAttribute("inert", operationInProgress || confirmationVisible);
39950
40293
  this.bottomToolbarElement.toggleAttribute(
39951
40294
  "inert",
@@ -39977,11 +40320,12 @@ class StudioWorkspace {
39977
40320
  );
39978
40321
  this.downloadButtonElement.disabled = !state.downloadEnabled;
39979
40322
  this.downloadButtonElement.setAttribute("aria-busy", String(downloadInProgress));
39980
- const operationError = state.inventoryError ?? state.downloadError;
40323
+ const operationError = state.simulationOutcome === "failure" ? "静态仿真模拟失败!检测石头可能会倾倒,请重新编辑再进行仿真" : state.inventoryError ?? state.downloadError;
40324
+ const operationSuccess = state.simulationOutcome === "success" ? "静态仿真模拟成功!检测石头不会倾倒" : state.downloadSuccess;
39981
40325
  this.downloadStatusElement.hidden = operationError === void 0;
39982
40326
  this.downloadStatusElement.textContent = operationError ?? "";
39983
- this.downloadSuccessElement.hidden = state.downloadSuccess === void 0;
39984
- this.downloadSuccessElement.textContent = state.downloadSuccess ?? "";
40327
+ this.downloadSuccessElement.hidden = operationSuccess === void 0;
40328
+ this.downloadSuccessElement.textContent = operationSuccess ?? "";
39985
40329
  this.editButtonElement.disabled = !state.editVisible || !state.editEnabled || state.editActive;
39986
40330
  this.editButtonElement.hidden = !state.editVisible;
39987
40331
  this.editButtonElement.classList.toggle("is-active", state.editVisible && state.editActive);
@@ -40137,6 +40481,7 @@ class Application {
40137
40481
  downloadController;
40138
40482
  downloadError;
40139
40483
  downloadSuccess;
40484
+ simulationOutcome;
40140
40485
  downloading = false;
40141
40486
  exportContextFactory = new ModelExportContextFactory();
40142
40487
  exportSceneContext;
@@ -40175,7 +40520,8 @@ class Application {
40175
40520
  this.handleBooleanEditCancelled.bind(this),
40176
40521
  this.handleModelVisibilityRequested.bind(this),
40177
40522
  this.handleInventoryRequested.bind(this),
40178
- this.handleEditOperationBlocked.bind(this)
40523
+ this.handleEditOperationBlocked.bind(this),
40524
+ this.handleSimulationRequested.bind(this)
40179
40525
  );
40180
40526
  this.workspace = workspace;
40181
40527
  try {
@@ -40678,6 +41024,22 @@ class Application {
40678
41024
  }
40679
41025
  renderer.requestBooleanOperation();
40680
41026
  }
41027
+ handleSimulationRequested() {
41028
+ const renderer = this.renderer;
41029
+ if (renderer === void 0 || this.disposed || this.downloading || this.inventorying || this.inventoryConfirming || this.archiveImporting || this.booleanEditConfirmationVisible || this.latestRendererWorkspaceState?.simulation?.enabled !== true)
41030
+ return;
41031
+ this.simulationOutcome = void 0;
41032
+ this.downloadError = void 0;
41033
+ this.downloadSuccess = void 0;
41034
+ this.inventoryError = void 0;
41035
+ const generation = this.importGeneration;
41036
+ void renderer.requestSimulation().then((response) => {
41037
+ if (this.disposed || generation !== this.importGeneration || response.status === "cancelled")
41038
+ return;
41039
+ this.simulationOutcome = response.status === "completed" && response.result.outcome === "supported" ? "success" : "failure";
41040
+ this.publishWorkspaceState();
41041
+ });
41042
+ }
40681
41043
  handleModelVisibilityRequested(role, visible) {
40682
41044
  if (this.disposed || this.downloading || this.inventorying || this.archiveImporting || this.booleanEditConfirmationVisible) {
40683
41045
  return;
@@ -40711,6 +41073,7 @@ class Application {
40711
41073
  return;
40712
41074
  }
40713
41075
  if (downloading) {
41076
+ this.simulationOutcome = void 0;
40714
41077
  this.booleanEditConfirmationVisible = false;
40715
41078
  this.inventoryError = void 0;
40716
41079
  }
@@ -40722,6 +41085,7 @@ class Application {
40722
41085
  return;
40723
41086
  }
40724
41087
  if (inventorying) {
41088
+ this.simulationOutcome = void 0;
40725
41089
  this.booleanEditConfirmationVisible = false;
40726
41090
  this.editStatusMessage = void 0;
40727
41091
  this.downloadError = void 0;
@@ -40781,6 +41145,9 @@ class Application {
40781
41145
  if (this.disposed) {
40782
41146
  return;
40783
41147
  }
41148
+ if (state.scene.contextLost || state.scene.importInProgress || state.scene.interactionInProgress || state.booleanOperation?.inProgress === true || state.scene.revision !== this.latestRendererWorkspaceState?.scene.revision) {
41149
+ this.simulationOutcome = void 0;
41150
+ }
40784
41151
  this.latestRendererWorkspaceState = state;
40785
41152
  if (!state.edit.active) {
40786
41153
  this.editStatusMessage = void 0;
@@ -40892,6 +41259,9 @@ class Application {
40892
41259
  return;
40893
41260
  }
40894
41261
  const toolbarState = {
41262
+ simulationEnabled: state.simulation?.enabled === true && !this.downloading && !this.inventorying && !this.inventoryConfirming && !this.archiveImporting && !this.booleanEditConfirmationVisible,
41263
+ simulationInProgress: state.simulation?.inProgress ?? false,
41264
+ ...this.simulationOutcome === void 0 ? {} : { simulationOutcome: this.simulationOutcome },
40895
41265
  booleanEnabled: state.booleanOperation?.enabled ?? false,
40896
41266
  booleanInProgress: state.booleanOperation?.inProgress ?? false,
40897
41267
  booleanReediting: state.booleanOperation?.reediting ?? false,
@@ -40935,6 +41305,8 @@ class Application {
40935
41305
  }
40936
41306
  createToolbarBusinessStateKey(state) {
40937
41307
  return JSON.stringify([
41308
+ state.simulation?.inProgress ?? false,
41309
+ this.simulationOutcome ?? null,
40938
41310
  state.booleanOperation?.hasResult ?? false,
40939
41311
  state.booleanOperation?.inProgress ?? false,
40940
41312
  state.booleanOperation?.reediting ?? false,
@@ -40963,7 +41335,7 @@ class Application {
40963
41335
  ]);
40964
41336
  }
40965
41337
  isSameToolbarState(state, previousState) {
40966
- return previousState !== void 0 && 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);
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);
40967
41339
  }
40968
41340
  isSameModelVisibility(state, previousState) {
40969
41341
  return state?.baseVisible === previousState?.baseVisible && state?.enabled === previousState?.enabled && state?.stoneVisible === previousState?.stoneVisible;
@@ -40979,14 +41351,14 @@ class Application {
40979
41351
  }
40980
41352
  isInventorySceneReady(state) {
40981
41353
  const context = this.exportSceneContext;
40982
- return this.isInventoryVisible(state) && context?.revision === (state.scene.resourceRevision ?? state.scene.revision) && !this.archiveImporting && state.scene.available && !state.scene.contextLost && !state.scene.importInProgress && !state.scene.interactionInProgress && !state.edit.active && !(state.booleanOperation?.reediting ?? false) && !(state.booleanOperation?.inProgress ?? false) && !this.booleanEditConfirmationVisible;
41354
+ return this.isInventoryVisible(state) && context?.revision === (state.scene.resourceRevision ?? state.scene.revision) && !this.archiveImporting && state.scene.available && !state.scene.contextLost && !state.scene.importInProgress && !state.scene.interactionInProgress && !state.edit.active && !(state.booleanOperation?.reediting ?? false) && !(state.booleanOperation?.inProgress ?? false) && !(state.simulation?.inProgress ?? false) && !this.booleanEditConfirmationVisible;
40983
41355
  }
40984
41356
  isInventoryVisible(state) {
40985
41357
  return this.exportSceneContext?.base !== void 0 && state.modelVisibility !== void 0 && state.scene.available && !state.scene.contextLost;
40986
41358
  }
40987
41359
  isExportReady(state) {
40988
41360
  const context = this.exportSceneContext;
40989
- return context?.revision === (state.scene.resourceRevision ?? state.scene.revision) && !this.archiveImporting && !this.inventorying && state.scene.available && !state.scene.contextLost && !state.scene.importInProgress && !state.scene.interactionInProgress && !state.edit.active && !(state.booleanOperation?.reediting ?? false) && !(state.booleanOperation?.inProgress ?? false) && (state.modelVisibility === void 0 || state.modelVisibility.stoneVisible || state.modelVisibility.baseVisible) && !this.booleanEditConfirmationVisible;
41361
+ return context?.revision === (state.scene.resourceRevision ?? state.scene.revision) && !this.archiveImporting && !this.inventorying && state.scene.available && !state.scene.contextLost && !state.scene.importInProgress && !state.scene.interactionInProgress && !state.edit.active && !(state.booleanOperation?.reediting ?? false) && !(state.booleanOperation?.inProgress ?? false) && !(state.simulation?.inProgress ?? false) && (state.modelVisibility === void 0 || state.modelVisibility.stoneVisible || state.modelVisibility.baseVisible) && !this.booleanEditConfirmationVisible;
40990
41362
  }
40991
41363
  assertNotDisposed() {
40992
41364
  if (this.disposed) {
@@ -41028,6 +41400,9 @@ class Application {
41028
41400
  };
41029
41401
  }
41030
41402
  startImportGeneration() {
41403
+ if (this.latestRendererWorkspaceState?.simulation?.inProgress === true)
41404
+ this.renderer?.cancelSimulation();
41405
+ this.simulationOutcome = void 0;
41031
41406
  this.invalidateImportGeneration();
41032
41407
  return this.importGeneration;
41033
41408
  }