@omrips/viewstl 2.0.0-rc.7 → 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 +5 -3
- package/build/stl_viewer.min.js +6 -6
- package/build/viewstl.cjs +479 -2392
- package/build/viewstl.d.cts +95 -45
- package/build/viewstl.d.ts +95 -45
- package/build/viewstl.js +479 -2392
- package/build/viewstl.min.js +6 -6
- package/docs/releases/2.0.0-rc.9.md +20 -0
- package/docs/v2-usage.md +80 -125
- package/docs/v2-verification.md +743 -698
- package/package.json +2 -2
- package/docs/releases/2.0.0-rc.7.md +0 -16
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);
|
|
@@ -4629,7 +4377,7 @@ function bindModelMove(canvas, {
|
|
|
4629
4377
|
blocked.add(e.pointerId);
|
|
4630
4378
|
return;
|
|
4631
4379
|
}
|
|
4632
|
-
const model = getModel();
|
|
4380
|
+
const model = getModel(e);
|
|
4633
4381
|
if (!model) return;
|
|
4634
4382
|
drag = {
|
|
4635
4383
|
tool: getTool(),
|
|
@@ -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,1527 +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, selectionPointer = null, 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
|
-
selectionMode = !!enabled && !busy && records.size > 0;
|
|
5642
|
-
selectionPointer = null;
|
|
5643
|
-
if (viewer) {
|
|
5644
|
-
viewer.controls.enabled = !selectionMode;
|
|
5645
|
-
viewer.canvas.setAttribute(
|
|
5646
|
-
"aria-label",
|
|
5647
|
-
selectionMode ? "Select a model with a click. Escape returns to viewing." : "3D model viewer. Drag to rotate, shift-drag to pan, scroll to zoom."
|
|
5648
|
-
);
|
|
5649
|
-
}
|
|
5650
|
-
$("workspace").classList.toggle("selecting-model-mode", selectionMode);
|
|
5651
|
-
syncShortcuts();
|
|
5652
|
-
syncSelection();
|
|
5653
|
-
}
|
|
5654
|
-
function editModelAt(event) {
|
|
5655
|
-
const hit = viewer.pick_model(event.clientX, event.clientY);
|
|
5656
|
-
if (!hit || !records.has(hit.id)) return;
|
|
5657
|
-
event.preventDefault();
|
|
5658
|
-
selectModel(hit.id);
|
|
5659
|
-
closePanels();
|
|
5660
|
-
$("scene-panel").hidden = false;
|
|
5661
|
-
$("scene-button").setAttribute("aria-expanded", "true");
|
|
5662
|
-
$("position-details").open = true;
|
|
5663
|
-
syncSelection();
|
|
5664
|
-
announce(
|
|
5665
|
-
records.get(hit.id).name + " selected. Choose Move, Rotate or Depth."
|
|
5666
|
-
);
|
|
4515
|
+
get grid() {
|
|
4516
|
+
return { ...this._grid };
|
|
5667
4517
|
}
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
$(name + "-button").setAttribute("aria-expanded", "false");
|
|
5671
|
-
syncSelection();
|
|
5672
|
-
}
|
|
5673
|
-
function closePanels() {
|
|
5674
|
-
panelNames.forEach(closePanel);
|
|
5675
|
-
}
|
|
5676
|
-
function togglePanel(name) {
|
|
5677
|
-
const open = $(name + "-panel").hidden;
|
|
5678
|
-
closePanels();
|
|
5679
|
-
$(name + "-panel").hidden = !open;
|
|
5680
|
-
$(name + "-button").setAttribute("aria-expanded", String(open));
|
|
5681
|
-
syncSelection();
|
|
5682
|
-
if (open && name === "info") populateInfo();
|
|
5683
|
-
if (open && (name === "scene" || name === "view")) toolsUI?.sync();
|
|
5684
|
-
}
|
|
5685
|
-
function fileSize(size) {
|
|
5686
|
-
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`;
|
|
5687
|
-
}
|
|
5688
|
-
function syncColors() {
|
|
5689
|
-
const record = records.get(modelID), color = /^#[a-f0-9]{6}$/i.test(record?.color) ? record.color : "#ffffff";
|
|
5690
|
-
$("model-color").value = color;
|
|
5691
|
-
$("model-color-value").textContent = record?.color === null ? "File colors" : color.toUpperCase();
|
|
5692
|
-
$("restore-color").textContent = record?.embedded ? "Restore file colors" : "Restore original color";
|
|
5693
|
-
root.querySelectorAll("[data-model-color]").forEach(
|
|
5694
|
-
(b) => b.setAttribute(
|
|
5695
|
-
"aria-pressed",
|
|
5696
|
-
String(b.dataset.modelColor === record?.color)
|
|
5697
|
-
)
|
|
5698
|
-
);
|
|
5699
|
-
root.querySelectorAll("[data-background]").forEach(
|
|
5700
|
-
(b) => b.setAttribute(
|
|
5701
|
-
"aria-pressed",
|
|
5702
|
-
String(b.dataset.background === background)
|
|
5703
|
-
)
|
|
5704
|
-
);
|
|
5705
|
-
$("background-color").value = background === "transparent" ? "#fafbfb" : background;
|
|
5706
|
-
$("background-value").textContent = background === "transparent" ? "Default" : background.toUpperCase();
|
|
5707
|
-
}
|
|
5708
|
-
function syncControls() {
|
|
5709
|
-
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
5710
|
-
const info = modelID === null ? null : viewer.get_model_info(modelID, { measurements: false }), mode = info?.display === "smooth" ? "flat" : info?.display || "flat";
|
|
5711
|
-
for (const input of modeInputs) {
|
|
5712
|
-
input.checked = input.value === mode;
|
|
5713
|
-
input.disabled = busy;
|
|
5714
|
-
}
|
|
5715
|
-
$("view-label").textContent = toolsUI ? "View" : viewNames[mode];
|
|
5716
|
-
$("view-button").title = "View \xB7 " + viewNames[mode];
|
|
5717
|
-
$("smooth-shading").checked = info?.smooth_shading || false;
|
|
5718
|
-
$("smooth-shading").disabled = busy || mode === "wireframe";
|
|
5719
|
-
$("view-status").textContent = mode === "wireframe" ? "Smooth shading is available in surface views." : "Shading changes the appearance, not the geometry.";
|
|
5720
|
-
const text = mode === "orientation" ? "Blue: front faces \xB7 Red: back faces" : "";
|
|
5721
|
-
$("view-description").textContent = text;
|
|
5722
|
-
$("view-description").hidden = !text;
|
|
5723
|
-
$("color-mode-note").hidden = mode !== "orientation";
|
|
5724
|
-
const section = info?.section;
|
|
5725
|
-
$("section-enabled").checked = !!section;
|
|
5726
|
-
$("section-axis").value = section?.axis || "z";
|
|
5727
|
-
$("section-position").value = (section?.position ?? 0.5) * 100;
|
|
5728
|
-
$("section-value").textContent = $("section-position").value + "%";
|
|
5729
|
-
$("section-flip").setAttribute(
|
|
5730
|
-
"aria-pressed",
|
|
5731
|
-
String(section?.flip || false)
|
|
5732
|
-
);
|
|
5733
|
-
$("section-button").setAttribute("aria-pressed", String(!!section));
|
|
5734
|
-
root.querySelectorAll("[data-selected-name]").forEach((el) => el.textContent = records.get(modelID)?.name || "");
|
|
5735
|
-
syncColors();
|
|
5736
|
-
syncPosition();
|
|
5737
|
-
exportUI?.sync();
|
|
5738
|
-
printUI?.sync();
|
|
5739
|
-
animationUI?.sync();
|
|
5740
|
-
toolsUI?.sync();
|
|
5741
|
-
syncShortcuts();
|
|
5742
|
-
}
|
|
5743
|
-
function populateInfo() {
|
|
5744
|
-
if (modelID === null) return;
|
|
5745
|
-
const info = viewer.get_model_info(modelID, { measurements: false }), record = records.get(modelID), factor = viewer.display_units === "inch" ? 25.4 : 1;
|
|
5746
|
-
$("info-triangles").textContent = info.triangles.toLocaleString();
|
|
5747
|
-
$("info-dimensions").textContent = [info.dims.x, info.dims.y, info.dims.z].map((x) => formatNumber.format(x / factor)).join(" \xD7 ") + (factor === 1 ? " mm" : " in");
|
|
5748
|
-
$("info-size").textContent = fileSize(record.size);
|
|
5749
|
-
const size = viewer.get_model_size(modelID);
|
|
5750
|
-
$("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.";
|
|
5751
|
-
toolsUI?.syncInfo();
|
|
5752
|
-
}
|
|
5753
|
-
function renderList() {
|
|
5754
|
-
$("model-list").replaceChildren(
|
|
5755
|
-
...[...records.values()].map((record) => {
|
|
5756
|
-
const row = document2.createElement("li"), color = document2.createElement("button"), pick = document2.createElement("button"), remove = document2.createElement("button");
|
|
5757
|
-
row.className = "model-row";
|
|
5758
|
-
row.classList.toggle("selected", record.id === modelID);
|
|
5759
|
-
color.className = "model-swatch";
|
|
5760
|
-
color.dataset.colorModel = record.id;
|
|
5761
|
-
color.style.backgroundColor = record.color || "#b7c5c9";
|
|
5762
|
-
color.setAttribute("aria-label", "Color for " + record.name);
|
|
5763
|
-
color.title = "Change color";
|
|
5764
|
-
pick.className = "model-pick";
|
|
5765
|
-
pick.dataset.selectModel = record.id;
|
|
5766
|
-
pick.textContent = record.name;
|
|
5767
|
-
pick.title = record.name;
|
|
5768
|
-
pick.setAttribute("aria-pressed", String(record.id === modelID));
|
|
5769
|
-
remove.className = "icon-button";
|
|
5770
|
-
remove.dataset.removeModel = record.id;
|
|
5771
|
-
remove.textContent = "\xD7";
|
|
5772
|
-
remove.setAttribute("aria-label", "Remove " + record.name);
|
|
5773
|
-
row.append(color, pick, remove);
|
|
5774
|
-
return row;
|
|
5775
|
-
})
|
|
5776
|
-
);
|
|
5777
|
-
$("scene-count").textContent = records.size;
|
|
5778
|
-
$("scene-arrangement").disabled = records.size === 0;
|
|
5779
|
-
}
|
|
5780
|
-
function renderScene() {
|
|
5781
|
-
const has = records.size > 0;
|
|
5782
|
-
if (!has && selectionMode) setSelectionMode(false);
|
|
5783
|
-
$("workspace").classList.toggle("has-models", has);
|
|
5784
|
-
$("viewer").hidden = !has && !busy;
|
|
5785
|
-
$("empty-state").hidden = has || busy;
|
|
5786
|
-
$("file-heading").hidden = !has;
|
|
5787
|
-
$("model-controls").hidden = !has;
|
|
5788
|
-
$("filename").textContent = records.get(modelID)?.name || "";
|
|
5789
|
-
$("filename").title = $("filename").textContent;
|
|
5790
|
-
$("open-label").textContent = has ? "Add models" : "Open files";
|
|
5791
|
-
$("drop-message").textContent = has ? "Add to your scene." : "Drop it here.";
|
|
5792
|
-
renderList();
|
|
5793
|
-
syncControls();
|
|
5794
|
-
populateInfo();
|
|
5795
|
-
if (!has) closePanels();
|
|
5796
|
-
syncSelection();
|
|
5797
|
-
}
|
|
5798
|
-
function setBusy(value) {
|
|
5799
|
-
if (value) exportUI?.close();
|
|
5800
|
-
if (disposed) return;
|
|
5801
|
-
if (value && selectionMode) setSelectionMode(false);
|
|
5802
|
-
if (value) setMoveMode(false);
|
|
5803
|
-
busy = value;
|
|
5804
|
-
$("loading-state").hidden = !value;
|
|
5805
|
-
$("workspace").setAttribute("aria-busy", String(value));
|
|
5806
|
-
renderScene();
|
|
5807
|
-
}
|
|
5808
|
-
function selectModel(id) {
|
|
5809
|
-
exportUI?.close();
|
|
5810
|
-
if (!records.has(id)) return;
|
|
5811
|
-
setMoveMode(false);
|
|
5812
|
-
appearanceRequest++;
|
|
5813
|
-
modelID = id;
|
|
5814
|
-
renderScene();
|
|
5815
|
-
}
|
|
5816
|
-
function placements() {
|
|
5817
|
-
return scenePlacement([...records.values()], $("scene-arrangement").value);
|
|
5818
|
-
}
|
|
5819
|
-
function updateGrid() {
|
|
5820
|
-
viewer?.set_infinite_grid(gridEnabled, gridSpacing, gridHeight);
|
|
5821
|
-
$("grid-button").setAttribute("aria-pressed", String(gridEnabled));
|
|
5822
|
-
}
|
|
5823
|
-
function arrangeScene() {
|
|
5824
|
-
setMoveMode(false);
|
|
5825
|
-
for (const record of records.values())
|
|
5826
|
-
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
5827
|
-
if ($("scene-arrangement").value === "custom") return;
|
|
5828
|
-
for (const record of records.values()) prepareManual(record.id);
|
|
5829
|
-
if ($("scene-arrangement").value === "original")
|
|
5830
|
-
for (const record of records.values())
|
|
5831
|
-
record.rotation = {
|
|
5832
|
-
...record.originalRotation || { x: 0, y: 0, z: 0 }
|
|
5833
|
-
};
|
|
5834
|
-
const positions = placements();
|
|
5835
|
-
for (const p of positions) {
|
|
5836
|
-
records.get(p.id).position = { x: p.x, y: p.y, z: p.z };
|
|
5837
|
-
renderTransform(records.get(p.id));
|
|
5838
|
-
}
|
|
5839
|
-
if ($("scene-arrangement").value !== "custom" || !records.size)
|
|
5840
|
-
gridHeight = sceneFloor([...records.values()], positions);
|
|
5841
|
-
syncPosition();
|
|
5842
|
-
updateGrid();
|
|
5843
|
-
viewer?.set_auto_zoom();
|
|
5844
|
-
}
|
|
5845
|
-
function syncPosition(preserveFocus = false) {
|
|
5846
|
-
const record = records.get(modelID);
|
|
5847
|
-
for (const axis of ["x", "y", "z"]) {
|
|
5848
|
-
const input = $("position-" + axis);
|
|
5849
|
-
if (!preserveFocus || document2.activeElement !== input)
|
|
5850
|
-
input.value = Number((record?.position?.[axis] || 0).toPrecision(12));
|
|
5851
|
-
input.disabled = !record;
|
|
5852
|
-
input.removeAttribute("aria-invalid");
|
|
5853
|
-
}
|
|
5854
|
-
$("position-error").hidden = true;
|
|
5855
|
-
$("position-units").textContent = record?.animationPivot ? "Position of the model center in the scene, in mm." : "Offset from file coordinates, in mm.";
|
|
5856
|
-
$("move-model-button").disabled = !record || busy;
|
|
5857
|
-
$("reset-position").disabled = !record;
|
|
5858
|
-
syncRotation(preserveFocus);
|
|
5859
|
-
for (const id of [
|
|
5860
|
-
"rotate-model-button",
|
|
5861
|
-
"depth-model-button",
|
|
5862
|
-
"depth-nearer",
|
|
5863
|
-
"depth-farther"
|
|
5864
|
-
])
|
|
5865
|
-
$(id).disabled = !record || busy;
|
|
4518
|
+
get memoryPolicy() {
|
|
4519
|
+
return { ...this._policy, limits: { ...this._policy.limits } };
|
|
5866
4520
|
}
|
|
5867
|
-
|
|
5868
|
-
if (
|
|
5869
|
-
(v) => Number.isFinite(v) && Math.abs(v) <= 1e12
|
|
5870
|
-
))
|
|
5871
|
-
return false;
|
|
5872
|
-
records.get(id).position = { ...position };
|
|
5873
|
-
renderTransform(records.get(id));
|
|
5874
|
-
$("scene-arrangement").value = "custom";
|
|
5875
|
-
if (id === modelID && refreshInputs) syncPosition();
|
|
5876
|
-
return true;
|
|
5877
|
-
}
|
|
5878
|
-
function renderTransform(record) {
|
|
5879
|
-
const p = enginePosition(record), r = record.rotation;
|
|
5880
|
-
viewer.set_rotation(
|
|
5881
|
-
record.id,
|
|
5882
|
-
...["x", "y", "z"].map((axis) => r[axis] % 360 * Math.PI / 180)
|
|
5883
|
-
);
|
|
5884
|
-
viewer.set_position(record.id, p.x, p.y, p.z);
|
|
4521
|
+
_assertActive() {
|
|
4522
|
+
if (this._disposed) throw new Error("ViewSTL session is disposed");
|
|
5885
4523
|
}
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
const record = records.get(modelID);
|
|
5889
|
-
return record ? { ...record, center: modelCenter(record), radius: modelRadius(record) } : null;
|
|
4524
|
+
_emit(event) {
|
|
4525
|
+
if (!this._disposed) this._call(this._options.on_event, event);
|
|
5890
4526
|
}
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
input.value = Number((record?.rotation?.[axis] || 0).toPrecision(10));
|
|
5897
|
-
input.disabled = !record;
|
|
5898
|
-
input.removeAttribute("aria-invalid");
|
|
4527
|
+
_call(callback, ...args) {
|
|
4528
|
+
try {
|
|
4529
|
+
callback?.(...args);
|
|
4530
|
+
} catch (error) {
|
|
4531
|
+
console.error(error);
|
|
5899
4532
|
}
|
|
5900
|
-
$("rotation-error").hidden = true;
|
|
5901
|
-
$("reset-rotation").disabled = !record;
|
|
5902
|
-
}
|
|
5903
|
-
function applyRotation(id, rotation2, refreshInputs = true) {
|
|
5904
|
-
if (!records.has(id) || !Object.values(rotation2).every(
|
|
5905
|
-
(v) => Number.isFinite(v) && Math.abs(v) <= 36e4
|
|
5906
|
-
))
|
|
5907
|
-
return false;
|
|
5908
|
-
records.get(id).rotation = { ...rotation2 };
|
|
5909
|
-
renderTransform(records.get(id));
|
|
5910
|
-
$("scene-arrangement").value = "custom";
|
|
5911
|
-
if (refreshInputs && id === modelID) syncRotation();
|
|
5912
|
-
return true;
|
|
5913
4533
|
}
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
rotate: "Drag to rotate the selected model",
|
|
5918
|
-
depth: "Drag up for nearer, down for farther"
|
|
5919
|
-
};
|
|
5920
|
-
function syncTool() {
|
|
5921
|
-
if (viewer?.canvas) viewer.canvas.dataset.modelTool = transformTool;
|
|
5922
|
-
root.querySelectorAll("[data-transform-tab]").forEach(
|
|
5923
|
-
(b) => b.setAttribute(
|
|
5924
|
-
"aria-pressed",
|
|
5925
|
-
String(b.dataset.transformTab === transformTool)
|
|
5926
|
-
)
|
|
5927
|
-
);
|
|
5928
|
-
for (const name of ["move", "rotate", "depth"]) {
|
|
5929
|
-
$(name + "-controls").hidden = name !== transformTool;
|
|
5930
|
-
$(name + "-model-button").setAttribute(
|
|
5931
|
-
"aria-pressed",
|
|
5932
|
-
String(moveMode && name === transformTool)
|
|
5933
|
-
);
|
|
5934
|
-
}
|
|
5935
|
-
$("model-tool").value = transformTool;
|
|
5936
|
-
$("move-gesture-message").textContent = toolHints[transformTool] + " \xB7 Scroll to adjust depth";
|
|
5937
|
-
}
|
|
5938
|
-
function setMoveMode(enabled, tool = transformTool, retainPaused = false) {
|
|
5939
|
-
if (enabled && selectionMode) setSelectionMode(false);
|
|
5940
|
-
modelMover?.setEnabled(false);
|
|
5941
|
-
transformTool = tool;
|
|
5942
|
-
moveMode = !!enabled && modelID !== null && !busy;
|
|
5943
|
-
if (moveMode && !retainPaused) prepareManual(modelID);
|
|
5944
|
-
modelMover?.setEnabled(moveMode);
|
|
5945
|
-
syncTool();
|
|
5946
|
-
$("move-mode").hidden = !moveMode;
|
|
5947
|
-
$("move-mode-name").textContent = records.get(modelID)?.name || "";
|
|
5948
|
-
$("workspace").classList.toggle("moving-model-mode", moveMode);
|
|
5949
|
-
if (viewer?.canvas)
|
|
5950
|
-
viewer.canvas.setAttribute(
|
|
5951
|
-
"aria-label",
|
|
5952
|
-
moveMode ? toolHints[transformTool] + ". Scroll changes only its depth. Escape exits." : selectionMode ? "Select a model with a click. Escape returns to viewing." : "3D model viewer. Drag to rotate, shift-drag to pan, scroll to zoom."
|
|
5953
|
-
);
|
|
5954
|
-
if (moveMode) {
|
|
5955
|
-
closePanels();
|
|
5956
|
-
viewer.canvas.focus({ preventScroll: true });
|
|
5957
|
-
announce(
|
|
5958
|
-
toolLabels[transformTool] + " tool for " + records.get(modelID).name + ". Done or Escape returns to viewing."
|
|
5959
|
-
);
|
|
5960
|
-
}
|
|
5961
|
-
syncSelection();
|
|
5962
|
-
}
|
|
5963
|
-
for (const axis of ["x", "y", "z"]) {
|
|
5964
|
-
const input = $("rotation-" + axis);
|
|
5965
|
-
listen(input, "input", () => {
|
|
5966
|
-
if (modelID === null || !input.value.trim()) return;
|
|
5967
|
-
prepareManual(modelID);
|
|
5968
|
-
if (applyRotation(
|
|
5969
|
-
modelID,
|
|
5970
|
-
{ ...records.get(modelID).rotation, [axis]: input.valueAsNumber },
|
|
5971
|
-
false
|
|
5972
|
-
)) {
|
|
5973
|
-
input.removeAttribute("aria-invalid");
|
|
5974
|
-
$("rotation-error").hidden = true;
|
|
5975
|
-
}
|
|
5976
|
-
});
|
|
5977
|
-
listen(input, "change", () => {
|
|
5978
|
-
if (modelID === null) return;
|
|
5979
|
-
prepareManual(modelID);
|
|
5980
|
-
if (!input.value.trim() || !applyRotation(modelID, {
|
|
5981
|
-
...records.get(modelID).rotation,
|
|
5982
|
-
[axis]: input.valueAsNumber
|
|
5983
|
-
})) {
|
|
5984
|
-
input.setAttribute("aria-invalid", "true");
|
|
5985
|
-
$("rotation-error").hidden = false;
|
|
5986
|
-
} else announce("Model rotation updated.");
|
|
5987
|
-
});
|
|
4534
|
+
_setBusy(value) {
|
|
4535
|
+
this._busy = value;
|
|
4536
|
+
this._emit({ type: "busy", busy: value });
|
|
5988
4537
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
});
|
|
5996
|
-
announce("Selected model rotation reset.");
|
|
5997
|
-
}
|
|
5998
|
-
});
|
|
5999
|
-
for (const name of ["move", "rotate", "depth"])
|
|
6000
|
-
listen(
|
|
6001
|
-
$(name + "-model-button"),
|
|
6002
|
-
"click",
|
|
6003
|
-
() => setMoveMode(!moveMode || transformTool !== name, name)
|
|
6004
|
-
);
|
|
6005
|
-
for (const [id, amount] of [
|
|
6006
|
-
["depth-nearer", -0.12],
|
|
6007
|
-
["depth-farther", 0.12]
|
|
6008
|
-
])
|
|
6009
|
-
listen($(id), "click", () => {
|
|
6010
|
-
const model = selectedTransform();
|
|
6011
|
-
if (!model) return;
|
|
6012
|
-
applyPosition(
|
|
6013
|
-
model.id,
|
|
6014
|
-
depthPosition(model, viewer.get_camera_state(), amount)
|
|
6015
|
-
);
|
|
6016
|
-
announce(
|
|
6017
|
-
amount < 0 ? "Selected model moved nearer." : "Selected model moved farther."
|
|
6018
|
-
);
|
|
6019
|
-
});
|
|
6020
|
-
root.querySelectorAll("[data-transform-tab]").forEach(
|
|
6021
|
-
(b) => listen(b, "click", () => setMoveMode(false, b.dataset.transformTab))
|
|
6022
|
-
);
|
|
6023
|
-
listen(
|
|
6024
|
-
$("model-tool"),
|
|
6025
|
-
"change",
|
|
6026
|
-
() => setMoveMode(true, $("model-tool").value)
|
|
6027
|
-
);
|
|
6028
|
-
for (const axis of ["x", "y", "z"]) {
|
|
6029
|
-
const input = $("position-" + axis);
|
|
6030
|
-
listen(input, "input", () => {
|
|
6031
|
-
if (modelID === null || !input.value.trim()) return;
|
|
6032
|
-
prepareManual(modelID);
|
|
6033
|
-
if (applyPosition(
|
|
6034
|
-
modelID,
|
|
6035
|
-
{ ...records.get(modelID).position, [axis]: input.valueAsNumber },
|
|
6036
|
-
false
|
|
6037
|
-
)) {
|
|
6038
|
-
input.removeAttribute("aria-invalid");
|
|
6039
|
-
$("position-error").hidden = true;
|
|
6040
|
-
}
|
|
6041
|
-
});
|
|
6042
|
-
listen(input, "change", () => {
|
|
6043
|
-
if (modelID === null) return;
|
|
6044
|
-
prepareManual(modelID);
|
|
6045
|
-
if (!input.value.trim() || !applyPosition(modelID, {
|
|
6046
|
-
...records.get(modelID).position,
|
|
6047
|
-
[axis]: input.valueAsNumber
|
|
6048
|
-
})) {
|
|
6049
|
-
input.setAttribute("aria-invalid", "true");
|
|
6050
|
-
$("position-error").hidden = false;
|
|
6051
|
-
} else announce("Model position 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
|
|
6052
4544
|
});
|
|
6053
4545
|
}
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
applyPosition(modelID, {
|
|
6063
|
-
...records.get(modelID).originalPosition || { x: 0, y: 0, z: 0 }
|
|
6064
|
-
});
|
|
6065
|
-
announce("Selected model restored to its original position.");
|
|
6066
|
-
}
|
|
6067
|
-
});
|
|
6068
|
-
listen(window2, "blur", () => {
|
|
6069
|
-
if (moveMode) setMoveMode(false);
|
|
6070
|
-
});
|
|
6071
|
-
function cancelLoading() {
|
|
6072
|
-
generation++;
|
|
6073
|
-
queue.length = 0;
|
|
6074
|
-
sceneImport?.abort();
|
|
6075
|
-
if (loadingID !== null) {
|
|
6076
|
-
viewer?.remove_model(loadingID);
|
|
6077
|
-
loadingID = null;
|
|
6078
|
-
}
|
|
6079
|
-
setBusy(false);
|
|
6080
|
-
announce("Opening cancelled.");
|
|
6081
|
-
}
|
|
6082
|
-
function clearScene() {
|
|
6083
|
-
exportUI?.close();
|
|
6084
|
-
setMoveMode(false);
|
|
6085
|
-
cancelLoading();
|
|
6086
|
-
appearanceRequest++;
|
|
6087
|
-
viewer?.clean();
|
|
6088
|
-
records.clear();
|
|
6089
|
-
modelID = null;
|
|
6090
|
-
gridEnabled = false;
|
|
6091
|
-
gridHeight = 0;
|
|
6092
|
-
gridSpacing = 1;
|
|
6093
|
-
$("scene-arrangement").value = "original";
|
|
6094
|
-
updateGrid();
|
|
6095
|
-
$("notice").hidden = true;
|
|
6096
|
-
renderScene();
|
|
6097
|
-
announce("Scene cleared.");
|
|
6098
|
-
}
|
|
6099
|
-
function removeModel(id) {
|
|
6100
|
-
if (!records.has(id)) return;
|
|
6101
|
-
exportUI?.close();
|
|
6102
|
-
const name = records.get(id).name;
|
|
6103
|
-
setMoveMode(false);
|
|
6104
|
-
viewer.remove_model(id);
|
|
6105
|
-
records.delete(id);
|
|
6106
|
-
appearanceRequest++;
|
|
6107
|
-
if (modelID === id) modelID = [...records.keys()].at(-1) ?? null;
|
|
6108
|
-
arrangeScene();
|
|
6109
|
-
renderScene();
|
|
6110
|
-
announce(name + " removed.");
|
|
6111
|
-
}
|
|
6112
|
-
function prepareManual(id) {
|
|
6113
|
-
if (!records.has(id)) return;
|
|
6114
|
-
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6115
|
-
viewer?.animate_model(id, null);
|
|
6116
|
-
}
|
|
6117
|
-
function refreshAnimatedPose() {
|
|
6118
|
-
if (disposed || modelID === null) return;
|
|
6119
|
-
syncAnimatedRecord(records.get(modelID), viewer?._models.get(modelID));
|
|
6120
|
-
if (!$("info-panel").hidden) populateInfo();
|
|
6121
|
-
if (!$("scene-panel").hidden && $("position-details").open)
|
|
6122
|
-
syncPosition(true);
|
|
6123
|
-
}
|
|
6124
|
-
function animationChanged(id, state, reason) {
|
|
6125
|
-
if (disposed) return;
|
|
6126
|
-
if (reason === "restored" && state.status === "running" && window2.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
6127
|
-
viewer.pause_model_animation(id);
|
|
6128
|
-
syncAnimatedRecord(records.get(id), viewer?._models.get(id));
|
|
6129
|
-
if (["started", "restored", "resumed"].includes(reason))
|
|
6130
|
-
$("scene-arrangement").value = "custom";
|
|
6131
|
-
if (id === modelID) refreshAnimatedPose();
|
|
6132
|
-
animationUI?.sync();
|
|
6133
|
-
printUI?.sync();
|
|
6134
|
-
options.animation_state_callback?.(id, state, reason);
|
|
6135
|
-
}
|
|
6136
|
-
async function getViewer() {
|
|
6137
|
-
if (!viewerPromise)
|
|
6138
|
-
viewerPromise = Promise.resolve().then(async () => {
|
|
6139
|
-
if (disposed) throw new Error("ViewSTL app is disposed");
|
|
6140
|
-
viewer = new StlViewer($("viewer"), {
|
|
6141
|
-
...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,
|
|
6142
4554
|
worker_count: 1,
|
|
6143
|
-
worker_timeout:
|
|
6144
|
-
worker_url: options.worker_url,
|
|
6145
|
-
worker_factory: options.worker_factory,
|
|
6146
|
-
url_import_endpoint: options.url_import_endpoint,
|
|
4555
|
+
worker_timeout: 6e5,
|
|
6147
4556
|
center_models: false,
|
|
6148
|
-
bg_color: background,
|
|
6149
4557
|
allow_drag_and_drop: false,
|
|
6150
|
-
|
|
6151
|
-
animation_complete_callback: (...args) => {
|
|
6152
|
-
if (!disposed) options.animation_complete_callback?.(...args);
|
|
6153
|
-
},
|
|
6154
|
-
camera_state: {
|
|
6155
|
-
position: { x: 90, y: -130, z: 90 },
|
|
6156
|
-
target: { x: 0, y: 0, z: 0 },
|
|
6157
|
-
up: { x: 0, y: 0, z: 1 }
|
|
6158
|
-
},
|
|
4558
|
+
...supplied,
|
|
6159
4559
|
loading_progress_callback: (status) => {
|
|
6160
|
-
if (
|
|
6161
|
-
|
|
6162
|
-
|
|
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);
|
|
6163
4567
|
},
|
|
6164
4568
|
model_loaded_callback: (id) => {
|
|
6165
|
-
|
|
4569
|
+
this._lastLoadedID = id;
|
|
4570
|
+
this._call(supplied.model_loaded_callback, id);
|
|
6166
4571
|
},
|
|
6167
4572
|
load_error_callback: (message) => {
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
);
|
|
6171
|
-
}
|
|
6172
|
-
});
|
|
6173
|
-
await viewer.when_ready;
|
|
6174
|
-
if (disposed) throw new Error("ViewSTL app is disposed");
|
|
6175
|
-
modelMover = bindModelMove(viewer.canvas, {
|
|
6176
|
-
getCamera: () => viewer.get_camera_state(),
|
|
6177
|
-
getModel: selectedTransform,
|
|
6178
|
-
getTool: () => transformTool,
|
|
6179
|
-
onRotate: (id, rotation2) => applyRotation(id, rotation2),
|
|
6180
|
-
onMove: (id, position) => applyPosition(id, position),
|
|
6181
|
-
onStart: () => {
|
|
6182
|
-
dragArrangement = $("scene-arrangement").value;
|
|
4573
|
+
this._error(new Error(message));
|
|
4574
|
+
this._call(supplied.load_error_callback, message);
|
|
6183
4575
|
},
|
|
6184
|
-
|
|
6185
|
-
if (
|
|
6186
|
-
|
|
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);
|
|
4581
|
+
},
|
|
4582
|
+
animation_complete_callback: (...args) => {
|
|
4583
|
+
if (!this._disposed)
|
|
4584
|
+
this._call(supplied.animation_complete_callback, ...args);
|
|
6187
4585
|
}
|
|
6188
4586
|
});
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
});
|
|
6193
|
-
listen(viewer.canvas, "pointerdown", (event) => {
|
|
6194
|
-
if (!selectionMode || busy || event.button !== 0 || !event.isPrimary)
|
|
6195
|
-
return;
|
|
6196
|
-
selectionPointer = {
|
|
6197
|
-
id: event.pointerId,
|
|
6198
|
-
x: event.clientX,
|
|
6199
|
-
y: event.clientY,
|
|
6200
|
-
moved: false
|
|
6201
|
-
};
|
|
6202
|
-
viewer.canvas.focus({ preventScroll: true });
|
|
6203
|
-
viewer.canvas.setPointerCapture(event.pointerId);
|
|
6204
|
-
event.preventDefault();
|
|
6205
|
-
});
|
|
6206
|
-
listen(viewer.canvas, "pointermove", (event) => {
|
|
6207
|
-
if (selectionPointer?.id === event.pointerId && Math.hypot(
|
|
6208
|
-
event.clientX - selectionPointer.x,
|
|
6209
|
-
event.clientY - selectionPointer.y
|
|
6210
|
-
) > 6)
|
|
6211
|
-
selectionPointer.moved = true;
|
|
6212
|
-
});
|
|
6213
|
-
listen(viewer.canvas, "pointerup", (event) => {
|
|
6214
|
-
const pointer = selectionPointer;
|
|
6215
|
-
selectionPointer = null;
|
|
6216
|
-
if (selectionMode && !busy && pointer?.id === event.pointerId && !pointer.moved)
|
|
6217
|
-
editModelAt(event);
|
|
6218
|
-
});
|
|
6219
|
-
for (const event of ["pointercancel", "lostpointercapture"])
|
|
6220
|
-
listen(viewer.canvas, event, () => {
|
|
6221
|
-
selectionPointer = null;
|
|
6222
|
-
});
|
|
4587
|
+
await viewer.when_ready;
|
|
4588
|
+
this._assertActive();
|
|
4589
|
+
this._emit({ type: "ready" });
|
|
6223
4590
|
return viewer;
|
|
6224
4591
|
}).catch((error) => {
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
viewer = null;
|
|
6229
|
-
viewerPromise = null;
|
|
4592
|
+
this._viewer?.dispose();
|
|
4593
|
+
this._viewer = null;
|
|
4594
|
+
this._viewerPromise = null;
|
|
6230
4595
|
throw error;
|
|
6231
4596
|
});
|
|
6232
|
-
|
|
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);
|
|
6233
4668
|
}
|
|
6234
|
-
|
|
6235
|
-
|
|
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";
|
|
4678
|
+
}
|
|
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();
|
|
6236
4786
|
const accepted = [];
|
|
6237
4787
|
for (const source of files) {
|
|
6238
|
-
if (
|
|
6239
|
-
|
|
6240
|
-
"
|
|
4788
|
+
if (!formats.test(source.name))
|
|
4789
|
+
this._error(
|
|
4790
|
+
failure("unsupported-format", "Unsupported model or scene format"),
|
|
4791
|
+
source
|
|
6241
4792
|
);
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
showNotice("This file is empty. Choose a file containing a 3D model.");
|
|
6246
|
-
continue;
|
|
6247
|
-
}
|
|
6248
|
-
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);
|
|
6249
4796
|
}
|
|
6250
4797
|
const scenes = accepted.filter((f) => sceneFormat(f.name));
|
|
6251
4798
|
if (scenes.length) {
|
|
@@ -6255,161 +4802,130 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6255
4802
|
failed: false
|
|
6256
4803
|
};
|
|
6257
4804
|
for (const sceneFile of scenes)
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
} else
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
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());
|
|
6269
4815
|
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
6270
4816
|
throw new Error(
|
|
6271
4817
|
"Use a public HTTP or HTTPS file URL without login credentials."
|
|
6272
4818
|
);
|
|
6273
|
-
if (
|
|
6274
|
-
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");
|
|
6275
4821
|
url.hash = "";
|
|
6276
4822
|
const name = decodeURIComponent(
|
|
6277
4823
|
url.pathname.split("/").at(-1) || "Remote model"
|
|
6278
4824
|
).slice(0, 200);
|
|
6279
|
-
|
|
4825
|
+
this._queue.push({
|
|
6280
4826
|
name,
|
|
6281
4827
|
remote: url.href,
|
|
6282
|
-
format
|
|
4828
|
+
format,
|
|
6283
4829
|
size: 0,
|
|
6284
|
-
scene: ["vsb", "vsj"].includes(
|
|
4830
|
+
scene: ["vsb", "vsj"].includes(format) ? format : sceneFormat(name)
|
|
6285
4831
|
});
|
|
6286
|
-
|
|
6287
|
-
return drainPromise || Promise.resolve();
|
|
4832
|
+
return this._drain();
|
|
6288
4833
|
}
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
$("url-input").value = "";
|
|
6295
|
-
$("url-error").hidden = true;
|
|
6296
|
-
$("url-dialog").close();
|
|
6297
|
-
work.catch((error) => showNotice(error.message));
|
|
6298
|
-
} catch (error) {
|
|
6299
|
-
$("url-error").textContent = error.message;
|
|
6300
|
-
$("url-error").hidden = false;
|
|
6301
|
-
}
|
|
6302
|
-
});
|
|
4834
|
+
_drain() {
|
|
4835
|
+
if (!this._drainPromise && this._queue.length) {
|
|
4836
|
+
this._drainPromise = Promise.resolve().then(() => this._drainQueue());
|
|
4837
|
+
}
|
|
4838
|
+
return this._drainPromise || Promise.resolve();
|
|
6303
4839
|
}
|
|
6304
|
-
async
|
|
6305
|
-
if (draining) return;
|
|
6306
|
-
draining = true;
|
|
4840
|
+
async _drainQueue() {
|
|
6307
4841
|
try {
|
|
6308
|
-
while (
|
|
6309
|
-
|
|
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;
|
|
6310
4848
|
if (source.remaining) {
|
|
6311
4849
|
if (!source.remaining.failed)
|
|
6312
|
-
|
|
4850
|
+
this._queue.unshift(
|
|
6313
4851
|
...source.remaining.files.filter(
|
|
6314
4852
|
(f) => !source.remaining.used.has(f)
|
|
6315
4853
|
)
|
|
6316
4854
|
);
|
|
6317
4855
|
continue;
|
|
6318
4856
|
}
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
$("loading-label").textContent = "Opening " + source.name + "\u2026";
|
|
6322
|
-
announce("Opening " + source.name);
|
|
4857
|
+
this._setBusy(true);
|
|
4858
|
+
this._emit({ type: "opening", name: source.name });
|
|
6323
4859
|
try {
|
|
6324
4860
|
if (source.sceneFile || source.scene) {
|
|
6325
|
-
await
|
|
4861
|
+
await this._openScene(source, token);
|
|
6326
4862
|
continue;
|
|
6327
4863
|
}
|
|
6328
4864
|
const estimated = await estimateFile(source);
|
|
6329
|
-
if (token !==
|
|
6330
|
-
const resident = [...
|
|
4865
|
+
if (token !== this._generation) continue;
|
|
4866
|
+
const resident = [...this._records.values()].reduce(
|
|
6331
4867
|
(sum, r) => sum + r.estimated,
|
|
6332
4868
|
0
|
|
6333
4869
|
);
|
|
6334
|
-
if (estimated + resident >
|
|
6335
|
-
throw
|
|
6336
|
-
|
|
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"
|
|
6337
4874
|
);
|
|
6338
|
-
const
|
|
6339
|
-
if (token !==
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
const id =
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
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({
|
|
6346
4883
|
id,
|
|
6347
4884
|
display: "flat",
|
|
6348
4885
|
...source.remote ? { remote_url: source.remote, remote_format: source.format } : source.url ? { filename: source.url } : { local_file: source }
|
|
6349
4886
|
});
|
|
6350
|
-
await
|
|
6351
|
-
if (token !==
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
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 });
|
|
6356
4892
|
continue;
|
|
6357
4893
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
id,
|
|
6367
|
-
name: source.name,
|
|
6368
|
-
size: source.size,
|
|
6369
|
-
units: loaded.source_units || (/\.3mf$/i.test(source.name) ? "mm" : null),
|
|
6370
|
-
bounds: loaded.bounds,
|
|
6371
|
-
position: { x: 0, y: 0, z: 0 },
|
|
6372
|
-
rotation: { x: 0, y: 0, z: 0 },
|
|
6373
|
-
embedded,
|
|
6374
|
-
originalColor,
|
|
6375
|
-
color: originalColor,
|
|
6376
|
-
estimated: loaded.triangles * 224,
|
|
6377
|
-
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
|
|
6378
4902
|
});
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
arrangeScene();
|
|
6382
|
-
renderScene();
|
|
6383
|
-
announce(source.name + " is ready.");
|
|
6384
|
-
engine.canvas.focus({ preventScroll: true });
|
|
4903
|
+
this.setArrangement(this._arrangement);
|
|
4904
|
+
this._emit({ type: "loaded", id, name: this._records.get(id).name });
|
|
6385
4905
|
} catch (error) {
|
|
6386
4906
|
if (source.batch) source.batch.failed = true;
|
|
6387
|
-
if (token ===
|
|
6388
|
-
if (
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
showNotice(
|
|
6393
|
-
/WebGL/i.test(error.message) ? "3D viewing needs WebGL 2. Try enabling hardware acceleration in your browser, or use another device." : error.message
|
|
6394
|
-
);
|
|
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);
|
|
6395
4912
|
}
|
|
6396
4913
|
}
|
|
6397
4914
|
}
|
|
6398
4915
|
} finally {
|
|
6399
|
-
|
|
6400
|
-
|
|
4916
|
+
this._drainPromise = null;
|
|
4917
|
+
if (this._busy) this._setBusy(false);
|
|
6401
4918
|
}
|
|
6402
4919
|
}
|
|
6403
|
-
async
|
|
6404
|
-
const
|
|
6405
|
-
if (token !==
|
|
6406
|
-
const controller = new
|
|
6407
|
-
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();
|
|
6408
4924
|
try {
|
|
6409
4925
|
const chosen = source.batch?.files || [];
|
|
6410
4926
|
const cached = [
|
|
6411
4927
|
...new Set(
|
|
6412
|
-
[...
|
|
4928
|
+
[...this._records.values()].map((r) => r.sourceFile).filter(Boolean)
|
|
6413
4929
|
)
|
|
6414
4930
|
];
|
|
6415
4931
|
const companions = [
|
|
@@ -6418,498 +4934,69 @@ function mountViewstlApp(root, options = {}) {
|
|
|
6418
4934
|
(f) => !chosen.some((s) => s.name === f.name) && cached.filter((s) => s.name === f.name).length === 1
|
|
6419
4935
|
) : []
|
|
6420
4936
|
];
|
|
6421
|
-
const result = await
|
|
4937
|
+
const result = await viewer.import_scene(
|
|
6422
4938
|
source.sceneFile || source.remote,
|
|
6423
4939
|
{
|
|
6424
4940
|
format: source.scene || sceneFormat(source.name),
|
|
6425
4941
|
signal: controller.signal,
|
|
6426
4942
|
files: companions,
|
|
6427
|
-
on_progress(
|
|
6428
|
-
if (token
|
|
6429
|
-
|
|
4943
|
+
on_progress: (progress) => {
|
|
4944
|
+
if (token === this._generation)
|
|
4945
|
+
this._emit({ type: "progress", scene: true, progress });
|
|
6430
4946
|
}
|
|
6431
4947
|
}
|
|
6432
4948
|
);
|
|
6433
|
-
if (token !==
|
|
4949
|
+
if (token !== this._generation) return;
|
|
6434
4950
|
for (const file of result.used_files) source.batch?.used.add(file);
|
|
6435
4951
|
for (const id of result.model_ids) {
|
|
6436
|
-
const m =
|
|
6437
|
-
|
|
6438
|
-
["min", "max"].map((key) => [
|
|
6439
|
-
key,
|
|
6440
|
-
g.bounds[key].map((v, i) => (v - m.center[i]) * m.scaleVector[i])
|
|
6441
|
-
])
|
|
6442
|
-
);
|
|
6443
|
-
const record = {
|
|
4952
|
+
const m = viewer._models.get(id);
|
|
4953
|
+
this._adopt(
|
|
6444
4954
|
id,
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
units: g.units || m.units,
|
|
6448
|
-
bounds,
|
|
6449
|
-
position: { x: 0, y: 0, z: 0 },
|
|
6450
|
-
rotation: {
|
|
6451
|
-
x: m.rotationx * 180 / Math.PI,
|
|
6452
|
-
y: m.rotationy * 180 / Math.PI,
|
|
6453
|
-
z: m.rotationz * 180 / Math.PI
|
|
4955
|
+
{
|
|
4956
|
+
sourceFile: result.used_files.includes(m.local_file) ? m.local_file : null
|
|
6454
4957
|
},
|
|
6455
|
-
|
|
6456
|
-
color: m.color || (g.colors ? null : "#909090"),
|
|
6457
|
-
estimated: g.triangles * 224,
|
|
6458
|
-
sourceFile: result.used_files.includes(m.local_file) ? m.local_file : null
|
|
6459
|
-
};
|
|
6460
|
-
const offset = enginePosition(record);
|
|
6461
|
-
for (const axis of ["x", "y", "z"])
|
|
6462
|
-
record.position[axis] = m[axis] - offset[axis];
|
|
6463
|
-
record.originalPosition = { ...record.position };
|
|
6464
|
-
record.originalRotation = { ...record.rotation };
|
|
6465
|
-
record.originalColor = record.color;
|
|
6466
|
-
records.set(id, record);
|
|
6467
|
-
}
|
|
6468
|
-
nextID = Math.max(nextID, engine.max_model_id + 1);
|
|
6469
|
-
modelID = result.model_ids.at(-1);
|
|
6470
|
-
appearanceRequest++;
|
|
6471
|
-
$("scene-arrangement").value = "custom";
|
|
6472
|
-
gridEnabled = !!engine.grid;
|
|
6473
|
-
gridHeight = engine.grid?.height || 0;
|
|
6474
|
-
gridSpacing = engine.grid?.spacing || 1;
|
|
6475
|
-
$("grid-button").setAttribute("aria-pressed", String(gridEnabled));
|
|
6476
|
-
setBackground(engine.bg_color);
|
|
6477
|
-
renderScene();
|
|
6478
|
-
announce(
|
|
6479
|
-
source.name + " opened. " + result.model_ids.length + " models added."
|
|
6480
|
-
);
|
|
6481
|
-
engine.canvas.focus({ preventScroll: true });
|
|
6482
|
-
} finally {
|
|
6483
|
-
if (sceneImport === controller) sceneImport = null;
|
|
6484
|
-
}
|
|
6485
|
-
}
|
|
6486
|
-
root.querySelectorAll("[data-open-file]").forEach((b) => listen(b, "click", () => $("file-input").click()));
|
|
6487
|
-
for (const button of shortcutButtons)
|
|
6488
|
-
listen(button, "click", () => {
|
|
6489
|
-
if (button.disabled || busy || !records.size) return;
|
|
6490
|
-
const action = button.dataset.viewstlShortcut;
|
|
6491
|
-
if (action === "save") $("save-button")?.click();
|
|
6492
|
-
else if (action === "select") {
|
|
6493
|
-
setSelectionMode(!selectionMode);
|
|
6494
|
-
closePanels();
|
|
6495
|
-
announce(
|
|
6496
|
-
selectionMode ? "Selection mode. Click a model; Escape returns to viewing." : "Viewing mode. Drag to rotate the scene."
|
|
4958
|
+
true
|
|
6497
4959
|
);
|
|
6498
|
-
} else if ([
|
|
6499
|
-
"front",
|
|
6500
|
-
"back",
|
|
6501
|
-
"left",
|
|
6502
|
-
"right",
|
|
6503
|
-
"top",
|
|
6504
|
-
"bottom",
|
|
6505
|
-
"isometric"
|
|
6506
|
-
].includes(action)) {
|
|
6507
|
-
setMoveMode(false);
|
|
6508
|
-
setSelectionMode(false);
|
|
6509
|
-
viewer.set_camera_view(action);
|
|
6510
|
-
toolsUI?.sync();
|
|
6511
|
-
announce(button.title || action + " view");
|
|
6512
4960
|
}
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
});
|
|
6525
|
-
$("sample-button").hidden = !sampleURL;
|
|
6526
|
-
listen($("sample-button"), "click", () => {
|
|
6527
|
-
if (sampleURL)
|
|
6528
|
-
void addFiles([
|
|
6529
|
-
{ name: options.sample_name || "Sample.stl", url: sampleURL }
|
|
6530
|
-
]);
|
|
6531
|
-
});
|
|
6532
|
-
listen($("close-model"), "click", () => removeModel(modelID));
|
|
6533
|
-
listen($("cancel-load"), "click", cancelLoading);
|
|
6534
|
-
listen($("clear-scene"), "click", clearScene);
|
|
6535
|
-
listen($("dismiss-notice"), "click", () => {
|
|
6536
|
-
$("notice").hidden = true;
|
|
6537
|
-
});
|
|
6538
|
-
listen($("fit-button"), "click", () => viewer?.set_auto_zoom());
|
|
6539
|
-
listen($("scene-arrangement"), "change", arrangeScene);
|
|
6540
|
-
listen($("model-list"), "click", (event) => {
|
|
6541
|
-
const b = event.target.closest("button");
|
|
6542
|
-
if (!b) return;
|
|
6543
|
-
if (b.dataset.removeModel !== void 0)
|
|
6544
|
-
removeModel(Number(b.dataset.removeModel));
|
|
6545
|
-
else if (b.dataset.selectModel !== void 0)
|
|
6546
|
-
selectModel(Number(b.dataset.selectModel));
|
|
6547
|
-
else if (b.dataset.colorModel !== void 0) {
|
|
6548
|
-
selectModel(Number(b.dataset.colorModel));
|
|
6549
|
-
togglePanel("colors");
|
|
6550
|
-
}
|
|
6551
|
-
});
|
|
6552
|
-
async function applyView() {
|
|
6553
|
-
if (modelID === null || busy) return;
|
|
6554
|
-
const request = ++appearanceRequest;
|
|
6555
|
-
const display = modeInputs.find((input) => input.checked).value;
|
|
6556
|
-
modeInputs.forEach((input) => {
|
|
6557
|
-
input.disabled = true;
|
|
6558
|
-
});
|
|
6559
|
-
$("smooth-shading").disabled = true;
|
|
6560
|
-
$("view-status").textContent = "Preparing view\u2026";
|
|
6561
|
-
$("notice").hidden = true;
|
|
6562
|
-
try {
|
|
6563
|
-
viewer.set_view(modelID, {
|
|
6564
|
-
display,
|
|
6565
|
-
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
|
|
6566
4972
|
});
|
|
6567
|
-
await viewer.when_idle();
|
|
6568
4973
|
} finally {
|
|
6569
|
-
if (
|
|
6570
|
-
syncControls();
|
|
6571
|
-
announce($("view-label").textContent + " view ready.");
|
|
6572
|
-
}
|
|
4974
|
+
if (this._sceneImport === controller) this._sceneImport = null;
|
|
6573
4975
|
}
|
|
6574
4976
|
}
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
applySection();
|
|
6584
|
-
}
|
|
6585
|
-
});
|
|
6586
|
-
listen($("close-" + name), "click", () => {
|
|
6587
|
-
closePanel(name);
|
|
6588
|
-
if (name === "colors") {
|
|
6589
|
-
togglePanel("view");
|
|
6590
|
-
}
|
|
6591
|
-
$(name + "-button").focus();
|
|
6592
|
-
});
|
|
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);
|
|
6593
4985
|
}
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
const
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
position: Number($("section-position").value) / 100,
|
|
6602
|
-
flip: $("section-flip").getAttribute("aria-pressed") === "true"
|
|
6603
|
-
} : null
|
|
6604
|
-
);
|
|
6605
|
-
$("section-value").textContent = $("section-position").value + "%";
|
|
6606
|
-
$("section-button").setAttribute("aria-pressed", String(enabled));
|
|
6607
|
-
}
|
|
6608
|
-
listen($("section-enabled"), "change", applySection);
|
|
6609
|
-
listen($("section-axis"), "change", applySection);
|
|
6610
|
-
listen($("section-position"), "input", applySection);
|
|
6611
|
-
listen($("section-flip"), "click", () => {
|
|
6612
|
-
$("section-flip").setAttribute(
|
|
6613
|
-
"aria-pressed",
|
|
6614
|
-
String($("section-flip").getAttribute("aria-pressed") !== "true")
|
|
6615
|
-
);
|
|
6616
|
-
applySection();
|
|
6617
|
-
});
|
|
6618
|
-
listen($("grid-button"), "click", () => {
|
|
6619
|
-
gridEnabled = !gridEnabled;
|
|
6620
|
-
updateGrid();
|
|
6621
|
-
});
|
|
6622
|
-
function setColor(color) {
|
|
6623
|
-
if (modelID === null) return;
|
|
6624
|
-
viewer.set_color(modelID, color);
|
|
6625
|
-
records.get(modelID).color = color;
|
|
6626
|
-
syncColors();
|
|
6627
|
-
renderList();
|
|
6628
|
-
}
|
|
6629
|
-
listen($("model-color"), "input", () => setColor($("model-color").value));
|
|
6630
|
-
listen(
|
|
6631
|
-
$("restore-color"),
|
|
6632
|
-
"click",
|
|
6633
|
-
() => setColor(records.get(modelID)?.originalColor)
|
|
6634
|
-
);
|
|
6635
|
-
root.querySelectorAll("[data-model-color]").forEach((b) => listen(b, "click", () => setColor(b.dataset.modelColor)));
|
|
6636
|
-
function setBackground(color) {
|
|
6637
|
-
background = color;
|
|
6638
|
-
const rgb = color === "transparent" ? [255, 255, 255] : [1, 3, 5].map((i) => parseInt(color.slice(i, i + 2), 16));
|
|
6639
|
-
$("workspace").classList.toggle(
|
|
6640
|
-
"dark-background",
|
|
6641
|
-
rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722 < 115
|
|
6642
|
-
);
|
|
6643
|
-
viewer?.set_bg_color(color);
|
|
6644
|
-
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);
|
|
6645
4993
|
}
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
(
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
if (modelID === null) return;
|
|
6656
|
-
const name = records.size > 1 ? "viewstl-scene.png" : records.get(modelID).name.replace(/\.[^.]+$/, "") + ".png";
|
|
6657
|
-
try {
|
|
6658
|
-
const blob = await viewer.get_image_blob("image/png");
|
|
6659
|
-
if (disposed) return;
|
|
6660
|
-
const url = URL2.createObjectURL(blob), link = document2.createElement("a");
|
|
6661
|
-
link.href = url;
|
|
6662
|
-
link.download = name;
|
|
6663
|
-
link.click();
|
|
6664
|
-
setTimeout(() => URL2.revokeObjectURL(url), 1e3);
|
|
6665
|
-
announce("Image ready to download.");
|
|
6666
|
-
} catch {
|
|
6667
|
-
showNotice("The image could not be saved. Please try again.");
|
|
6668
|
-
}
|
|
6669
|
-
});
|
|
6670
|
-
let dragDepth = 0;
|
|
6671
|
-
const acceptedDrag = (event) => {
|
|
6672
|
-
const kind = dropKind(event.dataTransfer);
|
|
6673
|
-
if (kind === "files") return kind;
|
|
6674
|
-
if (!options.url_import_endpoint || event.target?.isContentEditable || event.target?.closest?.("input, textarea"))
|
|
6675
|
-
return null;
|
|
6676
|
-
return kind;
|
|
6677
|
-
};
|
|
6678
|
-
const clearDrop = () => {
|
|
6679
|
-
dragDepth = 0;
|
|
6680
|
-
$("drop-overlay").hidden = true;
|
|
6681
|
-
};
|
|
6682
|
-
listen(dropTarget, "dragenter", (event) => {
|
|
6683
|
-
if (acceptedDrag(event)) {
|
|
6684
|
-
event.preventDefault();
|
|
6685
|
-
dragDepth++;
|
|
6686
|
-
$("drop-overlay").hidden = false;
|
|
6687
|
-
}
|
|
6688
|
-
});
|
|
6689
|
-
listen(dropTarget, "dragover", (event) => {
|
|
6690
|
-
if (acceptedDrag(event)) {
|
|
6691
|
-
event.preventDefault();
|
|
6692
|
-
event.dataTransfer.dropEffect = "copy";
|
|
6693
|
-
}
|
|
6694
|
-
});
|
|
6695
|
-
listen(dropTarget, "dragleave", (event) => {
|
|
6696
|
-
if (dragDepth > 0 && --dragDepth === 0) clearDrop();
|
|
6697
|
-
});
|
|
6698
|
-
listen(dropTarget, "drop", (event) => {
|
|
6699
|
-
const kind = acceptedDrag(event);
|
|
6700
|
-
clearDrop();
|
|
6701
|
-
if (!kind) return;
|
|
6702
|
-
event.preventDefault();
|
|
6703
|
-
try {
|
|
6704
|
-
if (kind === "files")
|
|
6705
|
-
addFiles([...event.dataTransfer.files]).catch(
|
|
6706
|
-
(error) => showNotice(error.message)
|
|
6707
|
-
);
|
|
6708
|
-
else
|
|
6709
|
-
for (const url of droppedURLs(event.dataTransfer))
|
|
6710
|
-
openURL(url).catch((error) => showNotice(error.message));
|
|
6711
|
-
} catch (error) {
|
|
6712
|
-
showNotice(error.message);
|
|
6713
|
-
}
|
|
6714
|
-
});
|
|
6715
|
-
listen(dropTarget, "dragend", clearDrop);
|
|
6716
|
-
listen(window2, "blur", clearDrop);
|
|
6717
|
-
root.querySelectorAll("[data-dialog]").forEach((b) => listen(b, "click", () => $(b.dataset.dialog).showModal()));
|
|
6718
|
-
root.querySelectorAll("dialog").forEach(
|
|
6719
|
-
(dialog) => listen(dialog, "click", (event) => {
|
|
6720
|
-
if (event.target === dialog) {
|
|
6721
|
-
const r = dialog.getBoundingClientRect();
|
|
6722
|
-
if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom)
|
|
6723
|
-
if (dialog === $("save-dialog")) exportUI.close();
|
|
6724
|
-
else dialog.close();
|
|
6725
|
-
}
|
|
6726
|
-
})
|
|
6727
|
-
);
|
|
6728
|
-
listen(root, "keydown", (event) => {
|
|
6729
|
-
if (event.key !== "Escape") return;
|
|
6730
|
-
if ($("save-dialog")?.open) return;
|
|
6731
|
-
if (selectionMode) {
|
|
6732
|
-
setSelectionMode(false);
|
|
6733
|
-
closePanels();
|
|
6734
|
-
announce("Viewing mode. Drag to rotate the scene.");
|
|
6735
|
-
return;
|
|
6736
|
-
}
|
|
6737
|
-
if (moveMode) {
|
|
6738
|
-
setMoveMode(false);
|
|
6739
|
-
announce("Move tool closed.");
|
|
6740
|
-
return;
|
|
6741
|
-
}
|
|
6742
|
-
const active = panelNames.find((name) => !$(name + "-panel").hidden);
|
|
6743
|
-
if (active) {
|
|
6744
|
-
closePanels();
|
|
6745
|
-
if (active === "colors") togglePanel("view");
|
|
6746
|
-
$(active + "-button").focus();
|
|
6747
|
-
}
|
|
6748
|
-
});
|
|
6749
|
-
listen(root, "pointerdown", (event) => {
|
|
6750
|
-
if (!event.target.closest(
|
|
6751
|
-
".view-panel,.info-panel,.toolbar,.scene-heading-button,.move-mode"
|
|
6752
|
-
))
|
|
6753
|
-
closePanels();
|
|
6754
|
-
});
|
|
6755
|
-
$("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.`;
|
|
6756
|
-
$("scene-arrangement").value = "original";
|
|
6757
|
-
$("notice").hidden = true;
|
|
6758
|
-
$("drop-overlay").hidden = true;
|
|
6759
|
-
if ($("save-button"))
|
|
6760
|
-
exportUI = bindAppExport({
|
|
6761
|
-
$,
|
|
6762
|
-
listen,
|
|
6763
|
-
window: window2,
|
|
6764
|
-
getState: () => ({
|
|
6765
|
-
busy,
|
|
6766
|
-
selectedId: modelID,
|
|
6767
|
-
models: [...records.values()]
|
|
6768
|
-
}),
|
|
6769
|
-
getViewer: () => viewer,
|
|
6770
|
-
onOpen: () => {
|
|
6771
|
-
setMoveMode(false);
|
|
6772
|
-
closePanels();
|
|
6773
|
-
},
|
|
6774
|
-
announce
|
|
6775
|
-
});
|
|
6776
|
-
if ($("print-button"))
|
|
6777
|
-
printUI = bindAppPrint({
|
|
6778
|
-
$,
|
|
6779
|
-
listen,
|
|
6780
|
-
window: window2,
|
|
6781
|
-
getState: () => ({
|
|
6782
|
-
busy,
|
|
6783
|
-
selectedId: modelID,
|
|
6784
|
-
models: [...records.values()]
|
|
6785
|
-
}),
|
|
6786
|
-
getViewer: () => viewer,
|
|
6787
|
-
onOpen: () => {
|
|
6788
|
-
setMoveMode(false);
|
|
6789
|
-
closePanels();
|
|
6790
|
-
},
|
|
6791
|
-
announce
|
|
6792
|
-
});
|
|
6793
|
-
if ($("animation-details"))
|
|
6794
|
-
animationUI = bindAppAnimation({
|
|
6795
|
-
$,
|
|
6796
|
-
listen,
|
|
6797
|
-
window: window2,
|
|
6798
|
-
getState: () => ({
|
|
6799
|
-
busy,
|
|
6800
|
-
selectedId: modelID,
|
|
6801
|
-
models: [...records.values()]
|
|
6802
|
-
}),
|
|
6803
|
-
getViewer: () => viewer,
|
|
6804
|
-
beforePlay(id) {
|
|
6805
|
-
setMoveMode(false);
|
|
6806
|
-
prepareManual(id);
|
|
6807
|
-
centerAnimationPivot(records.get(id), viewer._models.get(id));
|
|
6808
|
-
$("scene-arrangement").value = "custom";
|
|
6809
|
-
},
|
|
6810
|
-
refreshPose: refreshAnimatedPose,
|
|
6811
|
-
announce
|
|
6812
|
-
});
|
|
6813
|
-
if ($("size-details"))
|
|
6814
|
-
toolsUI = bindAppTools({
|
|
6815
|
-
$,
|
|
6816
|
-
listen,
|
|
6817
|
-
window: window2,
|
|
6818
|
-
getViewer: () => viewer,
|
|
6819
|
-
getState: () => ({ selectedId: modelID, busy }),
|
|
6820
|
-
refresh: renderScene,
|
|
6821
|
-
announce,
|
|
6822
|
-
changeSize(operation) {
|
|
6823
|
-
setMoveMode(false);
|
|
6824
|
-
const record = records.get(modelID);
|
|
6825
|
-
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6826
|
-
operation();
|
|
6827
|
-
syncAnimatedRecord(record, viewer._models.get(modelID));
|
|
6828
|
-
$("scene-arrangement").value = "custom";
|
|
6829
|
-
renderScene();
|
|
6830
|
-
},
|
|
6831
|
-
duplicate() {
|
|
6832
|
-
const original = records.get(modelID), id = viewer.duplicate_model(modelID), m = viewer._models.get(id);
|
|
6833
|
-
const record = {
|
|
6834
|
-
...original,
|
|
6835
|
-
id,
|
|
6836
|
-
name: m.display_name,
|
|
6837
|
-
position: { ...original.position },
|
|
6838
|
-
rotation: { ...original.rotation }
|
|
6839
|
-
};
|
|
6840
|
-
syncAnimatedRecord(record, m);
|
|
6841
|
-
record.originalPosition = { ...record.position };
|
|
6842
|
-
record.originalRotation = { ...record.rotation };
|
|
6843
|
-
records.set(id, record);
|
|
6844
|
-
nextID = Math.max(nextID, id + 1);
|
|
6845
|
-
$("scene-arrangement").value = "custom";
|
|
6846
|
-
selectModel(id);
|
|
6847
|
-
setMoveMode(true, "move", true);
|
|
6848
|
-
announce("Copy selected at the same position. Drag to move it.");
|
|
6849
|
-
}
|
|
6850
|
-
});
|
|
6851
|
-
updateGrid();
|
|
6852
|
-
setBusy(false);
|
|
6853
|
-
setMoveMode(false);
|
|
6854
|
-
const api = {
|
|
6855
|
-
openFiles: addFiles,
|
|
6856
|
-
openURL,
|
|
6857
|
-
cancelLoading() {
|
|
6858
|
-
assertActive();
|
|
6859
|
-
cancelLoading();
|
|
6860
|
-
},
|
|
6861
|
-
clear() {
|
|
6862
|
-
assertActive();
|
|
6863
|
-
clearScene();
|
|
6864
|
-
},
|
|
6865
|
-
select(id) {
|
|
6866
|
-
assertActive();
|
|
6867
|
-
selectModel(id);
|
|
6868
|
-
},
|
|
6869
|
-
remove(id) {
|
|
6870
|
-
assertActive();
|
|
6871
|
-
removeModel(id);
|
|
6872
|
-
},
|
|
6873
|
-
getState() {
|
|
6874
|
-
assertActive();
|
|
6875
|
-
for (const record of records.values())
|
|
6876
|
-
syncAnimatedRecord(record, viewer?._models.get(record.id));
|
|
6877
|
-
return {
|
|
6878
|
-
selectedId: modelID,
|
|
6879
|
-
busy,
|
|
6880
|
-
arrangement: $("scene-arrangement").value,
|
|
6881
|
-
memoryBudget: policy.budget,
|
|
6882
|
-
models: [...records.values()].map((record) => ({
|
|
6883
|
-
id: record.id,
|
|
6884
|
-
name: record.name,
|
|
6885
|
-
size: record.size,
|
|
6886
|
-
units: record.units,
|
|
6887
|
-
position: { ...record.position },
|
|
6888
|
-
rotation: { ...record.rotation },
|
|
6889
|
-
color: record.color,
|
|
6890
|
-
animation: viewer.get_model_animation_state(record.id)
|
|
6891
|
-
}))
|
|
6892
|
-
};
|
|
6893
|
-
},
|
|
6894
|
-
dispose() {
|
|
6895
|
-
if (disposed) return;
|
|
6896
|
-
exportUI?.dispose();
|
|
6897
|
-
printUI?.dispose();
|
|
6898
|
-
animationUI?.dispose();
|
|
6899
|
-
toolsUI?.dispose();
|
|
6900
|
-
cancelLoading();
|
|
6901
|
-
setMoveMode(false);
|
|
6902
|
-
appearanceRequest++;
|
|
6903
|
-
disposed = true;
|
|
6904
|
-
abort.abort();
|
|
6905
|
-
modelMover?.dispose();
|
|
6906
|
-
viewer?.dispose();
|
|
6907
|
-
records.clear();
|
|
6908
|
-
if (options.id_prefix)
|
|
6909
|
-
modeInputs.forEach((input) => input.name = "view-mode");
|
|
6910
|
-
mountedRoots.delete(root);
|
|
6911
|
-
}
|
|
6912
|
-
};
|
|
6913
|
-
mountedRoots.set(root, api);
|
|
6914
|
-
return api;
|
|
6915
|
-
}
|
|
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
|
+
};
|