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