@omrips/viewstl 2.0.0-rc.8 → 2.0.0-rc.9
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/README.md +40 -38
- package/build/stl_viewer.min.js +6 -6
- package/build/viewstl.cjs +477 -2369
- package/build/viewstl.d.cts +95 -45
- package/build/viewstl.d.ts +95 -45
- package/build/viewstl.js +477 -2369
- package/build/viewstl.min.js +6 -6
- package/docs/releases/2.0.0-rc.9.md +20 -0
- package/docs/v2-usage.md +945 -993
- package/docs/v2-verification.md +31 -0
- package/package.json +2 -2
- package/docs/releases/2.0.0-rc.8.md +0 -11
package/build/viewstl.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//=========== Stl Viewer v2.0.0-rc.
|
|
1
|
+
//=========== Stl Viewer v2.0.0-rc.9, by Omri Rips, Viewstl.com, November 2026 ; admin@viewstl.com ===========
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -21,8 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var commonjs_exports = {};
|
|
22
22
|
__export(commonjs_exports, {
|
|
23
23
|
StlViewer: () => StlViewer,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
ViewerSession: () => ViewerSession,
|
|
25
|
+
default: () => viewer_default
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(commonjs_exports);
|
|
28
28
|
|
|
@@ -1733,8 +1733,8 @@ function importScene(source, options = {}) {
|
|
|
1733
1733
|
await this.when_ready;
|
|
1734
1734
|
check();
|
|
1735
1735
|
oldCamera = this.get_camera_state();
|
|
1736
|
-
const
|
|
1737
|
-
if (!["vsb", "vsj"].includes(
|
|
1736
|
+
const format = options.format || sceneFormat(typeof source === "string" ? source : source.name);
|
|
1737
|
+
if (!["vsb", "vsj"].includes(format))
|
|
1738
1738
|
throw new Error("Choose VSB or VSJ for a scene");
|
|
1739
1739
|
let remote, relay;
|
|
1740
1740
|
if (typeof source === "string") {
|
|
@@ -1767,8 +1767,8 @@ function importScene(source, options = {}) {
|
|
|
1767
1767
|
source,
|
|
1768
1768
|
remote,
|
|
1769
1769
|
relay,
|
|
1770
|
-
format
|
|
1771
|
-
name: `scene.${
|
|
1770
|
+
format,
|
|
1771
|
+
name: `scene.${format}`,
|
|
1772
1772
|
limits
|
|
1773
1773
|
},
|
|
1774
1774
|
(p) => {
|
|
@@ -2129,8 +2129,8 @@ function exportFilename(filename, extension) {
|
|
|
2129
2129
|
function exportOptions(options = {}) {
|
|
2130
2130
|
const aliases = { stp: "step", igs: "iges" };
|
|
2131
2131
|
const requested = String(options.format || "").toLowerCase();
|
|
2132
|
-
const
|
|
2133
|
-
if (!["stl", "obj", "3mf", "step", "iges"].includes(
|
|
2132
|
+
const format = aliases[requested] || requested;
|
|
2133
|
+
if (!["stl", "obj", "3mf", "step", "iges"].includes(format))
|
|
2134
2134
|
throw new Error("Export format must be stl, obj, 3mf, step or iges");
|
|
2135
2135
|
const units = options.units ?? "mm";
|
|
2136
2136
|
if (!["mm", "inch"].includes(units))
|
|
@@ -2140,10 +2140,10 @@ function exportOptions(options = {}) {
|
|
|
2140
2140
|
throw new Error(`${key} must be a boolean`);
|
|
2141
2141
|
if (options.on_progress !== void 0 && typeof options.on_progress !== "function")
|
|
2142
2142
|
throw new Error("on_progress must be a function");
|
|
2143
|
-
const colors = options.colors !== false && ["obj", "3mf"].includes(
|
|
2144
|
-
const extension =
|
|
2143
|
+
const colors = options.colors !== false && ["obj", "3mf"].includes(format);
|
|
2144
|
+
const extension = format === "obj" && colors ? "zip" : format;
|
|
2145
2145
|
return {
|
|
2146
|
-
format
|
|
2146
|
+
format,
|
|
2147
2147
|
units,
|
|
2148
2148
|
colors,
|
|
2149
2149
|
apply_transforms: options.apply_transforms !== false,
|
|
@@ -2370,7 +2370,7 @@ function modelOptions(input) {
|
|
|
2370
2370
|
return out;
|
|
2371
2371
|
}
|
|
2372
2372
|
var StlViewer = class _StlViewer {
|
|
2373
|
-
static version = "2.0.0-rc.
|
|
2373
|
+
static version = "2.0.0-rc.9";
|
|
2374
2374
|
static workerURL = null;
|
|
2375
2375
|
constructor(parent, options = {}) {
|
|
2376
2376
|
if (!parent?.appendChild)
|
|
@@ -3624,8 +3624,8 @@ var StlViewer = class _StlViewer {
|
|
|
3624
3624
|
);
|
|
3625
3625
|
}
|
|
3626
3626
|
async export_scene(options = {}) {
|
|
3627
|
-
const
|
|
3628
|
-
if (!["vsb", "vsj"].includes(
|
|
3627
|
+
const format = String(options.format || "").toLowerCase();
|
|
3628
|
+
if (!["vsb", "vsj"].includes(format))
|
|
3629
3629
|
throw new Error("Scene format must be vsb or vsj");
|
|
3630
3630
|
if (options.force_basename !== void 0 && typeof options.force_basename !== "boolean")
|
|
3631
3631
|
throw new Error("force_basename must be a boolean");
|
|
@@ -3648,7 +3648,7 @@ var StlViewer = class _StlViewer {
|
|
|
3648
3648
|
if (!models.length) throw new Error("No models to export");
|
|
3649
3649
|
if (models.some((m) => !m.geometry))
|
|
3650
3650
|
throw new Error("A model has not finished loading");
|
|
3651
|
-
if (models.length + (
|
|
3651
|
+
if (models.length + (format === "vsb" ? 1 : 0) > this.limits.max_archive_entries)
|
|
3652
3652
|
throw new Error("Scene model count limit exceeded");
|
|
3653
3653
|
const triangles = models.reduce((sum, m) => sum + m.geometry.triangles, 0);
|
|
3654
3654
|
if (triangles > this.limits.max_triangles)
|
|
@@ -3656,19 +3656,19 @@ var StlViewer = class _StlViewer {
|
|
|
3656
3656
|
const scene = this.get_vsj(
|
|
3657
3657
|
true,
|
|
3658
3658
|
!!options.force_basename,
|
|
3659
|
-
|
|
3659
|
+
format === "vsb"
|
|
3660
3660
|
);
|
|
3661
3661
|
const json = JSON.stringify(scene), manifestBytes = new Blob([json]).size;
|
|
3662
3662
|
if (manifestBytes > this.limits.max_file_bytes)
|
|
3663
3663
|
throw new Error("Scene manifest size limit exceeded");
|
|
3664
|
-
if (
|
|
3664
|
+
if (format === "vsb" && manifestBytes + models.length * 84 + triangles * 50 > this.limits.max_uncompressed_bytes)
|
|
3665
3665
|
throw new Error("Scene archive limit exceeded");
|
|
3666
|
-
const working = MEMORY_OVERHEAD + json.length * 8 + (
|
|
3666
|
+
const working = MEMORY_OVERHEAD + json.length * 8 + (format === "vsb" ? triangles * 512 : 0);
|
|
3667
3667
|
assertMemory(this.limits, this._occupiedMemory() + working, "scene export");
|
|
3668
3668
|
this._exportBytes += working;
|
|
3669
3669
|
let job, cancel, callbackError;
|
|
3670
3670
|
try {
|
|
3671
|
-
const snapshot =
|
|
3671
|
+
const snapshot = format === "vsb" ? models.map((m, index) => ({
|
|
3672
3672
|
id: m.id,
|
|
3673
3673
|
triangles: m.geometry.triangles,
|
|
3674
3674
|
positions: m.geometry.positions.slice(),
|
|
@@ -3683,7 +3683,7 @@ var StlViewer = class _StlViewer {
|
|
|
3683
3683
|
});
|
|
3684
3684
|
checkAbort();
|
|
3685
3685
|
let blob;
|
|
3686
|
-
if (
|
|
3686
|
+
if (format === "vsj") {
|
|
3687
3687
|
blob = new Blob([json], { type: "application/json" });
|
|
3688
3688
|
options.on_progress?.({
|
|
3689
3689
|
loaded: models.length,
|
|
@@ -3723,8 +3723,8 @@ var StlViewer = class _StlViewer {
|
|
|
3723
3723
|
checkAbort();
|
|
3724
3724
|
return {
|
|
3725
3725
|
blob,
|
|
3726
|
-
format
|
|
3727
|
-
filename: exportFilename(options.filename,
|
|
3726
|
+
format,
|
|
3727
|
+
filename: exportFilename(options.filename, format),
|
|
3728
3728
|
model_ids: models.map((m) => m.id),
|
|
3729
3729
|
triangles,
|
|
3730
3730
|
representation: "scene"
|
|
@@ -4094,299 +4094,6 @@ var StlViewer = class _StlViewer {
|
|
|
4094
4094
|
};
|
|
4095
4095
|
var viewer_default = StlViewer;
|
|
4096
4096
|
|
|
4097
|
-
// src/app-tools.js
|
|
4098
|
-
var toolsControls = [
|
|
4099
|
-
"size-details",
|
|
4100
|
-
"size-fields",
|
|
4101
|
-
"size-x",
|
|
4102
|
-
"size-y",
|
|
4103
|
-
"size-z",
|
|
4104
|
-
"size-lock",
|
|
4105
|
-
"size-percent",
|
|
4106
|
-
"size-apply",
|
|
4107
|
-
"size-cancel",
|
|
4108
|
-
"size-reset",
|
|
4109
|
-
"size-error",
|
|
4110
|
-
"file-units",
|
|
4111
|
-
"file-units-note",
|
|
4112
|
-
"display-units",
|
|
4113
|
-
"size-unit-label",
|
|
4114
|
-
"duplicate-model",
|
|
4115
|
-
"camera-preset",
|
|
4116
|
-
"bookmark-name",
|
|
4117
|
-
"bookmark-list",
|
|
4118
|
-
"bookmark-save",
|
|
4119
|
-
"bookmark-restore",
|
|
4120
|
-
"bookmark-rename",
|
|
4121
|
-
"bookmark-delete",
|
|
4122
|
-
"camera-error",
|
|
4123
|
-
"info-area",
|
|
4124
|
-
"info-volume",
|
|
4125
|
-
"measure-model",
|
|
4126
|
-
"measure-cancel",
|
|
4127
|
-
"measure-status"
|
|
4128
|
-
];
|
|
4129
|
-
var axes2 = ["x", "y", "z"];
|
|
4130
|
-
var format = new Intl.NumberFormat(void 0, { maximumSignificantDigits: 7 });
|
|
4131
|
-
function bindAppTools({
|
|
4132
|
-
$,
|
|
4133
|
-
listen,
|
|
4134
|
-
window: window2,
|
|
4135
|
-
getViewer,
|
|
4136
|
-
getState,
|
|
4137
|
-
changeSize,
|
|
4138
|
-
duplicate,
|
|
4139
|
-
refresh,
|
|
4140
|
-
announce
|
|
4141
|
-
}) {
|
|
4142
|
-
let draftKind = null, lastAxis = 0, base = null, selected = null, measurement = null, disposed = false;
|
|
4143
|
-
const error = (id, message = "") => {
|
|
4144
|
-
$(id).textContent = message;
|
|
4145
|
-
$(id).hidden = !message;
|
|
4146
|
-
};
|
|
4147
|
-
const factor = () => getViewer()?.display_units === "inch" ? 25.4 : 1;
|
|
4148
|
-
const value = (v) => Number(v.toPrecision(10));
|
|
4149
|
-
function syncInfo() {
|
|
4150
|
-
const v = getViewer(), { selectedId, busy } = getState(), m = v?._models.get(selectedId);
|
|
4151
|
-
const s = m?.scaleVector, cached = m?._measurements;
|
|
4152
|
-
const valid = cached && cached.positions === m.geometry.positions && cached.key === s.join(",");
|
|
4153
|
-
const unit = factor() === 1 ? "mm" : "in";
|
|
4154
|
-
const area = valid ? cached.result.area_mm2 : s && s[0] === s[1] && s[1] === s[2] ? m.geometry.area * s[0] ** 2 : null;
|
|
4155
|
-
$("info-area").textContent = area == null ? "Calculate" : format.format(area / factor() ** 2) + " " + unit + "\xB2";
|
|
4156
|
-
$("info-volume").textContent = valid ? cached.result.volume_valid ? format.format(cached.result.volume_mm3 / factor() ** 3) + " " + unit + "\xB3" : "Unavailable" : "Not checked";
|
|
4157
|
-
if (!measurement)
|
|
4158
|
-
$("measure-status").textContent = valid ? cached.result.volume_reason : "Calculate to check enclosed volume. Runs on your device.";
|
|
4159
|
-
$("measure-model").disabled = busy || !m || !!measurement;
|
|
4160
|
-
$("measure-cancel").hidden = !measurement;
|
|
4161
|
-
}
|
|
4162
|
-
function syncBookmarks() {
|
|
4163
|
-
const v = getViewer(), previous = $("bookmark-list").value, bookmarks = v?.get_camera_bookmarks() || [];
|
|
4164
|
-
$("bookmark-list").replaceChildren(
|
|
4165
|
-
...bookmarks.map((b) => {
|
|
4166
|
-
const option = window2.document.createElement("option");
|
|
4167
|
-
option.value = b.name;
|
|
4168
|
-
option.textContent = b.name;
|
|
4169
|
-
return option;
|
|
4170
|
-
})
|
|
4171
|
-
);
|
|
4172
|
-
if (bookmarks.some((b) => b.name === previous))
|
|
4173
|
-
$("bookmark-list").value = previous;
|
|
4174
|
-
for (const id of [
|
|
4175
|
-
"bookmark-restore",
|
|
4176
|
-
"bookmark-rename",
|
|
4177
|
-
"bookmark-delete",
|
|
4178
|
-
"bookmark-list"
|
|
4179
|
-
])
|
|
4180
|
-
$(id).disabled = !bookmarks.length;
|
|
4181
|
-
$("bookmark-save").disabled = !v || bookmarks.length >= 32;
|
|
4182
|
-
}
|
|
4183
|
-
function sync() {
|
|
4184
|
-
if (disposed) return;
|
|
4185
|
-
const { selectedId, busy } = getState(), v = getViewer(), m = v?._models.get(selectedId);
|
|
4186
|
-
if (selectedId !== selected) {
|
|
4187
|
-
measurement?.abort();
|
|
4188
|
-
selected = selectedId;
|
|
4189
|
-
}
|
|
4190
|
-
draftKind = null;
|
|
4191
|
-
base = m?.geometry ? v.get_model_size(selectedId) : null;
|
|
4192
|
-
$("size-fields").disabled = busy || !base;
|
|
4193
|
-
$("duplicate-model").disabled = busy || !base;
|
|
4194
|
-
$("display-units").value = v?.display_units || "mm";
|
|
4195
|
-
$("size-unit-label").textContent = factor() === 1 ? "Dimensions in mm" : "Dimensions in inches";
|
|
4196
|
-
$("file-units").value = base?.file_units || "mm";
|
|
4197
|
-
$("file-units").disabled = busy || !base?.editable;
|
|
4198
|
-
$("file-units-note").textContent = base?.editable ? "STL/OBJ have no units. Millimetres are assumed; choose inches to correct the physical size." : "Explicit file units were converted to millimetres on import.";
|
|
4199
|
-
axes2.forEach((a, i) => {
|
|
4200
|
-
$("size-" + a).value = base ? value(base.dimensions_mm[i] / factor()) : "";
|
|
4201
|
-
$("size-" + a).disabled = !base || base.dimensions_mm[i] === 0;
|
|
4202
|
-
});
|
|
4203
|
-
const ratios = base?.baseline_mm.map((v2, i) => v2 ? base.dimensions_mm[i] / v2 : null).filter((v2) => v2 != null) || [];
|
|
4204
|
-
$("size-percent").value = ratios.length && ratios.every((v2) => Math.abs(v2 / ratios[0] - 1) < 1e-8) ? value(ratios[0] * 100) : "";
|
|
4205
|
-
error("size-error");
|
|
4206
|
-
syncInfo();
|
|
4207
|
-
syncBookmarks();
|
|
4208
|
-
}
|
|
4209
|
-
const attempt = (id, action) => {
|
|
4210
|
-
try {
|
|
4211
|
-
action();
|
|
4212
|
-
error(id);
|
|
4213
|
-
} catch (e) {
|
|
4214
|
-
error(id, e.message);
|
|
4215
|
-
}
|
|
4216
|
-
};
|
|
4217
|
-
axes2.forEach(
|
|
4218
|
-
(a, i) => listen($("size-" + a), "input", () => {
|
|
4219
|
-
if (!base) return;
|
|
4220
|
-
draftKind = "axis";
|
|
4221
|
-
lastAxis = i;
|
|
4222
|
-
const n = $("size-" + a).valueAsNumber;
|
|
4223
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
4224
|
-
if ($("size-lock").checked)
|
|
4225
|
-
axes2.forEach((axis, j) => {
|
|
4226
|
-
if (i !== j)
|
|
4227
|
-
$("size-" + axis).value = value(
|
|
4228
|
-
n * base.dimensions_mm[j] / base.dimensions_mm[i]
|
|
4229
|
-
);
|
|
4230
|
-
});
|
|
4231
|
-
$("size-percent").value = "";
|
|
4232
|
-
})
|
|
4233
|
-
);
|
|
4234
|
-
listen($("size-percent"), "input", () => {
|
|
4235
|
-
draftKind = "percent";
|
|
4236
|
-
const n = $("size-percent").valueAsNumber;
|
|
4237
|
-
if (base && Number.isFinite(n) && n > 0)
|
|
4238
|
-
axes2.forEach(
|
|
4239
|
-
(a, i) => $("size-" + a).value = value(
|
|
4240
|
-
base.baseline_mm[i] * n / 100 / factor()
|
|
4241
|
-
)
|
|
4242
|
-
);
|
|
4243
|
-
});
|
|
4244
|
-
listen(
|
|
4245
|
-
$("size-apply"),
|
|
4246
|
-
"click",
|
|
4247
|
-
() => attempt("size-error", () => {
|
|
4248
|
-
if (!base) return;
|
|
4249
|
-
if (!draftKind) return;
|
|
4250
|
-
const current = getViewer().get_model_size(selected).dimensions_mm;
|
|
4251
|
-
let desired = axes2.map((a) => $("size-" + a).valueAsNumber * factor());
|
|
4252
|
-
if (draftKind === "percent") {
|
|
4253
|
-
const percent = $("size-percent").valueAsNumber;
|
|
4254
|
-
if (!Number.isFinite(percent) || percent <= 0)
|
|
4255
|
-
throw new Error("Enter a positive scale percentage.");
|
|
4256
|
-
desired = base.baseline_mm.map((v) => v * percent / 100);
|
|
4257
|
-
} else if ($("size-lock").checked) {
|
|
4258
|
-
const ratio = desired[lastAxis] / base.dimensions_mm[lastAxis];
|
|
4259
|
-
desired = base.dimensions_mm.map((v) => v * ratio);
|
|
4260
|
-
}
|
|
4261
|
-
let ratios = desired.map((n, i) => {
|
|
4262
|
-
if (!Number.isFinite(n) || n < 0 || current[i] > 0 && n === 0)
|
|
4263
|
-
throw new Error("Enter positive dimensions for each non-flat axis.");
|
|
4264
|
-
return current[i] ? n / current[i] : 1;
|
|
4265
|
-
});
|
|
4266
|
-
if (ratios.every((v) => Math.abs(v / ratios[0] - 1) < 1e-12))
|
|
4267
|
-
ratios = ratios.map(() => ratios[0]);
|
|
4268
|
-
changeSize(() => getViewer().resize_model(selected, ratios));
|
|
4269
|
-
announce("Model size updated.");
|
|
4270
|
-
})
|
|
4271
|
-
);
|
|
4272
|
-
listen($("size-cancel"), "click", sync);
|
|
4273
|
-
listen(
|
|
4274
|
-
$("size-reset"),
|
|
4275
|
-
"click",
|
|
4276
|
-
() => attempt("size-error", () => {
|
|
4277
|
-
changeSize(() => getViewer().reset_model_size(selected));
|
|
4278
|
-
announce("Original size restored; file-unit correction retained.");
|
|
4279
|
-
})
|
|
4280
|
-
);
|
|
4281
|
-
listen(
|
|
4282
|
-
$("file-units"),
|
|
4283
|
-
"change",
|
|
4284
|
-
() => attempt("size-error", () => {
|
|
4285
|
-
changeSize(
|
|
4286
|
-
() => getViewer().set_file_units(selected, $("file-units").value)
|
|
4287
|
-
);
|
|
4288
|
-
announce("File units corrected.");
|
|
4289
|
-
})
|
|
4290
|
-
);
|
|
4291
|
-
listen($("display-units"), "change", () => {
|
|
4292
|
-
getViewer()?.set_display_units($("display-units").value);
|
|
4293
|
-
refresh();
|
|
4294
|
-
});
|
|
4295
|
-
listen($("duplicate-model"), "click", () => attempt("size-error", duplicate));
|
|
4296
|
-
listen(
|
|
4297
|
-
$("camera-preset"),
|
|
4298
|
-
"change",
|
|
4299
|
-
() => attempt("camera-error", () => {
|
|
4300
|
-
if ($("camera-preset").value)
|
|
4301
|
-
getViewer().set_camera_view($("camera-preset").value);
|
|
4302
|
-
announce("Camera view updated.");
|
|
4303
|
-
$("camera-preset").value = "";
|
|
4304
|
-
})
|
|
4305
|
-
);
|
|
4306
|
-
const editBookmarks = (action) => attempt("camera-error", () => {
|
|
4307
|
-
const v = getViewer(), bookmarks = v.get_camera_bookmarks();
|
|
4308
|
-
action(bookmarks, v);
|
|
4309
|
-
v.set_camera_bookmarks(bookmarks);
|
|
4310
|
-
syncBookmarks();
|
|
4311
|
-
});
|
|
4312
|
-
listen(
|
|
4313
|
-
$("bookmark-save"),
|
|
4314
|
-
"click",
|
|
4315
|
-
() => editBookmarks((b, v) => {
|
|
4316
|
-
b.push({ name: $("bookmark-name").value, camera: v.get_camera_state() });
|
|
4317
|
-
})
|
|
4318
|
-
);
|
|
4319
|
-
listen(
|
|
4320
|
-
$("bookmark-rename"),
|
|
4321
|
-
"click",
|
|
4322
|
-
() => editBookmarks((b) => {
|
|
4323
|
-
b.find((b2) => b2.name === $("bookmark-list").value).name = $("bookmark-name").value;
|
|
4324
|
-
})
|
|
4325
|
-
);
|
|
4326
|
-
listen(
|
|
4327
|
-
$("bookmark-delete"),
|
|
4328
|
-
"click",
|
|
4329
|
-
() => editBookmarks((b) => {
|
|
4330
|
-
b.splice(
|
|
4331
|
-
b.findIndex((b2) => b2.name === $("bookmark-list").value),
|
|
4332
|
-
1
|
|
4333
|
-
);
|
|
4334
|
-
})
|
|
4335
|
-
);
|
|
4336
|
-
listen(
|
|
4337
|
-
$("bookmark-restore"),
|
|
4338
|
-
"click",
|
|
4339
|
-
() => attempt("camera-error", () => {
|
|
4340
|
-
const b = getViewer().get_camera_bookmarks().find((b2) => b2.name === $("bookmark-list").value);
|
|
4341
|
-
if (b) {
|
|
4342
|
-
getViewer().set_camera_state(b.camera);
|
|
4343
|
-
announce("Saved view restored.");
|
|
4344
|
-
}
|
|
4345
|
-
})
|
|
4346
|
-
);
|
|
4347
|
-
listen(
|
|
4348
|
-
$("bookmark-list"),
|
|
4349
|
-
"change",
|
|
4350
|
-
() => $("bookmark-name").value = $("bookmark-list").value
|
|
4351
|
-
);
|
|
4352
|
-
listen($("measure-cancel"), "click", () => measurement?.abort());
|
|
4353
|
-
listen($("measure-model"), "click", async () => {
|
|
4354
|
-
const id = selected, v = getViewer(), controller = new window2.AbortController();
|
|
4355
|
-
measurement = controller;
|
|
4356
|
-
syncInfo();
|
|
4357
|
-
$("measure-status").textContent = "Calculating area and checking volume\u2026";
|
|
4358
|
-
try {
|
|
4359
|
-
await v.measure_model(id, {
|
|
4360
|
-
signal: controller.signal,
|
|
4361
|
-
on_progress: (p) => {
|
|
4362
|
-
if (selected === id && !disposed)
|
|
4363
|
-
$("measure-status").textContent = p.phase + "\u2026";
|
|
4364
|
-
}
|
|
4365
|
-
});
|
|
4366
|
-
if (!disposed && selected === id) announce("Measurements ready.");
|
|
4367
|
-
} catch (e) {
|
|
4368
|
-
if (!disposed && selected === id)
|
|
4369
|
-
$("measure-status").textContent = e.name === "AbortError" ? "Calculation cancelled." : e.message;
|
|
4370
|
-
} finally {
|
|
4371
|
-
const message = $("measure-status").textContent;
|
|
4372
|
-
if (measurement === controller) measurement = null;
|
|
4373
|
-
if (!disposed) {
|
|
4374
|
-
syncInfo();
|
|
4375
|
-
if (!v._models.get(id)?._measurements && selected === id)
|
|
4376
|
-
$("measure-status").textContent = message;
|
|
4377
|
-
}
|
|
4378
|
-
}
|
|
4379
|
-
});
|
|
4380
|
-
return {
|
|
4381
|
-
sync,
|
|
4382
|
-
syncInfo,
|
|
4383
|
-
dispose() {
|
|
4384
|
-
disposed = true;
|
|
4385
|
-
measurement?.abort();
|
|
4386
|
-
}
|
|
4387
|
-
};
|
|
4388
|
-
}
|
|
4389
|
-
|
|
4390
4097
|
// src/memory-policy.js
|
|
4391
4098
|
var GiB = 1024 ** 3;
|
|
4392
4099
|
var MiB = 1024 ** 2;
|
|
@@ -4418,9 +4125,9 @@ async function estimateFile(file) {
|
|
|
4418
4125
|
}
|
|
4419
4126
|
|
|
4420
4127
|
// src/model-transform.js
|
|
4421
|
-
var
|
|
4422
|
-
var vector2 = (p) =>
|
|
4423
|
-
var object2 = (p) => Object.fromEntries(
|
|
4128
|
+
var axes2 = ["x", "y", "z"];
|
|
4129
|
+
var vector2 = (p) => axes2.map((a) => p[a]);
|
|
4130
|
+
var object2 = (p) => Object.fromEntries(axes2.map((a, i) => [a, p[i]]));
|
|
4424
4131
|
var dot2 = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
4425
4132
|
var norm2 = (a) => {
|
|
4426
4133
|
const n = Math.hypot(...a) || 1;
|
|
@@ -4452,7 +4159,7 @@ function rotationMatrix(degrees = { x: 0, y: 0, z: 0 }) {
|
|
|
4452
4159
|
var centerOf = (bounds) => bounds.min.map((v, i) => (v + bounds.max[i]) / 2);
|
|
4453
4160
|
function modelCenter(model) {
|
|
4454
4161
|
return object2(
|
|
4455
|
-
centerOf(model.bounds).map((v, i) => v + model.position[
|
|
4162
|
+
centerOf(model.bounds).map((v, i) => v + model.position[axes2[i]])
|
|
4456
4163
|
);
|
|
4457
4164
|
}
|
|
4458
4165
|
function modelRadius(model) {
|
|
@@ -4463,7 +4170,7 @@ function modelRadius(model) {
|
|
|
4463
4170
|
}
|
|
4464
4171
|
function enginePosition(model) {
|
|
4465
4172
|
const c = centerOf(model.bounds), rotated = transform2(rotationMatrix(model.rotation), c);
|
|
4466
|
-
return object2(c.map((v, i) => model.position[
|
|
4173
|
+
return object2(c.map((v, i) => model.position[axes2[i]] + v - rotated[i]));
|
|
4467
4174
|
}
|
|
4468
4175
|
function modelBounds(model) {
|
|
4469
4176
|
if (!model.rotation || !Object.values(model.rotation).some((v) => v % 360))
|
|
@@ -4559,6 +4266,48 @@ function sceneFloor(models, placement) {
|
|
|
4559
4266
|
) : 0;
|
|
4560
4267
|
}
|
|
4561
4268
|
|
|
4269
|
+
// src/model-state.js
|
|
4270
|
+
var axes3 = ["x", "y", "z"];
|
|
4271
|
+
function syncAnimatedRecord(record, model) {
|
|
4272
|
+
if (!record || !model?.geometry) return;
|
|
4273
|
+
record.bounds = Object.fromEntries(
|
|
4274
|
+
["min", "max"].map((key) => [
|
|
4275
|
+
key,
|
|
4276
|
+
model.geometry.bounds[key].map(
|
|
4277
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4278
|
+
)
|
|
4279
|
+
])
|
|
4280
|
+
);
|
|
4281
|
+
record.rotation = Object.fromEntries(
|
|
4282
|
+
axes3.map((a) => [a, model["rotation" + a] * 180 / Math.PI % 360])
|
|
4283
|
+
);
|
|
4284
|
+
record.position = { x: 0, y: 0, z: 0 };
|
|
4285
|
+
const offset = enginePosition(record);
|
|
4286
|
+
for (const a of axes3) record.position[a] = model[a] - offset[a];
|
|
4287
|
+
}
|
|
4288
|
+
function centerAnimationPivot(record, model) {
|
|
4289
|
+
syncAnimatedRecord(record, model);
|
|
4290
|
+
const center = model.geometry.bounds.min.map(
|
|
4291
|
+
(v, i) => (v + model.geometry.bounds.max[i]) / 2
|
|
4292
|
+
);
|
|
4293
|
+
const delta = center.map(
|
|
4294
|
+
(v, i) => (v - model.center[i]) * model.scaleVector[i]
|
|
4295
|
+
);
|
|
4296
|
+
const rotation2 = rotationMatrix(record.rotation);
|
|
4297
|
+
const original = record.originalPosition || { x: 0, y: 0, z: 0 };
|
|
4298
|
+
record.originalPosition = Object.fromEntries(
|
|
4299
|
+
axes3.map((a, i) => [a, original[a] + delta[i]])
|
|
4300
|
+
);
|
|
4301
|
+
for (let i = 0; i < 3; i++)
|
|
4302
|
+
model[axes3[i]] += delta.reduce(
|
|
4303
|
+
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
4304
|
+
0
|
|
4305
|
+
);
|
|
4306
|
+
model.center = center;
|
|
4307
|
+
record.animationPivot = true;
|
|
4308
|
+
syncAnimatedRecord(record, model);
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4562
4311
|
// src/model-move.js
|
|
4563
4312
|
var vector3 = (p) => [p.x, p.y, p.z];
|
|
4564
4313
|
var sub2 = (a, b) => a.map((v, i) => v - b[i]);
|
|
@@ -4617,7 +4366,6 @@ function bindModelMove(canvas, {
|
|
|
4617
4366
|
if (current.tool === "rotate") onRotate?.(current.id, current.rotation);
|
|
4618
4367
|
else onMove(current.id, current.position);
|
|
4619
4368
|
}
|
|
4620
|
-
canvas.classList.remove("moving-model");
|
|
4621
4369
|
if (canvas.hasPointerCapture(current.pointer))
|
|
4622
4370
|
canvas.releasePointerCapture(current.pointer);
|
|
4623
4371
|
onEnd?.(current.id, cancel);
|
|
@@ -4654,7 +4402,6 @@ function bindModelMove(canvas, {
|
|
|
4654
4402
|
};
|
|
4655
4403
|
onStart?.(model.id);
|
|
4656
4404
|
canvas.setPointerCapture(e.pointerId);
|
|
4657
|
-
canvas.classList.add("moving-model");
|
|
4658
4405
|
canvas.focus({ preventScroll: true });
|
|
4659
4406
|
});
|
|
4660
4407
|
on("pointermove", (e) => {
|
|
@@ -4716,7 +4463,6 @@ function bindModelMove(canvas, {
|
|
|
4716
4463
|
end(true);
|
|
4717
4464
|
blocked.clear();
|
|
4718
4465
|
enabled = value;
|
|
4719
|
-
canvas.classList.toggle("model-move-ready", value);
|
|
4720
4466
|
},
|
|
4721
4467
|
dispose() {
|
|
4722
4468
|
end(true);
|
|
@@ -4725,1506 +4471,328 @@ function bindModelMove(canvas, {
|
|
|
4725
4471
|
};
|
|
4726
4472
|
}
|
|
4727
4473
|
|
|
4728
|
-
// src/
|
|
4729
|
-
function dropKind(transfer) {
|
|
4730
|
-
const types = Array.from(transfer?.types || [], (type) => type.toLowerCase());
|
|
4731
|
-
if (types.includes("files") || transfer?.files?.length) return "files";
|
|
4732
|
-
if (types.includes("text/uri-list") || types.includes("text/x-moz-url"))
|
|
4733
|
-
return "url";
|
|
4734
|
-
if (types.includes("text/plain")) return "text";
|
|
4735
|
-
return null;
|
|
4736
|
-
}
|
|
4737
|
-
function droppedURLs(transfer) {
|
|
4738
|
-
if (dropKind(transfer) === "files") return [];
|
|
4739
|
-
const uriList = transfer?.getData("text/uri-list")?.trim();
|
|
4740
|
-
const mozURL = !uriList && transfer?.getData("text/x-moz-url")?.trim();
|
|
4741
|
-
const plain = !uriList && !mozURL && transfer?.getData("text/plain")?.trim();
|
|
4742
|
-
let lines;
|
|
4743
|
-
if (uriList)
|
|
4744
|
-
lines = uriList.split(/\r?\n/).map((s) => s.trim()).filter((s) => s && !s.startsWith("#"));
|
|
4745
|
-
else if (mozURL)
|
|
4746
|
-
lines = [mozURL.split(/\r?\n/)[0]];
|
|
4747
|
-
else if (plain) {
|
|
4748
|
-
lines = plain.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
4749
|
-
if (!lines.every((s) => /^https?:\/\/\S+$/i.test(s))) return [];
|
|
4750
|
-
} else return [];
|
|
4751
|
-
const urls = /* @__PURE__ */ new Set();
|
|
4752
|
-
for (const line of lines) {
|
|
4753
|
-
let url;
|
|
4754
|
-
try {
|
|
4755
|
-
url = new URL(line);
|
|
4756
|
-
} catch {
|
|
4757
|
-
}
|
|
4758
|
-
if (!url || !["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
4759
|
-
throw new Error(
|
|
4760
|
-
"Drop a public HTTP or HTTPS file URL without login credentials."
|
|
4761
|
-
);
|
|
4762
|
-
url.hash = "";
|
|
4763
|
-
urls.add(url.href);
|
|
4764
|
-
}
|
|
4765
|
-
return [...urls];
|
|
4766
|
-
}
|
|
4767
|
-
|
|
4768
|
-
// src/app-export.js
|
|
4769
|
-
var exportControls = [
|
|
4770
|
-
"save-button",
|
|
4771
|
-
"save-dialog",
|
|
4772
|
-
"save-form",
|
|
4773
|
-
"save-fields",
|
|
4774
|
-
"save-name",
|
|
4775
|
-
"save-format",
|
|
4776
|
-
"save-scope",
|
|
4777
|
-
"save-colors",
|
|
4778
|
-
"save-colors-row",
|
|
4779
|
-
"save-format-note",
|
|
4780
|
-
"save-cad-note",
|
|
4781
|
-
"save-progress",
|
|
4782
|
-
"save-status",
|
|
4783
|
-
"save-error",
|
|
4784
|
-
"save-submit",
|
|
4785
|
-
"save-cancel",
|
|
4786
|
-
"save-close"
|
|
4787
|
-
];
|
|
4788
|
-
function bindAppExport({
|
|
4789
|
-
$,
|
|
4790
|
-
listen,
|
|
4791
|
-
window: window2,
|
|
4792
|
-
getState,
|
|
4793
|
-
getViewer,
|
|
4794
|
-
onOpen,
|
|
4795
|
-
announce
|
|
4796
|
-
}) {
|
|
4797
|
-
const dialog = $("save-dialog");
|
|
4798
|
-
let job = null, disposed = false, lastSuggestedName = "";
|
|
4799
|
-
const state = () => getState();
|
|
4800
|
-
const available = () => !disposed && !state().busy && state().models.length > 0;
|
|
4801
|
-
const isScene = () => ["vsb", "vsj"].includes($("save-format").value);
|
|
4802
|
-
const clearError = () => {
|
|
4803
|
-
$("save-error").hidden = true;
|
|
4804
|
-
$("save-error").textContent = "";
|
|
4805
|
-
};
|
|
4806
|
-
function formatChanged() {
|
|
4807
|
-
const format2 = $("save-format").value;
|
|
4808
|
-
if (!$("save-name").value || $("save-name").value === lastSuggestedName) {
|
|
4809
|
-
suggestedName();
|
|
4810
|
-
}
|
|
4811
|
-
if ($("save-mesh-note")) $("save-mesh-note").hidden = isScene();
|
|
4812
|
-
if ($("save-scene-note")) $("save-scene-note").hidden = !isScene();
|
|
4813
|
-
$("save-colors-row").hidden = !["obj", "3mf"].includes(format2);
|
|
4814
|
-
$("save-cad-note").hidden = !["step", "iges"].includes(format2);
|
|
4815
|
-
$("save-format-note").textContent = $("save-format").selectedOptions[0]?.dataset.note || "";
|
|
4816
|
-
$("save-submit").textContent = format2 === "obj" && $("save-colors").checked ? "Download ZIP" : "Download file";
|
|
4817
|
-
sync();
|
|
4818
|
-
}
|
|
4819
|
-
function suggestedName() {
|
|
4820
|
-
const { models, selectedId } = state();
|
|
4821
|
-
$("save-name").value = isScene() ? "viewstl-scene" : (models.find((m) => m.id === selectedId)?.name || "model").replace(
|
|
4822
|
-
/\.[^.]+$/,
|
|
4823
|
-
""
|
|
4824
|
-
);
|
|
4825
|
-
lastSuggestedName = $("save-name").value;
|
|
4826
|
-
}
|
|
4827
|
-
function sync() {
|
|
4828
|
-
$("save-button").disabled = !available() || !!job;
|
|
4829
|
-
$("save-fields").disabled = !!job;
|
|
4830
|
-
$("save-submit").disabled = !available() || !!job;
|
|
4831
|
-
$("save-progress").hidden = !job;
|
|
4832
|
-
$("save-cancel").textContent = job ? "Cancel saving" : "Cancel";
|
|
4833
|
-
const { models, selectedId } = state();
|
|
4834
|
-
const target = isScene() ? `Entire scene \xB7 ${models.length} ${models.length === 1 ? "model" : "models"}` : "Selected model: " + (models.find((m) => m.id === selectedId)?.name || "model");
|
|
4835
|
-
if ($("save-scope").textContent !== target)
|
|
4836
|
-
$("save-scope").textContent = target;
|
|
4837
|
-
}
|
|
4838
|
-
function cancel() {
|
|
4839
|
-
if (job) {
|
|
4840
|
-
const previous = job;
|
|
4841
|
-
job = null;
|
|
4842
|
-
previous.abort();
|
|
4843
|
-
announce("Saving cancelled.");
|
|
4844
|
-
}
|
|
4845
|
-
$("save-status").textContent = "";
|
|
4846
|
-
sync();
|
|
4847
|
-
}
|
|
4848
|
-
function close() {
|
|
4849
|
-
cancel();
|
|
4850
|
-
if (dialog.open) dialog.close();
|
|
4851
|
-
}
|
|
4852
|
-
listen($("save-button"), "click", () => {
|
|
4853
|
-
if (!available() || job) return;
|
|
4854
|
-
onOpen();
|
|
4855
|
-
clearError();
|
|
4856
|
-
$("save-status").textContent = "";
|
|
4857
|
-
suggestedName();
|
|
4858
|
-
formatChanged();
|
|
4859
|
-
sync();
|
|
4860
|
-
dialog.showModal();
|
|
4861
|
-
});
|
|
4862
|
-
listen($("save-format"), "change", () => {
|
|
4863
|
-
clearError();
|
|
4864
|
-
formatChanged();
|
|
4865
|
-
});
|
|
4866
|
-
listen($("save-colors"), "change", formatChanged);
|
|
4867
|
-
listen($("save-cancel"), "click", close);
|
|
4868
|
-
listen($("save-close"), "click", close);
|
|
4869
|
-
listen(dialog, "cancel", (event) => {
|
|
4870
|
-
event.preventDefault();
|
|
4871
|
-
close();
|
|
4872
|
-
});
|
|
4873
|
-
listen(dialog, "close", () => {
|
|
4874
|
-
if (!dialog.open) cancel();
|
|
4875
|
-
});
|
|
4876
|
-
listen($("save-form"), "submit", async (event) => {
|
|
4877
|
-
event.preventDefault();
|
|
4878
|
-
if (job || !available()) return;
|
|
4879
|
-
clearError();
|
|
4880
|
-
const { selectedId } = state();
|
|
4881
|
-
const controller = new window2.AbortController();
|
|
4882
|
-
job = controller;
|
|
4883
|
-
$("save-progress").removeAttribute("value");
|
|
4884
|
-
$("save-status").textContent = "Preparing your file\u2026";
|
|
4885
|
-
sync();
|
|
4886
|
-
try {
|
|
4887
|
-
const settings = {
|
|
4888
|
-
format: $("save-format").value,
|
|
4889
|
-
filename: $("save-name").value.trim() || "model",
|
|
4890
|
-
// App selection remains meaningful even when its editing frame is hidden.
|
|
4891
|
-
model_ids: [selectedId],
|
|
4892
|
-
units: "mm",
|
|
4893
|
-
apply_transforms: true,
|
|
4894
|
-
colors: $("save-colors").checked,
|
|
4895
|
-
signal: controller.signal,
|
|
4896
|
-
on_progress({ loaded, total, phase }) {
|
|
4897
|
-
if (job !== controller || controller.signal.aborted) return;
|
|
4898
|
-
const percent = total ? Math.min(100, Math.floor(100 * loaded / total)) : 0;
|
|
4899
|
-
$("save-progress").value = percent;
|
|
4900
|
-
$("save-status").textContent = phase === "packaging" ? "Packing your scene\u2026" : percent === 100 ? "Finishing your file\u2026" : `Saving\u2026 ${percent}%`;
|
|
4901
|
-
}
|
|
4902
|
-
};
|
|
4903
|
-
const result = isScene() ? await getViewer().download_scene({
|
|
4904
|
-
format: settings.format,
|
|
4905
|
-
filename: settings.filename,
|
|
4906
|
-
signal: settings.signal,
|
|
4907
|
-
on_progress: settings.on_progress,
|
|
4908
|
-
force_basename: true
|
|
4909
|
-
}) : await getViewer().download_models(settings);
|
|
4910
|
-
if (job !== controller || disposed) return;
|
|
4911
|
-
job = null;
|
|
4912
|
-
close();
|
|
4913
|
-
announce(result.filename + " ready to download.");
|
|
4914
|
-
} catch (error) {
|
|
4915
|
-
if (job !== controller || disposed) return;
|
|
4916
|
-
job = null;
|
|
4917
|
-
$("save-status").textContent = "";
|
|
4918
|
-
if (error?.name !== "AbortError") {
|
|
4919
|
-
$("save-error").textContent = error?.message || "This file could not be saved. Try another format.";
|
|
4920
|
-
$("save-error").hidden = false;
|
|
4921
|
-
}
|
|
4922
|
-
} finally {
|
|
4923
|
-
if (!disposed && (!job || job === controller)) sync();
|
|
4924
|
-
}
|
|
4925
|
-
});
|
|
4926
|
-
formatChanged();
|
|
4927
|
-
sync();
|
|
4928
|
-
return {
|
|
4929
|
-
sync,
|
|
4930
|
-
close,
|
|
4931
|
-
dispose() {
|
|
4932
|
-
close();
|
|
4933
|
-
disposed = true;
|
|
4934
|
-
}
|
|
4935
|
-
};
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
|
-
// src/app-print.js
|
|
4939
|
-
var printControls = [
|
|
4940
|
-
"print-button",
|
|
4941
|
-
"print-panel",
|
|
4942
|
-
"print-close",
|
|
4943
|
-
"print-form",
|
|
4944
|
-
"print-fields",
|
|
4945
|
-
"print-scope",
|
|
4946
|
-
"print-level",
|
|
4947
|
-
"print-units",
|
|
4948
|
-
"print-fdm",
|
|
4949
|
-
"print-wall",
|
|
4950
|
-
"print-angle",
|
|
4951
|
-
"print-bed-x",
|
|
4952
|
-
"print-bed-y",
|
|
4953
|
-
"print-bed-z",
|
|
4954
|
-
"print-run",
|
|
4955
|
-
"print-cancel",
|
|
4956
|
-
"print-status",
|
|
4957
|
-
"print-results",
|
|
4958
|
-
"print-clear",
|
|
4959
|
-
"print-target"
|
|
4960
|
-
];
|
|
4961
|
-
function bindAppPrint({
|
|
4962
|
-
$,
|
|
4963
|
-
listen,
|
|
4964
|
-
window: window2,
|
|
4965
|
-
getState,
|
|
4966
|
-
getViewer,
|
|
4967
|
-
onOpen,
|
|
4968
|
-
announce
|
|
4969
|
-
}) {
|
|
4970
|
-
let job = null, report = null, timer = null, disposed = false, scopeKey = "", previousScope = "", unavailable = false;
|
|
4971
|
-
const panel = $("print-panel"), document2 = panel.ownerDocument;
|
|
4972
|
-
const pausedForCheck = /* @__PURE__ */ new Set();
|
|
4973
|
-
const summary = {
|
|
4974
|
-
issues_found: "Issues found",
|
|
4975
|
-
incomplete: "Check incomplete",
|
|
4976
|
-
no_issues_found: "No issues found in completed checks"
|
|
4977
|
-
};
|
|
4978
|
-
const text = (tag, value) => {
|
|
4979
|
-
const el = document2.createElement(tag);
|
|
4980
|
-
el.textContent = value;
|
|
4981
|
-
return el;
|
|
4982
|
-
};
|
|
4983
|
-
const stateIds = () => $("print-scope").value === "all" ? getState().models.map((m) => m.id) : [getState().selectedId];
|
|
4984
|
-
const key = () => JSON.stringify(stateIds());
|
|
4985
|
-
const clear = () => {
|
|
4986
|
-
getViewer()?.clear_printability();
|
|
4987
|
-
$("print-clear").hidden = true;
|
|
4988
|
-
};
|
|
4989
|
-
function sync() {
|
|
4990
|
-
if (disposed) return;
|
|
4991
|
-
const s = getState();
|
|
4992
|
-
$("print-button").disabled = s.busy || !s.models.length;
|
|
4993
|
-
if (panel.hidden) return;
|
|
4994
|
-
for (const id of pausedForCheck)
|
|
4995
|
-
if (!s.models.some((m) => m.id === id) || getViewer()?.get_model_animation_state(id)?.status !== "paused")
|
|
4996
|
-
pausedForCheck.delete(id);
|
|
4997
|
-
if ($("print-resume")) {
|
|
4998
|
-
$("print-resume").hidden = !pausedForCheck.size;
|
|
4999
|
-
$("print-resume").disabled = !!job || s.busy;
|
|
5000
|
-
}
|
|
5001
|
-
if ($("print-animation-note"))
|
|
5002
|
-
$("print-animation-note").hidden = !pausedForCheck.size;
|
|
5003
|
-
const wasUnavailable = unavailable;
|
|
5004
|
-
unavailable = s.busy || !s.models.length;
|
|
5005
|
-
$("print-run").disabled = !!job || unavailable;
|
|
5006
|
-
if (previousScope !== key()) {
|
|
5007
|
-
$("print-units").checked = false;
|
|
5008
|
-
previousScope = key();
|
|
5009
|
-
}
|
|
5010
|
-
$("print-target").textContent = $("print-scope").value === "all" ? `All ${s.models.length} models, checked separately` : s.models.find((m) => m.id === s.selectedId)?.name || "No model selected";
|
|
5011
|
-
if (job && (unavailable || scopeKey !== key())) cancel();
|
|
5012
|
-
if (report && (!getViewer()?.is_printability_current(report) || scopeKey !== key())) {
|
|
5013
|
-
report = null;
|
|
5014
|
-
$("print-units").checked = false;
|
|
5015
|
-
clear();
|
|
5016
|
-
$("print-results").replaceChildren();
|
|
5017
|
-
$("print-status").textContent = "Model or selection changed. Run the check again.";
|
|
5018
|
-
}
|
|
5019
|
-
if (unavailable) {
|
|
5020
|
-
if (!s.models.length) {
|
|
5021
|
-
report = null;
|
|
5022
|
-
clear();
|
|
5023
|
-
$("print-results").replaceChildren();
|
|
5024
|
-
}
|
|
5025
|
-
$("print-status").textContent = s.busy ? "Opening models. Checks will be available when loading finishes." : "Open a model to run a print check.";
|
|
5026
|
-
} else if (wasUnavailable && !job) {
|
|
5027
|
-
$("print-status").textContent = report ? summary[report.summary] : "Ready to check. Choose Run check to analyze the current model selection.";
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
|
-
function render(value) {
|
|
5031
|
-
const elements = [];
|
|
5032
|
-
for (const model of value.models) {
|
|
5033
|
-
const section = document2.createElement("section");
|
|
5034
|
-
section.append(
|
|
5035
|
-
text(
|
|
5036
|
-
"h3",
|
|
5037
|
-
getState().models.find((m) => m.id === model.model_id)?.name || model.name
|
|
5038
|
-
)
|
|
5039
|
-
);
|
|
5040
|
-
section.append(
|
|
5041
|
-
text(
|
|
5042
|
-
"p",
|
|
5043
|
-
`${model.dimensions_mm?.map((v) => Number(v.toPrecision(5))).join(" \xD7 ") || "Unknown size"} mm \xB7 ${(model.elapsed_ms / 1e3).toFixed(2)} s`
|
|
5044
|
-
)
|
|
5045
|
-
);
|
|
5046
|
-
if (model.volume_valid)
|
|
5047
|
-
section.append(
|
|
5048
|
-
text(
|
|
5049
|
-
"p",
|
|
5050
|
-
`Enclosed volume at current mm scale: ${Number(model.volume_mm3.toPrecision(6))} mm\xB3`
|
|
5051
|
-
)
|
|
5052
|
-
);
|
|
5053
|
-
for (const c of model.checks) {
|
|
5054
|
-
const details = document2.createElement("details");
|
|
5055
|
-
details.dataset.check = c.code;
|
|
5056
|
-
details.dataset.status = c.status;
|
|
5057
|
-
const status = {
|
|
5058
|
-
pass: "Passed",
|
|
5059
|
-
issue: c.severity === "info" ? "Information" : "Review",
|
|
5060
|
-
inconclusive: "Inconclusive",
|
|
5061
|
-
not_run: "Not run"
|
|
5062
|
-
}[c.status];
|
|
5063
|
-
details.append(
|
|
5064
|
-
text(
|
|
5065
|
-
"summary",
|
|
5066
|
-
`${status} \xB7 ${c.label}${c.count ? ` (${c.count})` : ""}${c.estimated ? " \u2014 estimate" : ""}`
|
|
5067
|
-
)
|
|
5068
|
-
);
|
|
5069
|
-
details.append(text("p", c.message));
|
|
5070
|
-
if (c.area_mm2 != null)
|
|
5071
|
-
details.append(
|
|
5072
|
-
text("p", `Area: ${Number(c.area_mm2.toPrecision(5))} mm\xB2`)
|
|
5073
|
-
);
|
|
5074
|
-
if (c.minimum_sampled_mm != null)
|
|
5075
|
-
details.append(
|
|
5076
|
-
text(
|
|
5077
|
-
"p",
|
|
5078
|
-
`Smallest sampled distance: ${Number(c.minimum_sampled_mm.toPrecision(5))} mm`
|
|
5079
|
-
)
|
|
5080
|
-
);
|
|
5081
|
-
if (c.triangle_ids.length || c.edge_vertices.length) {
|
|
5082
|
-
const button = text("button", "Show locations");
|
|
5083
|
-
button.type = "button";
|
|
5084
|
-
button.dataset.issue = c.code;
|
|
5085
|
-
button.dataset.model = model.model_id;
|
|
5086
|
-
details.append(button);
|
|
5087
|
-
details.append(
|
|
5088
|
-
text(
|
|
5089
|
-
"p",
|
|
5090
|
-
"Highlights show a bounded sample, including edges hidden behind surfaces."
|
|
5091
|
-
)
|
|
5092
|
-
);
|
|
5093
|
-
}
|
|
5094
|
-
section.append(details);
|
|
5095
|
-
}
|
|
5096
|
-
elements.push(section);
|
|
5097
|
-
}
|
|
5098
|
-
$("print-results").replaceChildren(...elements);
|
|
5099
|
-
}
|
|
5100
|
-
function cancel() {
|
|
5101
|
-
job?.abort();
|
|
5102
|
-
}
|
|
5103
|
-
function close(focus = true) {
|
|
5104
|
-
cancel();
|
|
5105
|
-
panel.hidden = true;
|
|
5106
|
-
$("workspace")?.classList.remove("print-check-open");
|
|
5107
|
-
clear();
|
|
5108
|
-
window2.clearInterval(timer);
|
|
5109
|
-
timer = null;
|
|
5110
|
-
$("print-button").setAttribute("aria-expanded", "false");
|
|
5111
|
-
if (focus && !disposed)
|
|
5112
|
-
($("print-button").offsetParent && !$("print-button").disabled ? $("print-button") : $("info-button").offsetParent ? $("info-button") : $("sample-button"))?.focus();
|
|
5113
|
-
}
|
|
5114
|
-
function changed() {
|
|
5115
|
-
cancel();
|
|
5116
|
-
report = null;
|
|
5117
|
-
clear();
|
|
5118
|
-
$("print-results").replaceChildren();
|
|
5119
|
-
$("print-status").textContent = "Settings changed. Run the check again.";
|
|
5120
|
-
sync();
|
|
5121
|
-
}
|
|
5122
|
-
listen($("print-button"), "click", () => {
|
|
5123
|
-
if (!panel.hidden) {
|
|
5124
|
-
onOpen();
|
|
5125
|
-
sync();
|
|
5126
|
-
panel.scrollTop = 0;
|
|
5127
|
-
$("print-run").focus();
|
|
5128
|
-
return;
|
|
5129
|
-
}
|
|
5130
|
-
onOpen();
|
|
5131
|
-
panel.hidden = false;
|
|
5132
|
-
$("workspace")?.classList.add("print-check-open");
|
|
5133
|
-
$("print-button").setAttribute("aria-expanded", "true");
|
|
5134
|
-
sync();
|
|
5135
|
-
timer = window2.setInterval(sync, 400);
|
|
5136
|
-
$("print-run").focus();
|
|
5137
|
-
});
|
|
5138
|
-
listen($("print-close"), "click", () => close());
|
|
5139
|
-
listen(panel, "keydown", (e) => {
|
|
5140
|
-
if (e.key === "Escape") {
|
|
5141
|
-
e.stopPropagation();
|
|
5142
|
-
e.preventDefault();
|
|
5143
|
-
close();
|
|
5144
|
-
}
|
|
5145
|
-
});
|
|
5146
|
-
listen($("print-fields"), "change", changed);
|
|
5147
|
-
listen($("print-clear"), "click", clear);
|
|
5148
|
-
listen($("print-cancel"), "click", cancel);
|
|
5149
|
-
if ($("print-resume"))
|
|
5150
|
-
listen($("print-resume"), "click", () => {
|
|
5151
|
-
if (job || getState().busy) return;
|
|
5152
|
-
for (const id of pausedForCheck)
|
|
5153
|
-
if (getState().models.some((m) => m.id === id) && getViewer().get_model_animation_state(id)?.status === "paused")
|
|
5154
|
-
getViewer().resume_model_animation(id);
|
|
5155
|
-
pausedForCheck.clear();
|
|
5156
|
-
report = null;
|
|
5157
|
-
clear();
|
|
5158
|
-
$("print-results").replaceChildren();
|
|
5159
|
-
$("print-status").textContent = "Animation resumed. Pause and run the check again for the new pose.";
|
|
5160
|
-
sync();
|
|
5161
|
-
});
|
|
5162
|
-
listen($("print-results"), "click", (e) => {
|
|
5163
|
-
const b = e.target.closest("button[data-issue]");
|
|
5164
|
-
if (!b || !report) return;
|
|
5165
|
-
try {
|
|
5166
|
-
getViewer().show_printability(report, {
|
|
5167
|
-
issue: b.dataset.issue,
|
|
5168
|
-
model_id: Number(b.dataset.model)
|
|
5169
|
-
});
|
|
5170
|
-
$("print-clear").hidden = false;
|
|
5171
|
-
} catch (error) {
|
|
5172
|
-
$("print-status").textContent = error.message;
|
|
5173
|
-
}
|
|
5174
|
-
});
|
|
5175
|
-
listen($("print-form"), "submit", async (e) => {
|
|
5176
|
-
e.preventDefault();
|
|
5177
|
-
if (job || getState().busy || !getState().models.length) return;
|
|
5178
|
-
const controller = new window2.AbortController();
|
|
5179
|
-
job = controller;
|
|
5180
|
-
report = null;
|
|
5181
|
-
clear();
|
|
5182
|
-
$("print-results").replaceChildren();
|
|
5183
|
-
$("print-status").textContent = "Checking on your device\u2026";
|
|
5184
|
-
$("print-fields").disabled = true;
|
|
5185
|
-
$("print-run").disabled = true;
|
|
5186
|
-
$("print-cancel").hidden = false;
|
|
5187
|
-
try {
|
|
5188
|
-
let profile = null;
|
|
5189
|
-
if ($("print-fdm").checked) {
|
|
5190
|
-
profile = { technology: "fdm" };
|
|
5191
|
-
if ($("print-wall").value)
|
|
5192
|
-
profile.min_wall_mm = Number($("print-wall").value);
|
|
5193
|
-
if ($("print-angle").value)
|
|
5194
|
-
profile.overhang_degrees = Number($("print-angle").value);
|
|
5195
|
-
const bed = ["x", "y", "z"].map((a) => $("print-bed-" + a).value);
|
|
5196
|
-
if (bed.some(Boolean)) {
|
|
5197
|
-
if (!bed.every(Boolean))
|
|
5198
|
-
throw new Error(
|
|
5199
|
-
"Enter all three build-volume dimensions, or leave all three empty."
|
|
5200
|
-
);
|
|
5201
|
-
profile.build_volume_mm = bed.map(Number);
|
|
5202
|
-
}
|
|
5203
|
-
}
|
|
5204
|
-
scopeKey = key();
|
|
5205
|
-
for (const id of stateIds()) {
|
|
5206
|
-
if (getViewer().get_model_animation_state(id)?.status === "running") {
|
|
5207
|
-
getViewer().pause_model_animation(id);
|
|
5208
|
-
pausedForCheck.add(id);
|
|
5209
|
-
}
|
|
5210
|
-
}
|
|
5211
|
-
sync();
|
|
5212
|
-
const result = await getViewer().check_printability({
|
|
5213
|
-
model_ids: stateIds(),
|
|
5214
|
-
level: $("print-level").value,
|
|
5215
|
-
units_confirmed: $("print-units").checked,
|
|
5216
|
-
profile,
|
|
5217
|
-
signal: controller.signal,
|
|
5218
|
-
on_progress: (p) => {
|
|
5219
|
-
if (job === controller)
|
|
5220
|
-
$("print-status").textContent = `${p.phase} \xB7 model ${(p.model_index ?? 0) + 1} of ${p.total_models}`;
|
|
5221
|
-
}
|
|
5222
|
-
});
|
|
5223
|
-
if (disposed || controller.signal.aborted) return;
|
|
5224
|
-
report = result;
|
|
5225
|
-
render(report);
|
|
5226
|
-
$("print-status").textContent = summary[report.summary];
|
|
5227
|
-
announce(summary[report.summary]);
|
|
5228
|
-
} catch (error) {
|
|
5229
|
-
if (!disposed)
|
|
5230
|
-
$("print-status").textContent = error.name === "AbortError" ? "Check cancelled. Your model is unchanged." : error.message;
|
|
5231
|
-
} finally {
|
|
5232
|
-
if (job === controller) job = null;
|
|
5233
|
-
if (!disposed) {
|
|
5234
|
-
$("print-fields").disabled = false;
|
|
5235
|
-
$("print-run").disabled = false;
|
|
5236
|
-
$("print-cancel").hidden = true;
|
|
5237
|
-
sync();
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
|
-
});
|
|
5241
|
-
sync();
|
|
5242
|
-
return {
|
|
5243
|
-
sync,
|
|
5244
|
-
close,
|
|
5245
|
-
dispose() {
|
|
5246
|
-
disposed = true;
|
|
5247
|
-
close(false);
|
|
5248
|
-
report = null;
|
|
5249
|
-
pausedForCheck.clear();
|
|
5250
|
-
}
|
|
5251
|
-
};
|
|
5252
|
-
}
|
|
5253
|
-
|
|
5254
|
-
// src/app-animation.js
|
|
5255
|
-
var animationControls = [
|
|
5256
|
-
"animation-details",
|
|
5257
|
-
"animation-fields",
|
|
5258
|
-
"animation-axis",
|
|
5259
|
-
"animation-seconds",
|
|
5260
|
-
"animation-direction",
|
|
5261
|
-
"animation-play",
|
|
5262
|
-
"animation-pause",
|
|
5263
|
-
"animation-stop",
|
|
5264
|
-
"animation-status",
|
|
5265
|
-
"animation-error"
|
|
5266
|
-
];
|
|
4474
|
+
// src/session.js
|
|
5267
4475
|
var axes4 = ["x", "y", "z"];
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
);
|
|
5298
|
-
for (let i = 0; i < 3; i++)
|
|
5299
|
-
model[axes4[i]] += delta.reduce(
|
|
5300
|
-
(sum, v, j) => sum + rotation2[i * 3 + j] * v,
|
|
5301
|
-
0
|
|
5302
|
-
);
|
|
5303
|
-
model.center = center;
|
|
5304
|
-
record.animationPivot = true;
|
|
5305
|
-
syncAnimatedRecord(record, model);
|
|
5306
|
-
}
|
|
5307
|
-
function spinDefinition(axis, seconds, direction) {
|
|
5308
|
-
if (!axes4.includes(axis) || !Number.isFinite(seconds) || seconds < 0.1 || seconds > 3600 || !["1", "-1"].includes(direction))
|
|
5309
|
-
throw new Error("Choose an axis and a duration from 0.1 to 3,600 seconds.");
|
|
5310
|
-
return {
|
|
5311
|
-
delta: {
|
|
5312
|
-
["rotation" + axis]: 2 * Math.PI * Number(direction),
|
|
5313
|
-
msec: seconds * 1e3,
|
|
5314
|
-
loop: true
|
|
5315
|
-
}
|
|
5316
|
-
};
|
|
5317
|
-
}
|
|
5318
|
-
function bindAppAnimation({
|
|
5319
|
-
$,
|
|
5320
|
-
listen,
|
|
5321
|
-
window: window2,
|
|
5322
|
-
getState,
|
|
5323
|
-
getViewer,
|
|
5324
|
-
beforePlay,
|
|
5325
|
-
refreshPose,
|
|
5326
|
-
announce
|
|
5327
|
-
}) {
|
|
5328
|
-
const settings = /* @__PURE__ */ new Map();
|
|
5329
|
-
let selected = null, timer = null, disposed = false;
|
|
5330
|
-
const state = () => {
|
|
5331
|
-
const s = getState();
|
|
5332
|
-
return {
|
|
5333
|
-
...s,
|
|
5334
|
-
playback: s.selectedId == null ? null : getViewer()?.get_model_animation_state(s.selectedId)
|
|
5335
|
-
};
|
|
5336
|
-
};
|
|
5337
|
-
function sync() {
|
|
5338
|
-
if (disposed) return;
|
|
5339
|
-
const s = state(), available = s.models.some((m) => m.id === s.selectedId);
|
|
5340
|
-
for (const id of settings.keys())
|
|
5341
|
-
if (!s.models.some((m) => m.id === id)) settings.delete(id);
|
|
5342
|
-
$("animation-details").hidden = !available;
|
|
5343
|
-
$("animation-fields").disabled = s.busy || !available;
|
|
5344
|
-
if (selected !== s.selectedId) {
|
|
5345
|
-
selected = s.selectedId;
|
|
5346
|
-
let value = settings.get(selected);
|
|
5347
|
-
if (!value) {
|
|
5348
|
-
value = { axis: "z", seconds: 5, direction: "1" };
|
|
5349
|
-
const a = getViewer()?._models.get(selected)?.animation;
|
|
5350
|
-
const entries = Object.entries(a?.delta || {}).filter(
|
|
5351
|
-
([k, v]) => k.startsWith("rotation") && v !== 0
|
|
5352
|
-
);
|
|
5353
|
-
if (a?.delta?.loop && !a.exact && entries.length === 1) {
|
|
5354
|
-
value = {
|
|
5355
|
-
axis: entries[0][0].slice(-1),
|
|
5356
|
-
seconds: a.delta.msec * 2 * Math.PI / Math.abs(entries[0][1]) / 1e3,
|
|
5357
|
-
direction: entries[0][1] < 0 ? "-1" : "1"
|
|
5358
|
-
};
|
|
5359
|
-
}
|
|
5360
|
-
settings.set(selected, value);
|
|
5361
|
-
}
|
|
5362
|
-
$("animation-axis").value = value.axis;
|
|
5363
|
-
$("animation-seconds").value = value.seconds;
|
|
5364
|
-
$("animation-direction").value = value.direction;
|
|
5365
|
-
$("animation-error").hidden = true;
|
|
5366
|
-
}
|
|
5367
|
-
const status = s.playback?.status || "idle", active = ["running", "paused"].includes(status);
|
|
5368
|
-
$("animation-play").textContent = active ? "Restart spin" : "Play";
|
|
5369
|
-
$("animation-pause").textContent = status === "paused" ? "Resume" : "Pause";
|
|
5370
|
-
$("animation-pause").disabled = !active;
|
|
5371
|
-
$("animation-stop").disabled = !active;
|
|
5372
|
-
const label = {
|
|
5373
|
-
idle: "Ready",
|
|
5374
|
-
running: "Playing",
|
|
5375
|
-
paused: "Paused",
|
|
5376
|
-
stopped: "Stopped",
|
|
5377
|
-
completed: "Finished"
|
|
5378
|
-
}[status];
|
|
5379
|
-
const text = `${label}. ${active ? "Stop keeps the current pose." : "Spin the selected model around its center."}`;
|
|
5380
|
-
if ($("animation-status").textContent !== text)
|
|
5381
|
-
$("animation-status").textContent = text;
|
|
5382
|
-
if (status === "running" && !timer)
|
|
5383
|
-
timer = window2.setInterval(() => {
|
|
5384
|
-
if (!disposed && !window2.document.hidden) refreshPose();
|
|
5385
|
-
}, 250);
|
|
5386
|
-
if (status !== "running" && timer) {
|
|
5387
|
-
window2.clearInterval(timer);
|
|
5388
|
-
timer = null;
|
|
5389
|
-
}
|
|
4476
|
+
var formats = /\.(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/i;
|
|
4477
|
+
var zero = () => ({ x: 0, y: 0, z: 0 });
|
|
4478
|
+
var failure = (code, message) => Object.assign(new Error(message), { code });
|
|
4479
|
+
var ViewerSession = class {
|
|
4480
|
+
constructor(container, options = {}) {
|
|
4481
|
+
if (options.viewer_options?.models)
|
|
4482
|
+
throw new TypeError("Use session.openFiles/openURL for initial models");
|
|
4483
|
+
if (options.viewer_options?.allow_drag_and_drop)
|
|
4484
|
+
throw new TypeError("Pass dropped files to session.openFiles");
|
|
4485
|
+
this._container = container;
|
|
4486
|
+
this._options = options;
|
|
4487
|
+
this._policy = memoryPolicy(options.device_memory);
|
|
4488
|
+
for (const key of Object.keys(this._policy.limits))
|
|
4489
|
+
if (options.viewer_options?.[key] !== void 0)
|
|
4490
|
+
this._policy.limits[key] = options.viewer_options[key];
|
|
4491
|
+
this._policy.budget = this._policy.limits.max_memory_bytes;
|
|
4492
|
+
this._records = /* @__PURE__ */ new Map();
|
|
4493
|
+
this._queue = [];
|
|
4494
|
+
this._generation = 0;
|
|
4495
|
+
this._nextID = 0;
|
|
4496
|
+
this._loadingID = null;
|
|
4497
|
+
this._viewer = null;
|
|
4498
|
+
this._viewerPromise = null;
|
|
4499
|
+
this._drainPromise = null;
|
|
4500
|
+
this._busy = false;
|
|
4501
|
+
this._disposed = false;
|
|
4502
|
+
this._arrangement = "original";
|
|
4503
|
+
this._grid = { enabled: false, spacing: 1, height: 0 };
|
|
4504
|
+
this._bindings = /* @__PURE__ */ new Set();
|
|
5390
4505
|
}
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
if (s.busy || s.selectedId == null) return;
|
|
5394
|
-
try {
|
|
5395
|
-
const value = {
|
|
5396
|
-
axis: $("animation-axis").value,
|
|
5397
|
-
seconds: $("animation-seconds").valueAsNumber,
|
|
5398
|
-
direction: $("animation-direction").value
|
|
5399
|
-
};
|
|
5400
|
-
const definition = spinDefinition(
|
|
5401
|
-
value.axis,
|
|
5402
|
-
value.seconds,
|
|
5403
|
-
value.direction
|
|
5404
|
-
);
|
|
5405
|
-
beforePlay(s.selectedId);
|
|
5406
|
-
getViewer().animate_model(s.selectedId, definition);
|
|
5407
|
-
if (keepPaused && s.playback?.status === "paused")
|
|
5408
|
-
getViewer().pause_model_animation(s.selectedId);
|
|
5409
|
-
settings.set(s.selectedId, value);
|
|
5410
|
-
$("animation-error").hidden = true;
|
|
5411
|
-
announce(
|
|
5412
|
-
keepPaused && s.playback?.status === "paused" ? "Spin updated. Still paused." : "Animation playing."
|
|
5413
|
-
);
|
|
5414
|
-
sync();
|
|
5415
|
-
} catch (error) {
|
|
5416
|
-
$("animation-error").textContent = error.message;
|
|
5417
|
-
$("animation-error").hidden = false;
|
|
5418
|
-
}
|
|
4506
|
+
get viewer() {
|
|
4507
|
+
return this._viewer;
|
|
5419
4508
|
}
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
const s = state();
|
|
5423
|
-
if (s.busy || s.selectedId == null) return;
|
|
5424
|
-
getViewer()[s.playback?.status === "paused" ? "resume_model_animation" : "pause_model_animation"](s.selectedId);
|
|
5425
|
-
sync();
|
|
5426
|
-
});
|
|
5427
|
-
listen($("animation-stop"), "click", () => {
|
|
5428
|
-
const s = state();
|
|
5429
|
-
if (!s.busy && s.selectedId != null)
|
|
5430
|
-
getViewer().animate_model(s.selectedId, null);
|
|
5431
|
-
sync();
|
|
5432
|
-
});
|
|
5433
|
-
for (const id of [
|
|
5434
|
-
"animation-axis",
|
|
5435
|
-
"animation-seconds",
|
|
5436
|
-
"animation-direction"
|
|
5437
|
-
])
|
|
5438
|
-
listen($(id), "change", () => {
|
|
5439
|
-
if (["running", "paused"].includes(state().playback?.status)) play(true);
|
|
5440
|
-
else
|
|
5441
|
-
settings.set(selected, {
|
|
5442
|
-
axis: $("animation-axis").value,
|
|
5443
|
-
seconds: $("animation-seconds").valueAsNumber,
|
|
5444
|
-
direction: $("animation-direction").value
|
|
5445
|
-
});
|
|
5446
|
-
});
|
|
5447
|
-
sync();
|
|
5448
|
-
return {
|
|
5449
|
-
sync,
|
|
5450
|
-
dispose() {
|
|
5451
|
-
disposed = true;
|
|
5452
|
-
window2.clearInterval(timer);
|
|
5453
|
-
settings.clear();
|
|
5454
|
-
}
|
|
5455
|
-
};
|
|
5456
|
-
}
|
|
5457
|
-
|
|
5458
|
-
// src/app.js
|
|
5459
|
-
var mountedRoots = /* @__PURE__ */ new WeakMap();
|
|
5460
|
-
var requiredControls = [
|
|
5461
|
-
"background-color",
|
|
5462
|
-
"background-value",
|
|
5463
|
-
"cancel-load",
|
|
5464
|
-
"clear-scene",
|
|
5465
|
-
"close-colors",
|
|
5466
|
-
"close-info",
|
|
5467
|
-
"close-model",
|
|
5468
|
-
"close-scene",
|
|
5469
|
-
"close-section",
|
|
5470
|
-
"close-view",
|
|
5471
|
-
"color-mode-note",
|
|
5472
|
-
"colors-button",
|
|
5473
|
-
"colors-panel",
|
|
5474
|
-
"depth-controls",
|
|
5475
|
-
"depth-model-button",
|
|
5476
|
-
"dismiss-notice",
|
|
5477
|
-
"drop-message",
|
|
5478
|
-
"drop-overlay",
|
|
5479
|
-
"empty-state",
|
|
5480
|
-
"file-heading",
|
|
5481
|
-
"file-input",
|
|
5482
|
-
"filename",
|
|
5483
|
-
"finish-move",
|
|
5484
|
-
"fit-button",
|
|
5485
|
-
"grid-button",
|
|
5486
|
-
"image-button",
|
|
5487
|
-
"info-button",
|
|
5488
|
-
"info-dimensions",
|
|
5489
|
-
"info-panel",
|
|
5490
|
-
"info-size",
|
|
5491
|
-
"info-triangles",
|
|
5492
|
-
"live-status",
|
|
5493
|
-
"loading-label",
|
|
5494
|
-
"loading-state",
|
|
5495
|
-
"memory-budget",
|
|
5496
|
-
"model-color",
|
|
5497
|
-
"model-color-value",
|
|
5498
|
-
"model-controls",
|
|
5499
|
-
"model-list",
|
|
5500
|
-
"model-tool",
|
|
5501
|
-
"move-controls",
|
|
5502
|
-
"move-gesture-message",
|
|
5503
|
-
"move-mode",
|
|
5504
|
-
"move-mode-name",
|
|
5505
|
-
"move-model-button",
|
|
5506
|
-
"notice",
|
|
5507
|
-
"notice-message",
|
|
5508
|
-
"open-label",
|
|
5509
|
-
"position-details",
|
|
5510
|
-
"position-error",
|
|
5511
|
-
"position-units",
|
|
5512
|
-
"position-x",
|
|
5513
|
-
"position-y",
|
|
5514
|
-
"position-z",
|
|
5515
|
-
"reset-position",
|
|
5516
|
-
"reset-rotation",
|
|
5517
|
-
"restore-color",
|
|
5518
|
-
"rotate-controls",
|
|
5519
|
-
"rotate-model-button",
|
|
5520
|
-
"rotation-error",
|
|
5521
|
-
"rotation-x",
|
|
5522
|
-
"rotation-y",
|
|
5523
|
-
"rotation-z",
|
|
5524
|
-
"sample-button",
|
|
5525
|
-
"scene-arrangement",
|
|
5526
|
-
"scene-button",
|
|
5527
|
-
"scene-count",
|
|
5528
|
-
"scene-panel",
|
|
5529
|
-
"section-axis",
|
|
5530
|
-
"section-button",
|
|
5531
|
-
"section-enabled",
|
|
5532
|
-
"section-flip",
|
|
5533
|
-
"section-panel",
|
|
5534
|
-
"section-position",
|
|
5535
|
-
"section-value",
|
|
5536
|
-
"smooth-shading",
|
|
5537
|
-
"unit-note",
|
|
5538
|
-
"view-button",
|
|
5539
|
-
"view-description",
|
|
5540
|
-
"view-label",
|
|
5541
|
-
"view-panel",
|
|
5542
|
-
"view-status",
|
|
5543
|
-
"viewer",
|
|
5544
|
-
"workspace"
|
|
5545
|
-
];
|
|
5546
|
-
function mountViewstlApp(root, options = {}) {
|
|
5547
|
-
if (!root?.querySelectorAll)
|
|
5548
|
-
throw new TypeError("A Document or HTMLElement root is required");
|
|
5549
|
-
if (mountedRoots.has(root))
|
|
5550
|
-
throw new Error("ViewSTL app is already mounted on this root");
|
|
5551
|
-
const document2 = root.ownerDocument || root;
|
|
5552
|
-
const window2 = document2.defaultView;
|
|
5553
|
-
const URL2 = window2.URL;
|
|
5554
|
-
const controls = new Map(
|
|
5555
|
-
[...root.querySelectorAll("[id]")].map((el) => [el.id, el])
|
|
5556
|
-
);
|
|
5557
|
-
const $ = (id) => controls.get((options.id_prefix || "") + id);
|
|
5558
|
-
const missing = requiredControls.filter((id) => !$(id));
|
|
5559
|
-
if (exportControls.some((id) => $(id)))
|
|
5560
|
-
missing.push(...exportControls.filter((id) => !$(id)));
|
|
5561
|
-
if (printControls.some((id) => $(id)))
|
|
5562
|
-
missing.push(...printControls.filter((id) => !$(id)));
|
|
5563
|
-
if (animationControls.some((id) => $(id)))
|
|
5564
|
-
missing.push(...animationControls.filter((id) => !$(id)));
|
|
5565
|
-
if (toolsControls.some((id) => $(id)))
|
|
5566
|
-
missing.push(...toolsControls.filter((id) => !$(id)));
|
|
5567
|
-
if (missing.length)
|
|
5568
|
-
throw new Error("Missing ViewSTL controls: " + missing.join(", "));
|
|
5569
|
-
const abort = new window2.AbortController();
|
|
5570
|
-
const dropTarget = root === document2 ? window2 : root;
|
|
5571
|
-
let disposed = false, drainPromise = null, lastLoadedID = null;
|
|
5572
|
-
const assertActive = () => {
|
|
5573
|
-
if (disposed) throw new Error("ViewSTL app is disposed");
|
|
5574
|
-
};
|
|
5575
|
-
const listen = (target, type, handler, options2 = {}) => target.addEventListener(type, handler, {
|
|
5576
|
-
...options2,
|
|
5577
|
-
signal: abort.signal
|
|
5578
|
-
});
|
|
5579
|
-
const policy = memoryPolicy(window2.navigator.deviceMemory);
|
|
5580
|
-
const panelNames = ["view", "section", "info", "scene", "colors"];
|
|
5581
|
-
const records = /* @__PURE__ */ new Map(), queue = [];
|
|
5582
|
-
const palette = [
|
|
5583
|
-
"#64a5b2",
|
|
5584
|
-
"#dd766b",
|
|
5585
|
-
"#edb14c",
|
|
5586
|
-
"#7ca984",
|
|
5587
|
-
"#927fbc",
|
|
5588
|
-
"#5a8ec7",
|
|
5589
|
-
"#89949d"
|
|
5590
|
-
];
|
|
5591
|
-
let viewerPromise, viewer, generation = 0, appearanceRequest = 0, modelID = null, nextID = 0, loadingID = null;
|
|
5592
|
-
let exportUI;
|
|
5593
|
-
let printUI;
|
|
5594
|
-
let animationUI;
|
|
5595
|
-
let toolsUI;
|
|
5596
|
-
let sceneImport;
|
|
5597
|
-
let draining = false, busy = false, gridEnabled = false, gridHeight = 0, gridSpacing = 1, moveMode = false, selectionMode = false, transformTool = "move", modelMover, dragArrangement, background = "transparent";
|
|
5598
|
-
const viewNames = {
|
|
5599
|
-
flat: "Solid",
|
|
5600
|
-
edges: "Solid + edges",
|
|
5601
|
-
wireframe: "Wireframe",
|
|
5602
|
-
xray: "X-ray",
|
|
5603
|
-
mesh: "Mesh overlay",
|
|
5604
|
-
studio: "Studio",
|
|
5605
|
-
orientation: "Face orientation"
|
|
5606
|
-
};
|
|
5607
|
-
const modeInputs = [...root.querySelectorAll('[name="view-mode"]')];
|
|
5608
|
-
const shortcutButtons = [...root.querySelectorAll("[data-viewstl-shortcut]")];
|
|
5609
|
-
if (options.id_prefix)
|
|
5610
|
-
modeInputs.forEach(
|
|
5611
|
-
(input) => input.name = options.id_prefix + "view-mode"
|
|
5612
|
-
);
|
|
5613
|
-
const sampleURL = options.sample_url;
|
|
5614
|
-
const formatNumber = new Intl.NumberFormat(void 0, {
|
|
5615
|
-
maximumFractionDigits: 2
|
|
5616
|
-
});
|
|
5617
|
-
function announce(text) {
|
|
5618
|
-
if (disposed) return;
|
|
5619
|
-
$("live-status").textContent = text;
|
|
5620
|
-
}
|
|
5621
|
-
function showNotice(text) {
|
|
5622
|
-
if (disposed) return;
|
|
5623
|
-
$("notice-message").textContent = text;
|
|
5624
|
-
$("notice").hidden = false;
|
|
5625
|
-
}
|
|
5626
|
-
function syncSelection() {
|
|
5627
|
-
viewer?.set_selection(
|
|
5628
|
-
!busy && records.has(modelID) && (selectionMode || moveMode || !$("scene-panel").hidden) ? modelID : null
|
|
5629
|
-
);
|
|
4509
|
+
get busy() {
|
|
4510
|
+
return this._busy;
|
|
5630
4511
|
}
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
const action = button.dataset.viewstlShortcut;
|
|
5634
|
-
button.disabled = busy || !records.size || action === "save" && !exportUI;
|
|
5635
|
-
if (action === "select")
|
|
5636
|
-
button.setAttribute("aria-pressed", String(selectionMode));
|
|
5637
|
-
}
|
|
4512
|
+
get arrangement() {
|
|
4513
|
+
return this._arrangement;
|
|
5638
4514
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
$("workspace").classList.toggle("selecting-model-mode", selectionMode);
|
|
5642
|
-
setMoveMode(selectionMode, "move", true);
|
|
5643
|
-
syncShortcuts();
|
|
5644
|
-
}
|
|
5645
|
-
function editModelAt(event) {
|
|
5646
|
-
const hit = viewer.pick_model(event.clientX, event.clientY);
|
|
5647
|
-
if (!hit || !records.has(hit.id)) return;
|
|
5648
|
-
event.preventDefault();
|
|
5649
|
-
selectModel(hit.id);
|
|
5650
|
-
closePanels();
|
|
5651
|
-
$("scene-panel").hidden = false;
|
|
5652
|
-
$("scene-button").setAttribute("aria-expanded", "true");
|
|
5653
|
-
$("position-details").open = true;
|
|
5654
|
-
syncSelection();
|
|
5655
|
-
announce(
|
|
5656
|
-
records.get(hit.id).name + " selected. Choose Move, Rotate or Depth."
|
|
5657
|
-
);
|
|
4515
|
+
get grid() {
|
|
4516
|
+
return { ...this._grid };
|
|
5658
4517
|
}
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
$(name + "-button").setAttribute("aria-expanded", "false");
|
|
5662
|
-
syncSelection();
|
|
5663
|
-
}
|
|
5664
|
-
function closePanels() {
|
|
5665
|
-
panelNames.forEach(closePanel);
|
|
5666
|
-
}
|
|
5667
|
-
function togglePanel(name) {
|
|
5668
|
-
const open = $(name + "-panel").hidden;
|
|
5669
|
-
closePanels();
|
|
5670
|
-
$(name + "-panel").hidden = !open;
|
|
5671
|
-
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
5672
|
-
syncSelection();
|
|
5673
|
-
if (open && name === "info") populateInfo();
|
|
5674
|
-
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
5675
|
-
}
|
|
5676
|
-
function fileSize(size) {
|
|
5677
|
-
return size == null ? "Sample model" : size < 1024 ** 2 ? `${formatNumber.format(size / 1024)} KiB` : size < 1024 ** 3 ? `${formatNumber.format(size / 1024 ** 2)} MiB` : `${formatNumber.format(size / 1024 ** 3)} GiB`;
|
|
5678
|
-
}
|
|
5679
|
-
function syncColors() {
|
|
5680
|
-
const record = records.get(modelID), color = /^#[a-f0-9]{6}$/i.test(record?.color) ? record.color : "#ffffff";
|
|
5681
|
-
$("model-color").value = color;
|
|
5682
|
-
$("model-color-value").textContent = record?.color === null ? "File colors" : color.toUpperCase();
|
|
5683
|
-
$("restore-color").textContent = record?.embedded ? "Restore file colors" : "Restore original color";
|
|
5684
|
-
root.querySelectorAll("[data-model-color]").forEach(
|
|
5685
|
-
(b) => b.setAttribute(
|
|
5686
|
-
"aria-pressed",
|
|
5687
|
-
String(b.dataset.modelColor === record?.color)
|
|
5688
|
-
)
|
|
5689
|
-
);
|
|
5690
|
-
root.querySelectorAll("[data-background]").forEach(
|
|
5691
|
-
(b) => b.setAttribute(
|
|
5692
|
-
"aria-pressed",
|
|
5693
|
-
String(b.dataset.background === background)
|
|
5694
|
-
)
|
|
5695
|
-
);
|
|
5696
|
-
$("background-color").value = background === "transparent" ? "#fafbfb" : background;
|
|
5697
|
-
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
5698
|
-
}
|
|
5699
|
-
function syncControls() {
|
|
5700
|
-
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5701
|
-
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
5702
|
-
for (const input of modeInputs) {
|
|
5703
|
-
input.checked = input.value === mode;
|
|
5704
|
-
input.disabled = busy;
|
|
5705
|
-
}
|
|
5706
|
-
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5707
|
-
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
5708
|
-
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
5709
|
-
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
5710
|
-
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
5711
|
-
const text = mode === "orientation" ? "Blue: front faces \xB7 Red: back faces" : "";
|
|
5712
|
-
$("view-description").textContent = text;
|
|
5713
|
-
$("view-description").hidden = !text;
|
|
5714
|
-
$("color-mode-note").hidden = mode !== "orientation";
|
|
5715
|
-
const section = info?.section;
|
|
5716
|
-
$("section-enabled").checked = !!section;
|
|
5717
|
-
$("section-axis").value = section?.axis || "z";
|
|
5718
|
-
$("section-position").value = (section?.position ?? 0.5) * 100;
|
|
5719
|
-
$("section-value").textContent = $("section-position").value + "%";
|
|
5720
|
-
$("section-flip").setAttribute(
|
|
5721
|
-
"aria-pressed",
|
|
5722
|
-
String(section?.flip || false)
|
|
5723
|
-
);
|
|
5724
|
-
$("section-button").setAttribute("aria-pressed", String(!!section));
|
|
5725
|
-
root.querySelectorAll("[data-selected-name]").forEach((el) => el.textContent = records.get(modelID)?.name || "");
|
|
5726
|
-
syncColors();
|
|
5727
|
-
syncPosition();
|
|
5728
|
-
exportUI?.sync();
|
|
5729
|
-
printUI?.sync();
|
|
5730
|
-
animationUI?.sync();
|
|
5731
|
-
toolsUI?.sync();
|
|
5732
|
-
syncShortcuts();
|
|
5733
|
-
}
|
|
5734
|
-
function populateInfo() {
|
|
5735
|
-
if (modelID === null) return;
|
|
5736
|
-
const info = viewer.get_model_info(modelID, { measurements: false }), record = records.get(modelID), factor = viewer.display_units === "inch" ? 25.4 : 1;
|
|
5737
|
-
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
5738
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x / factor)).join(" \xD7 ") + (factor === 1 ? " mm" : " in");
|
|
5739
|
-
$("info-size").textContent = fileSize(record.size);
|
|
5740
|
-
const size = viewer.get_model_size(modelID);
|
|
5741
|
-
$("unit-note").textContent = size.editable ? "File coordinates interpreted as " + (size.file_units === "inch" ? "inches." : "millimetres (assumed).") + " Change file units in Models \u2192 Size & units." : "Explicit file units were converted to millimetres on import.";
|
|
5742
|
-
toolsUI?.syncInfo();
|
|
5743
|
-
}
|
|
5744
|
-
function renderList() {
|
|
5745
|
-
$("model-list").replaceChildren(
|
|
5746
|
-
...[...records.values()].map((record) => {
|
|
5747
|
-
const row = document2.createElement("li"), color = document2.createElement("button"), pick = document2.createElement("button"), remove = document2.createElement("button");
|
|
5748
|
-
row.className = "model-row";
|
|
5749
|
-
row.classList.toggle("selected", record.id === modelID);
|
|
5750
|
-
color.className = "model-swatch";
|
|
5751
|
-
color.dataset.colorModel = record.id;
|
|
5752
|
-
color.style.backgroundColor = record.color || "#b7c5c9";
|
|
5753
|
-
color.setAttribute("aria-label", "Color for " + record.name);
|
|
5754
|
-
color.title = "Change color";
|
|
5755
|
-
pick.className = "model-pick";
|
|
5756
|
-
pick.dataset.selectModel = record.id;
|
|
5757
|
-
pick.textContent = record.name;
|
|
5758
|
-
pick.title = record.name;
|
|
5759
|
-
pick.setAttribute("aria-pressed", String(record.id === modelID));
|
|
5760
|
-
remove.className = "icon-button";
|
|
5761
|
-
remove.dataset.removeModel = record.id;
|
|
5762
|
-
remove.textContent = "\xD7";
|
|
5763
|
-
remove.setAttribute("aria-label", "Remove " + record.name);
|
|
5764
|
-
row.append(color, pick, remove);
|
|
5765
|
-
return row;
|
|
5766
|
-
})
|
|
5767
|
-
);
|
|
5768
|
-
$("scene-count").textContent = records.size;
|
|
5769
|
-
$("scene-arrangement").disabled = records.size === 0;
|
|
5770
|
-
}
|
|
5771
|
-
function renderScene() {
|
|
5772
|
-
const has = records.size > 0;
|
|
5773
|
-
if (!has && selectionMode) setSelectionMode(false);
|
|
5774
|
-
$("workspace").classList.toggle("has-models", has);
|
|
5775
|
-
$("viewer").hidden = !has && !busy;
|
|
5776
|
-
$("empty-state").hidden = has || busy;
|
|
5777
|
-
$("file-heading").hidden = !has;
|
|
5778
|
-
$("model-controls").hidden = !has;
|
|
5779
|
-
$("filename").textContent = records.get(modelID)?.name || "";
|
|
5780
|
-
$("filename").title = $("filename").textContent;
|
|
5781
|
-
$("open-label").textContent = has ? "Add models" : "Open files";
|
|
5782
|
-
$("drop-message").textContent = has ? "Add to your scene." : "Drop it here.";
|
|
5783
|
-
renderList();
|
|
5784
|
-
syncControls();
|
|
5785
|
-
populateInfo();
|
|
5786
|
-
if (!has) closePanels();
|
|
5787
|
-
syncSelection();
|
|
5788
|
-
}
|
|
5789
|
-
function setBusy(value) {
|
|
5790
|
-
if (value) exportUI?.close();
|
|
5791
|
-
if (disposed) return;
|
|
5792
|
-
if (value && selectionMode) setSelectionMode(false);
|
|
5793
|
-
if (value) setMoveMode(false);
|
|
5794
|
-
busy = value;
|
|
5795
|
-
$("loading-state").hidden = !value;
|
|
5796
|
-
$("workspace").setAttribute("aria-busy", String(value));
|
|
5797
|
-
renderScene();
|
|
5798
|
-
}
|
|
5799
|
-
function selectModel(id) {
|
|
5800
|
-
exportUI?.close();
|
|
5801
|
-
if (!records.has(id)) return;
|
|
5802
|
-
if (selectionMode) modelMover?.setEnabled(false);
|
|
5803
|
-
else setMoveMode(false);
|
|
5804
|
-
appearanceRequest++;
|
|
5805
|
-
modelID = id;
|
|
5806
|
-
renderScene();
|
|
5807
|
-
if (selectionMode) setMoveMode(true, transformTool, true);
|
|
5808
|
-
}
|
|
5809
|
-
function placements() {
|
|
5810
|
-
return scenePlacement([...records.values()], $("scene-arrangement").value);
|
|
5811
|
-
}
|
|
5812
|
-
function updateGrid() {
|
|
5813
|
-
viewer?.set_infinite_grid(gridEnabled, gridSpacing, gridHeight);
|
|
5814
|
-
$("grid-button").setAttribute("aria-pressed", String(gridEnabled));
|
|
5815
|
-
}
|
|
5816
|
-
function arrangeScene() {
|
|
5817
|
-
setMoveMode(false);
|
|
5818
|
-
for (const record of records.values())
|
|
5819
|
-
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5820
|
-
if ($("scene-arrangement").value === "custom") return;
|
|
5821
|
-
for (const record of records.values()) prepareManual(record.id);
|
|
5822
|
-
if ($("scene-arrangement").value === "original")
|
|
5823
|
-
for (const record of records.values())
|
|
5824
|
-
record.rotation = {
|
|
5825
|
-
...record.originalRotation || { x: 0, y: 0, z: 0 }
|
|
5826
|
-
};
|
|
5827
|
-
const positions = placements();
|
|
5828
|
-
for (const p of positions) {
|
|
5829
|
-
records.get(p.id).position = { x: p.x, y: p.y, z: p.z };
|
|
5830
|
-
renderTransform(records.get(p.id));
|
|
5831
|
-
}
|
|
5832
|
-
if ($("scene-arrangement").value !== "custom" || !records.size)
|
|
5833
|
-
gridHeight = sceneFloor([...records.values()], positions);
|
|
5834
|
-
syncPosition();
|
|
5835
|
-
updateGrid();
|
|
5836
|
-
viewer?.set_auto_zoom();
|
|
5837
|
-
}
|
|
5838
|
-
function syncPosition(preserveFocus = false) {
|
|
5839
|
-
const record = records.get(modelID);
|
|
5840
|
-
for (const axis of ["x", "y", "z"]) {
|
|
5841
|
-
const input = $("position-" + axis);
|
|
5842
|
-
if (!preserveFocus || document2.activeElement !== input)
|
|
5843
|
-
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
5844
|
-
input.disabled = !record;
|
|
5845
|
-
input.removeAttribute("aria-invalid");
|
|
5846
|
-
}
|
|
5847
|
-
$("position-error").hidden = true;
|
|
5848
|
-
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
5849
|
-
$("move-model-button").disabled = !record || busy;
|
|
5850
|
-
$("reset-position").disabled = !record;
|
|
5851
|
-
syncRotation(preserveFocus);
|
|
5852
|
-
for (const id of [
|
|
5853
|
-
"rotate-model-button",
|
|
5854
|
-
"depth-model-button",
|
|
5855
|
-
"depth-nearer",
|
|
5856
|
-
"depth-farther"
|
|
5857
|
-
])
|
|
5858
|
-
$(id).disabled = !record || busy;
|
|
4518
|
+
get memoryPolicy() {
|
|
4519
|
+
return { ...this._policy, limits: { ...this._policy.limits } };
|
|
5859
4520
|
}
|
|
5860
|
-
|
|
5861
|
-
if (
|
|
5862
|
-
(v) => Number.isFinite(v) && Math.abs(v) <= 1e12
|
|
5863
|
-
))
|
|
5864
|
-
return false;
|
|
5865
|
-
records.get(id).position = { ...position };
|
|
5866
|
-
renderTransform(records.get(id));
|
|
5867
|
-
$("scene-arrangement").value = "custom";
|
|
5868
|
-
if (id === modelID && refreshInputs) syncPosition();
|
|
5869
|
-
return true;
|
|
5870
|
-
}
|
|
5871
|
-
function renderTransform(record) {
|
|
5872
|
-
const p = enginePosition(record), r = record.rotation;
|
|
5873
|
-
viewer.set_rotation(
|
|
5874
|
-
record.id,
|
|
5875
|
-
...["x", "y", "z"].map((axis) => r[axis] % 360 * Math.PI / 180)
|
|
5876
|
-
);
|
|
5877
|
-
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4521
|
+
_assertActive() {
|
|
4522
|
+
if (this._disposed) throw new Error("ViewSTL session is disposed");
|
|
5878
4523
|
}
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
const record = records.get(modelID);
|
|
5882
|
-
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4524
|
+
_emit(event) {
|
|
4525
|
+
if (!this._disposed) this._call(this._options.on_event, event);
|
|
5883
4526
|
}
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
5890
|
-
input.disabled = !record;
|
|
5891
|
-
input.removeAttribute("aria-invalid");
|
|
4527
|
+
_call(callback, ...args) {
|
|
4528
|
+
try {
|
|
4529
|
+
callback?.(...args);
|
|
4530
|
+
} catch (error) {
|
|
4531
|
+
console.error(error);
|
|
5892
4532
|
}
|
|
5893
|
-
$("rotation-error").hidden = true;
|
|
5894
|
-
$("reset-rotation").disabled = !record;
|
|
5895
4533
|
}
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
))
|
|
5900
|
-
return false;
|
|
5901
|
-
records.get(id).rotation = { ...rotation2 };
|
|
5902
|
-
renderTransform(records.get(id));
|
|
5903
|
-
$("scene-arrangement").value = "custom";
|
|
5904
|
-
if (refreshInputs && id === modelID) syncRotation();
|
|
5905
|
-
return true;
|
|
4534
|
+
_setBusy(value) {
|
|
4535
|
+
this._busy = value;
|
|
4536
|
+
this._emit({ type: "busy", busy: value });
|
|
5906
4537
|
}
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
function syncTool() {
|
|
5914
|
-
if (viewer?.canvas) viewer.canvas.dataset.modelTool = transformTool;
|
|
5915
|
-
root.querySelectorAll("[data-transform-tab]").forEach(
|
|
5916
|
-
(b) => b.setAttribute(
|
|
5917
|
-
"aria-pressed",
|
|
5918
|
-
String(b.dataset.transformTab === transformTool)
|
|
5919
|
-
)
|
|
5920
|
-
);
|
|
5921
|
-
for (const name of ["move", "rotate", "depth"]) {
|
|
5922
|
-
$(name + "-controls").hidden = name !== transformTool;
|
|
5923
|
-
$(name + "-model-button").setAttribute(
|
|
5924
|
-
"aria-pressed",
|
|
5925
|
-
String(moveMode && name === transformTool)
|
|
5926
|
-
);
|
|
5927
|
-
}
|
|
5928
|
-
$("model-tool").value = transformTool;
|
|
5929
|
-
$("move-gesture-message").textContent = (selectionMode ? "Click a model to select \xB7 " : "") + toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
5930
|
-
}
|
|
5931
|
-
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
5932
|
-
modelMover?.setEnabled(false);
|
|
5933
|
-
transformTool = tool;
|
|
5934
|
-
moveMode = !!enabled && modelID !== null && !busy;
|
|
5935
|
-
if (!moveMode && selectionMode) {
|
|
5936
|
-
selectionMode = false;
|
|
5937
|
-
$("workspace").classList.remove("selecting-model-mode");
|
|
5938
|
-
}
|
|
5939
|
-
if (viewer) viewer.controls.enabled = !selectionMode;
|
|
5940
|
-
syncShortcuts();
|
|
5941
|
-
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
5942
|
-
modelMover?.setEnabled(moveMode);
|
|
5943
|
-
syncTool();
|
|
5944
|
-
$("move-mode").hidden = !moveMode;
|
|
5945
|
-
$("move-mode-name").textContent = records.get(modelID)?.name || "";
|
|
5946
|
-
$("workspace").classList.toggle("moving-model-mode", moveMode);
|
|
5947
|
-
if (viewer?.canvas)
|
|
5948
|
-
viewer.canvas.setAttribute(
|
|
5949
|
-
"aria-label",
|
|
5950
|
-
moveMode ? (selectionMode ? "Click a model to select. " : "") + toolHints[transformTool] + ". Scroll changes only its depth. Escape exits." : "3D model viewer. Drag to rotate, shift-drag to pan, scroll to zoom."
|
|
5951
|
-
);
|
|
5952
|
-
if (moveMode) {
|
|
5953
|
-
closePanels();
|
|
5954
|
-
viewer.canvas.focus({ preventScroll: true });
|
|
5955
|
-
announce(
|
|
5956
|
-
toolLabels[transformTool] + " tool for " + records.get(modelID).name + ". Done or Escape returns to viewing."
|
|
5957
|
-
);
|
|
5958
|
-
}
|
|
5959
|
-
syncSelection();
|
|
5960
|
-
}
|
|
5961
|
-
for (const axis of ["x", "y", "z"]) {
|
|
5962
|
-
const input = $("rotation-" + axis);
|
|
5963
|
-
listen(input, "input", () => {
|
|
5964
|
-
if (modelID === null || !input.value.trim()) return;
|
|
5965
|
-
prepareManual(modelID);
|
|
5966
|
-
if (applyRotation(
|
|
5967
|
-
modelID,
|
|
5968
|
-
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
5969
|
-
false
|
|
5970
|
-
)) {
|
|
5971
|
-
input.removeAttribute("aria-invalid");
|
|
5972
|
-
$("rotation-error").hidden = true;
|
|
5973
|
-
}
|
|
5974
|
-
});
|
|
5975
|
-
listen(input, "change", () => {
|
|
5976
|
-
if (modelID === null) return;
|
|
5977
|
-
prepareManual(modelID);
|
|
5978
|
-
if (!input.value.trim() || !applyRotation(modelID, {
|
|
5979
|
-
...records.get(modelID).rotation,
|
|
5980
|
-
[axis]: input.valueAsNumber
|
|
5981
|
-
})) {
|
|
5982
|
-
input.setAttribute("aria-invalid", "true");
|
|
5983
|
-
$("rotation-error").hidden = false;
|
|
5984
|
-
} else announce("Model rotation updated.");
|
|
4538
|
+
_error(error, source) {
|
|
4539
|
+
this._emit({
|
|
4540
|
+
type: "error",
|
|
4541
|
+
code: error.code || "load-failed",
|
|
4542
|
+
message: error.message,
|
|
4543
|
+
source: source?.name
|
|
5985
4544
|
});
|
|
5986
4545
|
}
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
if (
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
}
|
|
5996
|
-
});
|
|
5997
|
-
for (const name of ["move", "rotate", "depth"])
|
|
5998
|
-
listen(
|
|
5999
|
-
$(name + "-model-button"),
|
|
6000
|
-
"click",
|
|
6001
|
-
() => setMoveMode(!moveMode || transformTool !== name, name)
|
|
6002
|
-
);
|
|
6003
|
-
for (const [id, amount] of [
|
|
6004
|
-
["depth-nearer", -0.12],
|
|
6005
|
-
["depth-farther", 0.12]
|
|
6006
|
-
])
|
|
6007
|
-
listen($(id), "click", () => {
|
|
6008
|
-
const model = selectedTransform();
|
|
6009
|
-
if (!model) return;
|
|
6010
|
-
applyPosition(
|
|
6011
|
-
model.id,
|
|
6012
|
-
depthPosition(model, viewer.get_camera_state(), amount)
|
|
6013
|
-
);
|
|
6014
|
-
announce(
|
|
6015
|
-
amount < 0 ? "Selected model moved nearer." : "Selected model moved farther."
|
|
6016
|
-
);
|
|
6017
|
-
});
|
|
6018
|
-
root.querySelectorAll("[data-transform-tab]").forEach(
|
|
6019
|
-
(b) => listen(
|
|
6020
|
-
b,
|
|
6021
|
-
"click",
|
|
6022
|
-
() => setMoveMode(selectionMode, b.dataset.transformTab, true)
|
|
6023
|
-
)
|
|
6024
|
-
);
|
|
6025
|
-
listen(
|
|
6026
|
-
$("model-tool"),
|
|
6027
|
-
"change",
|
|
6028
|
-
() => setMoveMode(true, $("model-tool").value, selectionMode)
|
|
6029
|
-
);
|
|
6030
|
-
for (const axis of ["x", "y", "z"]) {
|
|
6031
|
-
const input = $("position-" + axis);
|
|
6032
|
-
listen(input, "input", () => {
|
|
6033
|
-
if (modelID === null || !input.value.trim()) return;
|
|
6034
|
-
prepareManual(modelID);
|
|
6035
|
-
if (applyPosition(
|
|
6036
|
-
modelID,
|
|
6037
|
-
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
6038
|
-
false
|
|
6039
|
-
)) {
|
|
6040
|
-
input.removeAttribute("aria-invalid");
|
|
6041
|
-
$("position-error").hidden = true;
|
|
6042
|
-
}
|
|
6043
|
-
});
|
|
6044
|
-
listen(input, "change", () => {
|
|
6045
|
-
if (modelID === null) return;
|
|
6046
|
-
prepareManual(modelID);
|
|
6047
|
-
if (!input.value.trim() || !applyPosition(modelID, {
|
|
6048
|
-
...records.get(modelID).position,
|
|
6049
|
-
[axis]: input.valueAsNumber
|
|
6050
|
-
})) {
|
|
6051
|
-
input.setAttribute("aria-invalid", "true");
|
|
6052
|
-
$("position-error").hidden = false;
|
|
6053
|
-
} else announce("Model position updated.");
|
|
6054
|
-
});
|
|
6055
|
-
}
|
|
6056
|
-
listen($("finish-move"), "click", () => {
|
|
6057
|
-
setMoveMode(false);
|
|
6058
|
-
viewer?.canvas.focus({ preventScroll: true });
|
|
6059
|
-
});
|
|
6060
|
-
listen($("reset-position"), "click", () => {
|
|
6061
|
-
setMoveMode(false);
|
|
6062
|
-
if (modelID !== null) {
|
|
6063
|
-
prepareManual(modelID);
|
|
6064
|
-
applyPosition(modelID, {
|
|
6065
|
-
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
6066
|
-
});
|
|
6067
|
-
announce("Selected model restored to its original position.");
|
|
6068
|
-
}
|
|
6069
|
-
});
|
|
6070
|
-
listen(window2, "blur", () => {
|
|
6071
|
-
if (moveMode) setMoveMode(false);
|
|
6072
|
-
});
|
|
6073
|
-
function cancelLoading() {
|
|
6074
|
-
generation++;
|
|
6075
|
-
queue.length = 0;
|
|
6076
|
-
sceneImport?.abort();
|
|
6077
|
-
if (loadingID !== null) {
|
|
6078
|
-
viewer?.remove_model(loadingID);
|
|
6079
|
-
loadingID = null;
|
|
6080
|
-
}
|
|
6081
|
-
setBusy(false);
|
|
6082
|
-
announce("Opening cancelled.");
|
|
6083
|
-
}
|
|
6084
|
-
function clearScene() {
|
|
6085
|
-
exportUI?.close();
|
|
6086
|
-
setMoveMode(false);
|
|
6087
|
-
cancelLoading();
|
|
6088
|
-
appearanceRequest++;
|
|
6089
|
-
viewer?.clean();
|
|
6090
|
-
records.clear();
|
|
6091
|
-
modelID = null;
|
|
6092
|
-
gridEnabled = false;
|
|
6093
|
-
gridHeight = 0;
|
|
6094
|
-
gridSpacing = 1;
|
|
6095
|
-
$("scene-arrangement").value = "original";
|
|
6096
|
-
updateGrid();
|
|
6097
|
-
$("notice").hidden = true;
|
|
6098
|
-
renderScene();
|
|
6099
|
-
announce("Scene cleared.");
|
|
6100
|
-
}
|
|
6101
|
-
function removeModel(id) {
|
|
6102
|
-
if (!records.has(id)) return;
|
|
6103
|
-
exportUI?.close();
|
|
6104
|
-
const name = records.get(id).name;
|
|
6105
|
-
setMoveMode(false);
|
|
6106
|
-
viewer.remove_model(id);
|
|
6107
|
-
records.delete(id);
|
|
6108
|
-
appearanceRequest++;
|
|
6109
|
-
if (modelID === id) modelID = [...records.keys()].at(-1) ?? null;
|
|
6110
|
-
arrangeScene();
|
|
6111
|
-
renderScene();
|
|
6112
|
-
announce(name + " removed.");
|
|
6113
|
-
}
|
|
6114
|
-
function prepareManual(id) {
|
|
6115
|
-
if (!records.has(id)) return;
|
|
6116
|
-
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6117
|
-
viewer?.animate_model(id, null);
|
|
6118
|
-
}
|
|
6119
|
-
function refreshAnimatedPose() {
|
|
6120
|
-
if (disposed || modelID === null) return;
|
|
6121
|
-
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6122
|
-
if (!$("info-panel").hidden) populateInfo();
|
|
6123
|
-
if (!$("scene-panel").hidden && $("position-details").open)
|
|
6124
|
-
syncPosition(true);
|
|
6125
|
-
}
|
|
6126
|
-
function animationChanged(id, state, reason) {
|
|
6127
|
-
if (disposed) return;
|
|
6128
|
-
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
6129
|
-
viewer.pause_model_animation(id);
|
|
6130
|
-
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6131
|
-
if (["started", "restored", "resumed"].includes(reason))
|
|
6132
|
-
$("scene-arrangement").value = "custom";
|
|
6133
|
-
if (id === modelID) refreshAnimatedPose();
|
|
6134
|
-
animationUI?.sync();
|
|
6135
|
-
printUI?.sync();
|
|
6136
|
-
options.animation_state_callback?.(id, state, reason);
|
|
6137
|
-
}
|
|
6138
|
-
async function getViewer() {
|
|
6139
|
-
if (!viewerPromise)
|
|
6140
|
-
viewerPromise = Promise.resolve().then(async () => {
|
|
6141
|
-
if (disposed) throw new Error("ViewSTL app is disposed");
|
|
6142
|
-
viewer = new StlViewer($("viewer"), {
|
|
6143
|
-
...policy.limits,
|
|
4546
|
+
async ready() {
|
|
4547
|
+
this._assertActive();
|
|
4548
|
+
if (!this._viewerPromise) {
|
|
4549
|
+
this._viewerPromise = Promise.resolve().then(async () => {
|
|
4550
|
+
this._assertActive();
|
|
4551
|
+
const supplied = this._options.viewer_options || {};
|
|
4552
|
+
const viewer = this._viewer = new StlViewer(this._container, {
|
|
4553
|
+
...this._policy.limits,
|
|
6144
4554
|
worker_count: 1,
|
|
6145
|
-
worker_timeout:
|
|
6146
|
-
worker_url: options.worker_url,
|
|
6147
|
-
worker_factory: options.worker_factory,
|
|
6148
|
-
url_import_endpoint: options.url_import_endpoint,
|
|
4555
|
+
worker_timeout: 6e5,
|
|
6149
4556
|
center_models: false,
|
|
6150
|
-
bg_color: background,
|
|
6151
4557
|
allow_drag_and_drop: false,
|
|
6152
|
-
|
|
6153
|
-
animation_complete_callback: (...args) => {
|
|
6154
|
-
if (!disposed) options.animation_complete_callback?.(...args);
|
|
6155
|
-
},
|
|
6156
|
-
camera_state: {
|
|
6157
|
-
position: { x: 90, y: -130, z: 90 },
|
|
6158
|
-
target: { x: 0, y: 0, z: 0 },
|
|
6159
|
-
up: { x: 0, y: 0, z: 1 }
|
|
6160
|
-
},
|
|
4558
|
+
...supplied,
|
|
6161
4559
|
loading_progress_callback: (status) => {
|
|
6162
|
-
if (
|
|
6163
|
-
|
|
6164
|
-
|
|
4560
|
+
if (this._busy && !this._sceneImport)
|
|
4561
|
+
this._emit({
|
|
4562
|
+
type: "progress",
|
|
4563
|
+
scene: false,
|
|
4564
|
+
progress: status[this._loadingID] || {}
|
|
4565
|
+
});
|
|
4566
|
+
this._call(supplied.loading_progress_callback, status);
|
|
6165
4567
|
},
|
|
6166
4568
|
model_loaded_callback: (id) => {
|
|
6167
|
-
|
|
4569
|
+
this._lastLoadedID = id;
|
|
4570
|
+
this._call(supplied.model_loaded_callback, id);
|
|
6168
4571
|
},
|
|
6169
4572
|
load_error_callback: (message) => {
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
);
|
|
6173
|
-
}
|
|
6174
|
-
});
|
|
6175
|
-
await viewer.when_ready;
|
|
6176
|
-
if (disposed) throw new Error("ViewSTL app is disposed");
|
|
6177
|
-
modelMover = bindModelMove(viewer.canvas, {
|
|
6178
|
-
getCamera: () => viewer.get_camera_state(),
|
|
6179
|
-
getModel: (event) => {
|
|
6180
|
-
if (selectionMode && event) {
|
|
6181
|
-
const hit = viewer.pick_model(event.clientX, event.clientY);
|
|
6182
|
-
if (!hit || !records.has(hit.id)) return null;
|
|
6183
|
-
if (hit.id !== modelID) selectModel(hit.id);
|
|
6184
|
-
}
|
|
6185
|
-
return selectedTransform();
|
|
4573
|
+
this._error(new Error(message));
|
|
4574
|
+
this._call(supplied.load_error_callback, message);
|
|
6186
4575
|
},
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
4576
|
+
animation_state_callback: (id, state, reason) => {
|
|
4577
|
+
if (["started", "restored", "resumed"].includes(reason))
|
|
4578
|
+
this._arrangement = "custom";
|
|
4579
|
+
this._emit({ type: "animation", id, state, reason });
|
|
4580
|
+
this._call(supplied.animation_state_callback, id, state, reason);
|
|
6192
4581
|
},
|
|
6193
|
-
|
|
6194
|
-
if (
|
|
6195
|
-
|
|
4582
|
+
animation_complete_callback: (...args) => {
|
|
4583
|
+
if (!this._disposed)
|
|
4584
|
+
this._call(supplied.animation_complete_callback, ...args);
|
|
6196
4585
|
}
|
|
6197
4586
|
});
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
});
|
|
4587
|
+
await viewer.when_ready;
|
|
4588
|
+
this._assertActive();
|
|
4589
|
+
this._emit({ type: "ready" });
|
|
6202
4590
|
return viewer;
|
|
6203
4591
|
}).catch((error) => {
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
viewer = null;
|
|
6208
|
-
viewerPromise = null;
|
|
4592
|
+
this._viewer?.dispose();
|
|
4593
|
+
this._viewer = null;
|
|
4594
|
+
this._viewerPromise = null;
|
|
6209
4595
|
throw error;
|
|
6210
4596
|
});
|
|
6211
|
-
|
|
4597
|
+
}
|
|
4598
|
+
return this._viewerPromise;
|
|
4599
|
+
}
|
|
4600
|
+
_record(id) {
|
|
4601
|
+
const record = this._records.get(id), model = this._viewer?._models.get(id);
|
|
4602
|
+
if (!record || !model?.geometry) return null;
|
|
4603
|
+
syncAnimatedRecord(record, model);
|
|
4604
|
+
record.color = model.color || (model.geometry.colors ? null : "#909090");
|
|
4605
|
+
return record;
|
|
4606
|
+
}
|
|
4607
|
+
getModel(id) {
|
|
4608
|
+
this._assertActive();
|
|
4609
|
+
const r = this._record(id);
|
|
4610
|
+
if (!r) return null;
|
|
4611
|
+
const { sourceFile, estimated, ...data } = r;
|
|
4612
|
+
return structuredClone({
|
|
4613
|
+
...data,
|
|
4614
|
+
animation: this._viewer.get_model_animation_state(id),
|
|
4615
|
+
animationDefinition: this._viewer._models.get(id)?.animation || null
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
getModels() {
|
|
4619
|
+
this._assertActive();
|
|
4620
|
+
return [...this._records.keys()].map((id) => this.getModel(id)).filter(Boolean);
|
|
4621
|
+
}
|
|
4622
|
+
_adopt(id, source = {}, imported = false) {
|
|
4623
|
+
const m = this._viewer._models.get(id), g = m.geometry;
|
|
4624
|
+
const record = {
|
|
4625
|
+
id,
|
|
4626
|
+
name: source.name || String(m.display_name || m.orig_filename || m.filename).split(/[\\/]/).pop().split(/[?#]/)[0],
|
|
4627
|
+
size: source.size ?? g.file_bytes ?? m.local_file?.size ?? null,
|
|
4628
|
+
units: g.units || m.units || null,
|
|
4629
|
+
embedded: !!g.colors,
|
|
4630
|
+
color: m.color || (g.colors ? null : "#909090"),
|
|
4631
|
+
estimated: g.triangles * 224,
|
|
4632
|
+
sourceFile: source.sourceFile || null
|
|
4633
|
+
};
|
|
4634
|
+
syncAnimatedRecord(record, m);
|
|
4635
|
+
record.originalPosition = imported ? { ...record.position } : zero();
|
|
4636
|
+
record.originalRotation = imported ? { ...record.rotation } : zero();
|
|
4637
|
+
record.originalColor = record.color;
|
|
4638
|
+
this._records.set(id, record);
|
|
4639
|
+
}
|
|
4640
|
+
prepareManual(id) {
|
|
4641
|
+
this._assertActive();
|
|
4642
|
+
if (!this._record(id)) return;
|
|
4643
|
+
this._viewer.animate_model(id, null);
|
|
4644
|
+
}
|
|
4645
|
+
_renderTransform(record) {
|
|
4646
|
+
const p = enginePosition(record), r = record.rotation;
|
|
4647
|
+
this._viewer.set_rotation(
|
|
4648
|
+
record.id,
|
|
4649
|
+
...axes4.map((a) => r[a] % 360 * Math.PI / 180)
|
|
4650
|
+
);
|
|
4651
|
+
this._viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4652
|
+
}
|
|
4653
|
+
_setTransform(id, kind, value, limit) {
|
|
4654
|
+
this._assertActive();
|
|
4655
|
+
if (!this._records.has(id) || !value || !axes4.every(
|
|
4656
|
+
(a) => Number.isFinite(value[a]) && Math.abs(value[a]) <= limit
|
|
4657
|
+
))
|
|
4658
|
+
return false;
|
|
4659
|
+
this.prepareManual(id);
|
|
4660
|
+
const record = this._records.get(id);
|
|
4661
|
+
record[kind] = { ...value };
|
|
4662
|
+
this._renderTransform(record);
|
|
4663
|
+
this._arrangement = "custom";
|
|
4664
|
+
return true;
|
|
4665
|
+
}
|
|
4666
|
+
setPosition(id, position) {
|
|
4667
|
+
return this._setTransform(id, "position", position, 1e12);
|
|
4668
|
+
}
|
|
4669
|
+
setRotation(id, degrees) {
|
|
4670
|
+
return this._setTransform(id, "rotation", degrees, 36e4);
|
|
4671
|
+
}
|
|
4672
|
+
centerPivot(id) {
|
|
4673
|
+
this.prepareManual(id);
|
|
4674
|
+
const record = this._records.get(id);
|
|
4675
|
+
if (!record) return;
|
|
4676
|
+
centerAnimationPivot(record, this._viewer._models.get(id));
|
|
4677
|
+
this._arrangement = "custom";
|
|
6212
4678
|
}
|
|
6213
|
-
|
|
6214
|
-
|
|
4679
|
+
_manipulationModel(id) {
|
|
4680
|
+
this.prepareManual(id);
|
|
4681
|
+
const record = this._record(id);
|
|
4682
|
+
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4683
|
+
}
|
|
4684
|
+
moveDepth(id, amount) {
|
|
4685
|
+
if (!Number.isFinite(amount))
|
|
4686
|
+
throw new TypeError("Depth amount must be finite");
|
|
4687
|
+
const model = this._manipulationModel(id);
|
|
4688
|
+
return model ? this.setPosition(
|
|
4689
|
+
id,
|
|
4690
|
+
depthPosition(model, this._viewer.get_camera_state(), amount)
|
|
4691
|
+
) : false;
|
|
4692
|
+
}
|
|
4693
|
+
bindManipulation(options) {
|
|
4694
|
+
this._assertActive();
|
|
4695
|
+
if (!this._viewer?.ready)
|
|
4696
|
+
throw new Error("Await session.ready() before binding manipulation");
|
|
4697
|
+
let arrangement, disposed = false;
|
|
4698
|
+
const binding = bindModelMove(this._viewer.canvas, {
|
|
4699
|
+
getCamera: () => this._viewer.get_camera_state(),
|
|
4700
|
+
getModel: (event) => this._manipulationModel(options.getModelId(event)),
|
|
4701
|
+
getTool: options.getTool,
|
|
4702
|
+
onMove: (id, position) => {
|
|
4703
|
+
this.setPosition(id, position);
|
|
4704
|
+
options.onChange?.(id);
|
|
4705
|
+
},
|
|
4706
|
+
onRotate: (id, rotation2) => {
|
|
4707
|
+
this.setRotation(id, rotation2);
|
|
4708
|
+
options.onChange?.(id);
|
|
4709
|
+
},
|
|
4710
|
+
onStart: (id) => {
|
|
4711
|
+
arrangement = this._arrangement;
|
|
4712
|
+
options.onStart?.(id);
|
|
4713
|
+
},
|
|
4714
|
+
onEnd: (id, cancelled) => {
|
|
4715
|
+
if (cancelled) this._arrangement = arrangement;
|
|
4716
|
+
options.onEnd?.(id, cancelled);
|
|
4717
|
+
}
|
|
4718
|
+
});
|
|
4719
|
+
const handle = {
|
|
4720
|
+
setEnabled: (value) => {
|
|
4721
|
+
if (!disposed) binding.setEnabled(value);
|
|
4722
|
+
},
|
|
4723
|
+
dispose: () => {
|
|
4724
|
+
if (disposed) return;
|
|
4725
|
+
disposed = true;
|
|
4726
|
+
binding.dispose();
|
|
4727
|
+
this._bindings.delete(handle);
|
|
4728
|
+
}
|
|
4729
|
+
};
|
|
4730
|
+
this._bindings.add(handle);
|
|
4731
|
+
return handle;
|
|
4732
|
+
}
|
|
4733
|
+
setArrangement(mode) {
|
|
4734
|
+
this._assertActive();
|
|
4735
|
+
if (!["original", "side-by-side", "custom"].includes(mode))
|
|
4736
|
+
throw new TypeError("Unknown arrangement");
|
|
4737
|
+
this._arrangement = mode;
|
|
4738
|
+
for (const id of this._records.keys()) this._record(id);
|
|
4739
|
+
if (mode === "custom") return;
|
|
4740
|
+
for (const record of this._records.values()) {
|
|
4741
|
+
this.prepareManual(record.id);
|
|
4742
|
+
if (mode === "original") record.rotation = { ...record.originalRotation };
|
|
4743
|
+
}
|
|
4744
|
+
const records = [...this._records.values()], positions = scenePlacement(records, mode);
|
|
4745
|
+
for (const { id, x, y, z } of positions) {
|
|
4746
|
+
const r = this._records.get(id);
|
|
4747
|
+
r.position = { x, y, z };
|
|
4748
|
+
this._renderTransform(r);
|
|
4749
|
+
}
|
|
4750
|
+
this.setGrid(
|
|
4751
|
+
this._grid.enabled,
|
|
4752
|
+
this._grid.spacing,
|
|
4753
|
+
sceneFloor(records, positions)
|
|
4754
|
+
);
|
|
4755
|
+
this._viewer?.set_auto_zoom();
|
|
4756
|
+
}
|
|
4757
|
+
setGrid(enabled, spacing = this._grid.spacing, height = this._grid.height) {
|
|
4758
|
+
this._assertActive();
|
|
4759
|
+
if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(height))
|
|
4760
|
+
throw new TypeError("Grid spacing must be positive and height finite");
|
|
4761
|
+
this._viewer?.set_infinite_grid(enabled, spacing, height);
|
|
4762
|
+
this._grid = { enabled: !!enabled, spacing, height };
|
|
4763
|
+
}
|
|
4764
|
+
duplicate(id) {
|
|
4765
|
+
this._assertActive();
|
|
4766
|
+
const original = this._record(id);
|
|
4767
|
+
if (!original) throw new Error("Unknown model");
|
|
4768
|
+
const copy = this._viewer.duplicate_model(id);
|
|
4769
|
+
this._adopt(copy, { sourceFile: original.sourceFile }, true);
|
|
4770
|
+
const record = this._records.get(copy);
|
|
4771
|
+
record.animationPivot = original.animationPivot;
|
|
4772
|
+
record.originalColor = original.originalColor;
|
|
4773
|
+
this._nextID = Math.max(this._nextID, copy + 1);
|
|
4774
|
+
this._arrangement = "custom";
|
|
4775
|
+
return copy;
|
|
4776
|
+
}
|
|
4777
|
+
remove(id) {
|
|
4778
|
+
this._assertActive();
|
|
4779
|
+
if (!this._records.has(id)) return;
|
|
4780
|
+
this._viewer.remove_model(id);
|
|
4781
|
+
this._records.delete(id);
|
|
4782
|
+
this.setArrangement(this._arrangement);
|
|
4783
|
+
}
|
|
4784
|
+
openFiles(files) {
|
|
4785
|
+
this._assertActive();
|
|
6215
4786
|
const accepted = [];
|
|
6216
4787
|
for (const source of files) {
|
|
6217
|
-
if (
|
|
6218
|
-
|
|
6219
|
-
"
|
|
4788
|
+
if (!formats.test(source.name))
|
|
4789
|
+
this._error(
|
|
4790
|
+
failure("unsupported-format", "Unsupported model or scene format"),
|
|
4791
|
+
source
|
|
6220
4792
|
);
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
showNotice("This file is empty. Choose a file containing a 3D model.");
|
|
6225
|
-
continue;
|
|
6226
|
-
}
|
|
6227
|
-
accepted.push(source);
|
|
4793
|
+
else if (source.size === 0)
|
|
4794
|
+
this._error(failure("empty-file", "The file is empty"), source);
|
|
4795
|
+
else accepted.push(source);
|
|
6228
4796
|
}
|
|
6229
4797
|
const scenes = accepted.filter((f) => sceneFormat(f.name));
|
|
6230
4798
|
if (scenes.length) {
|
|
@@ -6234,161 +4802,130 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6234
4802
|
failed: false
|
|
6235
4803
|
};
|
|
6236
4804
|
for (const sceneFile of scenes)
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
} else
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
const url = new URL2(value.trim());
|
|
4805
|
+
this._queue.push({ name: sceneFile.name, sceneFile, batch });
|
|
4806
|
+
this._queue.push({ remaining: batch });
|
|
4807
|
+
} else this._queue.push(...accepted);
|
|
4808
|
+
return this._drain();
|
|
4809
|
+
}
|
|
4810
|
+
openURL(value, format = "") {
|
|
4811
|
+
this._assertActive();
|
|
4812
|
+
if (!this._options.viewer_options?.url_import_endpoint)
|
|
4813
|
+
throw new Error("URL imports are not configured");
|
|
4814
|
+
const url = new URL(value.trim());
|
|
6248
4815
|
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
6249
4816
|
throw new Error(
|
|
6250
4817
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
6251
4818
|
);
|
|
6252
|
-
if (
|
|
6253
|
-
throw new Error("
|
|
4819
|
+
if (format && !/^(stl|obj|3mf|step|stp|iges|igs|vsb|vsj)$/.test(format))
|
|
4820
|
+
throw new Error("Unsupported model or scene format");
|
|
6254
4821
|
url.hash = "";
|
|
6255
4822
|
const name = decodeURIComponent(
|
|
6256
4823
|
url.pathname.split("/").at(-1) || "Remote model"
|
|
6257
4824
|
).slice(0, 200);
|
|
6258
|
-
|
|
4825
|
+
this._queue.push({
|
|
6259
4826
|
name,
|
|
6260
4827
|
remote: url.href,
|
|
6261
|
-
format
|
|
4828
|
+
format,
|
|
6262
4829
|
size: 0,
|
|
6263
|
-
scene: ["vsb", "vsj"].includes(
|
|
4830
|
+
scene: ["vsb", "vsj"].includes(format) ? format : sceneFormat(name)
|
|
6264
4831
|
});
|
|
6265
|
-
|
|
6266
|
-
return drainPromise || Promise.resolve();
|
|
4832
|
+
return this._drain();
|
|
6267
4833
|
}
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
$("url-input").value = "";
|
|
6274
|
-
$("url-error").hidden = true;
|
|
6275
|
-
$("url-dialog").close();
|
|
6276
|
-
work.catch((error) => showNotice(error.message));
|
|
6277
|
-
} catch (error) {
|
|
6278
|
-
$("url-error").textContent = error.message;
|
|
6279
|
-
$("url-error").hidden = false;
|
|
6280
|
-
}
|
|
6281
|
-
});
|
|
4834
|
+
_drain() {
|
|
4835
|
+
if (!this._drainPromise && this._queue.length) {
|
|
4836
|
+
this._drainPromise = Promise.resolve().then(() => this._drainQueue());
|
|
4837
|
+
}
|
|
4838
|
+
return this._drainPromise || Promise.resolve();
|
|
6282
4839
|
}
|
|
6283
|
-
async
|
|
6284
|
-
if (draining) return;
|
|
6285
|
-
draining = true;
|
|
4840
|
+
async _drainQueue() {
|
|
6286
4841
|
try {
|
|
6287
|
-
while (
|
|
6288
|
-
|
|
4842
|
+
while (true) {
|
|
4843
|
+
if (!this._queue.length) {
|
|
4844
|
+
this._setBusy(false);
|
|
4845
|
+
if (!this._queue.length) break;
|
|
4846
|
+
}
|
|
4847
|
+
const source = this._queue.shift(), token = this._generation;
|
|
6289
4848
|
if (source.remaining) {
|
|
6290
4849
|
if (!source.remaining.failed)
|
|
6291
|
-
|
|
4850
|
+
this._queue.unshift(
|
|
6292
4851
|
...source.remaining.files.filter(
|
|
6293
4852
|
(f) => !source.remaining.used.has(f)
|
|
6294
4853
|
)
|
|
6295
4854
|
);
|
|
6296
4855
|
continue;
|
|
6297
4856
|
}
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
$("loading-label").textContent = "Opening " + source.name + "\u2026";
|
|
6301
|
-
announce("Opening " + source.name);
|
|
4857
|
+
this._setBusy(true);
|
|
4858
|
+
this._emit({ type: "opening", name: source.name });
|
|
6302
4859
|
try {
|
|
6303
4860
|
if (source.sceneFile || source.scene) {
|
|
6304
|
-
await
|
|
4861
|
+
await this._openScene(source, token);
|
|
6305
4862
|
continue;
|
|
6306
4863
|
}
|
|
6307
4864
|
const estimated = await estimateFile(source);
|
|
6308
|
-
if (token !==
|
|
6309
|
-
const resident = [...
|
|
4865
|
+
if (token !== this._generation) continue;
|
|
4866
|
+
const resident = [...this._records.values()].reduce(
|
|
6310
4867
|
(sum, r) => sum + r.estimated,
|
|
6311
4868
|
0
|
|
6312
4869
|
);
|
|
6313
|
-
if (estimated + resident >
|
|
6314
|
-
throw
|
|
6315
|
-
|
|
4870
|
+
if (estimated + resident > this._policy.budget || source.size > this._policy.limits.max_file_bytes)
|
|
4871
|
+
throw failure(
|
|
4872
|
+
"memory-budget",
|
|
4873
|
+
"Estimated scene memory budget exceeded"
|
|
6316
4874
|
);
|
|
6317
|
-
const
|
|
6318
|
-
if (token !==
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
const id =
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
4875
|
+
const viewer = await this.ready();
|
|
4876
|
+
if (token !== this._generation) continue;
|
|
4877
|
+
viewer.do_resize();
|
|
4878
|
+
this._nextID = Math.max(this._nextID, viewer.max_model_id + 1);
|
|
4879
|
+
const id = this._nextID++;
|
|
4880
|
+
this._loadingID = id;
|
|
4881
|
+
this._lastLoadedID = null;
|
|
4882
|
+
viewer.add_model({
|
|
6325
4883
|
id,
|
|
6326
4884
|
display: "flat",
|
|
6327
4885
|
...source.remote ? { remote_url: source.remote, remote_format: source.format } : source.url ? { filename: source.url } : { local_file: source }
|
|
6328
4886
|
});
|
|
6329
|
-
await
|
|
6330
|
-
if (token !==
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
announce(source.name + " could not be opened.");
|
|
4887
|
+
await viewer.when_idle();
|
|
4888
|
+
if (token !== this._generation) continue;
|
|
4889
|
+
this._loadingID = null;
|
|
4890
|
+
if (this._lastLoadedID !== id) {
|
|
4891
|
+
this._emit({ type: "load-failed", name: source.name });
|
|
6335
4892
|
continue;
|
|
6336
4893
|
}
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
id,
|
|
6346
|
-
name: source.name,
|
|
6347
|
-
size: source.size,
|
|
6348
|
-
units: loaded.source_units || (/\.3mf$/i.test(source.name) ? "mm" : null),
|
|
6349
|
-
bounds: loaded.bounds,
|
|
6350
|
-
position: { x: 0, y: 0, z: 0 },
|
|
6351
|
-
rotation: { x: 0, y: 0, z: 0 },
|
|
6352
|
-
embedded,
|
|
6353
|
-
originalColor,
|
|
6354
|
-
color: originalColor,
|
|
6355
|
-
estimated: loaded.triangles * 224,
|
|
6356
|
-
sourceFile: source instanceof window2.File ? source : null
|
|
4894
|
+
const loaded = viewer.get_model_info(id, { measurements: false });
|
|
4895
|
+
const colors = this._options.colors || [];
|
|
4896
|
+
if (!loaded.has_colors && colors.length)
|
|
4897
|
+
viewer.set_color(id, colors[this._records.size % colors.length]);
|
|
4898
|
+
this._adopt(id, {
|
|
4899
|
+
name: source.remote ? loaded.name : source.name,
|
|
4900
|
+
size: source.remote ? loaded.file_bytes : source.size,
|
|
4901
|
+
sourceFile: typeof source.slice === "function" ? source : null
|
|
6357
4902
|
});
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
arrangeScene();
|
|
6361
|
-
renderScene();
|
|
6362
|
-
announce(source.name + " is ready.");
|
|
6363
|
-
engine.canvas.focus({ preventScroll: true });
|
|
4903
|
+
this.setArrangement(this._arrangement);
|
|
4904
|
+
this._emit({ type: "loaded", id, name: this._records.get(id).name });
|
|
6364
4905
|
} catch (error) {
|
|
6365
4906
|
if (source.batch) source.batch.failed = true;
|
|
6366
|
-
if (token ===
|
|
6367
|
-
if (
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
showNotice(
|
|
6372
|
-
/WebGL/i.test(error.message) ? "3D viewing needs WebGL 2. Try enabling hardware acceleration in your browser, or use another device." : error.message
|
|
6373
|
-
);
|
|
4907
|
+
if (token === this._generation) {
|
|
4908
|
+
if (this._loadingID !== null)
|
|
4909
|
+
this._viewer?.remove_model(this._loadingID);
|
|
4910
|
+
this._loadingID = null;
|
|
4911
|
+
this._error(error, source);
|
|
6374
4912
|
}
|
|
6375
4913
|
}
|
|
6376
4914
|
}
|
|
6377
4915
|
} finally {
|
|
6378
|
-
|
|
6379
|
-
|
|
4916
|
+
this._drainPromise = null;
|
|
4917
|
+
if (this._busy) this._setBusy(false);
|
|
6380
4918
|
}
|
|
6381
4919
|
}
|
|
6382
|
-
async
|
|
6383
|
-
const
|
|
6384
|
-
if (token !==
|
|
6385
|
-
const controller = new
|
|
6386
|
-
sceneImport = controller;
|
|
4920
|
+
async _openScene(source, token) {
|
|
4921
|
+
const viewer = await this.ready();
|
|
4922
|
+
if (token !== this._generation) return;
|
|
4923
|
+
const controller = this._sceneImport = new AbortController();
|
|
6387
4924
|
try {
|
|
6388
4925
|
const chosen = source.batch?.files || [];
|
|
6389
4926
|
const cached = [
|
|
6390
4927
|
...new Set(
|
|
6391
|
-
[...
|
|
4928
|
+
[...this._records.values()].map((r) => r.sourceFile).filter(Boolean)
|
|
6392
4929
|
)
|
|
6393
4930
|
];
|
|
6394
4931
|
const companions = [
|
|
@@ -6397,498 +4934,69 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6397
4934
|
(f) => !chosen.some((s) => s.name === f.name) && cached.filter((s) => s.name === f.name).length === 1
|
|
6398
4935
|
) : []
|
|
6399
4936
|
];
|
|
6400
|
-
const result = await
|
|
4937
|
+
const result = await viewer.import_scene(
|
|
6401
4938
|
source.sceneFile || source.remote,
|
|
6402
4939
|
{
|
|
6403
4940
|
format: source.scene || sceneFormat(source.name),
|
|
6404
4941
|
signal: controller.signal,
|
|
6405
4942
|
files: companions,
|
|
6406
|
-
on_progress(
|
|
6407
|
-
if (token
|
|
6408
|
-
|
|
4943
|
+
on_progress: (progress) => {
|
|
4944
|
+
if (token === this._generation)
|
|
4945
|
+
this._emit({ type: "progress", scene: true, progress });
|
|
6409
4946
|
}
|
|
6410
4947
|
}
|
|
6411
4948
|
);
|
|
6412
|
-
if (token !==
|
|
4949
|
+
if (token !== this._generation) return;
|
|
6413
4950
|
for (const file of result.used_files) source.batch?.used.add(file);
|
|
6414
4951
|
for (const id of result.model_ids) {
|
|
6415
|
-
const m =
|
|
6416
|
-
|
|
6417
|
-
["min", "max"].map((key) => [
|
|
6418
|
-
key,
|
|
6419
|
-
g.bounds[key].map((v, i) => (v - m.center[i]) * m.scaleVector[i])
|
|
6420
|
-
])
|
|
6421
|
-
);
|
|
6422
|
-
const record = {
|
|
4952
|
+
const m = viewer._models.get(id);
|
|
4953
|
+
this._adopt(
|
|
6423
4954
|
id,
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
units: g.units || m.units,
|
|
6427
|
-
bounds,
|
|
6428
|
-
position: { x: 0, y: 0, z: 0 },
|
|
6429
|
-
rotation: {
|
|
6430
|
-
x: m.rotationx * 180 / Math.PI,
|
|
6431
|
-
y: m.rotationy * 180 / Math.PI,
|
|
6432
|
-
z: m.rotationz * 180 / Math.PI
|
|
4955
|
+
{
|
|
4956
|
+
sourceFile: result.used_files.includes(m.local_file) ? m.local_file : null
|
|
6433
4957
|
},
|
|
6434
|
-
|
|
6435
|
-
color: m.color || (g.colors ? null : "#909090"),
|
|
6436
|
-
estimated: g.triangles * 224,
|
|
6437
|
-
sourceFile: result.used_files.includes(m.local_file) ? m.local_file : null
|
|
6438
|
-
};
|
|
6439
|
-
const offset = enginePosition(record);
|
|
6440
|
-
for (const axis of ["x", "y", "z"])
|
|
6441
|
-
record.position[axis] = m[axis] - offset[axis];
|
|
6442
|
-
record.originalPosition = { ...record.position };
|
|
6443
|
-
record.originalRotation = { ...record.rotation };
|
|
6444
|
-
record.originalColor = record.color;
|
|
6445
|
-
records.set(id, record);
|
|
6446
|
-
}
|
|
6447
|
-
nextID = Math.max(nextID, engine.max_model_id + 1);
|
|
6448
|
-
modelID = result.model_ids.at(-1);
|
|
6449
|
-
appearanceRequest++;
|
|
6450
|
-
$("scene-arrangement").value = "custom";
|
|
6451
|
-
gridEnabled = !!engine.grid;
|
|
6452
|
-
gridHeight = engine.grid?.height || 0;
|
|
6453
|
-
gridSpacing = engine.grid?.spacing || 1;
|
|
6454
|
-
$("grid-button").setAttribute("aria-pressed", String(gridEnabled));
|
|
6455
|
-
setBackground(engine.bg_color);
|
|
6456
|
-
renderScene();
|
|
6457
|
-
announce(
|
|
6458
|
-
source.name + " opened. " + result.model_ids.length + " models added."
|
|
6459
|
-
);
|
|
6460
|
-
engine.canvas.focus({ preventScroll: true });
|
|
6461
|
-
} finally {
|
|
6462
|
-
if (sceneImport === controller) sceneImport = null;
|
|
6463
|
-
}
|
|
6464
|
-
}
|
|
6465
|
-
root.querySelectorAll("[data-open-file]").forEach((b) => listen(b, "click", () => $("file-input").click()));
|
|
6466
|
-
for (const button of shortcutButtons)
|
|
6467
|
-
listen(button, "click", () => {
|
|
6468
|
-
if (button.disabled || busy || !records.size) return;
|
|
6469
|
-
const action = button.dataset.viewstlShortcut;
|
|
6470
|
-
if (action === "save") $("save-button")?.click();
|
|
6471
|
-
else if (action === "select") {
|
|
6472
|
-
setSelectionMode(!selectionMode);
|
|
6473
|
-
closePanels();
|
|
6474
|
-
announce(
|
|
6475
|
-
selectionMode ? "Selection mode. Click a model to select; drag to move. Done or Escape returns to viewing." : "Viewing mode. Drag to rotate the scene."
|
|
4958
|
+
true
|
|
6476
4959
|
);
|
|
6477
|
-
} else if ([
|
|
6478
|
-
"front",
|
|
6479
|
-
"back",
|
|
6480
|
-
"left",
|
|
6481
|
-
"right",
|
|
6482
|
-
"top",
|
|
6483
|
-
"bottom",
|
|
6484
|
-
"isometric"
|
|
6485
|
-
].includes(action)) {
|
|
6486
|
-
setMoveMode(false);
|
|
6487
|
-
setSelectionMode(false);
|
|
6488
|
-
viewer.set_camera_view(action);
|
|
6489
|
-
toolsUI?.sync();
|
|
6490
|
-
announce(button.title || action + " view");
|
|
6491
4960
|
}
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
});
|
|
6504
|
-
$("sample-button").hidden = !sampleURL;
|
|
6505
|
-
listen($("sample-button"), "click", () => {
|
|
6506
|
-
if (sampleURL)
|
|
6507
|
-
void addFiles([
|
|
6508
|
-
{ name: options.sample_name || "Sample.stl", url: sampleURL }
|
|
6509
|
-
]);
|
|
6510
|
-
});
|
|
6511
|
-
listen($("close-model"), "click", () => removeModel(modelID));
|
|
6512
|
-
listen($("cancel-load"), "click", cancelLoading);
|
|
6513
|
-
listen($("clear-scene"), "click", clearScene);
|
|
6514
|
-
listen($("dismiss-notice"), "click", () => {
|
|
6515
|
-
$("notice").hidden = true;
|
|
6516
|
-
});
|
|
6517
|
-
listen($("fit-button"), "click", () => viewer?.set_auto_zoom());
|
|
6518
|
-
listen($("scene-arrangement"), "change", arrangeScene);
|
|
6519
|
-
listen($("model-list"), "click", (event) => {
|
|
6520
|
-
const b = event.target.closest("button");
|
|
6521
|
-
if (!b) return;
|
|
6522
|
-
if (b.dataset.removeModel !== void 0)
|
|
6523
|
-
removeModel(Number(b.dataset.removeModel));
|
|
6524
|
-
else if (b.dataset.selectModel !== void 0)
|
|
6525
|
-
selectModel(Number(b.dataset.selectModel));
|
|
6526
|
-
else if (b.dataset.colorModel !== void 0) {
|
|
6527
|
-
selectModel(Number(b.dataset.colorModel));
|
|
6528
|
-
togglePanel("colors");
|
|
6529
|
-
}
|
|
6530
|
-
});
|
|
6531
|
-
async function applyView() {
|
|
6532
|
-
if (modelID === null || busy) return;
|
|
6533
|
-
const request = ++appearanceRequest;
|
|
6534
|
-
const display = modeInputs.find((input) => input.checked).value;
|
|
6535
|
-
modeInputs.forEach((input) => {
|
|
6536
|
-
input.disabled = true;
|
|
6537
|
-
});
|
|
6538
|
-
$("smooth-shading").disabled = true;
|
|
6539
|
-
$("view-status").textContent = "Preparing view\u2026";
|
|
6540
|
-
$("notice").hidden = true;
|
|
6541
|
-
try {
|
|
6542
|
-
viewer.set_view(modelID, {
|
|
6543
|
-
display,
|
|
6544
|
-
smooth_shading: $("smooth-shading").checked
|
|
4961
|
+
this._nextID = Math.max(this._nextID, viewer.max_model_id + 1);
|
|
4962
|
+
this._arrangement = "custom";
|
|
4963
|
+
this._grid = {
|
|
4964
|
+
enabled: !!viewer.grid,
|
|
4965
|
+
spacing: viewer.grid?.spacing || 1,
|
|
4966
|
+
height: viewer.grid?.height || 0
|
|
4967
|
+
};
|
|
4968
|
+
this._emit({
|
|
4969
|
+
type: "scene-loaded",
|
|
4970
|
+
name: source.name,
|
|
4971
|
+
ids: result.model_ids
|
|
6545
4972
|
});
|
|
6546
|
-
await viewer.when_idle();
|
|
6547
4973
|
} finally {
|
|
6548
|
-
if (
|
|
6549
|
-
syncControls();
|
|
6550
|
-
announce($("view-label").textContent + " view ready.");
|
|
6551
|
-
}
|
|
4974
|
+
if (this._sceneImport === controller) this._sceneImport = null;
|
|
6552
4975
|
}
|
|
6553
4976
|
}
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
applySection();
|
|
6563
|
-
}
|
|
6564
|
-
});
|
|
6565
|
-
listen($("close-" + name), "click", () => {
|
|
6566
|
-
closePanel(name);
|
|
6567
|
-
if (name === "colors") {
|
|
6568
|
-
togglePanel("view");
|
|
6569
|
-
}
|
|
6570
|
-
$(name + "-button").focus();
|
|
6571
|
-
});
|
|
4977
|
+
cancelLoading() {
|
|
4978
|
+
this._assertActive();
|
|
4979
|
+
this._generation++;
|
|
4980
|
+
this._queue.length = 0;
|
|
4981
|
+
this._sceneImport?.abort();
|
|
4982
|
+
if (this._loadingID !== null) this._viewer?.remove_model(this._loadingID);
|
|
4983
|
+
this._loadingID = null;
|
|
4984
|
+
this._setBusy(false);
|
|
6572
4985
|
}
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
const
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
position: Number($("section-position").value) / 100,
|
|
6581
|
-
flip: $("section-flip").getAttribute("aria-pressed") === "true"
|
|
6582
|
-
} : null
|
|
6583
|
-
);
|
|
6584
|
-
$("section-value").textContent = $("section-position").value + "%";
|
|
6585
|
-
$("section-button").setAttribute("aria-pressed", String(enabled));
|
|
6586
|
-
}
|
|
6587
|
-
listen($("section-enabled"), "change", applySection);
|
|
6588
|
-
listen($("section-axis"), "change", applySection);
|
|
6589
|
-
listen($("section-position"), "input", applySection);
|
|
6590
|
-
listen($("section-flip"), "click", () => {
|
|
6591
|
-
$("section-flip").setAttribute(
|
|
6592
|
-
"aria-pressed",
|
|
6593
|
-
String($("section-flip").getAttribute("aria-pressed") !== "true")
|
|
6594
|
-
);
|
|
6595
|
-
applySection();
|
|
6596
|
-
});
|
|
6597
|
-
listen($("grid-button"), "click", () => {
|
|
6598
|
-
gridEnabled = !gridEnabled;
|
|
6599
|
-
updateGrid();
|
|
6600
|
-
});
|
|
6601
|
-
function setColor(color) {
|
|
6602
|
-
if (modelID === null) return;
|
|
6603
|
-
viewer.set_color(modelID, color);
|
|
6604
|
-
records.get(modelID).color = color;
|
|
6605
|
-
syncColors();
|
|
6606
|
-
renderList();
|
|
6607
|
-
}
|
|
6608
|
-
listen($("model-color"), "input", () => setColor($("model-color").value));
|
|
6609
|
-
listen(
|
|
6610
|
-
$("restore-color"),
|
|
6611
|
-
"click",
|
|
6612
|
-
() => setColor(records.get(modelID)?.originalColor)
|
|
6613
|
-
);
|
|
6614
|
-
root.querySelectorAll("[data-model-color]").forEach((b) => listen(b, "click", () => setColor(b.dataset.modelColor)));
|
|
6615
|
-
function setBackground(color) {
|
|
6616
|
-
background = color;
|
|
6617
|
-
const rgb = color === "transparent" ? [255, 255, 255] : [1, 3, 5].map((i) => parseInt(color.slice(i, i + 2), 16));
|
|
6618
|
-
$("workspace").classList.toggle(
|
|
6619
|
-
"dark-background",
|
|
6620
|
-
rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722 < 115
|
|
6621
|
-
);
|
|
6622
|
-
viewer?.set_bg_color(color);
|
|
6623
|
-
syncColors();
|
|
4986
|
+
clear() {
|
|
4987
|
+
this.cancelLoading();
|
|
4988
|
+
for (const binding of this._bindings) binding.setEnabled(false);
|
|
4989
|
+
this._viewer?.clean();
|
|
4990
|
+
this._records.clear();
|
|
4991
|
+
this._arrangement = "original";
|
|
4992
|
+
this.setGrid(false, 1, 0);
|
|
6624
4993
|
}
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
(
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
if (modelID === null) return;
|
|
6635
|
-
const name = records.size > 1 ? "viewstl-scene.png" : records.get(modelID).name.replace(/\.[^.]+$/, "") + ".png";
|
|
6636
|
-
try {
|
|
6637
|
-
const blob = await viewer.get_image_blob("image/png");
|
|
6638
|
-
if (disposed) return;
|
|
6639
|
-
const url = URL2.createObjectURL(blob), link = document2.createElement("a");
|
|
6640
|
-
link.href = url;
|
|
6641
|
-
link.download = name;
|
|
6642
|
-
link.click();
|
|
6643
|
-
setTimeout(() => URL2.revokeObjectURL(url), 1e3);
|
|
6644
|
-
announce("Image ready to download.");
|
|
6645
|
-
} catch {
|
|
6646
|
-
showNotice("The image could not be saved. Please try again.");
|
|
6647
|
-
}
|
|
6648
|
-
});
|
|
6649
|
-
let dragDepth = 0;
|
|
6650
|
-
const acceptedDrag = (event) => {
|
|
6651
|
-
const kind = dropKind(event.dataTransfer);
|
|
6652
|
-
if (kind === "files") return kind;
|
|
6653
|
-
if (!options.url_import_endpoint || event.target?.isContentEditable || event.target?.closest?.("input, textarea"))
|
|
6654
|
-
return null;
|
|
6655
|
-
return kind;
|
|
6656
|
-
};
|
|
6657
|
-
const clearDrop = () => {
|
|
6658
|
-
dragDepth = 0;
|
|
6659
|
-
$("drop-overlay").hidden = true;
|
|
6660
|
-
};
|
|
6661
|
-
listen(dropTarget, "dragenter", (event) => {
|
|
6662
|
-
if (acceptedDrag(event)) {
|
|
6663
|
-
event.preventDefault();
|
|
6664
|
-
dragDepth++;
|
|
6665
|
-
$("drop-overlay").hidden = false;
|
|
6666
|
-
}
|
|
6667
|
-
});
|
|
6668
|
-
listen(dropTarget, "dragover", (event) => {
|
|
6669
|
-
if (acceptedDrag(event)) {
|
|
6670
|
-
event.preventDefault();
|
|
6671
|
-
event.dataTransfer.dropEffect = "copy";
|
|
6672
|
-
}
|
|
6673
|
-
});
|
|
6674
|
-
listen(dropTarget, "dragleave", (event) => {
|
|
6675
|
-
if (dragDepth > 0 && --dragDepth === 0) clearDrop();
|
|
6676
|
-
});
|
|
6677
|
-
listen(dropTarget, "drop", (event) => {
|
|
6678
|
-
const kind = acceptedDrag(event);
|
|
6679
|
-
clearDrop();
|
|
6680
|
-
if (!kind) return;
|
|
6681
|
-
event.preventDefault();
|
|
6682
|
-
try {
|
|
6683
|
-
if (kind === "files")
|
|
6684
|
-
addFiles([...event.dataTransfer.files]).catch(
|
|
6685
|
-
(error) => showNotice(error.message)
|
|
6686
|
-
);
|
|
6687
|
-
else
|
|
6688
|
-
for (const url of droppedURLs(event.dataTransfer))
|
|
6689
|
-
openURL(url).catch((error) => showNotice(error.message));
|
|
6690
|
-
} catch (error) {
|
|
6691
|
-
showNotice(error.message);
|
|
6692
|
-
}
|
|
6693
|
-
});
|
|
6694
|
-
listen(dropTarget, "dragend", clearDrop);
|
|
6695
|
-
listen(window2, "blur", clearDrop);
|
|
6696
|
-
root.querySelectorAll("[data-dialog]").forEach((b) => listen(b, "click", () => $(b.dataset.dialog).showModal()));
|
|
6697
|
-
root.querySelectorAll("dialog").forEach(
|
|
6698
|
-
(dialog) => listen(dialog, "click", (event) => {
|
|
6699
|
-
if (event.target === dialog) {
|
|
6700
|
-
const r = dialog.getBoundingClientRect();
|
|
6701
|
-
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom)
|
|
6702
|
-
if (dialog === $("save-dialog")) exportUI.close();
|
|
6703
|
-
else dialog.close();
|
|
6704
|
-
}
|
|
6705
|
-
})
|
|
6706
|
-
);
|
|
6707
|
-
listen(root, "keydown", (event) => {
|
|
6708
|
-
if (event.key !== "Escape") return;
|
|
6709
|
-
if ($("save-dialog")?.open) return;
|
|
6710
|
-
if (selectionMode) {
|
|
6711
|
-
setSelectionMode(false);
|
|
6712
|
-
closePanels();
|
|
6713
|
-
announce("Viewing mode. Drag to rotate the scene.");
|
|
6714
|
-
return;
|
|
6715
|
-
}
|
|
6716
|
-
if (moveMode) {
|
|
6717
|
-
setMoveMode(false);
|
|
6718
|
-
announce("Move tool closed.");
|
|
6719
|
-
return;
|
|
6720
|
-
}
|
|
6721
|
-
const active = panelNames.find((name) => !$(name + "-panel").hidden);
|
|
6722
|
-
if (active) {
|
|
6723
|
-
closePanels();
|
|
6724
|
-
if (active === "colors") togglePanel("view");
|
|
6725
|
-
$(active + "-button").focus();
|
|
6726
|
-
}
|
|
6727
|
-
});
|
|
6728
|
-
listen(root, "pointerdown", (event) => {
|
|
6729
|
-
if (!event.target.closest(
|
|
6730
|
-
".view-panel,.info-panel,.toolbar,.scene-heading-button,.move-mode"
|
|
6731
|
-
))
|
|
6732
|
-
closePanels();
|
|
6733
|
-
});
|
|
6734
|
-
$("memory-budget").textContent = `Scene memory budget: ${fileSize(policy.budget)}. Half of ${policy.reported ? "the memory estimate reported by your browser" : "a conservative 2 GiB estimate because this browser does not report device memory"} is kept in reserve. This is an estimate, not a guarantee of available memory.`;
|
|
6735
|
-
$("scene-arrangement").value = "original";
|
|
6736
|
-
$("notice").hidden = true;
|
|
6737
|
-
$("drop-overlay").hidden = true;
|
|
6738
|
-
if ($("save-button"))
|
|
6739
|
-
exportUI = bindAppExport({
|
|
6740
|
-
$,
|
|
6741
|
-
listen,
|
|
6742
|
-
window: window2,
|
|
6743
|
-
getState: () => ({
|
|
6744
|
-
busy,
|
|
6745
|
-
selectedId: modelID,
|
|
6746
|
-
models: [...records.values()]
|
|
6747
|
-
}),
|
|
6748
|
-
getViewer: () => viewer,
|
|
6749
|
-
onOpen: () => {
|
|
6750
|
-
setMoveMode(false);
|
|
6751
|
-
closePanels();
|
|
6752
|
-
},
|
|
6753
|
-
announce
|
|
6754
|
-
});
|
|
6755
|
-
if ($("print-button"))
|
|
6756
|
-
printUI = bindAppPrint({
|
|
6757
|
-
$,
|
|
6758
|
-
listen,
|
|
6759
|
-
window: window2,
|
|
6760
|
-
getState: () => ({
|
|
6761
|
-
busy,
|
|
6762
|
-
selectedId: modelID,
|
|
6763
|
-
models: [...records.values()]
|
|
6764
|
-
}),
|
|
6765
|
-
getViewer: () => viewer,
|
|
6766
|
-
onOpen: () => {
|
|
6767
|
-
setMoveMode(false);
|
|
6768
|
-
closePanels();
|
|
6769
|
-
},
|
|
6770
|
-
announce
|
|
6771
|
-
});
|
|
6772
|
-
if ($("animation-details"))
|
|
6773
|
-
animationUI = bindAppAnimation({
|
|
6774
|
-
$,
|
|
6775
|
-
listen,
|
|
6776
|
-
window: window2,
|
|
6777
|
-
getState: () => ({
|
|
6778
|
-
busy,
|
|
6779
|
-
selectedId: modelID,
|
|
6780
|
-
models: [...records.values()]
|
|
6781
|
-
}),
|
|
6782
|
-
getViewer: () => viewer,
|
|
6783
|
-
beforePlay(id) {
|
|
6784
|
-
setMoveMode(false);
|
|
6785
|
-
prepareManual(id);
|
|
6786
|
-
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
6787
|
-
$("scene-arrangement").value = "custom";
|
|
6788
|
-
},
|
|
6789
|
-
refreshPose: refreshAnimatedPose,
|
|
6790
|
-
announce
|
|
6791
|
-
});
|
|
6792
|
-
if ($("size-details"))
|
|
6793
|
-
toolsUI = bindAppTools({
|
|
6794
|
-
$,
|
|
6795
|
-
listen,
|
|
6796
|
-
window: window2,
|
|
6797
|
-
getViewer: () => viewer,
|
|
6798
|
-
getState: () => ({ selectedId: modelID, busy }),
|
|
6799
|
-
refresh: renderScene,
|
|
6800
|
-
announce,
|
|
6801
|
-
changeSize(operation) {
|
|
6802
|
-
setMoveMode(false);
|
|
6803
|
-
const record = records.get(modelID);
|
|
6804
|
-
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6805
|
-
operation();
|
|
6806
|
-
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6807
|
-
$("scene-arrangement").value = "custom";
|
|
6808
|
-
renderScene();
|
|
6809
|
-
},
|
|
6810
|
-
duplicate() {
|
|
6811
|
-
const original = records.get(modelID), id = viewer.duplicate_model(modelID), m = viewer._models.get(id);
|
|
6812
|
-
const record = {
|
|
6813
|
-
...original,
|
|
6814
|
-
id,
|
|
6815
|
-
name: m.display_name,
|
|
6816
|
-
position: { ...original.position },
|
|
6817
|
-
rotation: { ...original.rotation }
|
|
6818
|
-
};
|
|
6819
|
-
syncAnimatedRecord(record, m);
|
|
6820
|
-
record.originalPosition = { ...record.position };
|
|
6821
|
-
record.originalRotation = { ...record.rotation };
|
|
6822
|
-
records.set(id, record);
|
|
6823
|
-
nextID = Math.max(nextID, id + 1);
|
|
6824
|
-
$("scene-arrangement").value = "custom";
|
|
6825
|
-
selectModel(id);
|
|
6826
|
-
setMoveMode(true, "move", true);
|
|
6827
|
-
announce("Copy selected at the same position. Drag to move it.");
|
|
6828
|
-
}
|
|
6829
|
-
});
|
|
6830
|
-
updateGrid();
|
|
6831
|
-
setBusy(false);
|
|
6832
|
-
setMoveMode(false);
|
|
6833
|
-
const api = {
|
|
6834
|
-
openFiles: addFiles,
|
|
6835
|
-
openURL,
|
|
6836
|
-
cancelLoading() {
|
|
6837
|
-
assertActive();
|
|
6838
|
-
cancelLoading();
|
|
6839
|
-
},
|
|
6840
|
-
clear() {
|
|
6841
|
-
assertActive();
|
|
6842
|
-
clearScene();
|
|
6843
|
-
},
|
|
6844
|
-
select(id) {
|
|
6845
|
-
assertActive();
|
|
6846
|
-
selectModel(id);
|
|
6847
|
-
},
|
|
6848
|
-
remove(id) {
|
|
6849
|
-
assertActive();
|
|
6850
|
-
removeModel(id);
|
|
6851
|
-
},
|
|
6852
|
-
getState() {
|
|
6853
|
-
assertActive();
|
|
6854
|
-
for (const record of records.values())
|
|
6855
|
-
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
6856
|
-
return {
|
|
6857
|
-
selectedId: modelID,
|
|
6858
|
-
busy,
|
|
6859
|
-
arrangement: $("scene-arrangement").value,
|
|
6860
|
-
memoryBudget: policy.budget,
|
|
6861
|
-
models: [...records.values()].map((record) => ({
|
|
6862
|
-
id: record.id,
|
|
6863
|
-
name: record.name,
|
|
6864
|
-
size: record.size,
|
|
6865
|
-
units: record.units,
|
|
6866
|
-
position: { ...record.position },
|
|
6867
|
-
rotation: { ...record.rotation },
|
|
6868
|
-
color: record.color,
|
|
6869
|
-
animation: viewer.get_model_animation_state(record.id)
|
|
6870
|
-
}))
|
|
6871
|
-
};
|
|
6872
|
-
},
|
|
6873
|
-
dispose() {
|
|
6874
|
-
if (disposed) return;
|
|
6875
|
-
exportUI?.dispose();
|
|
6876
|
-
printUI?.dispose();
|
|
6877
|
-
animationUI?.dispose();
|
|
6878
|
-
toolsUI?.dispose();
|
|
6879
|
-
cancelLoading();
|
|
6880
|
-
setMoveMode(false);
|
|
6881
|
-
appearanceRequest++;
|
|
6882
|
-
disposed = true;
|
|
6883
|
-
abort.abort();
|
|
6884
|
-
modelMover?.dispose();
|
|
6885
|
-
viewer?.dispose();
|
|
6886
|
-
records.clear();
|
|
6887
|
-
if (options.id_prefix)
|
|
6888
|
-
modeInputs.forEach((input) => input.name = "view-mode");
|
|
6889
|
-
mountedRoots.delete(root);
|
|
6890
|
-
}
|
|
6891
|
-
};
|
|
6892
|
-
mountedRoots.set(root, api);
|
|
6893
|
-
return api;
|
|
6894
|
-
}
|
|
4994
|
+
dispose() {
|
|
4995
|
+
if (this._disposed) return;
|
|
4996
|
+
this.cancelLoading();
|
|
4997
|
+
for (const binding of this._bindings) binding.dispose();
|
|
4998
|
+
this._disposed = true;
|
|
4999
|
+
this._viewer?.dispose();
|
|
5000
|
+
this._records.clear();
|
|
5001
|
+
}
|
|
5002
|
+
};
|