gds-lens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -0
- package/LICENCE.md +21 -0
- package/README.md +372 -0
- package/THIRD-PARTY-LICENSES.md +261 -0
- package/dist/esm/gds-lens.js +4731 -0
- package/dist/inline-wasm/gds-lens-engine.js +0 -0
- package/dist/inline-wasm/gds-lens-host.js +82 -0
- package/dist/inline-wasm/gds-lens-worker.js +118 -0
- package/dist/inline-wasm/gds-lens.html +29 -0
- package/dist/inline-wasm/gds-lens.js +4705 -0
- package/dist/web/gds-lens-engine.js +2 -0
- package/dist/web/gds-lens-engine.wasm +0 -0
- package/dist/web/gds-lens-host.js +82 -0
- package/dist/web/gds-lens-worker.js +118 -0
- package/dist/web/gds-lens.html +29 -0
- package/dist/web/gds-lens.js +4705 -0
- package/package.json +113 -0
- package/src/cell-search.js +88 -0
- package/src/coord-parse.js +50 -0
- package/src/engine-source.esm.js +49 -0
- package/src/engine-source.js +20 -0
- package/src/gds-lens.js +175 -0
- package/src/hosts/browser.js +156 -0
- package/src/layout-bytes.js +143 -0
- package/src/load-errors.js +64 -0
- package/src/marker-parsers.js +672 -0
- package/src/mount-target.js +23 -0
- package/src/viewer-shell.html +105 -0
- package/src/viewer.css +469 -0
- package/src/viewer.html +29 -0
- package/src/viewer.js +2688 -0
- package/src/wasm-worker.js +157 -0
- package/types/cell-search.d.ts +33 -0
- package/types/coord-parse.d.ts +11 -0
- package/types/gds-lens.d.ts +135 -0
- package/types/hosts-browser.d.ts +13 -0
- package/types/layout-bytes.d.ts +49 -0
- package/types/load-errors.d.ts +27 -0
- package/types/parsers.d.ts +84 -0
|
@@ -0,0 +1,4705 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target2, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target2, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/mount-target.js
|
|
18
|
+
function setMountTarget(element) {
|
|
19
|
+
target = element;
|
|
20
|
+
}
|
|
21
|
+
function takeMountTarget() {
|
|
22
|
+
const element = target;
|
|
23
|
+
target = null;
|
|
24
|
+
return element;
|
|
25
|
+
}
|
|
26
|
+
var target;
|
|
27
|
+
var init_mount_target = __esm({
|
|
28
|
+
"src/mount-target.js"() {
|
|
29
|
+
target = null;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/cell-search.js
|
|
34
|
+
function rankCellMatches(cells, query) {
|
|
35
|
+
const needle = String(query).trim().toLowerCase();
|
|
36
|
+
if (!needle) return [];
|
|
37
|
+
const exact = [];
|
|
38
|
+
const prefix = [];
|
|
39
|
+
const rest = [];
|
|
40
|
+
for (let i = 0; i < cells.length; i++) {
|
|
41
|
+
const name = cells[i] && typeof cells[i].name === "string" ? cells[i].name : "";
|
|
42
|
+
const at = name.toLowerCase().indexOf(needle);
|
|
43
|
+
if (at < 0) continue;
|
|
44
|
+
if (name.length === needle.length) exact.push(i);
|
|
45
|
+
else if (at === 0) prefix.push(i);
|
|
46
|
+
else rest.push(i);
|
|
47
|
+
}
|
|
48
|
+
return exact.concat(prefix, rest);
|
|
49
|
+
}
|
|
50
|
+
function cellPathToTarget(cells, roots, target2, maxDepth) {
|
|
51
|
+
if (!cells || !cells[target2]) return null;
|
|
52
|
+
const limit = maxDepth > 0 ? maxDepth : 1;
|
|
53
|
+
const failed = /* @__PURE__ */ new Set();
|
|
54
|
+
const path = [];
|
|
55
|
+
function walk(index, depth) {
|
|
56
|
+
path.push(index);
|
|
57
|
+
if (index === target2) return true;
|
|
58
|
+
if (depth + 1 < limit && !failed.has(index)) {
|
|
59
|
+
const refs = cells[index] && cells[index].refs || [];
|
|
60
|
+
for (const ref of refs) {
|
|
61
|
+
if (!cells[ref.cell]) continue;
|
|
62
|
+
if (walk(ref.cell, depth + 1)) return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
path.pop();
|
|
66
|
+
failed.add(index);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
for (const root of roots || []) {
|
|
70
|
+
if (!cells[root]) continue;
|
|
71
|
+
if (walk(root, 0)) return path;
|
|
72
|
+
path.length = 0;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
var init_cell_search = __esm({
|
|
77
|
+
"src/cell-search.js"() {
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// src/marker-parsers.js
|
|
82
|
+
function sniffMarkerFormat(text) {
|
|
83
|
+
if (typeof text !== "string" || text.length === 0) return null;
|
|
84
|
+
let t = text;
|
|
85
|
+
if (t.charCodeAt(0) === 65279) t = t.slice(1);
|
|
86
|
+
t = t.replace(/^\s+/, "");
|
|
87
|
+
if (t.startsWith("<")) {
|
|
88
|
+
return t.slice(0, 2048).includes("<report-database") ? "lyrdb" : null;
|
|
89
|
+
}
|
|
90
|
+
const head = [];
|
|
91
|
+
for (const raw of t.slice(0, 8192).split("\n")) {
|
|
92
|
+
const line = raw.replace(/\r$/, "").trim();
|
|
93
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
94
|
+
head.push(line);
|
|
95
|
+
if (head.length === 3) break;
|
|
96
|
+
}
|
|
97
|
+
const header = /^(\S+)\s+(\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)$/.exec(head[0] || "");
|
|
98
|
+
if (!header) return null;
|
|
99
|
+
const resolution = parseFloat(header[2]);
|
|
100
|
+
if (!(resolution >= 1e-3 && resolution <= 1e6)) return null;
|
|
101
|
+
if (head.length < 3) return "drc";
|
|
102
|
+
return /^\d+\s+\d+\s+\d+(\s|$)/.test(head[2]) ? "drc" : null;
|
|
103
|
+
}
|
|
104
|
+
function parsePointList(text) {
|
|
105
|
+
const cleaned = text.trim().replace(/^\(/, "").replace(/\)$/, "").trim();
|
|
106
|
+
if (cleaned === "") return new Float64Array(0);
|
|
107
|
+
const parts = cleaned.split(";");
|
|
108
|
+
const pts = new Float64Array(parts.length * 2);
|
|
109
|
+
for (let i = 0; i < parts.length; i++) {
|
|
110
|
+
const xy = parts[i].split(",");
|
|
111
|
+
if (xy.length !== 2) throw new Error(`bad point "${parts[i].trim()}"`);
|
|
112
|
+
const x = parseFloat(xy[0]);
|
|
113
|
+
const y = parseFloat(xy[1]);
|
|
114
|
+
if (!isFinite(x) || !isFinite(y)) throw new Error(`bad point "${parts[i].trim()}"`);
|
|
115
|
+
pts[i * 2] = x;
|
|
116
|
+
pts[i * 2 + 1] = y;
|
|
117
|
+
}
|
|
118
|
+
return pts;
|
|
119
|
+
}
|
|
120
|
+
function parseLyrdbValue(raw, item, unknownTypes) {
|
|
121
|
+
const text = raw.replace(/\s+/g, " ").trim();
|
|
122
|
+
if (text === "") return "";
|
|
123
|
+
const m = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*([\s\S]*)$/.exec(text);
|
|
124
|
+
if (!m) return text;
|
|
125
|
+
const type = m[1].toLowerCase();
|
|
126
|
+
const body = m[2].trim();
|
|
127
|
+
try {
|
|
128
|
+
if (type === "box") {
|
|
129
|
+
const p = parsePointList(body);
|
|
130
|
+
if (p.length !== 4) throw new Error("box needs 2 points");
|
|
131
|
+
const l = Math.min(p[0], p[2]);
|
|
132
|
+
const r = Math.max(p[0], p[2]);
|
|
133
|
+
const b = Math.min(p[1], p[3]);
|
|
134
|
+
const t = Math.max(p[1], p[3]);
|
|
135
|
+
item.polygons.push(Float64Array.from([l, b, r, b, r, t, l, t]));
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
if (type === "polygon") {
|
|
139
|
+
const inner = body.replace(/^\(/, "").replace(/\)$/, "");
|
|
140
|
+
for (const ringText of inner.split("/")) {
|
|
141
|
+
const ring = parsePointList(ringText);
|
|
142
|
+
if (ring.length >= 6) item.polygons.push(ring);
|
|
143
|
+
}
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
if (type === "edge") {
|
|
147
|
+
const p = parsePointList(body);
|
|
148
|
+
if (p.length !== 4) throw new Error("edge needs 2 points");
|
|
149
|
+
item.edges.push(p[0], p[1], p[2], p[3]);
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
if (type === "edge-pair") {
|
|
153
|
+
const mm = /\(([^)]*)\)\s*[/|]\s*\(([^)]*)\)/.exec(body);
|
|
154
|
+
if (!mm) throw new Error("malformed edge-pair");
|
|
155
|
+
for (const part of [mm[1], mm[2]]) {
|
|
156
|
+
const p = parsePointList(part);
|
|
157
|
+
if (p.length !== 4) throw new Error("edge-pair edge needs 2 points");
|
|
158
|
+
item.edges.push(p[0], p[1], p[2], p[3]);
|
|
159
|
+
}
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
unknownTypes.add(type);
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
function computeItemBBox(item) {
|
|
169
|
+
let minX = Infinity;
|
|
170
|
+
let minY = Infinity;
|
|
171
|
+
let maxX = -Infinity;
|
|
172
|
+
let maxY = -Infinity;
|
|
173
|
+
const eat = (arr) => {
|
|
174
|
+
for (let i = 0; i + 1 < arr.length; i += 2) {
|
|
175
|
+
if (arr[i] < minX) minX = arr[i];
|
|
176
|
+
if (arr[i] > maxX) maxX = arr[i];
|
|
177
|
+
if (arr[i + 1] < minY) minY = arr[i + 1];
|
|
178
|
+
if (arr[i + 1] > maxY) maxY = arr[i + 1];
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
for (const ring of item.polygons) eat(ring);
|
|
182
|
+
eat(item.edges);
|
|
183
|
+
if (minX > maxX) return null;
|
|
184
|
+
return { minX, minY, maxX, maxY };
|
|
185
|
+
}
|
|
186
|
+
function finalizeModel(model) {
|
|
187
|
+
let id = 0;
|
|
188
|
+
for (const cat of model.categories) {
|
|
189
|
+
for (const item of cat.items) {
|
|
190
|
+
item.id = id++;
|
|
191
|
+
if (!(item.edges instanceof Float64Array)) item.edges = Float64Array.from(item.edges);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return model;
|
|
195
|
+
}
|
|
196
|
+
function parseLyrdb(text, domParserCtor) {
|
|
197
|
+
const doc = new domParserCtor().parseFromString(text, "text/xml");
|
|
198
|
+
const root = doc && doc.documentElement;
|
|
199
|
+
if (!root || root.nodeName !== "report-database") {
|
|
200
|
+
throw new Error("not a .lyrdb report database (no <report-database> root)");
|
|
201
|
+
}
|
|
202
|
+
const childElements = (node, name) => {
|
|
203
|
+
const out = [];
|
|
204
|
+
for (let c = node.firstChild; c; c = c.nextSibling) {
|
|
205
|
+
if (c.nodeType === 1 && c.nodeName === name) out.push(c);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
};
|
|
209
|
+
const childText = (node, name) => {
|
|
210
|
+
const els2 = childElements(node, name);
|
|
211
|
+
return els2.length ? els2[0].textContent || "" : null;
|
|
212
|
+
};
|
|
213
|
+
const topCell = (childText(root, "top-cell") || "").trim();
|
|
214
|
+
const warnings = [];
|
|
215
|
+
const model = { topCell, warnings, categories: [] };
|
|
216
|
+
const catByPath = /* @__PURE__ */ new Map();
|
|
217
|
+
const ensureCategory = (path, description) => {
|
|
218
|
+
let cat = catByPath.get(path);
|
|
219
|
+
if (!cat) {
|
|
220
|
+
cat = { name: path, description: description || "", items: [] };
|
|
221
|
+
catByPath.set(path, cat);
|
|
222
|
+
model.categories.push(cat);
|
|
223
|
+
} else if (description && !cat.description) {
|
|
224
|
+
cat.description = description;
|
|
225
|
+
}
|
|
226
|
+
return cat;
|
|
227
|
+
};
|
|
228
|
+
const walkCategories = (categoriesEl, prefix) => {
|
|
229
|
+
for (const catEl of childElements(categoriesEl, "category")) {
|
|
230
|
+
const name = (childText(catEl, "name") || "").trim();
|
|
231
|
+
if (!name) continue;
|
|
232
|
+
const path = prefix ? prefix + "." + name : name;
|
|
233
|
+
ensureCategory(path, (childText(catEl, "description") || "").trim());
|
|
234
|
+
for (const sub of childElements(catEl, "categories")) walkCategories(sub, path);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
for (const catsEl of childElements(root, "categories")) walkCategories(catsEl, "");
|
|
238
|
+
let nonTopCount = 0;
|
|
239
|
+
const unknownTypes = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const itemsEl of childElements(root, "items")) {
|
|
241
|
+
for (const itemEl of childElements(itemsEl, "item")) {
|
|
242
|
+
let catRef = (childText(itemEl, "category") || "").trim().replace(/^'+|'+$/g, "");
|
|
243
|
+
if (!catRef) catRef = "(uncategorized)";
|
|
244
|
+
const cat = ensureCategory(catRef, "");
|
|
245
|
+
const item = { id: -1, label: String(cat.items.length + 1), note: "", polygons: [], edges: [], bbox: null };
|
|
246
|
+
const notes = [];
|
|
247
|
+
const cellRef = (childText(itemEl, "cell") || "").trim();
|
|
248
|
+
if (cellRef) {
|
|
249
|
+
const baseCell = cellRef.split(":")[0];
|
|
250
|
+
if (topCell && baseCell !== topCell || cellRef.includes(":")) {
|
|
251
|
+
nonTopCount++;
|
|
252
|
+
notes.push("cell " + cellRef);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const mult = parseInt((childText(itemEl, "multiplicity") || "").trim(), 10);
|
|
256
|
+
if (mult > 1) notes.push("\xD7" + mult);
|
|
257
|
+
for (const valuesEl of childElements(itemEl, "values")) {
|
|
258
|
+
for (const valueEl of childElements(valuesEl, "value")) {
|
|
259
|
+
const note = parseLyrdbValue(valueEl.textContent || "", item, unknownTypes);
|
|
260
|
+
if (note) notes.push(note);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
item.note = notes.join(" \xB7 ");
|
|
264
|
+
item.bbox = computeItemBBox(item);
|
|
265
|
+
cat.items.push(item);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (nonTopCount > 0) {
|
|
269
|
+
warnings.push(nonTopCount + " marker(s) reference non-top cells; positions may be wrong");
|
|
270
|
+
}
|
|
271
|
+
if (unknownTypes.size > 0) {
|
|
272
|
+
warnings.push("values of unsupported type shown as text only: " + Array.from(unknownTypes).join(", "));
|
|
273
|
+
}
|
|
274
|
+
return finalizeModel(model);
|
|
275
|
+
}
|
|
276
|
+
function parseDrcAscii(text) {
|
|
277
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
278
|
+
const lines = text.split(/\r?\n/);
|
|
279
|
+
const warnings = [];
|
|
280
|
+
const model = { topCell: "", warnings, categories: [] };
|
|
281
|
+
let i = 0;
|
|
282
|
+
const peek = () => {
|
|
283
|
+
while (i < lines.length) {
|
|
284
|
+
const t = lines[i].trim();
|
|
285
|
+
if (t !== "" && !t.startsWith("//")) return lines[i];
|
|
286
|
+
i++;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
};
|
|
290
|
+
const take = () => {
|
|
291
|
+
const line = peek();
|
|
292
|
+
if (line !== null) i++;
|
|
293
|
+
return line;
|
|
294
|
+
};
|
|
295
|
+
const headerLine = peek();
|
|
296
|
+
const header = /^(\S+)\s+(\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)$/.exec((headerLine || "").trim());
|
|
297
|
+
if (!header) throw new Error("not a ASCII DRC results database (bad header line)");
|
|
298
|
+
model.topCell = header[1];
|
|
299
|
+
const resolution = parseFloat(header[2]);
|
|
300
|
+
if (!(resolution >= 1e-3 && resolution <= 1e6)) throw new Error("bad precision in ASCII DRC header");
|
|
301
|
+
i++;
|
|
302
|
+
const countsRe = /^(\d+)\s+(\d+)(?:\s+(\d+))?(?:\s+\S.*)?$/;
|
|
303
|
+
const recordRe = /^([pePE])\s+(\d+)\s+(\d+)\s*(\S.*)?$/;
|
|
304
|
+
const numberRe = /^-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?$/;
|
|
305
|
+
const waiverRe = /^WE(\d+)\s*(.*)$/;
|
|
306
|
+
const cnRe = /^CN\s+(\S+)((?:\s+[cC])?)((?:\s+-?\d+){6})?\s*$/;
|
|
307
|
+
const propRe = /^([A-Za-z_]\w*)\s+(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)\s*$/;
|
|
308
|
+
const lineNumbers = (line) => {
|
|
309
|
+
const parts = line.trim().split(/\s+/);
|
|
310
|
+
for (const p of parts) if (!numberRe.test(p)) return null;
|
|
311
|
+
return parts;
|
|
312
|
+
};
|
|
313
|
+
let unsupportedProps = 0;
|
|
314
|
+
let cellRefCount = 0;
|
|
315
|
+
let strayCoords = 0;
|
|
316
|
+
let pendingName = null;
|
|
317
|
+
while (true) {
|
|
318
|
+
let name = pendingName;
|
|
319
|
+
pendingName = null;
|
|
320
|
+
if (name === null) {
|
|
321
|
+
const line = take();
|
|
322
|
+
if (line === null) break;
|
|
323
|
+
name = line;
|
|
324
|
+
}
|
|
325
|
+
if (lineNumbers(name.trim())) {
|
|
326
|
+
strayCoords++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
name = name.trim().replace(/\.$/, "");
|
|
330
|
+
const cat = { name, description: "", items: [] };
|
|
331
|
+
model.categories.push(cat);
|
|
332
|
+
let resultCount = null;
|
|
333
|
+
let descCount = 0;
|
|
334
|
+
const next = peek();
|
|
335
|
+
const counts = next !== null && !recordRe.test(next.trim()) ? countsRe.exec(next.trim()) : null;
|
|
336
|
+
if (counts) {
|
|
337
|
+
i++;
|
|
338
|
+
resultCount = parseInt(counts[1], 10);
|
|
339
|
+
descCount = counts[3] === void 0 ? 0 : parseInt(counts[3], 10);
|
|
340
|
+
} else if (next !== null && !recordRe.test(next.trim())) {
|
|
341
|
+
warnings.push(`${name}: no counts line after the check name`);
|
|
342
|
+
}
|
|
343
|
+
const waivers = /* @__PURE__ */ new Map();
|
|
344
|
+
const descParts = [];
|
|
345
|
+
for (let d = 0; d < descCount; d++) {
|
|
346
|
+
const line = take();
|
|
347
|
+
if (line === null) {
|
|
348
|
+
warnings.push(`${name}: file ended inside the description block`);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
const trimmed = line.trim();
|
|
352
|
+
if (recordRe.test(trimmed)) {
|
|
353
|
+
i--;
|
|
354
|
+
warnings.push(`${name}: description count ${descCount} overruns the results`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
const we = waiverRe.exec(trimmed);
|
|
358
|
+
if (we) {
|
|
359
|
+
const n = parseInt(we[1], 10);
|
|
360
|
+
if (!waivers.has(n)) waivers.set(n, []);
|
|
361
|
+
else waivers.get(n).push(we[2]);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
descParts.push(trimmed.startsWith('"') ? trimmed.replace(/^"/, "").replace(/"$/, "") : line.replace(/\s+$/, ""));
|
|
365
|
+
}
|
|
366
|
+
cat.description = descParts.join("\n");
|
|
367
|
+
let cellName = "";
|
|
368
|
+
let xf = null;
|
|
369
|
+
let shape = 0;
|
|
370
|
+
while (resultCount === null || shape < resultCount) {
|
|
371
|
+
const line = take();
|
|
372
|
+
if (line === null) {
|
|
373
|
+
if (resultCount !== null && shape < resultCount) {
|
|
374
|
+
warnings.push(`${name}: file ended after ${shape} of ${resultCount} result(s)`);
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
const rec = recordRe.exec(line.trim());
|
|
379
|
+
if (!rec) {
|
|
380
|
+
if (lineNumbers(line.trim())) {
|
|
381
|
+
strayCoords++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (resultCount !== null && shape < resultCount) {
|
|
385
|
+
warnings.push(`${name}: results ended after ${shape} of ${resultCount}; continuing with the next check`);
|
|
386
|
+
}
|
|
387
|
+
pendingName = line;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
const kind = rec[1].toLowerCase();
|
|
391
|
+
const ordinal = rec[2];
|
|
392
|
+
const count = parseInt(rec[3], 10);
|
|
393
|
+
const item = {
|
|
394
|
+
id: -1,
|
|
395
|
+
label: `${kind} ${ordinal}`,
|
|
396
|
+
note: "",
|
|
397
|
+
polygons: [],
|
|
398
|
+
edges: [],
|
|
399
|
+
bbox: null,
|
|
400
|
+
waived: false
|
|
401
|
+
};
|
|
402
|
+
const notes = [];
|
|
403
|
+
if (rec[4]) notes.push(rec[4].trim());
|
|
404
|
+
while (true) {
|
|
405
|
+
const propLine = peek();
|
|
406
|
+
if (propLine === null) break;
|
|
407
|
+
const trimmed = propLine.trim();
|
|
408
|
+
if (lineNumbers(trimmed) || recordRe.test(trimmed)) break;
|
|
409
|
+
const cn = cnRe.exec(trimmed);
|
|
410
|
+
if (cn) {
|
|
411
|
+
i++;
|
|
412
|
+
cellName = cn[1];
|
|
413
|
+
const m = cn[3] ? cn[3].trim().split(/\s+/).map(Number) : [1, 0, 0, 1, 0, 0];
|
|
414
|
+
xf = cn[2].trim() === "" ? null : { m11: m[0], m21: m[1], m12: m[2], m22: m[3], tx: m[4], ty: m[5] };
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const prop = propRe.exec(trimmed);
|
|
418
|
+
if (prop) {
|
|
419
|
+
i++;
|
|
420
|
+
notes.push(`${prop[1]}=${prop[2]}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (/^[A-Za-z_]/.test(trimmed)) {
|
|
424
|
+
i++;
|
|
425
|
+
unsupportedProps++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
if (cellName && cellName !== model.topCell) {
|
|
431
|
+
cellRefCount++;
|
|
432
|
+
notes.push("cell " + cellName);
|
|
433
|
+
}
|
|
434
|
+
const wanted = kind === "p" ? count * 2 : count * 4;
|
|
435
|
+
const nums = [];
|
|
436
|
+
while (nums.length < wanted) {
|
|
437
|
+
const vline = peek();
|
|
438
|
+
if (vline === null) break;
|
|
439
|
+
const parts = lineNumbers(vline.trim());
|
|
440
|
+
if (!parts) break;
|
|
441
|
+
i++;
|
|
442
|
+
for (const p of parts) nums.push(parseFloat(p));
|
|
443
|
+
}
|
|
444
|
+
if (nums.length < wanted) {
|
|
445
|
+
warnings.push(
|
|
446
|
+
`${name} ${kind} ${ordinal}: ${nums.length / 2} of ${kind === "p" ? count : count * 2} point(s) present`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const mapX = xf ? (x, y) => (xf.m11 * x + xf.m12 * y + xf.tx) / resolution : (x) => x / resolution;
|
|
450
|
+
const mapY = xf ? (x, y) => (xf.m21 * x + xf.m22 * y + xf.ty) / resolution : (x, y) => y / resolution;
|
|
451
|
+
if (kind === "p") {
|
|
452
|
+
const usable = Math.floor(nums.length / 2) * 2;
|
|
453
|
+
if (usable >= 6) {
|
|
454
|
+
const ring = new Float64Array(usable);
|
|
455
|
+
for (let k = 0; k < usable; k += 2) {
|
|
456
|
+
ring[k] = mapX(nums[k], nums[k + 1]);
|
|
457
|
+
ring[k + 1] = mapY(nums[k], nums[k + 1]);
|
|
458
|
+
}
|
|
459
|
+
item.polygons.push(ring);
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
const usable = Math.floor(nums.length / 4) * 4;
|
|
463
|
+
for (let k = 0; k < usable; k += 2) {
|
|
464
|
+
item.edges.push(mapX(nums[k], nums[k + 1]), mapY(nums[k], nums[k + 1]));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const waiver = waivers.get(shape);
|
|
468
|
+
if (waiver) {
|
|
469
|
+
item.waived = true;
|
|
470
|
+
notes.push(waiver.length ? "waived: " + waiver.join(" ") : "waived");
|
|
471
|
+
}
|
|
472
|
+
item.note = notes.join(" \xB7 ");
|
|
473
|
+
item.bbox = computeItemBBox(item);
|
|
474
|
+
cat.items.push(item);
|
|
475
|
+
shape++;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (cellRefCount > 0) {
|
|
479
|
+
warnings.push(cellRefCount + " marker(s) placed in cells other than " + model.topCell);
|
|
480
|
+
}
|
|
481
|
+
if (unsupportedProps > 0) {
|
|
482
|
+
warnings.push(unsupportedProps + " unsupported per-result property record(s) ignored");
|
|
483
|
+
}
|
|
484
|
+
if (strayCoords > 0) {
|
|
485
|
+
warnings.push(strayCoords + " coordinate line(s) past the declared point counts ignored");
|
|
486
|
+
}
|
|
487
|
+
return finalizeModel(model);
|
|
488
|
+
}
|
|
489
|
+
function parseMarkerFile(text, domParserCtor) {
|
|
490
|
+
const format = sniffMarkerFormat(text);
|
|
491
|
+
if (format === "lyrdb") return parseLyrdb(text, domParserCtor);
|
|
492
|
+
if (format === "drc") return parseDrcAscii(text);
|
|
493
|
+
throw new Error("Unrecognized marker file format (expected .lyrdb XML or ASCII DRC results)");
|
|
494
|
+
}
|
|
495
|
+
function flattenMarkerModel(model) {
|
|
496
|
+
const categories = [];
|
|
497
|
+
let itemCount = 0;
|
|
498
|
+
let ringCount = 0;
|
|
499
|
+
let ringVertCount = 0;
|
|
500
|
+
let edgeSegCount = 0;
|
|
501
|
+
for (const cat of model.categories) {
|
|
502
|
+
categories.push({ itemStart: itemCount, itemCount: cat.items.length });
|
|
503
|
+
for (const item of cat.items) {
|
|
504
|
+
itemCount++;
|
|
505
|
+
for (const ring2 of item.polygons) {
|
|
506
|
+
ringCount++;
|
|
507
|
+
ringVertCount += ring2.length / 2;
|
|
508
|
+
}
|
|
509
|
+
edgeSegCount += Math.floor(item.edges.length / 4);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const itemCategory = new Int32Array(itemCount);
|
|
513
|
+
const itemBBoxes = new Float32Array(itemCount * 4);
|
|
514
|
+
const polyVerts = new Float32Array(ringVertCount * 2);
|
|
515
|
+
const polyVertCounts = new Uint32Array(ringCount);
|
|
516
|
+
const polyItemIds = new Uint32Array(ringCount);
|
|
517
|
+
const edgeVerts = new Float32Array(edgeSegCount * 4);
|
|
518
|
+
const edgeItemIds = new Uint32Array(edgeSegCount);
|
|
519
|
+
let ring = 0;
|
|
520
|
+
let vert = 0;
|
|
521
|
+
let seg = 0;
|
|
522
|
+
model.categories.forEach((cat, ci) => {
|
|
523
|
+
for (const item of cat.items) {
|
|
524
|
+
itemCategory[item.id] = ci;
|
|
525
|
+
const bb = item.bbox;
|
|
526
|
+
itemBBoxes.set(bb ? [bb.minX, bb.minY, bb.maxX, bb.maxY] : [0, 0, -1, -1], item.id * 4);
|
|
527
|
+
for (const r of item.polygons) {
|
|
528
|
+
polyVerts.set(r, vert * 2);
|
|
529
|
+
polyVertCounts[ring] = r.length / 2;
|
|
530
|
+
polyItemIds[ring] = item.id;
|
|
531
|
+
vert += r.length / 2;
|
|
532
|
+
ring++;
|
|
533
|
+
}
|
|
534
|
+
const segs = Math.floor(item.edges.length / 4);
|
|
535
|
+
edgeVerts.set(item.edges.subarray(0, segs * 4), seg * 4);
|
|
536
|
+
for (let k = 0; k < segs; k++) edgeItemIds[seg + k] = item.id;
|
|
537
|
+
seg += segs;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
return { categories, itemCategory, itemBBoxes, polyVerts, polyVertCounts, polyItemIds, edgeVerts, edgeItemIds };
|
|
541
|
+
}
|
|
542
|
+
var init_marker_parsers = __esm({
|
|
543
|
+
"src/marker-parsers.js"() {
|
|
544
|
+
"use strict";
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// src/load-errors.js
|
|
549
|
+
function describeLoadFailure(err, prefix) {
|
|
550
|
+
const text = err && err.message ? err.message : String(err);
|
|
551
|
+
if (OOM_PATTERN.test(text)) return `${OOM_MESSAGE}
|
|
552
|
+
|
|
553
|
+
(${text})`;
|
|
554
|
+
return prefix ? `${prefix}: ${text}` : text;
|
|
555
|
+
}
|
|
556
|
+
function describeDecodeFailure(result) {
|
|
557
|
+
if (!result || result.ok) return "";
|
|
558
|
+
if (result.reason === "too-large") {
|
|
559
|
+
const gb = (result.limit / (1024 * 1024 * 1024)).toFixed(1);
|
|
560
|
+
const claimed = result.storedSize ? ` The file's own trailer claims ${(result.storedSize / (1024 * 1024)).toFixed(0)} MB uncompressed.` : "";
|
|
561
|
+
return `This compressed layout is too large to expand.
|
|
562
|
+
|
|
563
|
+
Expanding stopped at the ${gb} GB limit.${claimed} The viewer parses layouts in a 32-bit WebAssembly module, so the expanded file and the geometry built from it both have to fit in one 4 GB address space. An uncompressed copy of the same design will not help; a design that reuses cells rather than flattening them will.`;
|
|
564
|
+
}
|
|
565
|
+
return `This layout looks gzipped, but the compressed data could not be read.
|
|
566
|
+
|
|
567
|
+
Usually a truncated or half-written file -- a copy that was interrupted, or a download that stopped early. (${result.detail})`;
|
|
568
|
+
}
|
|
569
|
+
var OOM_PATTERN, OOM_MESSAGE;
|
|
570
|
+
var init_load_errors = __esm({
|
|
571
|
+
"src/load-errors.js"() {
|
|
572
|
+
OOM_PATTERN = /memory access out of bounds|Cannot enlarge memory|Aborted\(|out of memory|Array buffer allocation failed|Invalid (typed )?array length/i;
|
|
573
|
+
OOM_MESSAGE = "Out of memory: this layout is too large to open.\n\nThe viewer parses layouts in a 32-bit WebAssembly module, so the whole flattened design has to fit in 4 GB. Layouts that reuse cells (arrays and repeated placements) go much further than fully flattened ones, because a repeated cell is drawn as GPU instances instead of being copied for every placement.";
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// src/layout-bytes.js
|
|
578
|
+
function looksGzipped(bytes) {
|
|
579
|
+
return !!bytes && bytes.length >= 2 && bytes[0] === GZIP_ID1 && bytes[1] === GZIP_ID2;
|
|
580
|
+
}
|
|
581
|
+
function gzipStoredSize(bytes) {
|
|
582
|
+
if (!bytes || bytes.length < GZIP_MIN_BYTES) return null;
|
|
583
|
+
const end = bytes.length;
|
|
584
|
+
return bytes[end - 4] + bytes[end - 3] * 256 + bytes[end - 2] * 65536 + bytes[end - 1] * 16777216;
|
|
585
|
+
}
|
|
586
|
+
async function gunzip(bytes, cap) {
|
|
587
|
+
const stream = new DecompressionStream("gzip");
|
|
588
|
+
const writer = stream.writable.getWriter();
|
|
589
|
+
const pump = writer.write(bytes).then(() => writer.close()).catch(() => {
|
|
590
|
+
});
|
|
591
|
+
const reader = stream.readable.getReader();
|
|
592
|
+
const chunks = [];
|
|
593
|
+
let total = 0;
|
|
594
|
+
for (; ; ) {
|
|
595
|
+
const { done, value } = await reader.read();
|
|
596
|
+
if (done) break;
|
|
597
|
+
total += value.byteLength;
|
|
598
|
+
if (total > cap) {
|
|
599
|
+
await reader.cancel();
|
|
600
|
+
await pump;
|
|
601
|
+
const err = new Error(`expands past the ${cap} byte limit`);
|
|
602
|
+
err.tooLarge = true;
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
chunks.push(value);
|
|
606
|
+
}
|
|
607
|
+
await pump;
|
|
608
|
+
if (chunks.length === 1) return chunks[0];
|
|
609
|
+
const out = new Uint8Array(total);
|
|
610
|
+
let at = 0;
|
|
611
|
+
for (const chunk of chunks) {
|
|
612
|
+
out.set(chunk, at);
|
|
613
|
+
at += chunk.byteLength;
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
async function decodeLayoutBytes(bytes, maxBytes) {
|
|
618
|
+
if (!looksGzipped(bytes)) return { ok: true, bytes, gzipped: false, storedSize: null };
|
|
619
|
+
const storedSize = gzipStoredSize(bytes);
|
|
620
|
+
const cap = Number.isFinite(maxBytes) ? maxBytes : Infinity;
|
|
621
|
+
try {
|
|
622
|
+
return { ok: true, bytes: await gunzip(bytes, cap), gzipped: true, storedSize };
|
|
623
|
+
} catch (err) {
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
reason: err && err.tooLarge ? "too-large" : "corrupt",
|
|
627
|
+
storedSize,
|
|
628
|
+
// Reported back rather than left for the caller to remember: it is
|
|
629
|
+
// the one number a "too large" message needs, and the caller that
|
|
630
|
+
// passed it is not always the one wording the failure.
|
|
631
|
+
limit: cap,
|
|
632
|
+
detail: err && err.message ? err.message : String(err)
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
var GZIP_ID1, GZIP_ID2, GZIP_MIN_BYTES;
|
|
637
|
+
var init_layout_bytes = __esm({
|
|
638
|
+
"src/layout-bytes.js"() {
|
|
639
|
+
"use strict";
|
|
640
|
+
GZIP_ID1 = 31;
|
|
641
|
+
GZIP_ID2 = 139;
|
|
642
|
+
GZIP_MIN_BYTES = 18;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// src/engine-source.js
|
|
647
|
+
function loadGdstkFactory() {
|
|
648
|
+
return Promise.resolve(globalThis.createGdstkModule);
|
|
649
|
+
}
|
|
650
|
+
var workerBundle;
|
|
651
|
+
var init_engine_source = __esm({
|
|
652
|
+
"src/engine-source.js"() {
|
|
653
|
+
workerBundle = null;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// src/viewer-shell.html
|
|
658
|
+
var viewer_shell_default;
|
|
659
|
+
var init_viewer_shell = __esm({
|
|
660
|
+
"src/viewer-shell.html"() {
|
|
661
|
+
viewer_shell_default = `<!-- lil-gui mounts its panel here rather than at document.body, so the
|
|
662
|
+
whole control surface lives inside the shadow root. -->
|
|
663
|
+
<div id="guiHost"></div>
|
|
664
|
+
<div id="loadingOverlay" role="status" aria-live="polite">
|
|
665
|
+
<div id="loadingBarTrack"><div id="loadingBarFill"></div></div>
|
|
666
|
+
<div id="loadingPhase">Loading layout...</div>
|
|
667
|
+
<div id="loadingPercent"></div>
|
|
668
|
+
</div>
|
|
669
|
+
<!-- role="alert" rather than a polite live region: this replaces the
|
|
670
|
+
view entirely, so it is the only thing left to read. -->
|
|
671
|
+
<div id="loadError" role="alert"></div>
|
|
672
|
+
<div id="reloadProgress" class="hidden" role="status" aria-live="polite">
|
|
673
|
+
<div id="reloadBarTrack"><div id="reloadBarFill"></div></div>
|
|
674
|
+
<div id="reloadLabel">Reloading...</div>
|
|
675
|
+
</div>
|
|
676
|
+
<div id="staleBanner" class="hidden" role="status">
|
|
677
|
+
<span id="staleText">A newer version of this file is on disk.</span>
|
|
678
|
+
<button id="staleReloadBtn" type="button">Reload</button>
|
|
679
|
+
<button id="staleAlwaysBtn" type="button" class="secondary"
|
|
680
|
+
title="Reload now, and automatically whenever this file changes from now on">Always</button>
|
|
681
|
+
<button id="staleDismiss" type="button" class="icon-only"
|
|
682
|
+
aria-label="Dismiss until the next change"
|
|
683
|
+
title="Dismiss until the next change">\u2715</button>
|
|
684
|
+
</div>
|
|
685
|
+
<!-- tabindex so the keyboard handlers in viewer.js are reachable without
|
|
686
|
+
a pointer; labelled because a bare <canvas> announces nothing. -->
|
|
687
|
+
<canvas id="glCanvas" tabindex="0" role="img"
|
|
688
|
+
aria-label="Layout view. Drag to pan, scroll to zoom."></canvas>
|
|
689
|
+
|
|
690
|
+
<!-- Cell hierarchy: rows are built by renderHierarchy in viewer.js from
|
|
691
|
+
the tree parseGdsToLayers hands back, lazily as branches open. -->
|
|
692
|
+
<div id="hierarchyPanel" class="hidden">
|
|
693
|
+
<div id="hierarchyHeader">
|
|
694
|
+
<span id="hierarchyTitle">Hierarchy</span>
|
|
695
|
+
<span id="hierarchyCount"></span>
|
|
696
|
+
<button id="hierarchyHide" type="button" class="icon-only"
|
|
697
|
+
aria-label="Hide the hierarchy tree"
|
|
698
|
+
title="Hide the hierarchy tree (H)">\u2715</button>
|
|
699
|
+
</div>
|
|
700
|
+
<!-- Find (see the search block in viewer.js): one box over two
|
|
701
|
+
haystacks, the design's cell names and its own TEXT labels.
|
|
702
|
+
Results take the tree's place while a query is up.
|
|
703
|
+
|
|
704
|
+
Collapsed by default, and folded away behind one row rather than
|
|
705
|
+
sitting open above the tree: the panel is here to browse the
|
|
706
|
+
hierarchy, and two more rows of chrome over it are two fewer rows
|
|
707
|
+
of the thing being browsed. "/" opens it and lands in the box, so
|
|
708
|
+
the fold costs nothing to whoever is actually searching. -->
|
|
709
|
+
<div id="hierarchyFind">
|
|
710
|
+
<button id="hierarchyFindToggle" type="button"
|
|
711
|
+
aria-expanded="false" aria-controls="hierarchySearch"
|
|
712
|
+
title="Find a cell by name, or a label by its text (/)">
|
|
713
|
+
<span id="hierarchyFindTwisty">\u25B8</span>
|
|
714
|
+
<span id="hierarchyFindLabel">Find</span>
|
|
715
|
+
<span id="hierarchySearchCount"></span>
|
|
716
|
+
</button>
|
|
717
|
+
<div id="hierarchySearch" class="hidden">
|
|
718
|
+
<input id="hierarchySearchInput" type="text" placeholder="cell name"
|
|
719
|
+
spellcheck="false" autocomplete="off"
|
|
720
|
+
title="Find a cell by name, or a label by its text (/ focuses this box)">
|
|
721
|
+
<div id="hierarchyScope">
|
|
722
|
+
<button id="hierarchyScopeCells" type="button" aria-pressed="true"
|
|
723
|
+
title="Search the design's cell names">Cells</button>
|
|
724
|
+
<button id="hierarchyScopeLabels" type="button" aria-pressed="false"
|
|
725
|
+
title="Search the layout's own TEXT labels">Labels</button>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
<div id="hierarchyTree" role="tree" aria-label="Cell hierarchy"></div>
|
|
730
|
+
<div id="hierarchyResults" class="hidden" role="listbox" aria-label="Search results"></div>
|
|
731
|
+
</div>
|
|
732
|
+
<button id="hierarchyShowBtn" type="button" class="hidden"
|
|
733
|
+
aria-expanded="false" aria-controls="hierarchyPanel"
|
|
734
|
+
title="Show the design's cell hierarchy (H)">Hierarchy</button>
|
|
735
|
+
|
|
736
|
+
<div id="debugPanel" class="hidden">
|
|
737
|
+
<div id="debugPanelHeader">
|
|
738
|
+
<span>Debug tools</span>
|
|
739
|
+
<button id="debugCopyBtn" type="button">Copy log</button>
|
|
740
|
+
</div>
|
|
741
|
+
<div id="debugBody">
|
|
742
|
+
<!-- Engine readout, written from renderer.cpp (set_inner_html("ui", ...)). -->
|
|
743
|
+
<div id="ui"></div>
|
|
744
|
+
<div id="debugLog"></div>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
<button id="debugToggleBtn" type="button"
|
|
748
|
+
aria-expanded="false" aria-controls="debugPanel">Debug tools</button>
|
|
749
|
+
|
|
750
|
+
<div id="viewReadout">
|
|
751
|
+
<div id="copyToast" class="hidden" role="status" aria-live="polite"></div>
|
|
752
|
+
<div id="scaleContainer">
|
|
753
|
+
<div id="scaleLabel">0 \u03BCm</div>
|
|
754
|
+
<div id="scaleBar" style="width: 100px;"></div>
|
|
755
|
+
</div>
|
|
756
|
+
<div id="coordReadout" class="hidden"></div>
|
|
757
|
+
</div>
|
|
758
|
+
|
|
759
|
+
<div id="measureLabels"></div>
|
|
760
|
+
|
|
761
|
+
<div id="canvasMenu" class="hidden" role="menu">
|
|
762
|
+
<button id="canvasMenuCopy" type="button" role="menuitem">
|
|
763
|
+
<span>Copy coordinate</span><span class="menu-value"></span>
|
|
764
|
+
</button>
|
|
765
|
+
</div>
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/viewer.css
|
|
771
|
+
var viewer_default;
|
|
772
|
+
var init_viewer = __esm({
|
|
773
|
+
"src/viewer.css"() {
|
|
774
|
+
viewer_default = ` /* ---- Theme tokens ----
|
|
775
|
+
Every color in this file comes from here, because the viewer follows
|
|
776
|
+
the embedder's light/dark theme and each one needs a counterpart.
|
|
777
|
+
Dark is the default: it's what the page paints as before any script
|
|
778
|
+
runs, and it's the far more common editor theme.
|
|
779
|
+
|
|
780
|
+
The light values ride on a single .theme-light class, which viewer.js's
|
|
781
|
+
theme block sets: it asks the host which theme is current and falls
|
|
782
|
+
back to the OS preference when the host has no opinion (see
|
|
783
|
+
applyTheme there). An embedder can also just set the class itself.
|
|
784
|
+
The canvas is not covered by any of this -- what the renderer draws is
|
|
785
|
+
themed in renderer.cpp (see setTheme). */
|
|
786
|
+
:host {
|
|
787
|
+
--bg: #0c0c0d;
|
|
788
|
+
--fg: #ffffff; /* ink over the canvas: scale bar, ruler readout */
|
|
789
|
+
--fg-dim: #a5a5a5;
|
|
790
|
+
--fg-faint: #808080;
|
|
791
|
+
--surface: #2a2a2c; /* small chrome fills: bar tracks, buttons */
|
|
792
|
+
--surface-fg: #dddddd;
|
|
793
|
+
--scrim: rgba(0, 0, 0, 0.88); /* debug panel, over the canvas */
|
|
794
|
+
--label-scrim: rgba(0, 0, 0, 0.75); /* ruler readout, over geometry */
|
|
795
|
+
--chip-scrim: rgba(12, 12, 13, 0.85); /* reload chip, over geometry */
|
|
796
|
+
--border: #333333;
|
|
797
|
+
--border-strong: #555555;
|
|
798
|
+
--accent: #4a9eff;
|
|
799
|
+
--accent-fg: #06121f; /* text on an --accent fill */
|
|
800
|
+
--accent-text: #cfe6ff; /* --accent as text, on --chip-scrim */
|
|
801
|
+
--accent-border: #2a3f57;
|
|
802
|
+
--danger: #ff4d4d;
|
|
803
|
+
--danger-dim: #ff9a9a;
|
|
804
|
+
--select-bg: #5a1f1f; /* selected marker's row */
|
|
805
|
+
--select-fg: #ffd9d9;
|
|
806
|
+
--error-bg: #2a1416;
|
|
807
|
+
--error-border: #7a2b2b;
|
|
808
|
+
--error-fg: #ffd9d9;
|
|
809
|
+
--warn-bg: #4a3410; /* "newer version on disk" banner */
|
|
810
|
+
--warn-border: #7a5a1c;
|
|
811
|
+
--warn-border-strong: #a37a26;
|
|
812
|
+
--warn-fg: #ffe9c2;
|
|
813
|
+
--warn-fg-dim: #e5cfa4;
|
|
814
|
+
--warn-btn-fg: #fff6e5;
|
|
815
|
+
--log-fg: #7fff7f;
|
|
816
|
+
--log-err: #ff7f7f;
|
|
817
|
+
/* lil-gui's own palette (mapped onto its variable names below). */
|
|
818
|
+
--panel-bg: #1f1f1f;
|
|
819
|
+
--panel-fg: #ebebeb;
|
|
820
|
+
--panel-title-bg: #111111;
|
|
821
|
+
--panel-widget: #424242;
|
|
822
|
+
--panel-widget-hover: #4f4f4f;
|
|
823
|
+
--panel-widget-focus: #595959;
|
|
824
|
+
}
|
|
825
|
+
:host(.theme-light) {
|
|
826
|
+
--bg: #fafafb;
|
|
827
|
+
--fg: #17171a;
|
|
828
|
+
--fg-dim: #56565c;
|
|
829
|
+
--fg-faint: #6b6b73; /* matches the dark side's contrast, not its lightness */
|
|
830
|
+
--surface: #e4e4e8;
|
|
831
|
+
--surface-fg: #2c2c2e;
|
|
832
|
+
--scrim: rgba(250, 250, 251, 0.93);
|
|
833
|
+
--label-scrim: rgba(255, 255, 255, 0.85);
|
|
834
|
+
--chip-scrim: rgba(255, 255, 255, 0.9);
|
|
835
|
+
--border: #d2d2d8;
|
|
836
|
+
--border-strong: #b4b4bc;
|
|
837
|
+
--accent: #1f6fd0;
|
|
838
|
+
--accent-fg: #ffffff;
|
|
839
|
+
--accent-text: #14538f;
|
|
840
|
+
--accent-border: #a8c8e8;
|
|
841
|
+
--danger: #b3261e;
|
|
842
|
+
--danger-dim: #d0574e;
|
|
843
|
+
--select-bg: #ffdcdc;
|
|
844
|
+
--select-fg: #7a1f1f;
|
|
845
|
+
--error-bg: #fdeceb;
|
|
846
|
+
--error-border: #dda3a0;
|
|
847
|
+
--error-fg: #7d201c;
|
|
848
|
+
--warn-bg: #fdf3d8;
|
|
849
|
+
--warn-border: #e0c07a;
|
|
850
|
+
--warn-border-strong: #c9a153;
|
|
851
|
+
--warn-fg: #5c431a;
|
|
852
|
+
--warn-fg-dim: #7d6231;
|
|
853
|
+
--warn-btn-fg: #3f2d0e;
|
|
854
|
+
--log-fg: #157f2a;
|
|
855
|
+
--log-err: #b3261e;
|
|
856
|
+
--panel-bg: #f4f4f6;
|
|
857
|
+
--panel-fg: #2c2c2e;
|
|
858
|
+
--panel-title-bg: #e4e4e8;
|
|
859
|
+
--panel-widget: #d8d8de;
|
|
860
|
+
--panel-widget-hover: #c8c8d0;
|
|
861
|
+
--panel-widget-focus: #babac2;
|
|
862
|
+
}
|
|
863
|
+
/* lil-gui's palette is its own set of custom properties, so the panel
|
|
864
|
+
themes by re-pointing those at the --panel-* tokens above rather
|
|
865
|
+
than by restyling any of its rows. Mapped here on .lil-gui rather
|
|
866
|
+
than on :root because lil-gui's own .lil-gui rule sets them, and a
|
|
867
|
+
rule matching the element itself beats an inherited value. (Source
|
|
868
|
+
order settles the tie: lil-gui inserts its stylesheet *before* the
|
|
869
|
+
first <style> already in <head> -- i.e. this one -- specifically so
|
|
870
|
+
page styles outrank its defaults.) */
|
|
871
|
+
.lil-gui {
|
|
872
|
+
--background-color: var(--panel-bg);
|
|
873
|
+
--text-color: var(--panel-fg);
|
|
874
|
+
--title-background-color: var(--panel-title-bg);
|
|
875
|
+
--title-text-color: var(--panel-fg);
|
|
876
|
+
--widget-color: var(--panel-widget);
|
|
877
|
+
--hover-color: var(--panel-widget-hover);
|
|
878
|
+
--focus-color: var(--panel-widget-focus);
|
|
879
|
+
--number-color: var(--accent);
|
|
880
|
+
}
|
|
881
|
+
/* A custom element is display:inline by default and would collapse to
|
|
882
|
+
zero height. The host owns the box; everything inside positions
|
|
883
|
+
against it rather than against the page. */
|
|
884
|
+
:host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden;
|
|
885
|
+
background: var(--bg); user-select: none; color: var(--fg); }
|
|
886
|
+
canvas { width: 100%; height: 100%; display: block; }
|
|
887
|
+
/* The engine readout (polygon/label counts + the per-frame #renderStats
|
|
888
|
+
span, both written from renderer.cpp). It lives inside #debugPanel
|
|
889
|
+
rather than floating over the canvas on its own: it's debug-only
|
|
890
|
+
either way, and two separate debug surfaces meant the same command
|
|
891
|
+
lit up two different corners of the screen. */
|
|
892
|
+
#ui { width: 320px; flex: none; overflow-y: auto; padding: 6px 8px; border-right: 1px solid var(--border);
|
|
893
|
+
color: var(--fg-dim); font-family: monospace; font-size: 11px; line-height: 1.5; user-select: text; }
|
|
894
|
+
/* The debug entry point is hidden until the "GDSLens: Toggle Debug
|
|
895
|
+
Tools" command adds .debug to <body>. */
|
|
896
|
+
#debugToggleBtn { display: none; }
|
|
897
|
+
:host(.debug) #debugToggleBtn { display: block; }
|
|
898
|
+
/* Bottom-right stack of view readouts: the scale bar, with the
|
|
899
|
+
pointer's world coordinate under it. One anchored column rather than
|
|
900
|
+
two separately positioned boxes, so the coordinate row -- whose width
|
|
901
|
+
varies with the number in it -- can't drift into or away from the bar
|
|
902
|
+
above it. */
|
|
903
|
+
#viewReadout { position: absolute; bottom: 20px; right: 20px; z-index: 10; pointer-events: none;
|
|
904
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
|
|
905
|
+
#scaleContainer { font-family: monospace; font-size: 11px; color: var(--fg); text-align: center; }
|
|
906
|
+
/* Pointer coordinate, rewritten by update_coord_readout (renderer.cpp)
|
|
907
|
+
on every move and every camera change; .hidden while the pointer is
|
|
908
|
+
off the canvas. Unlike the scale bar it's a run of text over
|
|
909
|
+
arbitrary geometry, so it carries the same scrim the ruler label
|
|
910
|
+
does rather than relying on an outline. */
|
|
911
|
+
#coordReadout { font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim);
|
|
912
|
+
border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 6px;
|
|
913
|
+
white-space: nowrap; }
|
|
914
|
+
#coordReadout.hidden { display: none; }
|
|
915
|
+
/* Confirmation that the canvas menu's Copy coordinate reached the
|
|
916
|
+
clipboard (see the contextmenu handler in viewer.js). Same scrim as
|
|
917
|
+
the readout it answers for. It sits first in the column on purpose:
|
|
918
|
+
#viewReadout is anchored by its bottom edge, so a line appearing at
|
|
919
|
+
the top leaves the scale bar and the coordinate exactly where they
|
|
920
|
+
were -- one added at the bottom would shove both up mid-read. */
|
|
921
|
+
#copyToast { font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim);
|
|
922
|
+
border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 6px;
|
|
923
|
+
white-space: nowrap; }
|
|
924
|
+
#copyToast.hidden { display: none; }
|
|
925
|
+
/* Right-click menu on the canvas (see the contextmenu handler in
|
|
926
|
+
viewer.js). VS Code's own webview menu is suppressed for it -- the
|
|
927
|
+
preload bails out of showing one when the page has already called
|
|
928
|
+
preventDefault -- so this has to look like a menu on its own. It
|
|
929
|
+
borrows the panel's palette rather than the readouts' scrim: it sits
|
|
930
|
+
over the layout but it's chrome, not an annotation of what's under
|
|
931
|
+
it. Above the debug panel (z 41), since a menu is the most recent
|
|
932
|
+
thing the user asked for. */
|
|
933
|
+
#canvasMenu { position: absolute; z-index: 50; min-width: 190px; padding: 4px;
|
|
934
|
+
background: var(--panel-bg); color: var(--panel-fg);
|
|
935
|
+
border: 1px solid var(--border-strong); border-radius: 4px;
|
|
936
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); }
|
|
937
|
+
#canvasMenu.hidden { display: none; }
|
|
938
|
+
#canvasMenu button { display: flex; align-items: baseline; gap: 14px; width: 100%;
|
|
939
|
+
background: none; border: 0; border-radius: 3px; padding: 4px 8px;
|
|
940
|
+
font-family: inherit; font-size: 12px; color: inherit;
|
|
941
|
+
text-align: left; white-space: nowrap; cursor: pointer; }
|
|
942
|
+
#canvasMenu button:hover, #canvasMenu button:focus { background: var(--accent); color: var(--accent-fg); outline: none; }
|
|
943
|
+
/* The coordinate itself, shown in the readout's monospace so it reads as
|
|
944
|
+
the value being copied rather than as a menu shortcut. */
|
|
945
|
+
#canvasMenu .menu-value { margin-left: auto; font-family: monospace; font-size: 11px; color: var(--fg-dim); }
|
|
946
|
+
#canvasMenu button:hover .menu-value, #canvasMenu button:focus .menu-value { color: inherit; }
|
|
947
|
+
/* The bar is drawn in the ink color over an outline of the background,
|
|
948
|
+
so it stays readable whichever of the two it happens to sit on top of
|
|
949
|
+
(it floats over the geometry, not over a panel). */
|
|
950
|
+
#scaleBar { height: 4px; background: var(--fg); border: 1px solid var(--bg); margin-top: 4px; }
|
|
951
|
+
/* Ruler readouts: one .measure-label per ruler, created and
|
|
952
|
+
repositioned to its measurement's midpoint on every redraw by
|
|
953
|
+
update_measure_labels (renderer.cpp). The translate lifts each one
|
|
954
|
+
just above its line so it doesn't cover what's being measured.
|
|
955
|
+
#measureLabels is a plain (unpositioned) container, so the labels
|
|
956
|
+
inside it still position against the page the way the single one
|
|
957
|
+
used to. */
|
|
958
|
+
#measureLabels .measure-label { position: absolute; transform: translate(-50%, -130%); font-family: monospace; font-size: 11px; color: var(--fg); background: var(--label-scrim); border: 1px solid var(--border-strong); border-radius: 3px; padding: 3px 7px; text-align: center; line-height: 1.4; white-space: nowrap; z-index: 15; pointer-events: none; }
|
|
959
|
+
/* lil-gui floats its own panel only when it appends to document.body,
|
|
960
|
+
by adding .lil-auto-place (position:fixed, right:15px, z 1001). We
|
|
961
|
+
pass a container instead so the panel lives inside the shadow root,
|
|
962
|
+
which skips that class entirely -- so without this the panel is a
|
|
963
|
+
plain block in normal flow, spanning the full width and pushing the
|
|
964
|
+
canvas down. Positioning it here also fixes two things lil-gui's own
|
|
965
|
+
rule got wrong for a component: absolute rather than fixed, so the
|
|
966
|
+
panel stays inside the component's box instead of pinning itself to
|
|
967
|
+
the viewport when embedded in a page; and z-index below the loading
|
|
968
|
+
overlay (30) and debug panel (40) rather than lil-gui's 1001. */
|
|
969
|
+
#guiHost { position: absolute; top: 0; right: 15px; max-height: 100%; z-index: 20; display: flex; }
|
|
970
|
+
#guiHost > .lil-gui.lil-root { max-height: 100%; }
|
|
971
|
+
/* Checkbox rows (layer list + top-level toggles): lil-gui gives the
|
|
972
|
+
label a 45% min-width and no overflow handling, so a long name like
|
|
973
|
+
"250/74 \u2013 SIN_WG_S" would push into the checkbox column. Widen the
|
|
974
|
+
label (the checkbox needs nowhere near half the row) and cut it
|
|
975
|
+
character-level with an ellipsis. */
|
|
976
|
+
.lil-gui .lil-controller.lil-boolean > .lil-name { min-width: 78%; width: 78%; overflow: hidden; text-overflow: ellipsis; }
|
|
977
|
+
/* ---- Layer rows (see addLayerRow in viewer.js) ----
|
|
978
|
+
A layer row carries two more things than the plain boolean above: its
|
|
979
|
+
shape count and a solo button, both appended after lil-gui's own
|
|
980
|
+
name/widget pair. .lil-controller is already a flex row, so they only
|
|
981
|
+
need flex:none to sit at the right edge; the name gives up the width
|
|
982
|
+
they take (this rule follows the 78% one above deliberately -- same
|
|
983
|
+
specificity, so source order decides). */
|
|
984
|
+
.lil-gui .lil-controller.layer-row > .lil-name { min-width: 58%; width: 58%; }
|
|
985
|
+
.lil-gui .lil-controller.layer-row .layer-count { flex: none; padding-left: 4px;
|
|
986
|
+
color: var(--fg-faint); font-size: 10px; }
|
|
987
|
+
.lil-gui .lil-controller.layer-row .layer-solo { flex: none; width: 16px; text-align: center;
|
|
988
|
+
margin-left: 4px; border-radius: 2px;
|
|
989
|
+
color: var(--fg-faint); font-size: 10px; cursor: pointer; }
|
|
990
|
+
.lil-gui .lil-controller.layer-row .layer-solo:hover { color: var(--fg); background: var(--panel-widget-hover); }
|
|
991
|
+
/* The soloed layer's own row, so it's clear which of the hidden
|
|
992
|
+
hundred is the one still on screen. */
|
|
993
|
+
.lil-gui .lil-controller.layer-row.layer-soloed .layer-solo { color: var(--accent-fg); background: var(--accent); }
|
|
994
|
+
/* Filter box at the top of the Layers folder. lil-gui styles the inputs
|
|
995
|
+
belonging to its own controllers; this one is hand-built, so it
|
|
996
|
+
borrows the same widget fill. */
|
|
997
|
+
.lil-gui .lil-controller.layer-filter-row input { width: 100%; box-sizing: border-box;
|
|
998
|
+
font-family: inherit; font-size: var(--input-font-size);
|
|
999
|
+
background: var(--panel-widget); color: var(--panel-fg);
|
|
1000
|
+
border: none; border-radius: var(--widget-border-radius);
|
|
1001
|
+
height: var(--widget-height); padding: 0 6px; }
|
|
1002
|
+
.lil-gui .lil-controller.layer-filter-row input:focus { background: var(--panel-widget-focus); outline: none; }
|
|
1003
|
+
/* Interaction-mode row (Pan | Measure), hand-built in viewer.js since
|
|
1004
|
+
lil-gui has no segmented-control type. Because the row carries
|
|
1005
|
+
lil-gui's own classes, its button theming (widget color, radius,
|
|
1006
|
+
height, :hover/:active) already applies -- all that's left is
|
|
1007
|
+
splitting the widget in two and filling in the selected mode. The
|
|
1008
|
+
name column is narrower than lil-gui's 45% default to leave both
|
|
1009
|
+
buttons a readable width. */
|
|
1010
|
+
.lil-gui .lil-controller.mode-row .lil-name { min-width: 30%; }
|
|
1011
|
+
.lil-gui .lil-controller.mode-row .mode-widget { gap: 4px; }
|
|
1012
|
+
.lil-gui .lil-controller.mode-row .mode-widget button { flex: 1; }
|
|
1013
|
+
/* :hover/:active spelled out too -- lil-gui's own button:hover rule
|
|
1014
|
+
outranks a bare .mode-active, which would grey out the selected
|
|
1015
|
+
mode as soon as the pointer crossed it. */
|
|
1016
|
+
.lil-gui .lil-controller.mode-row .mode-widget button.mode-active,
|
|
1017
|
+
.lil-gui .lil-controller.mode-row .mode-widget button.mode-active:hover,
|
|
1018
|
+
.lil-gui .lil-controller.mode-row .mode-widget button.mode-active:active { background: var(--accent); color: var(--accent-fg); font-weight: bold; }
|
|
1019
|
+
/* Loaded-.lyp chip: the row shows the filename with an \u2715 on the right
|
|
1020
|
+
(injected by setLypChip in viewer.js) to unload it. */
|
|
1021
|
+
.lil-gui .lil-controller.lyp-loaded { position: relative; }
|
|
1022
|
+
.lil-gui .lil-controller.lyp-loaded .lil-name { padding-right: 22px; }
|
|
1023
|
+
.lil-gui .lil-controller.lyp-loaded .lyp-unload { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); color: var(--danger-dim); cursor: pointer; font-size: 12px; z-index: 5; }
|
|
1024
|
+
.lil-gui .lil-controller.lyp-loaded .lyp-unload:hover { color: var(--danger); }
|
|
1025
|
+
/* Saved-view rows (see renderNamedViews in viewer.js): a full-width
|
|
1026
|
+
button that restores the view, with an \u2715 on the right that deletes
|
|
1027
|
+
it -- the same shape as the loaded-file chips above, and the same
|
|
1028
|
+
reason for spelling it out separately (lil-gui has no row that
|
|
1029
|
+
carries a second action). */
|
|
1030
|
+
.lil-gui .lil-controller.view-row { position: relative; }
|
|
1031
|
+
.lil-gui .lil-controller.view-row .lil-name { padding-right: 22px; }
|
|
1032
|
+
.lil-gui .lil-controller.view-row .view-delete { position: absolute; right: 8px; top: 50%;
|
|
1033
|
+
transform: translateY(-50%); color: var(--danger-dim);
|
|
1034
|
+
cursor: pointer; font-size: 12px; z-index: 5; }
|
|
1035
|
+
.lil-gui .lil-controller.view-row .view-delete:hover { color: var(--danger); }
|
|
1036
|
+
/* Marker browser (see renderMarkerBrowser in viewer.js): the selected
|
|
1037
|
+
violation's row, and the disabled "\u2026 N more" overflow row. The row
|
|
1038
|
+
is a full-width <button> in lil-gui, so tint the button itself
|
|
1039
|
+
(!important also beats lil-gui's :hover/:active backgrounds). */
|
|
1040
|
+
.lil-gui .lil-controller.marker-selected button { background: var(--select-bg) !important; }
|
|
1041
|
+
.lil-gui .lil-controller.marker-selected .lil-name { color: var(--select-fg); }
|
|
1042
|
+
.lil-gui .lil-controller.marker-more-row { opacity: 0.55; pointer-events: none; }
|
|
1043
|
+
/* ---- Hierarchy tree (see renderHierarchy in viewer.js) ----
|
|
1044
|
+
The design's cell tree, down the left edge. Like the lil-gui panel
|
|
1045
|
+
on the right it floats over the canvas rather than taking width
|
|
1046
|
+
from it: the canvas stays the full window, so nothing about the
|
|
1047
|
+
renderer's sizing, hit-testing or camera has to know this panel
|
|
1048
|
+
exists. It outranks both progress UIs (z 25/26) so a reload doesn't
|
|
1049
|
+
draw its hairline bar across the tree, but stays under the
|
|
1050
|
+
full-screen loading overlay (z 30) and the debug panel (z 40). */
|
|
1051
|
+
#hierarchyPanel { position: absolute; top: 0; left: 0; bottom: 0; width: 260px; z-index: 27;
|
|
1052
|
+
box-sizing: border-box; display: flex; flex-direction: column;
|
|
1053
|
+
background: var(--scrim); border-right: 1px solid var(--border);
|
|
1054
|
+
font-family: monospace; font-size: 11px; color: var(--surface-fg); }
|
|
1055
|
+
#hierarchyPanel.hidden { display: none; }
|
|
1056
|
+
#hierarchyHeader { display: flex; align-items: center; gap: 8px; flex: none; padding: 6px 8px;
|
|
1057
|
+
color: var(--fg-dim); border-bottom: 1px solid var(--border); }
|
|
1058
|
+
#hierarchyTitle { flex: 1; }
|
|
1059
|
+
#hierarchyCount { color: var(--fg-faint); }
|
|
1060
|
+
/* Same icon-only treatment as #staleDismiss: a real button, styled
|
|
1061
|
+
back down to the bare glyph it was before. */
|
|
1062
|
+
#hierarchyHide { background: none; border: 0; font: inherit; color: inherit;
|
|
1063
|
+
cursor: pointer; opacity: 0.7; padding: 0 2px; }
|
|
1064
|
+
#hierarchyHide:hover { opacity: 1; }
|
|
1065
|
+
#hierarchyTree { flex: 1; overflow: auto; padding: 4px 0 8px; }
|
|
1066
|
+
/* One row per node. The whole row frames its cell on click, so it's
|
|
1067
|
+
the click target; only the left indent varies with depth (set
|
|
1068
|
+
inline, since it's per-row). */
|
|
1069
|
+
.hier-row { display: flex; align-items: center; gap: 4px; padding: 2px 8px 2px 0;
|
|
1070
|
+
white-space: nowrap; cursor: pointer; }
|
|
1071
|
+
.hier-row:hover { background: var(--surface); }
|
|
1072
|
+
.hier-row.hier-selected, .hier-row.hier-selected:hover { background: var(--accent); color: var(--accent-fg); }
|
|
1073
|
+
/* A cell with no geometry anywhere under it -- there's nothing to
|
|
1074
|
+
frame, so the row only selects. */
|
|
1075
|
+
.hier-row.hier-boxless { opacity: 0.55; cursor: default; }
|
|
1076
|
+
.hier-twisty { flex: none; width: 12px; text-align: center; color: var(--fg-faint); }
|
|
1077
|
+
/* flex + min-width:0 is what lets a long cell name actually truncate:
|
|
1078
|
+
a flex item's default min-width is its content, and a cell name is
|
|
1079
|
+
one unbreakable word, so without this the row grows and the panel
|
|
1080
|
+
scrolls sideways instead. Taking the leftover width also pins the
|
|
1081
|
+
placement count to the right edge. */
|
|
1082
|
+
.hier-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1083
|
+
/* Placement count ("\xD764"), shown only when the cell is placed more
|
|
1084
|
+
than once by its parent. */
|
|
1085
|
+
.hier-count { flex: none; color: var(--fg-faint); }
|
|
1086
|
+
.hier-row.hier-selected .hier-twisty, .hier-row.hier-selected .hier-count { color: var(--accent-fg); }
|
|
1087
|
+
.hier-children.hidden { display: none; }
|
|
1088
|
+
/* ---- Find (see the search block in viewer.js) ----
|
|
1089
|
+
Sits between the header and the tree: it's a way into the same
|
|
1090
|
+
panel, not a second panel. One row while it's closed, which is how
|
|
1091
|
+
it starts -- see the markup below for why -- opening to the box and
|
|
1092
|
+
the scope pair. The pair gets its own row rather than sharing one
|
|
1093
|
+
with the input: at 260px a field narrowed by two buttons is too
|
|
1094
|
+
small to read a cell name in. */
|
|
1095
|
+
#hierarchyFind { flex: none; border-bottom: 1px solid var(--border); }
|
|
1096
|
+
/* The closed row. Styled like a tree row rather than like the panel
|
|
1097
|
+
header, because that's what it is: something you click to open. */
|
|
1098
|
+
#hierarchyFindToggle { display: flex; align-items: center; gap: 4px; padding: 3px 8px;
|
|
1099
|
+
color: var(--fg-dim); cursor: pointer; white-space: nowrap;
|
|
1100
|
+
/* A <button> now, for keyboard and screen readers: reset the
|
|
1101
|
+
chrome and let it fill the row as the div did. */
|
|
1102
|
+
width: 100%; background: none; border: 0; font: inherit;
|
|
1103
|
+
text-align: left; }
|
|
1104
|
+
#hierarchyFindToggle:hover { background: var(--surface); color: var(--fg); }
|
|
1105
|
+
#hierarchyFindTwisty { flex: none; width: 12px; text-align: center; color: var(--fg-faint); }
|
|
1106
|
+
#hierarchyFindLabel { flex: none; }
|
|
1107
|
+
#hierarchySearch { padding: 0 8px 6px; display: flex; flex-direction: column; gap: 5px; }
|
|
1108
|
+
#hierarchySearch.hidden { display: none; }
|
|
1109
|
+
/* user-select is off for the page as a whole (dragging the canvas would
|
|
1110
|
+
otherwise select the chrome over it), and that inherits into a text
|
|
1111
|
+
field -- where it costs you selecting what you typed, and makes the
|
|
1112
|
+
select() behind the "/" shortcut a no-op. */
|
|
1113
|
+
#hierarchySearchInput { width: 100%; box-sizing: border-box; font-family: inherit; font-size: 11px;
|
|
1114
|
+
background: var(--surface); color: var(--surface-fg);
|
|
1115
|
+
border: 1px solid var(--border-strong); border-radius: 3px;
|
|
1116
|
+
padding: 3px 6px; user-select: text; }
|
|
1117
|
+
#hierarchySearchInput:focus { outline: none; border-color: var(--accent); }
|
|
1118
|
+
#hierarchyScope { display: flex; gap: 4px; }
|
|
1119
|
+
#hierarchyScope button { font-family: inherit; font-size: 10px; background: var(--surface);
|
|
1120
|
+
color: var(--fg-dim); border: 1px solid var(--border-strong);
|
|
1121
|
+
border-radius: 3px; padding: 1px 8px; cursor: pointer; }
|
|
1122
|
+
#hierarchyScope button:hover { color: var(--fg); }
|
|
1123
|
+
#hierarchyScope button.scope-active, #hierarchyScope button.scope-active:hover {
|
|
1124
|
+
background: var(--accent); color: var(--accent-fg); border-color: var(--accent); font-weight: bold; }
|
|
1125
|
+
/* A design too large for a cell tree has no cell names on this side at
|
|
1126
|
+
all (see the omitted case in renderHierarchy), so Cells has nothing
|
|
1127
|
+
to search -- the button says so rather than returning nothing. */
|
|
1128
|
+
#hierarchyScope button:disabled { opacity: 0.4; cursor: default; }
|
|
1129
|
+
/* On the toggle row rather than beside the scope pair: it's the result
|
|
1130
|
+
of the search, so it belongs next to the thing you'd click to get
|
|
1131
|
+
back to it. Empty whenever the fold is closed, since closing clears
|
|
1132
|
+
the query. */
|
|
1133
|
+
#hierarchySearchCount { flex: 1; text-align: right; color: var(--fg-faint); overflow: hidden;
|
|
1134
|
+
text-overflow: ellipsis; }
|
|
1135
|
+
/* Results replace the tree rather than stacking under it, so the panel
|
|
1136
|
+
shows one list at a time; clearing the box brings the tree back with
|
|
1137
|
+
its open branches untouched. */
|
|
1138
|
+
#hierarchyResults { flex: 1; overflow: auto; padding: 4px 0 8px; }
|
|
1139
|
+
#hierarchyResults.hidden, #hierarchyTree.hidden { display: none; }
|
|
1140
|
+
/* A result row is the tree's row with the indent and twisty dropped and
|
|
1141
|
+
a right-hand detail added (a cell's shape count, a label's layer). */
|
|
1142
|
+
.find-row { display: flex; align-items: baseline; gap: 6px; padding: 2px 8px;
|
|
1143
|
+
white-space: nowrap; cursor: pointer; }
|
|
1144
|
+
.find-row:hover { background: var(--surface); }
|
|
1145
|
+
.find-row.find-active, .find-row.find-active:hover { background: var(--accent); color: var(--accent-fg); }
|
|
1146
|
+
.find-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1147
|
+
.find-meta { flex: none; color: var(--fg-faint); font-size: 10px; }
|
|
1148
|
+
.find-row.find-active .find-meta { color: var(--accent-fg); }
|
|
1149
|
+
/* A cell no top cell reaches (a reference cycle, or one the tree's own
|
|
1150
|
+
caps left out) has no path to open down to, so its row only says so. */
|
|
1151
|
+
.find-row.find-unreachable { opacity: 0.55; cursor: default; }
|
|
1152
|
+
#hierarchyResults .find-note { padding: 6px 10px; color: var(--fg-faint); white-space: normal; line-height: 1.5; }
|
|
1153
|
+
/* Stand-in for a tree: no cells to show, or too many to describe. */
|
|
1154
|
+
#hierarchyTree .hier-note { padding: 6px 10px; color: var(--fg-faint); white-space: normal; line-height: 1.5; }
|
|
1155
|
+
/* Reopens the panel after it's been hidden; sits where the panel's own
|
|
1156
|
+
header was. Hidden while the panel is open, and while there's no
|
|
1157
|
+
tree to reopen (viewer.js adds .hidden for that). */
|
|
1158
|
+
#hierarchyShowBtn { position: absolute; top: 8px; left: 8px; z-index: 27; font-family: monospace;
|
|
1159
|
+
font-size: 10px; background: var(--surface); color: var(--fg-dim);
|
|
1160
|
+
border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px;
|
|
1161
|
+
cursor: pointer; opacity: 0.6; }
|
|
1162
|
+
#hierarchyShowBtn:hover { opacity: 1; }
|
|
1163
|
+
#hierarchyShowBtn.hidden, :host(.hierarchy-open) #hierarchyShowBtn { display: none; }
|
|
1164
|
+
/* Everything else anchored to the left edge starts after whichever of
|
|
1165
|
+
the two is on screen -- the panel, or just the button that reopens
|
|
1166
|
+
it (.hierarchy-available). The open rule comes second so it wins
|
|
1167
|
+
when both classes are set. */
|
|
1168
|
+
:host(.hierarchy-available) #staleBanner { left: 88px; }
|
|
1169
|
+
:host(.hierarchy-open) #staleBanner { left: 268px; }
|
|
1170
|
+
:host(.hierarchy-open) #debugToggleBtn { left: 268px; }
|
|
1171
|
+
/* "Newer version on disk" header (see the fileChanged handler in
|
|
1172
|
+
viewer.js). Sits above the lil-gui panel (z 20) but below the
|
|
1173
|
+
loading overlay (z 30), since it's hidden while a load runs
|
|
1174
|
+
anyway. */
|
|
1175
|
+
/* Stops short of the right edge so it doesn't cover the lil-gui panel
|
|
1176
|
+
(260px wide, top-right) that it outranks on z-index. */
|
|
1177
|
+
#staleBanner { position: absolute; top: 0; left: 0; right: 276px; box-sizing: border-box; z-index: 25;
|
|
1178
|
+
display: flex; align-items: center; gap: 12px; padding: 7px 12px;
|
|
1179
|
+
font-family: monospace; font-size: 12px; color: var(--warn-fg);
|
|
1180
|
+
background: var(--warn-bg); border-bottom: 1px solid var(--warn-border); }
|
|
1181
|
+
#staleBanner.hidden { display: none; }
|
|
1182
|
+
#staleText { flex: 1; }
|
|
1183
|
+
#staleBanner button { font-family: monospace; font-size: 11px; background: var(--warn-border); color: var(--warn-btn-fg);
|
|
1184
|
+
border: 1px solid var(--warn-border-strong); border-radius: 3px; padding: 3px 12px; cursor: pointer; }
|
|
1185
|
+
#staleBanner button:hover { background: var(--warn-border-strong); }
|
|
1186
|
+
/* Secondary next to Reload: same action plus a persisted setting, so it
|
|
1187
|
+
reads as the quieter of the two. */
|
|
1188
|
+
#staleBanner button.secondary { background: transparent; border-color: transparent;
|
|
1189
|
+
color: var(--warn-fg-dim); padding: 3px 6px; }
|
|
1190
|
+
#staleBanner button.secondary:hover { background: transparent; color: var(--warn-btn-fg); text-decoration: underline; }
|
|
1191
|
+
/* An icon-only control: a real <button> for keyboard and screen
|
|
1192
|
+
readers, styled back down to bare text. The reset is explicit
|
|
1193
|
+
because #staleBanner button above is more specific than an id on its
|
|
1194
|
+
own and would otherwise give this the Reload button's chrome. */
|
|
1195
|
+
#staleBanner button#staleDismiss { background: none; border: 0; border-radius: 0;
|
|
1196
|
+
font: inherit; color: inherit; cursor: pointer;
|
|
1197
|
+
opacity: 0.7; padding: 0 2px; }
|
|
1198
|
+
#staleBanner button#staleDismiss:hover { background: none; opacity: 1; }
|
|
1199
|
+
#loadingOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; font-family: monospace; color: var(--fg-dim); background: var(--bg); z-index: 30; }
|
|
1200
|
+
#loadingOverlay.hidden { display: none; }
|
|
1201
|
+
#loadingBarTrack { width: 320px; height: 8px; background: var(--surface); border-radius: 4px; overflow: hidden; }
|
|
1202
|
+
#loadingBarFill { width: 0%; height: 100%; background: var(--accent); transition: width 0.15s ease-out; }
|
|
1203
|
+
#loadingPhase { font-size: 12px; }
|
|
1204
|
+
#loadingPercent { font-size: 11px; color: var(--fg-faint); }
|
|
1205
|
+
/* The reload flavour of the same progress (see beginProgress in
|
|
1206
|
+
viewer.js): a hairline bar along the top edge plus a small chip,
|
|
1207
|
+
over a viewport that keeps showing the geometry already loaded.
|
|
1208
|
+
Restoring the camera across a reload is pointless if the view is
|
|
1209
|
+
blacked out while it happens. pointer-events stay off so a drag
|
|
1210
|
+
that starts under the chip still pans the canvas. */
|
|
1211
|
+
#reloadProgress { position: absolute; top: 0; left: 0; right: 0; z-index: 26; pointer-events: none; }
|
|
1212
|
+
#reloadProgress.hidden { display: none; }
|
|
1213
|
+
#reloadBarTrack { height: 2px; background: var(--surface); }
|
|
1214
|
+
#reloadBarFill { width: 0%; height: 100%; background: var(--accent); transition: width 0.15s ease-out; }
|
|
1215
|
+
#reloadLabel { display: inline-block; margin: 6px 0 0 10px; padding: 3px 8px; font-family: monospace;
|
|
1216
|
+
font-size: 11px; color: var(--accent-text); background: var(--chip-scrim);
|
|
1217
|
+
border: 1px solid var(--accent-border); border-radius: 3px; }
|
|
1218
|
+
/* Load failures. This is deliberately NOT part of the debug tools:
|
|
1219
|
+
#ui above is display:none unless the debug command is run, so an
|
|
1220
|
+
error written only there is invisible to a normal user. Empty means
|
|
1221
|
+
no error, so setting/clearing the text is all it takes to show or
|
|
1222
|
+
hide the panel. */
|
|
1223
|
+
#loadError { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); max-width: 520px; z-index: 35; padding: 16px 20px; background: var(--error-bg); border: 1px solid var(--error-border); border-radius: 5px; font-family: monospace; font-size: 12px; line-height: 1.55; color: var(--error-fg); white-space: pre-line; user-select: text; }
|
|
1224
|
+
#loadError:empty { display: none; }
|
|
1225
|
+
#debugPanel { position: absolute; bottom: 0; left: 0; width: 100%; max-height: 240px; z-index: 40; background: var(--scrim); border-top: 1px solid var(--border); display: flex; flex-direction: column; }
|
|
1226
|
+
#debugPanel.hidden { display: none; }
|
|
1227
|
+
#debugPanelHeader { display: flex; align-items: center; justify-content: space-between; padding: 4px 8px; font-family: monospace; font-size: 11px; color: var(--fg-dim); border-bottom: 1px solid var(--border); }
|
|
1228
|
+
#debugPanelHeader button { font-family: monospace; font-size: 10px; background: var(--surface); color: var(--surface-fg); border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px; cursor: pointer; }
|
|
1229
|
+
/* Readout beside the log rather than above it: the panel is capped at
|
|
1230
|
+
240px tall, and stacking them would leave the log a few lines. */
|
|
1231
|
+
#debugBody { display: flex; flex: 1; min-height: 0; }
|
|
1232
|
+
#debugLog { flex: 1; overflow-y: auto; padding: 6px 8px; font-family: monospace; font-size: 10px; color: var(--log-fg); white-space: pre-wrap; word-break: break-all; user-select: text; }
|
|
1233
|
+
#debugLog .err { color: var(--log-err); }
|
|
1234
|
+
#debugToggleBtn { position: absolute; bottom: 8px; left: 8px; z-index: 41; font-family: monospace; font-size: 10px; background: var(--surface); color: var(--fg-dim); border: 1px solid var(--border-strong); border-radius: 3px; padding: 2px 8px; cursor: pointer; opacity: 0.6; }
|
|
1235
|
+
#debugToggleBtn:hover { opacity: 1; }
|
|
1236
|
+
|
|
1237
|
+
/* The progress bars are the only motion in the viewer. Someone who has
|
|
1238
|
+
asked the OS for less of it still needs the bar to show progress --
|
|
1239
|
+
so it still moves, it just stops easing there. */
|
|
1240
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1241
|
+
#loadingBarFill, #reloadBarFill { transition: none; }
|
|
1242
|
+
}
|
|
1243
|
+
`;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// node_modules/lil-gui/dist/lil-gui.esm.js
|
|
1248
|
+
function normalizeColorString(string) {
|
|
1249
|
+
let match, result;
|
|
1250
|
+
if (match = string.match(/(#|0x)?([a-f0-9]{6})/i)) {
|
|
1251
|
+
result = match[2];
|
|
1252
|
+
} else if (match = string.match(/rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\)/)) {
|
|
1253
|
+
result = parseInt(match[1]).toString(16).padStart(2, 0) + parseInt(match[2]).toString(16).padStart(2, 0) + parseInt(match[3]).toString(16).padStart(2, 0);
|
|
1254
|
+
} else if (match = string.match(/^#?([a-f0-9])([a-f0-9])([a-f0-9])$/i)) {
|
|
1255
|
+
result = match[1] + match[1] + match[2] + match[2] + match[3] + match[3];
|
|
1256
|
+
}
|
|
1257
|
+
if (result) {
|
|
1258
|
+
return "#" + result;
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
function getColorFormat(value) {
|
|
1263
|
+
return FORMATS.find((format) => format.match(value));
|
|
1264
|
+
}
|
|
1265
|
+
function _injectStyles(cssContent) {
|
|
1266
|
+
const injected = document.createElement("style");
|
|
1267
|
+
injected.innerHTML = cssContent;
|
|
1268
|
+
const before = document.querySelector("head link[rel=stylesheet], head style");
|
|
1269
|
+
if (before) {
|
|
1270
|
+
document.head.insertBefore(injected, before);
|
|
1271
|
+
} else {
|
|
1272
|
+
document.head.appendChild(injected);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
var Controller, BooleanController, STRING, INT, ARRAY, OBJECT, FORMATS, ColorController, FunctionController, NumberController, OptionController, StringController, stylesheet, stylesInjected, GUI;
|
|
1276
|
+
var init_lil_gui_esm = __esm({
|
|
1277
|
+
"node_modules/lil-gui/dist/lil-gui.esm.js"() {
|
|
1278
|
+
Controller = class _Controller {
|
|
1279
|
+
constructor(parent, object, property, className, elementType = "div") {
|
|
1280
|
+
this.parent = parent;
|
|
1281
|
+
this.object = object;
|
|
1282
|
+
this.property = property;
|
|
1283
|
+
this._disabled = false;
|
|
1284
|
+
this._hidden = false;
|
|
1285
|
+
this.initialValue = this.getValue();
|
|
1286
|
+
this.domElement = document.createElement(elementType);
|
|
1287
|
+
this.domElement.classList.add("lil-controller");
|
|
1288
|
+
this.domElement.classList.add(className);
|
|
1289
|
+
this.$name = document.createElement("div");
|
|
1290
|
+
this.$name.classList.add("lil-name");
|
|
1291
|
+
_Controller.nextNameID = _Controller.nextNameID || 0;
|
|
1292
|
+
this.$name.id = `lil-gui-name-${++_Controller.nextNameID}`;
|
|
1293
|
+
this.$widget = document.createElement("div");
|
|
1294
|
+
this.$widget.classList.add("lil-widget");
|
|
1295
|
+
this.$disable = this.$widget;
|
|
1296
|
+
this.domElement.appendChild(this.$name);
|
|
1297
|
+
this.domElement.appendChild(this.$widget);
|
|
1298
|
+
this.domElement.addEventListener("keydown", (e) => e.stopPropagation());
|
|
1299
|
+
this.domElement.addEventListener("keyup", (e) => e.stopPropagation());
|
|
1300
|
+
this.parent.children.push(this);
|
|
1301
|
+
this.parent.controllers.push(this);
|
|
1302
|
+
this.parent.$children.appendChild(this.domElement);
|
|
1303
|
+
this._listenCallback = this._listenCallback.bind(this);
|
|
1304
|
+
this.name(property);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Sets the name of the controller and its label in the GUI.
|
|
1308
|
+
* @param {string} name
|
|
1309
|
+
* @returns {this}
|
|
1310
|
+
*/
|
|
1311
|
+
name(name) {
|
|
1312
|
+
this._name = name;
|
|
1313
|
+
this.$name.textContent = name;
|
|
1314
|
+
return this;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Pass a function to be called whenever the value is modified by this controller.
|
|
1318
|
+
* The function receives the new value as its first parameter. The value of `this` will be the
|
|
1319
|
+
* controller.
|
|
1320
|
+
*
|
|
1321
|
+
* For function controllers, the `onChange` callback will be fired on click, after the function
|
|
1322
|
+
* executes.
|
|
1323
|
+
* @param {Function} callback
|
|
1324
|
+
* @returns {this}
|
|
1325
|
+
* @example
|
|
1326
|
+
* const controller = gui.add( object, 'property' );
|
|
1327
|
+
*
|
|
1328
|
+
* controller.onChange( function( v ) {
|
|
1329
|
+
* console.log( 'The value is now ' + v );
|
|
1330
|
+
* console.assert( this === controller );
|
|
1331
|
+
* } );
|
|
1332
|
+
*/
|
|
1333
|
+
onChange(callback) {
|
|
1334
|
+
this._onChange = callback;
|
|
1335
|
+
return this;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Calls the onChange methods of this controller and its parent GUI.
|
|
1339
|
+
* @protected
|
|
1340
|
+
*/
|
|
1341
|
+
_callOnChange() {
|
|
1342
|
+
this.parent._callOnChange(this);
|
|
1343
|
+
if (this._onChange !== void 0) {
|
|
1344
|
+
this._onChange.call(this, this.getValue());
|
|
1345
|
+
}
|
|
1346
|
+
this._changed = true;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Pass a function to be called after this controller has been modified and loses focus.
|
|
1350
|
+
* @param {Function} callback
|
|
1351
|
+
* @returns {this}
|
|
1352
|
+
* @example
|
|
1353
|
+
* const controller = gui.add( object, 'property' );
|
|
1354
|
+
*
|
|
1355
|
+
* controller.onFinishChange( function( v ) {
|
|
1356
|
+
* console.log( 'Changes complete: ' + v );
|
|
1357
|
+
* console.assert( this === controller );
|
|
1358
|
+
* } );
|
|
1359
|
+
*/
|
|
1360
|
+
onFinishChange(callback) {
|
|
1361
|
+
this._onFinishChange = callback;
|
|
1362
|
+
return this;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Should be called by Controller when its widgets lose focus.
|
|
1366
|
+
* @protected
|
|
1367
|
+
*/
|
|
1368
|
+
_callOnFinishChange() {
|
|
1369
|
+
if (this._changed) {
|
|
1370
|
+
this.parent._callOnFinishChange(this);
|
|
1371
|
+
if (this._onFinishChange !== void 0) {
|
|
1372
|
+
this._onFinishChange.call(this, this.getValue());
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
this._changed = false;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Sets the controller back to its initial value.
|
|
1379
|
+
* @returns {this}
|
|
1380
|
+
*/
|
|
1381
|
+
reset() {
|
|
1382
|
+
this.setValue(this.initialValue);
|
|
1383
|
+
this._callOnFinishChange();
|
|
1384
|
+
return this;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Enables this controller.
|
|
1388
|
+
* @param {boolean} enabled
|
|
1389
|
+
* @returns {this}
|
|
1390
|
+
* @example
|
|
1391
|
+
* controller.enable();
|
|
1392
|
+
* controller.enable( false ); // disable
|
|
1393
|
+
* controller.enable( controller._disabled ); // toggle
|
|
1394
|
+
*/
|
|
1395
|
+
enable(enabled = true) {
|
|
1396
|
+
return this.disable(!enabled);
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Disables this controller.
|
|
1400
|
+
* @param {boolean} disabled
|
|
1401
|
+
* @returns {this}
|
|
1402
|
+
* @example
|
|
1403
|
+
* controller.disable();
|
|
1404
|
+
* controller.disable( false ); // enable
|
|
1405
|
+
* controller.disable( !controller._disabled ); // toggle
|
|
1406
|
+
*/
|
|
1407
|
+
disable(disabled = true) {
|
|
1408
|
+
if (disabled === this._disabled) return this;
|
|
1409
|
+
this._disabled = disabled;
|
|
1410
|
+
this.domElement.classList.toggle("lil-disabled", disabled);
|
|
1411
|
+
this.$disable.toggleAttribute("disabled", disabled);
|
|
1412
|
+
return this;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Shows the Controller after it's been hidden.
|
|
1416
|
+
* @param {boolean} show
|
|
1417
|
+
* @returns {this}
|
|
1418
|
+
* @example
|
|
1419
|
+
* controller.show();
|
|
1420
|
+
* controller.show( false ); // hide
|
|
1421
|
+
* controller.show( controller._hidden ); // toggle
|
|
1422
|
+
*/
|
|
1423
|
+
show(show = true) {
|
|
1424
|
+
this._hidden = !show;
|
|
1425
|
+
this.domElement.style.display = this._hidden ? "none" : "";
|
|
1426
|
+
return this;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Hides the Controller.
|
|
1430
|
+
* @returns {this}
|
|
1431
|
+
*/
|
|
1432
|
+
hide() {
|
|
1433
|
+
return this.show(false);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Changes this controller into a dropdown of options.
|
|
1437
|
+
*
|
|
1438
|
+
* Calling this method on an option controller will simply update the options. However, if this
|
|
1439
|
+
* controller was not already an option controller, old references to this controller are
|
|
1440
|
+
* destroyed, and a new controller is added to the end of the GUI.
|
|
1441
|
+
* @example
|
|
1442
|
+
* // safe usage
|
|
1443
|
+
*
|
|
1444
|
+
* gui.add( obj, 'prop1' ).options( [ 'a', 'b', 'c' ] );
|
|
1445
|
+
* gui.add( obj, 'prop2' ).options( { Big: 10, Small: 1 } );
|
|
1446
|
+
* gui.add( obj, 'prop3' );
|
|
1447
|
+
*
|
|
1448
|
+
* // danger
|
|
1449
|
+
*
|
|
1450
|
+
* const ctrl1 = gui.add( obj, 'prop1' );
|
|
1451
|
+
* gui.add( obj, 'prop2' );
|
|
1452
|
+
*
|
|
1453
|
+
* // calling options out of order adds a new controller to the end...
|
|
1454
|
+
* const ctrl2 = ctrl1.options( [ 'a', 'b', 'c' ] );
|
|
1455
|
+
*
|
|
1456
|
+
* // ...and ctrl1 now references a controller that doesn't exist
|
|
1457
|
+
* assert( ctrl2 !== ctrl1 )
|
|
1458
|
+
* @param {object|Array} options
|
|
1459
|
+
* @returns {Controller}
|
|
1460
|
+
*/
|
|
1461
|
+
options(options) {
|
|
1462
|
+
const controller = this.parent.add(this.object, this.property, options);
|
|
1463
|
+
controller.name(this._name);
|
|
1464
|
+
this.destroy();
|
|
1465
|
+
return controller;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Sets the minimum value. Only works on number controllers.
|
|
1469
|
+
* @param {number} min
|
|
1470
|
+
* @returns {this}
|
|
1471
|
+
*/
|
|
1472
|
+
min(min) {
|
|
1473
|
+
return this;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Sets the maximum value. Only works on number controllers.
|
|
1477
|
+
* @param {number} max
|
|
1478
|
+
* @returns {this}
|
|
1479
|
+
*/
|
|
1480
|
+
max(max) {
|
|
1481
|
+
return this;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Values set by this controller will be rounded to multiples of `step`. Only works on number
|
|
1485
|
+
* controllers.
|
|
1486
|
+
* @param {number} step
|
|
1487
|
+
* @returns {this}
|
|
1488
|
+
*/
|
|
1489
|
+
step(step) {
|
|
1490
|
+
return this;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Rounds the displayed value to a fixed number of decimals, without affecting the actual value
|
|
1494
|
+
* like `step()`. Only works on number controllers.
|
|
1495
|
+
* @example
|
|
1496
|
+
* gui.add( object, 'property' ).listen().decimals( 4 );
|
|
1497
|
+
* @param {number} decimals
|
|
1498
|
+
* @returns {this}
|
|
1499
|
+
*/
|
|
1500
|
+
decimals(decimals) {
|
|
1501
|
+
return this;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Calls `updateDisplay()` every animation frame. Pass `false` to stop listening.
|
|
1505
|
+
* @param {boolean} listen
|
|
1506
|
+
* @returns {this}
|
|
1507
|
+
*/
|
|
1508
|
+
listen(listen = true) {
|
|
1509
|
+
this._listening = listen;
|
|
1510
|
+
if (this._listenCallbackID !== void 0) {
|
|
1511
|
+
cancelAnimationFrame(this._listenCallbackID);
|
|
1512
|
+
this._listenCallbackID = void 0;
|
|
1513
|
+
}
|
|
1514
|
+
if (this._listening) {
|
|
1515
|
+
this._listenCallback();
|
|
1516
|
+
}
|
|
1517
|
+
return this;
|
|
1518
|
+
}
|
|
1519
|
+
_listenCallback() {
|
|
1520
|
+
this._listenCallbackID = requestAnimationFrame(this._listenCallback);
|
|
1521
|
+
const curValue = this.save();
|
|
1522
|
+
if (curValue !== this._listenPrevValue) {
|
|
1523
|
+
this.updateDisplay();
|
|
1524
|
+
}
|
|
1525
|
+
this._listenPrevValue = curValue;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Returns `object[ property ]`.
|
|
1529
|
+
* @returns {any}
|
|
1530
|
+
*/
|
|
1531
|
+
getValue() {
|
|
1532
|
+
return this.object[this.property];
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display.
|
|
1536
|
+
* @param {any} value
|
|
1537
|
+
* @returns {this}
|
|
1538
|
+
*/
|
|
1539
|
+
setValue(value) {
|
|
1540
|
+
if (this.getValue() !== value) {
|
|
1541
|
+
this.object[this.property] = value;
|
|
1542
|
+
this._callOnChange();
|
|
1543
|
+
this.updateDisplay();
|
|
1544
|
+
}
|
|
1545
|
+
return this;
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Updates the display to keep it in sync with the current value. Useful for updating your
|
|
1549
|
+
* controllers when their values have been modified outside of the GUI.
|
|
1550
|
+
* @returns {this}
|
|
1551
|
+
*/
|
|
1552
|
+
updateDisplay() {
|
|
1553
|
+
return this;
|
|
1554
|
+
}
|
|
1555
|
+
load(value) {
|
|
1556
|
+
this.setValue(value);
|
|
1557
|
+
this._callOnFinishChange();
|
|
1558
|
+
return this;
|
|
1559
|
+
}
|
|
1560
|
+
save() {
|
|
1561
|
+
return this.getValue();
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Destroys this controller and removes it from the parent GUI.
|
|
1565
|
+
*/
|
|
1566
|
+
destroy() {
|
|
1567
|
+
this.listen(false);
|
|
1568
|
+
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
1569
|
+
this.parent.controllers.splice(this.parent.controllers.indexOf(this), 1);
|
|
1570
|
+
this.parent.$children.removeChild(this.domElement);
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
BooleanController = class extends Controller {
|
|
1574
|
+
constructor(parent, object, property) {
|
|
1575
|
+
super(parent, object, property, "lil-boolean", "label");
|
|
1576
|
+
this.$input = document.createElement("input");
|
|
1577
|
+
this.$input.setAttribute("type", "checkbox");
|
|
1578
|
+
this.$input.setAttribute("aria-labelledby", this.$name.id);
|
|
1579
|
+
this.$widget.appendChild(this.$input);
|
|
1580
|
+
this.$input.addEventListener("change", () => {
|
|
1581
|
+
this.setValue(this.$input.checked);
|
|
1582
|
+
this._callOnFinishChange();
|
|
1583
|
+
});
|
|
1584
|
+
this.$disable = this.$input;
|
|
1585
|
+
this.updateDisplay();
|
|
1586
|
+
}
|
|
1587
|
+
updateDisplay() {
|
|
1588
|
+
this.$input.checked = this.getValue();
|
|
1589
|
+
return this;
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
STRING = {
|
|
1593
|
+
isPrimitive: true,
|
|
1594
|
+
match: (v) => typeof v === "string",
|
|
1595
|
+
fromHexString: normalizeColorString,
|
|
1596
|
+
toHexString: normalizeColorString
|
|
1597
|
+
};
|
|
1598
|
+
INT = {
|
|
1599
|
+
isPrimitive: true,
|
|
1600
|
+
match: (v) => typeof v === "number",
|
|
1601
|
+
fromHexString: (string) => parseInt(string.substring(1), 16),
|
|
1602
|
+
toHexString: (value) => "#" + value.toString(16).padStart(6, 0)
|
|
1603
|
+
};
|
|
1604
|
+
ARRAY = {
|
|
1605
|
+
isPrimitive: false,
|
|
1606
|
+
match: (v) => Array.isArray(v) || ArrayBuffer.isView(v),
|
|
1607
|
+
fromHexString(string, target2, rgbScale = 1) {
|
|
1608
|
+
const int = INT.fromHexString(string);
|
|
1609
|
+
target2[0] = (int >> 16 & 255) / 255 * rgbScale;
|
|
1610
|
+
target2[1] = (int >> 8 & 255) / 255 * rgbScale;
|
|
1611
|
+
target2[2] = (int & 255) / 255 * rgbScale;
|
|
1612
|
+
},
|
|
1613
|
+
toHexString([r, g, b], rgbScale = 1) {
|
|
1614
|
+
rgbScale = 255 / rgbScale;
|
|
1615
|
+
const int = r * rgbScale << 16 ^ g * rgbScale << 8 ^ b * rgbScale << 0;
|
|
1616
|
+
return INT.toHexString(int);
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
OBJECT = {
|
|
1620
|
+
isPrimitive: false,
|
|
1621
|
+
match: (v) => Object(v) === v,
|
|
1622
|
+
fromHexString(string, target2, rgbScale = 1) {
|
|
1623
|
+
const int = INT.fromHexString(string);
|
|
1624
|
+
target2.r = (int >> 16 & 255) / 255 * rgbScale;
|
|
1625
|
+
target2.g = (int >> 8 & 255) / 255 * rgbScale;
|
|
1626
|
+
target2.b = (int & 255) / 255 * rgbScale;
|
|
1627
|
+
},
|
|
1628
|
+
toHexString({ r, g, b }, rgbScale = 1) {
|
|
1629
|
+
rgbScale = 255 / rgbScale;
|
|
1630
|
+
const int = r * rgbScale << 16 ^ g * rgbScale << 8 ^ b * rgbScale << 0;
|
|
1631
|
+
return INT.toHexString(int);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
FORMATS = [STRING, INT, ARRAY, OBJECT];
|
|
1635
|
+
ColorController = class extends Controller {
|
|
1636
|
+
constructor(parent, object, property, rgbScale) {
|
|
1637
|
+
super(parent, object, property, "lil-color");
|
|
1638
|
+
this.$input = document.createElement("input");
|
|
1639
|
+
this.$input.setAttribute("type", "color");
|
|
1640
|
+
this.$input.setAttribute("tabindex", -1);
|
|
1641
|
+
this.$input.setAttribute("aria-labelledby", this.$name.id);
|
|
1642
|
+
this.$text = document.createElement("input");
|
|
1643
|
+
this.$text.setAttribute("type", "text");
|
|
1644
|
+
this.$text.setAttribute("spellcheck", "false");
|
|
1645
|
+
this.$text.setAttribute("aria-labelledby", this.$name.id);
|
|
1646
|
+
this.$display = document.createElement("div");
|
|
1647
|
+
this.$display.classList.add("lil-display");
|
|
1648
|
+
this.$display.appendChild(this.$input);
|
|
1649
|
+
this.$widget.appendChild(this.$display);
|
|
1650
|
+
this.$widget.appendChild(this.$text);
|
|
1651
|
+
this._format = getColorFormat(this.initialValue);
|
|
1652
|
+
this._rgbScale = rgbScale;
|
|
1653
|
+
this._initialValueHexString = this.save();
|
|
1654
|
+
this._textFocused = false;
|
|
1655
|
+
this.$input.addEventListener("input", () => {
|
|
1656
|
+
this._setValueFromHexString(this.$input.value);
|
|
1657
|
+
});
|
|
1658
|
+
this.$input.addEventListener("blur", () => {
|
|
1659
|
+
this._callOnFinishChange();
|
|
1660
|
+
});
|
|
1661
|
+
this.$text.addEventListener("input", () => {
|
|
1662
|
+
const tryParse = normalizeColorString(this.$text.value);
|
|
1663
|
+
if (tryParse) {
|
|
1664
|
+
this._setValueFromHexString(tryParse);
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
this.$text.addEventListener("focus", () => {
|
|
1668
|
+
this._textFocused = true;
|
|
1669
|
+
this.$text.select();
|
|
1670
|
+
});
|
|
1671
|
+
this.$text.addEventListener("blur", () => {
|
|
1672
|
+
this._textFocused = false;
|
|
1673
|
+
this.updateDisplay();
|
|
1674
|
+
this._callOnFinishChange();
|
|
1675
|
+
});
|
|
1676
|
+
this.$disable = this.$text;
|
|
1677
|
+
this.updateDisplay();
|
|
1678
|
+
}
|
|
1679
|
+
reset() {
|
|
1680
|
+
this._setValueFromHexString(this._initialValueHexString);
|
|
1681
|
+
return this;
|
|
1682
|
+
}
|
|
1683
|
+
_setValueFromHexString(value) {
|
|
1684
|
+
if (this._format.isPrimitive) {
|
|
1685
|
+
const newValue = this._format.fromHexString(value);
|
|
1686
|
+
this.setValue(newValue);
|
|
1687
|
+
} else {
|
|
1688
|
+
this._format.fromHexString(value, this.getValue(), this._rgbScale);
|
|
1689
|
+
this._callOnChange();
|
|
1690
|
+
this.updateDisplay();
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
save() {
|
|
1694
|
+
return this._format.toHexString(this.getValue(), this._rgbScale);
|
|
1695
|
+
}
|
|
1696
|
+
load(value) {
|
|
1697
|
+
this._setValueFromHexString(value);
|
|
1698
|
+
this._callOnFinishChange();
|
|
1699
|
+
return this;
|
|
1700
|
+
}
|
|
1701
|
+
updateDisplay() {
|
|
1702
|
+
this.$input.value = this._format.toHexString(this.getValue(), this._rgbScale);
|
|
1703
|
+
if (!this._textFocused) {
|
|
1704
|
+
this.$text.value = this.$input.value.substring(1);
|
|
1705
|
+
}
|
|
1706
|
+
this.$display.style.backgroundColor = this.$input.value;
|
|
1707
|
+
return this;
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
FunctionController = class extends Controller {
|
|
1711
|
+
constructor(parent, object, property) {
|
|
1712
|
+
super(parent, object, property, "lil-function");
|
|
1713
|
+
this.$button = document.createElement("button");
|
|
1714
|
+
this.$button.appendChild(this.$name);
|
|
1715
|
+
this.$widget.appendChild(this.$button);
|
|
1716
|
+
this.$button.addEventListener("click", (e) => {
|
|
1717
|
+
e.preventDefault();
|
|
1718
|
+
this.getValue().call(this.object);
|
|
1719
|
+
this._callOnChange();
|
|
1720
|
+
});
|
|
1721
|
+
this.$button.addEventListener("touchstart", () => {
|
|
1722
|
+
}, { passive: true });
|
|
1723
|
+
this.$disable = this.$button;
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
NumberController = class extends Controller {
|
|
1727
|
+
constructor(parent, object, property, min, max, step) {
|
|
1728
|
+
super(parent, object, property, "lil-number");
|
|
1729
|
+
this._initInput();
|
|
1730
|
+
this.min(min);
|
|
1731
|
+
this.max(max);
|
|
1732
|
+
const stepExplicit = step !== void 0;
|
|
1733
|
+
this.step(stepExplicit ? step : this._getImplicitStep(), stepExplicit);
|
|
1734
|
+
this.updateDisplay();
|
|
1735
|
+
}
|
|
1736
|
+
decimals(decimals) {
|
|
1737
|
+
this._decimals = decimals;
|
|
1738
|
+
this.updateDisplay();
|
|
1739
|
+
return this;
|
|
1740
|
+
}
|
|
1741
|
+
min(min) {
|
|
1742
|
+
this._min = min;
|
|
1743
|
+
this._onUpdateMinMax();
|
|
1744
|
+
return this;
|
|
1745
|
+
}
|
|
1746
|
+
max(max) {
|
|
1747
|
+
this._max = max;
|
|
1748
|
+
this._onUpdateMinMax();
|
|
1749
|
+
return this;
|
|
1750
|
+
}
|
|
1751
|
+
step(step, explicit = true) {
|
|
1752
|
+
this._step = step;
|
|
1753
|
+
this._stepExplicit = explicit;
|
|
1754
|
+
return this;
|
|
1755
|
+
}
|
|
1756
|
+
updateDisplay() {
|
|
1757
|
+
const value = this.getValue();
|
|
1758
|
+
if (this._hasSlider) {
|
|
1759
|
+
let percent = (value - this._min) / (this._max - this._min);
|
|
1760
|
+
percent = Math.max(0, Math.min(percent, 1));
|
|
1761
|
+
this.$fill.style.width = percent * 100 + "%";
|
|
1762
|
+
}
|
|
1763
|
+
if (!this._inputFocused) {
|
|
1764
|
+
this.$input.value = this._decimals === void 0 ? value : value.toFixed(this._decimals);
|
|
1765
|
+
}
|
|
1766
|
+
return this;
|
|
1767
|
+
}
|
|
1768
|
+
_initInput() {
|
|
1769
|
+
this.$input = document.createElement("input");
|
|
1770
|
+
this.$input.setAttribute("type", "text");
|
|
1771
|
+
this.$input.setAttribute("aria-labelledby", this.$name.id);
|
|
1772
|
+
const isTouch = window.matchMedia("(pointer: coarse)").matches;
|
|
1773
|
+
if (isTouch) {
|
|
1774
|
+
this.$input.setAttribute("type", "number");
|
|
1775
|
+
this.$input.setAttribute("step", "any");
|
|
1776
|
+
}
|
|
1777
|
+
this.$widget.appendChild(this.$input);
|
|
1778
|
+
this.$disable = this.$input;
|
|
1779
|
+
const onInput = () => {
|
|
1780
|
+
let value = parseFloat(this.$input.value);
|
|
1781
|
+
if (isNaN(value)) return;
|
|
1782
|
+
if (this._stepExplicit) {
|
|
1783
|
+
value = this._snap(value);
|
|
1784
|
+
}
|
|
1785
|
+
this.setValue(this._clamp(value));
|
|
1786
|
+
};
|
|
1787
|
+
const increment = (delta) => {
|
|
1788
|
+
const value = parseFloat(this.$input.value);
|
|
1789
|
+
if (isNaN(value)) return;
|
|
1790
|
+
this._snapClampSetValue(value + delta);
|
|
1791
|
+
this.$input.value = this.getValue();
|
|
1792
|
+
};
|
|
1793
|
+
const onKeyDown = (e) => {
|
|
1794
|
+
if (e.key === "Enter") {
|
|
1795
|
+
this.$input.blur();
|
|
1796
|
+
}
|
|
1797
|
+
if (e.code === "ArrowUp") {
|
|
1798
|
+
e.preventDefault();
|
|
1799
|
+
increment(this._step * this._arrowKeyMultiplier(e));
|
|
1800
|
+
}
|
|
1801
|
+
if (e.code === "ArrowDown") {
|
|
1802
|
+
e.preventDefault();
|
|
1803
|
+
increment(this._step * this._arrowKeyMultiplier(e) * -1);
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
const onWheel = (e) => {
|
|
1807
|
+
if (this._inputFocused) {
|
|
1808
|
+
e.preventDefault();
|
|
1809
|
+
increment(this._step * this._normalizeMouseWheel(e));
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
let testingForVerticalDrag = false, initClientX, initClientY, prevClientY, initValue, dragDelta;
|
|
1813
|
+
const DRAG_THRESH = 5;
|
|
1814
|
+
const onMouseDown = (e) => {
|
|
1815
|
+
initClientX = e.clientX;
|
|
1816
|
+
initClientY = prevClientY = e.clientY;
|
|
1817
|
+
testingForVerticalDrag = true;
|
|
1818
|
+
initValue = this.getValue();
|
|
1819
|
+
dragDelta = 0;
|
|
1820
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
1821
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
1822
|
+
};
|
|
1823
|
+
const onMouseMove = (e) => {
|
|
1824
|
+
if (testingForVerticalDrag) {
|
|
1825
|
+
const dx = e.clientX - initClientX;
|
|
1826
|
+
const dy = e.clientY - initClientY;
|
|
1827
|
+
if (Math.abs(dy) > DRAG_THRESH) {
|
|
1828
|
+
e.preventDefault();
|
|
1829
|
+
this.$input.blur();
|
|
1830
|
+
testingForVerticalDrag = false;
|
|
1831
|
+
this._setDraggingStyle(true, "vertical");
|
|
1832
|
+
} else if (Math.abs(dx) > DRAG_THRESH) {
|
|
1833
|
+
onMouseUp();
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (!testingForVerticalDrag) {
|
|
1837
|
+
const dy = e.clientY - prevClientY;
|
|
1838
|
+
dragDelta -= dy * this._step * this._arrowKeyMultiplier(e);
|
|
1839
|
+
if (initValue + dragDelta > this._max) {
|
|
1840
|
+
dragDelta = this._max - initValue;
|
|
1841
|
+
} else if (initValue + dragDelta < this._min) {
|
|
1842
|
+
dragDelta = this._min - initValue;
|
|
1843
|
+
}
|
|
1844
|
+
this._snapClampSetValue(initValue + dragDelta);
|
|
1845
|
+
}
|
|
1846
|
+
prevClientY = e.clientY;
|
|
1847
|
+
};
|
|
1848
|
+
const onMouseUp = () => {
|
|
1849
|
+
this._setDraggingStyle(false, "vertical");
|
|
1850
|
+
this._callOnFinishChange();
|
|
1851
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
1852
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
1853
|
+
};
|
|
1854
|
+
const onFocus = () => {
|
|
1855
|
+
this._inputFocused = true;
|
|
1856
|
+
};
|
|
1857
|
+
const onBlur = () => {
|
|
1858
|
+
this._inputFocused = false;
|
|
1859
|
+
this.updateDisplay();
|
|
1860
|
+
this._callOnFinishChange();
|
|
1861
|
+
};
|
|
1862
|
+
this.$input.addEventListener("input", onInput);
|
|
1863
|
+
this.$input.addEventListener("keydown", onKeyDown);
|
|
1864
|
+
this.$input.addEventListener("wheel", onWheel, { passive: false });
|
|
1865
|
+
this.$input.addEventListener("mousedown", onMouseDown);
|
|
1866
|
+
this.$input.addEventListener("focus", onFocus);
|
|
1867
|
+
this.$input.addEventListener("blur", onBlur);
|
|
1868
|
+
}
|
|
1869
|
+
_initSlider() {
|
|
1870
|
+
this._hasSlider = true;
|
|
1871
|
+
this.$slider = document.createElement("div");
|
|
1872
|
+
this.$slider.classList.add("lil-slider");
|
|
1873
|
+
this.$fill = document.createElement("div");
|
|
1874
|
+
this.$fill.classList.add("lil-fill");
|
|
1875
|
+
this.$slider.appendChild(this.$fill);
|
|
1876
|
+
this.$widget.insertBefore(this.$slider, this.$input);
|
|
1877
|
+
this.domElement.classList.add("lil-has-slider");
|
|
1878
|
+
const map = (v, a, b, c, d) => {
|
|
1879
|
+
return (v - a) / (b - a) * (d - c) + c;
|
|
1880
|
+
};
|
|
1881
|
+
const setValueFromX = (clientX) => {
|
|
1882
|
+
const rect = this.$slider.getBoundingClientRect();
|
|
1883
|
+
let value = map(clientX, rect.left, rect.right, this._min, this._max);
|
|
1884
|
+
this._snapClampSetValue(value);
|
|
1885
|
+
};
|
|
1886
|
+
const mouseDown = (e) => {
|
|
1887
|
+
this._setDraggingStyle(true);
|
|
1888
|
+
setValueFromX(e.clientX);
|
|
1889
|
+
window.addEventListener("mousemove", mouseMove);
|
|
1890
|
+
window.addEventListener("mouseup", mouseUp);
|
|
1891
|
+
};
|
|
1892
|
+
const mouseMove = (e) => {
|
|
1893
|
+
setValueFromX(e.clientX);
|
|
1894
|
+
};
|
|
1895
|
+
const mouseUp = () => {
|
|
1896
|
+
this._callOnFinishChange();
|
|
1897
|
+
this._setDraggingStyle(false);
|
|
1898
|
+
window.removeEventListener("mousemove", mouseMove);
|
|
1899
|
+
window.removeEventListener("mouseup", mouseUp);
|
|
1900
|
+
};
|
|
1901
|
+
let testingForScroll = false, prevClientX, prevClientY;
|
|
1902
|
+
const beginTouchDrag = (e) => {
|
|
1903
|
+
e.preventDefault();
|
|
1904
|
+
this._setDraggingStyle(true);
|
|
1905
|
+
setValueFromX(e.touches[0].clientX);
|
|
1906
|
+
testingForScroll = false;
|
|
1907
|
+
};
|
|
1908
|
+
const onTouchStart = (e) => {
|
|
1909
|
+
if (e.touches.length > 1) return;
|
|
1910
|
+
if (this._hasScrollBar) {
|
|
1911
|
+
prevClientX = e.touches[0].clientX;
|
|
1912
|
+
prevClientY = e.touches[0].clientY;
|
|
1913
|
+
testingForScroll = true;
|
|
1914
|
+
} else {
|
|
1915
|
+
beginTouchDrag(e);
|
|
1916
|
+
}
|
|
1917
|
+
window.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
1918
|
+
window.addEventListener("touchend", onTouchEnd);
|
|
1919
|
+
};
|
|
1920
|
+
const onTouchMove = (e) => {
|
|
1921
|
+
if (testingForScroll) {
|
|
1922
|
+
const dx = e.touches[0].clientX - prevClientX;
|
|
1923
|
+
const dy = e.touches[0].clientY - prevClientY;
|
|
1924
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
1925
|
+
beginTouchDrag(e);
|
|
1926
|
+
} else {
|
|
1927
|
+
window.removeEventListener("touchmove", onTouchMove);
|
|
1928
|
+
window.removeEventListener("touchend", onTouchEnd);
|
|
1929
|
+
}
|
|
1930
|
+
} else {
|
|
1931
|
+
e.preventDefault();
|
|
1932
|
+
setValueFromX(e.touches[0].clientX);
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
const onTouchEnd = () => {
|
|
1936
|
+
this._callOnFinishChange();
|
|
1937
|
+
this._setDraggingStyle(false);
|
|
1938
|
+
window.removeEventListener("touchmove", onTouchMove);
|
|
1939
|
+
window.removeEventListener("touchend", onTouchEnd);
|
|
1940
|
+
};
|
|
1941
|
+
const callOnFinishChange = this._callOnFinishChange.bind(this);
|
|
1942
|
+
const WHEEL_DEBOUNCE_TIME = 400;
|
|
1943
|
+
let wheelFinishChangeTimeout;
|
|
1944
|
+
const onWheel = (e) => {
|
|
1945
|
+
const isVertical = Math.abs(e.deltaX) < Math.abs(e.deltaY);
|
|
1946
|
+
if (isVertical && this._hasScrollBar) return;
|
|
1947
|
+
e.preventDefault();
|
|
1948
|
+
const delta = this._normalizeMouseWheel(e) * this._step;
|
|
1949
|
+
this._snapClampSetValue(this.getValue() + delta);
|
|
1950
|
+
this.$input.value = this.getValue();
|
|
1951
|
+
clearTimeout(wheelFinishChangeTimeout);
|
|
1952
|
+
wheelFinishChangeTimeout = setTimeout(callOnFinishChange, WHEEL_DEBOUNCE_TIME);
|
|
1953
|
+
};
|
|
1954
|
+
this.$slider.addEventListener("mousedown", mouseDown);
|
|
1955
|
+
this.$slider.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
1956
|
+
this.$slider.addEventListener("wheel", onWheel, { passive: false });
|
|
1957
|
+
}
|
|
1958
|
+
_setDraggingStyle(active, axis = "horizontal") {
|
|
1959
|
+
if (this.$slider) {
|
|
1960
|
+
this.$slider.classList.toggle("lil-active", active);
|
|
1961
|
+
}
|
|
1962
|
+
document.body.classList.toggle("lil-dragging", active);
|
|
1963
|
+
document.body.classList.toggle(`lil-${axis}`, active);
|
|
1964
|
+
}
|
|
1965
|
+
_getImplicitStep() {
|
|
1966
|
+
if (this._hasMin && this._hasMax) {
|
|
1967
|
+
return (this._max - this._min) / 1e3;
|
|
1968
|
+
}
|
|
1969
|
+
return 0.1;
|
|
1970
|
+
}
|
|
1971
|
+
_onUpdateMinMax() {
|
|
1972
|
+
if (!this._hasSlider && this._hasMin && this._hasMax) {
|
|
1973
|
+
if (!this._stepExplicit) {
|
|
1974
|
+
this.step(this._getImplicitStep(), false);
|
|
1975
|
+
}
|
|
1976
|
+
this._initSlider();
|
|
1977
|
+
this.updateDisplay();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
_normalizeMouseWheel(e) {
|
|
1981
|
+
let { deltaX, deltaY } = e;
|
|
1982
|
+
if (Math.floor(e.deltaY) !== e.deltaY && e.wheelDelta) {
|
|
1983
|
+
deltaX = 0;
|
|
1984
|
+
deltaY = -e.wheelDelta / 120;
|
|
1985
|
+
deltaY *= this._stepExplicit ? 1 : 10;
|
|
1986
|
+
}
|
|
1987
|
+
const wheel = deltaX + -deltaY;
|
|
1988
|
+
return wheel;
|
|
1989
|
+
}
|
|
1990
|
+
_arrowKeyMultiplier(e) {
|
|
1991
|
+
let mult = this._stepExplicit ? 1 : 10;
|
|
1992
|
+
if (e.shiftKey) {
|
|
1993
|
+
mult *= 10;
|
|
1994
|
+
} else if (e.altKey) {
|
|
1995
|
+
mult /= 10;
|
|
1996
|
+
}
|
|
1997
|
+
return mult;
|
|
1998
|
+
}
|
|
1999
|
+
_snap(value) {
|
|
2000
|
+
let offset = 0;
|
|
2001
|
+
if (this._hasMin) {
|
|
2002
|
+
offset = this._min;
|
|
2003
|
+
} else if (this._hasMax) {
|
|
2004
|
+
offset = this._max;
|
|
2005
|
+
}
|
|
2006
|
+
value -= offset;
|
|
2007
|
+
value = Math.round(value / this._step) * this._step;
|
|
2008
|
+
value += offset;
|
|
2009
|
+
value = parseFloat(value.toPrecision(15));
|
|
2010
|
+
return value;
|
|
2011
|
+
}
|
|
2012
|
+
_clamp(value) {
|
|
2013
|
+
if (value < this._min) value = this._min;
|
|
2014
|
+
if (value > this._max) value = this._max;
|
|
2015
|
+
return value;
|
|
2016
|
+
}
|
|
2017
|
+
_snapClampSetValue(value) {
|
|
2018
|
+
this.setValue(this._clamp(this._snap(value)));
|
|
2019
|
+
}
|
|
2020
|
+
get _hasScrollBar() {
|
|
2021
|
+
const root = this.parent.root.$children;
|
|
2022
|
+
return root.scrollHeight > root.clientHeight;
|
|
2023
|
+
}
|
|
2024
|
+
get _hasMin() {
|
|
2025
|
+
return this._min !== void 0;
|
|
2026
|
+
}
|
|
2027
|
+
get _hasMax() {
|
|
2028
|
+
return this._max !== void 0;
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
OptionController = class extends Controller {
|
|
2032
|
+
constructor(parent, object, property, options) {
|
|
2033
|
+
super(parent, object, property, "lil-option");
|
|
2034
|
+
this.$select = document.createElement("select");
|
|
2035
|
+
this.$select.setAttribute("aria-labelledby", this.$name.id);
|
|
2036
|
+
this.$display = document.createElement("div");
|
|
2037
|
+
this.$display.classList.add("lil-display");
|
|
2038
|
+
this.$select.addEventListener("change", () => {
|
|
2039
|
+
this.setValue(this._values[this.$select.selectedIndex]);
|
|
2040
|
+
this._callOnFinishChange();
|
|
2041
|
+
});
|
|
2042
|
+
this.$select.addEventListener("focus", () => {
|
|
2043
|
+
this.$display.classList.add("lil-focus");
|
|
2044
|
+
});
|
|
2045
|
+
this.$select.addEventListener("blur", () => {
|
|
2046
|
+
this.$display.classList.remove("lil-focus");
|
|
2047
|
+
});
|
|
2048
|
+
this.$widget.appendChild(this.$select);
|
|
2049
|
+
this.$widget.appendChild(this.$display);
|
|
2050
|
+
this.$disable = this.$select;
|
|
2051
|
+
this.options(options);
|
|
2052
|
+
}
|
|
2053
|
+
options(options) {
|
|
2054
|
+
this._values = Array.isArray(options) ? options : Object.values(options);
|
|
2055
|
+
this._names = Array.isArray(options) ? options : Object.keys(options);
|
|
2056
|
+
this.$select.replaceChildren();
|
|
2057
|
+
this._names.forEach((name) => {
|
|
2058
|
+
const $option = document.createElement("option");
|
|
2059
|
+
$option.textContent = name;
|
|
2060
|
+
this.$select.appendChild($option);
|
|
2061
|
+
});
|
|
2062
|
+
this.updateDisplay();
|
|
2063
|
+
return this;
|
|
2064
|
+
}
|
|
2065
|
+
updateDisplay() {
|
|
2066
|
+
const value = this.getValue();
|
|
2067
|
+
const index = this._values.indexOf(value);
|
|
2068
|
+
this.$select.selectedIndex = index;
|
|
2069
|
+
this.$display.textContent = index === -1 ? value : this._names[index];
|
|
2070
|
+
return this;
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
StringController = class extends Controller {
|
|
2074
|
+
constructor(parent, object, property) {
|
|
2075
|
+
super(parent, object, property, "lil-string");
|
|
2076
|
+
this.$input = document.createElement("input");
|
|
2077
|
+
this.$input.setAttribute("type", "text");
|
|
2078
|
+
this.$input.setAttribute("spellcheck", "false");
|
|
2079
|
+
this.$input.setAttribute("aria-labelledby", this.$name.id);
|
|
2080
|
+
this.$input.addEventListener("input", () => {
|
|
2081
|
+
this.setValue(this.$input.value);
|
|
2082
|
+
});
|
|
2083
|
+
this.$input.addEventListener("keydown", (e) => {
|
|
2084
|
+
if (e.code === "Enter") {
|
|
2085
|
+
this.$input.blur();
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
this.$input.addEventListener("blur", () => {
|
|
2089
|
+
this._callOnFinishChange();
|
|
2090
|
+
});
|
|
2091
|
+
this.$widget.appendChild(this.$input);
|
|
2092
|
+
this.$disable = this.$input;
|
|
2093
|
+
this.updateDisplay();
|
|
2094
|
+
}
|
|
2095
|
+
updateDisplay() {
|
|
2096
|
+
this.$input.value = this.getValue();
|
|
2097
|
+
return this;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
stylesheet = `.lil-gui {
|
|
2101
|
+
font-family: var(--font-family);
|
|
2102
|
+
font-size: var(--font-size);
|
|
2103
|
+
line-height: 1;
|
|
2104
|
+
font-weight: normal;
|
|
2105
|
+
font-style: normal;
|
|
2106
|
+
text-align: left;
|
|
2107
|
+
color: var(--text-color);
|
|
2108
|
+
user-select: none;
|
|
2109
|
+
-webkit-user-select: none;
|
|
2110
|
+
touch-action: manipulation;
|
|
2111
|
+
--background-color: #1f1f1f;
|
|
2112
|
+
--text-color: #ebebeb;
|
|
2113
|
+
--title-background-color: #111111;
|
|
2114
|
+
--title-text-color: #ebebeb;
|
|
2115
|
+
--widget-color: #424242;
|
|
2116
|
+
--hover-color: #4f4f4f;
|
|
2117
|
+
--focus-color: #595959;
|
|
2118
|
+
--number-color: #2cc9ff;
|
|
2119
|
+
--string-color: #a2db3c;
|
|
2120
|
+
--font-size: 11px;
|
|
2121
|
+
--input-font-size: 11px;
|
|
2122
|
+
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
2123
|
+
--font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;
|
|
2124
|
+
--padding: 4px;
|
|
2125
|
+
--spacing: 4px;
|
|
2126
|
+
--widget-height: 20px;
|
|
2127
|
+
--title-height: calc(var(--widget-height) + var(--spacing) * 1.25);
|
|
2128
|
+
--name-width: 45%;
|
|
2129
|
+
--slider-knob-width: 2px;
|
|
2130
|
+
--slider-input-width: 27%;
|
|
2131
|
+
--color-input-width: 27%;
|
|
2132
|
+
--slider-input-min-width: 45px;
|
|
2133
|
+
--color-input-min-width: 45px;
|
|
2134
|
+
--folder-indent: 7px;
|
|
2135
|
+
--widget-padding: 0 0 0 3px;
|
|
2136
|
+
--widget-border-radius: 2px;
|
|
2137
|
+
--checkbox-size: calc(0.75 * var(--widget-height));
|
|
2138
|
+
--scrollbar-width: 5px;
|
|
2139
|
+
}
|
|
2140
|
+
.lil-gui, .lil-gui * {
|
|
2141
|
+
box-sizing: border-box;
|
|
2142
|
+
margin: 0;
|
|
2143
|
+
padding: 0;
|
|
2144
|
+
}
|
|
2145
|
+
.lil-gui.lil-root {
|
|
2146
|
+
width: var(--width, 245px);
|
|
2147
|
+
display: flex;
|
|
2148
|
+
flex-direction: column;
|
|
2149
|
+
background: var(--background-color);
|
|
2150
|
+
}
|
|
2151
|
+
.lil-gui.lil-root > .lil-title {
|
|
2152
|
+
background: var(--title-background-color);
|
|
2153
|
+
color: var(--title-text-color);
|
|
2154
|
+
}
|
|
2155
|
+
.lil-gui.lil-root > .lil-children {
|
|
2156
|
+
overflow-x: hidden;
|
|
2157
|
+
overflow-y: auto;
|
|
2158
|
+
}
|
|
2159
|
+
.lil-gui.lil-root > .lil-children::-webkit-scrollbar {
|
|
2160
|
+
width: var(--scrollbar-width);
|
|
2161
|
+
height: var(--scrollbar-width);
|
|
2162
|
+
background: var(--background-color);
|
|
2163
|
+
}
|
|
2164
|
+
.lil-gui.lil-root > .lil-children::-webkit-scrollbar-thumb {
|
|
2165
|
+
border-radius: var(--scrollbar-width);
|
|
2166
|
+
background: var(--focus-color);
|
|
2167
|
+
}
|
|
2168
|
+
@media (pointer: coarse) {
|
|
2169
|
+
.lil-gui.lil-allow-touch-styles, .lil-gui.lil-allow-touch-styles .lil-gui {
|
|
2170
|
+
--widget-height: 28px;
|
|
2171
|
+
--padding: 6px;
|
|
2172
|
+
--spacing: 6px;
|
|
2173
|
+
--font-size: 13px;
|
|
2174
|
+
--input-font-size: 16px;
|
|
2175
|
+
--folder-indent: 10px;
|
|
2176
|
+
--scrollbar-width: 7px;
|
|
2177
|
+
--slider-input-min-width: 50px;
|
|
2178
|
+
--color-input-min-width: 65px;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
.lil-gui.lil-force-touch-styles, .lil-gui.lil-force-touch-styles .lil-gui {
|
|
2182
|
+
--widget-height: 28px;
|
|
2183
|
+
--padding: 6px;
|
|
2184
|
+
--spacing: 6px;
|
|
2185
|
+
--font-size: 13px;
|
|
2186
|
+
--input-font-size: 16px;
|
|
2187
|
+
--folder-indent: 10px;
|
|
2188
|
+
--scrollbar-width: 7px;
|
|
2189
|
+
--slider-input-min-width: 50px;
|
|
2190
|
+
--color-input-min-width: 65px;
|
|
2191
|
+
}
|
|
2192
|
+
.lil-gui.lil-auto-place, .lil-gui.autoPlace {
|
|
2193
|
+
max-height: 100%;
|
|
2194
|
+
position: fixed;
|
|
2195
|
+
top: 0;
|
|
2196
|
+
right: 15px;
|
|
2197
|
+
z-index: 1001;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
.lil-controller {
|
|
2201
|
+
display: flex;
|
|
2202
|
+
align-items: center;
|
|
2203
|
+
padding: 0 var(--padding);
|
|
2204
|
+
margin: var(--spacing) 0;
|
|
2205
|
+
}
|
|
2206
|
+
.lil-controller.lil-disabled {
|
|
2207
|
+
opacity: 0.5;
|
|
2208
|
+
}
|
|
2209
|
+
.lil-controller.lil-disabled, .lil-controller.lil-disabled * {
|
|
2210
|
+
pointer-events: none !important;
|
|
2211
|
+
}
|
|
2212
|
+
.lil-controller > .lil-name {
|
|
2213
|
+
min-width: var(--name-width);
|
|
2214
|
+
flex-shrink: 0;
|
|
2215
|
+
white-space: pre;
|
|
2216
|
+
padding-right: var(--spacing);
|
|
2217
|
+
line-height: var(--widget-height);
|
|
2218
|
+
}
|
|
2219
|
+
.lil-controller .lil-widget {
|
|
2220
|
+
position: relative;
|
|
2221
|
+
display: flex;
|
|
2222
|
+
align-items: center;
|
|
2223
|
+
width: 100%;
|
|
2224
|
+
min-height: var(--widget-height);
|
|
2225
|
+
}
|
|
2226
|
+
.lil-controller.lil-string input {
|
|
2227
|
+
color: var(--string-color);
|
|
2228
|
+
}
|
|
2229
|
+
.lil-controller.lil-boolean {
|
|
2230
|
+
cursor: pointer;
|
|
2231
|
+
}
|
|
2232
|
+
.lil-controller.lil-color .lil-display {
|
|
2233
|
+
width: 100%;
|
|
2234
|
+
height: var(--widget-height);
|
|
2235
|
+
border-radius: var(--widget-border-radius);
|
|
2236
|
+
position: relative;
|
|
2237
|
+
}
|
|
2238
|
+
@media (hover: hover) {
|
|
2239
|
+
.lil-controller.lil-color .lil-display:hover:before {
|
|
2240
|
+
content: " ";
|
|
2241
|
+
display: block;
|
|
2242
|
+
position: absolute;
|
|
2243
|
+
border-radius: var(--widget-border-radius);
|
|
2244
|
+
border: 1px solid #fff9;
|
|
2245
|
+
top: 0;
|
|
2246
|
+
right: 0;
|
|
2247
|
+
bottom: 0;
|
|
2248
|
+
left: 0;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
.lil-controller.lil-color input[type=color] {
|
|
2252
|
+
opacity: 0;
|
|
2253
|
+
width: 100%;
|
|
2254
|
+
height: 100%;
|
|
2255
|
+
cursor: pointer;
|
|
2256
|
+
}
|
|
2257
|
+
.lil-controller.lil-color input[type=text] {
|
|
2258
|
+
margin-left: var(--spacing);
|
|
2259
|
+
font-family: var(--font-family-mono);
|
|
2260
|
+
min-width: var(--color-input-min-width);
|
|
2261
|
+
width: var(--color-input-width);
|
|
2262
|
+
flex-shrink: 0;
|
|
2263
|
+
}
|
|
2264
|
+
.lil-controller.lil-option select {
|
|
2265
|
+
opacity: 0;
|
|
2266
|
+
position: absolute;
|
|
2267
|
+
width: 100%;
|
|
2268
|
+
max-width: 100%;
|
|
2269
|
+
}
|
|
2270
|
+
.lil-controller.lil-option .lil-display {
|
|
2271
|
+
position: relative;
|
|
2272
|
+
pointer-events: none;
|
|
2273
|
+
border-radius: var(--widget-border-radius);
|
|
2274
|
+
height: var(--widget-height);
|
|
2275
|
+
line-height: var(--widget-height);
|
|
2276
|
+
max-width: 100%;
|
|
2277
|
+
overflow: hidden;
|
|
2278
|
+
word-break: break-all;
|
|
2279
|
+
padding-left: 0.55em;
|
|
2280
|
+
padding-right: 1.75em;
|
|
2281
|
+
background: var(--widget-color);
|
|
2282
|
+
}
|
|
2283
|
+
@media (hover: hover) {
|
|
2284
|
+
.lil-controller.lil-option .lil-display.lil-focus {
|
|
2285
|
+
background: var(--focus-color);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
.lil-controller.lil-option .lil-display.lil-active {
|
|
2289
|
+
background: var(--focus-color);
|
|
2290
|
+
}
|
|
2291
|
+
.lil-controller.lil-option .lil-display:after {
|
|
2292
|
+
font-family: "lil-gui";
|
|
2293
|
+
content: "\u2195";
|
|
2294
|
+
position: absolute;
|
|
2295
|
+
top: 0;
|
|
2296
|
+
right: 0;
|
|
2297
|
+
bottom: 0;
|
|
2298
|
+
padding-right: 0.375em;
|
|
2299
|
+
}
|
|
2300
|
+
.lil-controller.lil-option .lil-widget,
|
|
2301
|
+
.lil-controller.lil-option select {
|
|
2302
|
+
cursor: pointer;
|
|
2303
|
+
}
|
|
2304
|
+
@media (hover: hover) {
|
|
2305
|
+
.lil-controller.lil-option .lil-widget:hover .lil-display {
|
|
2306
|
+
background: var(--hover-color);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
.lil-controller.lil-number input {
|
|
2310
|
+
color: var(--number-color);
|
|
2311
|
+
}
|
|
2312
|
+
.lil-controller.lil-number.lil-has-slider input {
|
|
2313
|
+
margin-left: var(--spacing);
|
|
2314
|
+
width: var(--slider-input-width);
|
|
2315
|
+
min-width: var(--slider-input-min-width);
|
|
2316
|
+
flex-shrink: 0;
|
|
2317
|
+
}
|
|
2318
|
+
.lil-controller.lil-number .lil-slider {
|
|
2319
|
+
width: 100%;
|
|
2320
|
+
height: var(--widget-height);
|
|
2321
|
+
background: var(--widget-color);
|
|
2322
|
+
border-radius: var(--widget-border-radius);
|
|
2323
|
+
padding-right: var(--slider-knob-width);
|
|
2324
|
+
overflow: hidden;
|
|
2325
|
+
cursor: ew-resize;
|
|
2326
|
+
touch-action: pan-y;
|
|
2327
|
+
}
|
|
2328
|
+
@media (hover: hover) {
|
|
2329
|
+
.lil-controller.lil-number .lil-slider:hover {
|
|
2330
|
+
background: var(--hover-color);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
.lil-controller.lil-number .lil-slider.lil-active {
|
|
2334
|
+
background: var(--focus-color);
|
|
2335
|
+
}
|
|
2336
|
+
.lil-controller.lil-number .lil-slider.lil-active .lil-fill {
|
|
2337
|
+
opacity: 0.95;
|
|
2338
|
+
}
|
|
2339
|
+
.lil-controller.lil-number .lil-fill {
|
|
2340
|
+
height: 100%;
|
|
2341
|
+
border-right: var(--slider-knob-width) solid var(--number-color);
|
|
2342
|
+
box-sizing: content-box;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.lil-dragging .lil-gui {
|
|
2346
|
+
--hover-color: var(--widget-color);
|
|
2347
|
+
}
|
|
2348
|
+
.lil-dragging * {
|
|
2349
|
+
cursor: ew-resize !important;
|
|
2350
|
+
}
|
|
2351
|
+
.lil-dragging.lil-vertical * {
|
|
2352
|
+
cursor: ns-resize !important;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
.lil-gui .lil-title {
|
|
2356
|
+
height: var(--title-height);
|
|
2357
|
+
font-weight: 600;
|
|
2358
|
+
padding: 0 var(--padding);
|
|
2359
|
+
width: 100%;
|
|
2360
|
+
text-align: left;
|
|
2361
|
+
background: none;
|
|
2362
|
+
text-decoration-skip: objects;
|
|
2363
|
+
}
|
|
2364
|
+
.lil-gui .lil-title:before {
|
|
2365
|
+
font-family: "lil-gui";
|
|
2366
|
+
content: "\u25BE";
|
|
2367
|
+
padding-right: 2px;
|
|
2368
|
+
display: inline-block;
|
|
2369
|
+
}
|
|
2370
|
+
.lil-gui .lil-title:active {
|
|
2371
|
+
background: var(--title-background-color);
|
|
2372
|
+
opacity: 0.75;
|
|
2373
|
+
}
|
|
2374
|
+
@media (hover: hover) {
|
|
2375
|
+
body:not(.lil-dragging) .lil-gui .lil-title:hover {
|
|
2376
|
+
background: var(--title-background-color);
|
|
2377
|
+
opacity: 0.85;
|
|
2378
|
+
}
|
|
2379
|
+
.lil-gui .lil-title:focus {
|
|
2380
|
+
text-decoration: underline var(--focus-color);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
.lil-gui.lil-root > .lil-title:focus {
|
|
2384
|
+
text-decoration: none !important;
|
|
2385
|
+
}
|
|
2386
|
+
.lil-gui.lil-closed > .lil-title:before {
|
|
2387
|
+
content: "\u25B8";
|
|
2388
|
+
}
|
|
2389
|
+
.lil-gui.lil-closed > .lil-children {
|
|
2390
|
+
transform: translateY(-7px);
|
|
2391
|
+
opacity: 0;
|
|
2392
|
+
}
|
|
2393
|
+
.lil-gui.lil-closed:not(.lil-transition) > .lil-children {
|
|
2394
|
+
display: none;
|
|
2395
|
+
}
|
|
2396
|
+
.lil-gui.lil-transition > .lil-children {
|
|
2397
|
+
transition-duration: 300ms;
|
|
2398
|
+
transition-property: height, opacity, transform;
|
|
2399
|
+
transition-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1);
|
|
2400
|
+
overflow: hidden;
|
|
2401
|
+
pointer-events: none;
|
|
2402
|
+
}
|
|
2403
|
+
.lil-gui .lil-children:empty:before {
|
|
2404
|
+
content: "Empty";
|
|
2405
|
+
padding: 0 var(--padding);
|
|
2406
|
+
margin: var(--spacing) 0;
|
|
2407
|
+
display: block;
|
|
2408
|
+
height: var(--widget-height);
|
|
2409
|
+
font-style: italic;
|
|
2410
|
+
line-height: var(--widget-height);
|
|
2411
|
+
opacity: 0.5;
|
|
2412
|
+
}
|
|
2413
|
+
.lil-gui.lil-root > .lil-children > .lil-gui > .lil-title {
|
|
2414
|
+
border: 0 solid var(--widget-color);
|
|
2415
|
+
border-width: 1px 0;
|
|
2416
|
+
transition: border-color 300ms;
|
|
2417
|
+
}
|
|
2418
|
+
.lil-gui.lil-root > .lil-children > .lil-gui.lil-closed > .lil-title {
|
|
2419
|
+
border-bottom-color: transparent;
|
|
2420
|
+
}
|
|
2421
|
+
.lil-gui + .lil-controller {
|
|
2422
|
+
border-top: 1px solid var(--widget-color);
|
|
2423
|
+
margin-top: 0;
|
|
2424
|
+
padding-top: var(--spacing);
|
|
2425
|
+
}
|
|
2426
|
+
.lil-gui .lil-gui .lil-gui > .lil-title {
|
|
2427
|
+
border: none;
|
|
2428
|
+
}
|
|
2429
|
+
.lil-gui .lil-gui .lil-gui > .lil-children {
|
|
2430
|
+
border: none;
|
|
2431
|
+
margin-left: var(--folder-indent);
|
|
2432
|
+
border-left: 2px solid var(--widget-color);
|
|
2433
|
+
}
|
|
2434
|
+
.lil-gui .lil-gui .lil-controller {
|
|
2435
|
+
border: none;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
.lil-gui label, .lil-gui input, .lil-gui button {
|
|
2439
|
+
-webkit-tap-highlight-color: transparent;
|
|
2440
|
+
}
|
|
2441
|
+
.lil-gui input {
|
|
2442
|
+
border: 0;
|
|
2443
|
+
outline: none;
|
|
2444
|
+
font-family: var(--font-family);
|
|
2445
|
+
font-size: var(--input-font-size);
|
|
2446
|
+
border-radius: var(--widget-border-radius);
|
|
2447
|
+
height: var(--widget-height);
|
|
2448
|
+
background: var(--widget-color);
|
|
2449
|
+
color: var(--text-color);
|
|
2450
|
+
width: 100%;
|
|
2451
|
+
}
|
|
2452
|
+
@media (hover: hover) {
|
|
2453
|
+
.lil-gui input:hover {
|
|
2454
|
+
background: var(--hover-color);
|
|
2455
|
+
}
|
|
2456
|
+
.lil-gui input:active {
|
|
2457
|
+
background: var(--focus-color);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
.lil-gui input:disabled {
|
|
2461
|
+
opacity: 1;
|
|
2462
|
+
}
|
|
2463
|
+
.lil-gui input[type=text],
|
|
2464
|
+
.lil-gui input[type=number] {
|
|
2465
|
+
padding: var(--widget-padding);
|
|
2466
|
+
-moz-appearance: textfield;
|
|
2467
|
+
}
|
|
2468
|
+
.lil-gui input[type=text]:focus,
|
|
2469
|
+
.lil-gui input[type=number]:focus {
|
|
2470
|
+
background: var(--focus-color);
|
|
2471
|
+
}
|
|
2472
|
+
.lil-gui input[type=checkbox] {
|
|
2473
|
+
appearance: none;
|
|
2474
|
+
width: var(--checkbox-size);
|
|
2475
|
+
height: var(--checkbox-size);
|
|
2476
|
+
border-radius: var(--widget-border-radius);
|
|
2477
|
+
text-align: center;
|
|
2478
|
+
cursor: pointer;
|
|
2479
|
+
}
|
|
2480
|
+
.lil-gui input[type=checkbox]:checked:before {
|
|
2481
|
+
font-family: "lil-gui";
|
|
2482
|
+
content: "\u2713";
|
|
2483
|
+
font-size: var(--checkbox-size);
|
|
2484
|
+
line-height: var(--checkbox-size);
|
|
2485
|
+
}
|
|
2486
|
+
@media (hover: hover) {
|
|
2487
|
+
.lil-gui input[type=checkbox]:focus {
|
|
2488
|
+
box-shadow: inset 0 0 0 1px var(--focus-color);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
.lil-gui button {
|
|
2492
|
+
outline: none;
|
|
2493
|
+
cursor: pointer;
|
|
2494
|
+
font-family: var(--font-family);
|
|
2495
|
+
font-size: var(--font-size);
|
|
2496
|
+
color: var(--text-color);
|
|
2497
|
+
width: 100%;
|
|
2498
|
+
border: none;
|
|
2499
|
+
}
|
|
2500
|
+
.lil-gui .lil-controller button {
|
|
2501
|
+
height: var(--widget-height);
|
|
2502
|
+
text-transform: none;
|
|
2503
|
+
background: var(--widget-color);
|
|
2504
|
+
border-radius: var(--widget-border-radius);
|
|
2505
|
+
}
|
|
2506
|
+
@media (hover: hover) {
|
|
2507
|
+
.lil-gui .lil-controller button:hover {
|
|
2508
|
+
background: var(--hover-color);
|
|
2509
|
+
}
|
|
2510
|
+
.lil-gui .lil-controller button:focus {
|
|
2511
|
+
box-shadow: inset 0 0 0 1px var(--focus-color);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
.lil-gui .lil-controller button:active {
|
|
2515
|
+
background: var(--focus-color);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
@font-face {
|
|
2519
|
+
font-family: "lil-gui";
|
|
2520
|
+
src: url("data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAALkAAsAAAAABtQAAAKVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDMgqBBIEbATYCJAMUCwwABCAFhAoHgQQbHAbIDiUFEYVARAAAYQTVWNmz9MxhEgodq49wYRUFKE8GWNiUBxI2LBRaVnc51U83Gmhs0Q7JXWMiz5eteLwrKwuxHO8VFxUX9UpZBs6pa5ABRwHA+t3UxUnH20EvVknRerzQgX6xC/GH6ZUvTcAjAv122dF28OTqCXrPuyaDER30YBA1xnkVutDDo4oCi71Ca7rrV9xS8dZHbPHefsuwIyCpmT7j+MnjAH5X3984UZoFFuJ0yiZ4XEJFxjagEBeqs+e1iyK8Xf/nOuwF+vVK0ur765+vf7txotUi0m3N0m/84RGSrBCNrh8Ee5GjODjF4gnWP+dJrH/Lk9k4oT6d+gr6g/wssA2j64JJGP6cmx554vUZnpZfn6ZfX2bMwPPrlANsB86/DiHjhl0OP+c87+gaJo/gY084s3HoYL/ZkWHTRfBXvvoHnnkHvngKun4KBE/ede7tvq3/vQOxDXB1/fdNz6XbPdcr0Vhpojj9dG+owuSKFsslCi1tgEjirjXdwMiov2EioadxmqTHUCIwo8NgQaeIasAi0fTYSPTbSmwbMOFduyh9wvBrESGY0MtgRjtgQR8Q1bRPohn2UoCRZf9wyYANMXFeJTysqAe0I4mrherOekFdKMrYvJjLvOIUM9SuwYB5DVZUwwVjJJOaUnZCmcEkIZZrKqNvRGRMvmFZsmhP4VMKCSXBhSqUBxgMS7h0cZvEd71AWkEhGWaeMFcNnpqyJkyXgYL7PQ1MoSq0wDAkRtJIijkZSmqYTiSImfLiSWXIZwhRh3Rug2X0kk1Dgj+Iu43u5p98ghopcpSo0Uyc8SnjlYX59WUeaMoDqmVD2TOWD9a4pCRAzf2ECgwGcrHjPOWY9bNxq/OL3I/QjwEAAAA=") format("woff2");
|
|
2521
|
+
}`;
|
|
2522
|
+
stylesInjected = false;
|
|
2523
|
+
GUI = class _GUI {
|
|
2524
|
+
/**
|
|
2525
|
+
* Creates a panel that holds controllers.
|
|
2526
|
+
* @example
|
|
2527
|
+
* new GUI();
|
|
2528
|
+
* new GUI( { container: document.getElementById( 'custom' ) } );
|
|
2529
|
+
*
|
|
2530
|
+
* @param {object} [options]
|
|
2531
|
+
* @param {boolean} [options.autoPlace=true]
|
|
2532
|
+
* Adds the GUI to `document.body` and fixes it to the top right of the page.
|
|
2533
|
+
*
|
|
2534
|
+
* @param {Node} [options.container]
|
|
2535
|
+
* Adds the GUI to this DOM element. Overrides `autoPlace`.
|
|
2536
|
+
*
|
|
2537
|
+
* @param {number} [options.width=245]
|
|
2538
|
+
* Width of the GUI in pixels, usually set when name labels become too long. Note that you can make
|
|
2539
|
+
* name labels wider in CSS with `.lil‑gui { ‑‑name‑width: 55% }`.
|
|
2540
|
+
*
|
|
2541
|
+
* @param {string} [options.title=Controls]
|
|
2542
|
+
* Name to display in the title bar.
|
|
2543
|
+
*
|
|
2544
|
+
* @param {boolean} [options.closeFolders=false]
|
|
2545
|
+
* Pass `true` to close all folders in this GUI by default.
|
|
2546
|
+
*
|
|
2547
|
+
* @param {boolean} [options.injectStyles=true]
|
|
2548
|
+
* Injects the default stylesheet into the page if this is the first GUI.
|
|
2549
|
+
* Pass `false` to use your own stylesheet.
|
|
2550
|
+
*
|
|
2551
|
+
* @param {number} [options.touchStyles=true]
|
|
2552
|
+
* Makes controllers larger on touch devices. Pass `false` to disable touch styles.
|
|
2553
|
+
*
|
|
2554
|
+
* @param {GUI} [options.parent]
|
|
2555
|
+
* Adds this GUI as a child in another GUI. Usually this is done for you by `addFolder()`.
|
|
2556
|
+
*/
|
|
2557
|
+
constructor({
|
|
2558
|
+
parent,
|
|
2559
|
+
autoPlace = parent === void 0,
|
|
2560
|
+
container,
|
|
2561
|
+
width,
|
|
2562
|
+
title = "Controls",
|
|
2563
|
+
closeFolders = false,
|
|
2564
|
+
injectStyles = true,
|
|
2565
|
+
touchStyles = true
|
|
2566
|
+
} = {}) {
|
|
2567
|
+
this.parent = parent;
|
|
2568
|
+
this.root = parent ? parent.root : this;
|
|
2569
|
+
this.children = [];
|
|
2570
|
+
this.controllers = [];
|
|
2571
|
+
this.folders = [];
|
|
2572
|
+
this._closed = false;
|
|
2573
|
+
this._hidden = false;
|
|
2574
|
+
this.domElement = document.createElement("div");
|
|
2575
|
+
this.domElement.classList.add("lil-gui");
|
|
2576
|
+
this.$title = document.createElement("button");
|
|
2577
|
+
this.$title.classList.add("lil-title");
|
|
2578
|
+
this.$title.setAttribute("aria-expanded", true);
|
|
2579
|
+
this.$title.addEventListener("click", () => this.openAnimated(this._closed));
|
|
2580
|
+
this.$title.addEventListener("touchstart", () => {
|
|
2581
|
+
}, { passive: true });
|
|
2582
|
+
this.$children = document.createElement("div");
|
|
2583
|
+
this.$children.classList.add("lil-children");
|
|
2584
|
+
this.domElement.appendChild(this.$title);
|
|
2585
|
+
this.domElement.appendChild(this.$children);
|
|
2586
|
+
this.title(title);
|
|
2587
|
+
if (this.parent) {
|
|
2588
|
+
this.parent.children.push(this);
|
|
2589
|
+
this.parent.folders.push(this);
|
|
2590
|
+
this.parent.$children.appendChild(this.domElement);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
this.domElement.classList.add("lil-root");
|
|
2594
|
+
if (touchStyles) {
|
|
2595
|
+
this.domElement.classList.add("lil-allow-touch-styles");
|
|
2596
|
+
}
|
|
2597
|
+
if (!stylesInjected && injectStyles) {
|
|
2598
|
+
_injectStyles(stylesheet);
|
|
2599
|
+
stylesInjected = true;
|
|
2600
|
+
}
|
|
2601
|
+
if (container) {
|
|
2602
|
+
container.appendChild(this.domElement);
|
|
2603
|
+
} else if (autoPlace) {
|
|
2604
|
+
this.domElement.classList.add("lil-auto-place", "autoPlace");
|
|
2605
|
+
document.body.appendChild(this.domElement);
|
|
2606
|
+
}
|
|
2607
|
+
if (width) {
|
|
2608
|
+
this.domElement.style.setProperty("--width", width + "px");
|
|
2609
|
+
}
|
|
2610
|
+
this._closeFolders = closeFolders;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Adds a controller to the GUI, inferring controller type using the `typeof` operator.
|
|
2614
|
+
* @example
|
|
2615
|
+
* gui.add( object, 'property' );
|
|
2616
|
+
* gui.add( object, 'number', 0, 100, 1 );
|
|
2617
|
+
* gui.add( object, 'options', [ 1, 2, 3 ] );
|
|
2618
|
+
*
|
|
2619
|
+
* @param {object} object The object the controller will modify.
|
|
2620
|
+
* @param {string} property Name of the property to control.
|
|
2621
|
+
* @param {number|object|Array} [$1] Minimum value for number controllers, or the set of
|
|
2622
|
+
* selectable values for a dropdown.
|
|
2623
|
+
* @param {number} [max] Maximum value for number controllers.
|
|
2624
|
+
* @param {number} [step] Step value for number controllers.
|
|
2625
|
+
* @returns {Controller}
|
|
2626
|
+
*/
|
|
2627
|
+
add(object, property, $1, max, step) {
|
|
2628
|
+
if (Object($1) === $1) {
|
|
2629
|
+
return new OptionController(this, object, property, $1);
|
|
2630
|
+
}
|
|
2631
|
+
const initialValue = object[property];
|
|
2632
|
+
switch (typeof initialValue) {
|
|
2633
|
+
case "number":
|
|
2634
|
+
return new NumberController(this, object, property, $1, max, step);
|
|
2635
|
+
case "boolean":
|
|
2636
|
+
return new BooleanController(this, object, property);
|
|
2637
|
+
case "string":
|
|
2638
|
+
return new StringController(this, object, property);
|
|
2639
|
+
case "function":
|
|
2640
|
+
return new FunctionController(this, object, property);
|
|
2641
|
+
}
|
|
2642
|
+
console.error(`gui.add failed
|
|
2643
|
+
property:`, property, `
|
|
2644
|
+
object:`, object, `
|
|
2645
|
+
value:`, initialValue);
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Adds a color controller to the GUI.
|
|
2649
|
+
* @example
|
|
2650
|
+
* params = {
|
|
2651
|
+
* cssColor: '#ff00ff',
|
|
2652
|
+
* rgbColor: { r: 0, g: 0.2, b: 0.4 },
|
|
2653
|
+
* customRange: [ 0, 127, 255 ],
|
|
2654
|
+
* };
|
|
2655
|
+
*
|
|
2656
|
+
* gui.addColor( params, 'cssColor' );
|
|
2657
|
+
* gui.addColor( params, 'rgbColor' );
|
|
2658
|
+
* gui.addColor( params, 'customRange', 255 );
|
|
2659
|
+
*
|
|
2660
|
+
* @param {object} object The object the controller will modify.
|
|
2661
|
+
* @param {string} property Name of the property to control.
|
|
2662
|
+
* @param {number} rgbScale Maximum value for a color channel when using an RGB color. You may
|
|
2663
|
+
* need to set this to 255 if your colors are too bright.
|
|
2664
|
+
* @returns {Controller}
|
|
2665
|
+
*/
|
|
2666
|
+
addColor(object, property, rgbScale = 1) {
|
|
2667
|
+
return new ColorController(this, object, property, rgbScale);
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Adds a folder to the GUI, which is just another GUI. This method returns
|
|
2671
|
+
* the nested GUI so you can add controllers to it.
|
|
2672
|
+
* @example
|
|
2673
|
+
* const folder = gui.addFolder( 'Position' );
|
|
2674
|
+
* folder.add( position, 'x' );
|
|
2675
|
+
* folder.add( position, 'y' );
|
|
2676
|
+
* folder.add( position, 'z' );
|
|
2677
|
+
*
|
|
2678
|
+
* @param {string} title Name to display in the folder's title bar.
|
|
2679
|
+
* @returns {GUI}
|
|
2680
|
+
*/
|
|
2681
|
+
addFolder(title) {
|
|
2682
|
+
const folder = new _GUI({ parent: this, title });
|
|
2683
|
+
if (this.root._closeFolders) folder.close();
|
|
2684
|
+
return folder;
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* Recalls values that were saved with `gui.save()`.
|
|
2688
|
+
* @param {object} obj
|
|
2689
|
+
* @param {boolean} recursive Pass false to exclude folders descending from this GUI.
|
|
2690
|
+
* @returns {this}
|
|
2691
|
+
*/
|
|
2692
|
+
load(obj, recursive = true) {
|
|
2693
|
+
if (obj.controllers) {
|
|
2694
|
+
this.controllers.forEach((c) => {
|
|
2695
|
+
if (c instanceof FunctionController) return;
|
|
2696
|
+
if (c._name in obj.controllers) {
|
|
2697
|
+
c.load(obj.controllers[c._name]);
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if (recursive && obj.folders) {
|
|
2702
|
+
this.folders.forEach((f) => {
|
|
2703
|
+
if (f._title in obj.folders) {
|
|
2704
|
+
f.load(obj.folders[f._title]);
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
return this;
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Returns an object mapping controller names to values. The object can be passed to `gui.load()` to
|
|
2712
|
+
* recall these values.
|
|
2713
|
+
* @example
|
|
2714
|
+
* {
|
|
2715
|
+
* controllers: {
|
|
2716
|
+
* prop1: 1,
|
|
2717
|
+
* prop2: 'value',
|
|
2718
|
+
* ...
|
|
2719
|
+
* },
|
|
2720
|
+
* folders: {
|
|
2721
|
+
* folderName1: { controllers, folders },
|
|
2722
|
+
* folderName2: { controllers, folders }
|
|
2723
|
+
* ...
|
|
2724
|
+
* }
|
|
2725
|
+
* }
|
|
2726
|
+
*
|
|
2727
|
+
* @param {boolean} recursive Pass false to exclude folders descending from this GUI.
|
|
2728
|
+
* @returns {object}
|
|
2729
|
+
*/
|
|
2730
|
+
save(recursive = true) {
|
|
2731
|
+
const obj = {
|
|
2732
|
+
controllers: {},
|
|
2733
|
+
folders: {}
|
|
2734
|
+
};
|
|
2735
|
+
this.controllers.forEach((c) => {
|
|
2736
|
+
if (c instanceof FunctionController) return;
|
|
2737
|
+
if (c._name in obj.controllers) {
|
|
2738
|
+
throw new Error(`Cannot save GUI with duplicate property "${c._name}"`);
|
|
2739
|
+
}
|
|
2740
|
+
obj.controllers[c._name] = c.save();
|
|
2741
|
+
});
|
|
2742
|
+
if (recursive) {
|
|
2743
|
+
this.folders.forEach((f) => {
|
|
2744
|
+
if (f._title in obj.folders) {
|
|
2745
|
+
throw new Error(`Cannot save GUI with duplicate folder "${f._title}"`);
|
|
2746
|
+
}
|
|
2747
|
+
obj.folders[f._title] = f.save();
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
return obj;
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Opens a GUI or folder. GUI and folders are open by default.
|
|
2754
|
+
* @param {boolean} open Pass false to close.
|
|
2755
|
+
* @returns {this}
|
|
2756
|
+
* @example
|
|
2757
|
+
* gui.open(); // open
|
|
2758
|
+
* gui.open( false ); // close
|
|
2759
|
+
* gui.open( gui._closed ); // toggle
|
|
2760
|
+
*/
|
|
2761
|
+
open(open = true) {
|
|
2762
|
+
this._setClosed(!open);
|
|
2763
|
+
this.$title.setAttribute("aria-expanded", !this._closed);
|
|
2764
|
+
this.domElement.classList.toggle("lil-closed", this._closed);
|
|
2765
|
+
return this;
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Closes the GUI.
|
|
2769
|
+
* @returns {this}
|
|
2770
|
+
*/
|
|
2771
|
+
close() {
|
|
2772
|
+
return this.open(false);
|
|
2773
|
+
}
|
|
2774
|
+
_setClosed(closed) {
|
|
2775
|
+
if (this._closed === closed) return;
|
|
2776
|
+
this._closed = closed;
|
|
2777
|
+
this._callOnOpenClose(this);
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Shows the GUI after it's been hidden.
|
|
2781
|
+
* @param {boolean} show
|
|
2782
|
+
* @returns {this}
|
|
2783
|
+
* @example
|
|
2784
|
+
* gui.show();
|
|
2785
|
+
* gui.show( false ); // hide
|
|
2786
|
+
* gui.show( gui._hidden ); // toggle
|
|
2787
|
+
*/
|
|
2788
|
+
show(show = true) {
|
|
2789
|
+
this._hidden = !show;
|
|
2790
|
+
this.domElement.style.display = this._hidden ? "none" : "";
|
|
2791
|
+
return this;
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Hides the GUI.
|
|
2795
|
+
* @returns {this}
|
|
2796
|
+
*/
|
|
2797
|
+
hide() {
|
|
2798
|
+
return this.show(false);
|
|
2799
|
+
}
|
|
2800
|
+
openAnimated(open = true) {
|
|
2801
|
+
this._setClosed(!open);
|
|
2802
|
+
this.$title.setAttribute("aria-expanded", !this._closed);
|
|
2803
|
+
requestAnimationFrame(() => {
|
|
2804
|
+
const initialHeight = this.$children.clientHeight;
|
|
2805
|
+
this.$children.style.height = initialHeight + "px";
|
|
2806
|
+
this.domElement.classList.add("lil-transition");
|
|
2807
|
+
const onTransitionEnd = (e) => {
|
|
2808
|
+
if (e.target !== this.$children) return;
|
|
2809
|
+
this.$children.style.height = "";
|
|
2810
|
+
this.domElement.classList.remove("lil-transition");
|
|
2811
|
+
this.$children.removeEventListener("transitionend", onTransitionEnd);
|
|
2812
|
+
};
|
|
2813
|
+
this.$children.addEventListener("transitionend", onTransitionEnd);
|
|
2814
|
+
const targetHeight = !open ? 0 : this.$children.scrollHeight;
|
|
2815
|
+
this.domElement.classList.toggle("lil-closed", !open);
|
|
2816
|
+
requestAnimationFrame(() => {
|
|
2817
|
+
this.$children.style.height = targetHeight + "px";
|
|
2818
|
+
});
|
|
2819
|
+
});
|
|
2820
|
+
return this;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Change the title of this GUI.
|
|
2824
|
+
* @param {string} title
|
|
2825
|
+
* @returns {this}
|
|
2826
|
+
*/
|
|
2827
|
+
title(title) {
|
|
2828
|
+
this._title = title;
|
|
2829
|
+
this.$title.textContent = title;
|
|
2830
|
+
return this;
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Resets all controllers to their initial values.
|
|
2834
|
+
* @param {boolean} recursive Pass false to exclude folders descending from this GUI.
|
|
2835
|
+
* @returns {this}
|
|
2836
|
+
*/
|
|
2837
|
+
reset(recursive = true) {
|
|
2838
|
+
const controllers = recursive ? this.controllersRecursive() : this.controllers;
|
|
2839
|
+
controllers.forEach((c) => c.reset());
|
|
2840
|
+
return this;
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Pass a function to be called whenever a controller in this GUI changes.
|
|
2844
|
+
* @param {function({object:object, property:string, value:any, controller:Controller})} callback
|
|
2845
|
+
* @returns {this}
|
|
2846
|
+
* @example
|
|
2847
|
+
* gui.onChange( event => {
|
|
2848
|
+
* event.object // object that was modified
|
|
2849
|
+
* event.property // string, name of property
|
|
2850
|
+
* event.value // new value of controller
|
|
2851
|
+
* event.controller // controller that was modified
|
|
2852
|
+
* } );
|
|
2853
|
+
*/
|
|
2854
|
+
onChange(callback) {
|
|
2855
|
+
this._onChange = callback;
|
|
2856
|
+
return this;
|
|
2857
|
+
}
|
|
2858
|
+
_callOnChange(controller) {
|
|
2859
|
+
if (this.parent) {
|
|
2860
|
+
this.parent._callOnChange(controller);
|
|
2861
|
+
}
|
|
2862
|
+
if (this._onChange !== void 0) {
|
|
2863
|
+
this._onChange.call(this, {
|
|
2864
|
+
object: controller.object,
|
|
2865
|
+
property: controller.property,
|
|
2866
|
+
value: controller.getValue(),
|
|
2867
|
+
controller
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Pass a function to be called whenever a controller in this GUI has finished changing.
|
|
2873
|
+
* @param {function({object:object, property:string, value:any, controller:Controller})} callback
|
|
2874
|
+
* @returns {this}
|
|
2875
|
+
* @example
|
|
2876
|
+
* gui.onFinishChange( event => {
|
|
2877
|
+
* event.object // object that was modified
|
|
2878
|
+
* event.property // string, name of property
|
|
2879
|
+
* event.value // new value of controller
|
|
2880
|
+
* event.controller // controller that was modified
|
|
2881
|
+
* } );
|
|
2882
|
+
*/
|
|
2883
|
+
onFinishChange(callback) {
|
|
2884
|
+
this._onFinishChange = callback;
|
|
2885
|
+
return this;
|
|
2886
|
+
}
|
|
2887
|
+
_callOnFinishChange(controller) {
|
|
2888
|
+
if (this.parent) {
|
|
2889
|
+
this.parent._callOnFinishChange(controller);
|
|
2890
|
+
}
|
|
2891
|
+
if (this._onFinishChange !== void 0) {
|
|
2892
|
+
this._onFinishChange.call(this, {
|
|
2893
|
+
object: controller.object,
|
|
2894
|
+
property: controller.property,
|
|
2895
|
+
value: controller.getValue(),
|
|
2896
|
+
controller
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Pass a function to be called when this GUI or its descendants are opened or closed.
|
|
2902
|
+
* @param {function(GUI)} callback
|
|
2903
|
+
* @returns {this}
|
|
2904
|
+
* @example
|
|
2905
|
+
* gui.onOpenClose( changedGUI => {
|
|
2906
|
+
* console.log( changedGUI._closed );
|
|
2907
|
+
* } );
|
|
2908
|
+
*/
|
|
2909
|
+
onOpenClose(callback) {
|
|
2910
|
+
this._onOpenClose = callback;
|
|
2911
|
+
return this;
|
|
2912
|
+
}
|
|
2913
|
+
_callOnOpenClose(changedGUI) {
|
|
2914
|
+
if (this.parent) {
|
|
2915
|
+
this.parent._callOnOpenClose(changedGUI);
|
|
2916
|
+
}
|
|
2917
|
+
if (this._onOpenClose !== void 0) {
|
|
2918
|
+
this._onOpenClose.call(this, changedGUI);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* Destroys all DOM elements and event listeners associated with this GUI.
|
|
2923
|
+
*/
|
|
2924
|
+
destroy() {
|
|
2925
|
+
if (this.parent) {
|
|
2926
|
+
this.parent.children.splice(this.parent.children.indexOf(this), 1);
|
|
2927
|
+
this.parent.folders.splice(this.parent.folders.indexOf(this), 1);
|
|
2928
|
+
}
|
|
2929
|
+
if (this.domElement.parentElement) {
|
|
2930
|
+
this.domElement.parentElement.removeChild(this.domElement);
|
|
2931
|
+
}
|
|
2932
|
+
Array.from(this.children).forEach((c) => c.destroy());
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Returns an array of controllers contained by this GUI and its descendents.
|
|
2936
|
+
* @returns {Controller[]}
|
|
2937
|
+
*/
|
|
2938
|
+
controllersRecursive() {
|
|
2939
|
+
let controllers = Array.from(this.controllers);
|
|
2940
|
+
this.folders.forEach((f) => {
|
|
2941
|
+
controllers = controllers.concat(f.controllersRecursive());
|
|
2942
|
+
});
|
|
2943
|
+
return controllers;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Returns an array of folders contained by this GUI and its descendents.
|
|
2947
|
+
* @returns {GUI[]}
|
|
2948
|
+
*/
|
|
2949
|
+
foldersRecursive() {
|
|
2950
|
+
let folders = Array.from(this.folders);
|
|
2951
|
+
this.folders.forEach((f) => {
|
|
2952
|
+
folders = folders.concat(f.foldersRecursive());
|
|
2953
|
+
});
|
|
2954
|
+
return folders;
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
|
|
2960
|
+
// node_modules/lil-gui/dist/lil-gui.css
|
|
2961
|
+
var lil_gui_default;
|
|
2962
|
+
var init_lil_gui = __esm({
|
|
2963
|
+
"node_modules/lil-gui/dist/lil-gui.css"() {
|
|
2964
|
+
lil_gui_default = '.lil-gui {\n font-family: var(--font-family);\n font-size: var(--font-size);\n line-height: 1;\n font-weight: normal;\n font-style: normal;\n text-align: left;\n color: var(--text-color);\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n --background-color: #1f1f1f;\n --text-color: #ebebeb;\n --title-background-color: #111111;\n --title-text-color: #ebebeb;\n --widget-color: #424242;\n --hover-color: #4f4f4f;\n --focus-color: #595959;\n --number-color: #2cc9ff;\n --string-color: #a2db3c;\n --font-size: 11px;\n --input-font-size: 11px;\n --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;\n --font-family-mono: Menlo, Monaco, Consolas, "Droid Sans Mono", monospace;\n --padding: 4px;\n --spacing: 4px;\n --widget-height: 20px;\n --title-height: calc(var(--widget-height) + var(--spacing) * 1.25);\n --name-width: 45%;\n --slider-knob-width: 2px;\n --slider-input-width: 27%;\n --color-input-width: 27%;\n --slider-input-min-width: 45px;\n --color-input-min-width: 45px;\n --folder-indent: 7px;\n --widget-padding: 0 0 0 3px;\n --widget-border-radius: 2px;\n --checkbox-size: calc(0.75 * var(--widget-height));\n --scrollbar-width: 5px;\n}\n.lil-gui, .lil-gui * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n.lil-gui.lil-root {\n width: var(--width, 245px);\n display: flex;\n flex-direction: column;\n background: var(--background-color);\n}\n.lil-gui.lil-root > .lil-title {\n background: var(--title-background-color);\n color: var(--title-text-color);\n}\n.lil-gui.lil-root > .lil-children {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.lil-gui.lil-root > .lil-children::-webkit-scrollbar {\n width: var(--scrollbar-width);\n height: var(--scrollbar-width);\n background: var(--background-color);\n}\n.lil-gui.lil-root > .lil-children::-webkit-scrollbar-thumb {\n border-radius: var(--scrollbar-width);\n background: var(--focus-color);\n}\n@media (pointer: coarse) {\n .lil-gui.lil-allow-touch-styles, .lil-gui.lil-allow-touch-styles .lil-gui {\n --widget-height: 28px;\n --padding: 6px;\n --spacing: 6px;\n --font-size: 13px;\n --input-font-size: 16px;\n --folder-indent: 10px;\n --scrollbar-width: 7px;\n --slider-input-min-width: 50px;\n --color-input-min-width: 65px;\n }\n}\n.lil-gui.lil-force-touch-styles, .lil-gui.lil-force-touch-styles .lil-gui {\n --widget-height: 28px;\n --padding: 6px;\n --spacing: 6px;\n --font-size: 13px;\n --input-font-size: 16px;\n --folder-indent: 10px;\n --scrollbar-width: 7px;\n --slider-input-min-width: 50px;\n --color-input-min-width: 65px;\n}\n.lil-gui.lil-auto-place, .lil-gui.autoPlace {\n max-height: 100%;\n position: fixed;\n top: 0;\n right: 15px;\n z-index: 1001;\n}\n\n.lil-controller {\n display: flex;\n align-items: center;\n padding: 0 var(--padding);\n margin: var(--spacing) 0;\n}\n.lil-controller.lil-disabled {\n opacity: 0.5;\n}\n.lil-controller.lil-disabled, .lil-controller.lil-disabled * {\n pointer-events: none !important;\n}\n.lil-controller > .lil-name {\n min-width: var(--name-width);\n flex-shrink: 0;\n white-space: pre;\n padding-right: var(--spacing);\n line-height: var(--widget-height);\n}\n.lil-controller .lil-widget {\n position: relative;\n display: flex;\n align-items: center;\n width: 100%;\n min-height: var(--widget-height);\n}\n.lil-controller.lil-string input {\n color: var(--string-color);\n}\n.lil-controller.lil-boolean {\n cursor: pointer;\n}\n.lil-controller.lil-color .lil-display {\n width: 100%;\n height: var(--widget-height);\n border-radius: var(--widget-border-radius);\n position: relative;\n}\n@media (hover: hover) {\n .lil-controller.lil-color .lil-display:hover:before {\n content: " ";\n display: block;\n position: absolute;\n border-radius: var(--widget-border-radius);\n border: 1px solid #fff9;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n }\n}\n.lil-controller.lil-color input[type=color] {\n opacity: 0;\n width: 100%;\n height: 100%;\n cursor: pointer;\n}\n.lil-controller.lil-color input[type=text] {\n margin-left: var(--spacing);\n font-family: var(--font-family-mono);\n min-width: var(--color-input-min-width);\n width: var(--color-input-width);\n flex-shrink: 0;\n}\n.lil-controller.lil-option select {\n opacity: 0;\n position: absolute;\n width: 100%;\n max-width: 100%;\n}\n.lil-controller.lil-option .lil-display {\n position: relative;\n pointer-events: none;\n border-radius: var(--widget-border-radius);\n height: var(--widget-height);\n line-height: var(--widget-height);\n max-width: 100%;\n overflow: hidden;\n word-break: break-all;\n padding-left: 0.55em;\n padding-right: 1.75em;\n background: var(--widget-color);\n}\n@media (hover: hover) {\n .lil-controller.lil-option .lil-display.lil-focus {\n background: var(--focus-color);\n }\n}\n.lil-controller.lil-option .lil-display.lil-active {\n background: var(--focus-color);\n}\n.lil-controller.lil-option .lil-display:after {\n font-family: "lil-gui";\n content: "\u2195";\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n padding-right: 0.375em;\n}\n.lil-controller.lil-option .lil-widget,\n.lil-controller.lil-option select {\n cursor: pointer;\n}\n@media (hover: hover) {\n .lil-controller.lil-option .lil-widget:hover .lil-display {\n background: var(--hover-color);\n }\n}\n.lil-controller.lil-number input {\n color: var(--number-color);\n}\n.lil-controller.lil-number.lil-has-slider input {\n margin-left: var(--spacing);\n width: var(--slider-input-width);\n min-width: var(--slider-input-min-width);\n flex-shrink: 0;\n}\n.lil-controller.lil-number .lil-slider {\n width: 100%;\n height: var(--widget-height);\n background: var(--widget-color);\n border-radius: var(--widget-border-radius);\n padding-right: var(--slider-knob-width);\n overflow: hidden;\n cursor: ew-resize;\n touch-action: pan-y;\n}\n@media (hover: hover) {\n .lil-controller.lil-number .lil-slider:hover {\n background: var(--hover-color);\n }\n}\n.lil-controller.lil-number .lil-slider.lil-active {\n background: var(--focus-color);\n}\n.lil-controller.lil-number .lil-slider.lil-active .lil-fill {\n opacity: 0.95;\n}\n.lil-controller.lil-number .lil-fill {\n height: 100%;\n border-right: var(--slider-knob-width) solid var(--number-color);\n box-sizing: content-box;\n}\n\n.lil-dragging .lil-gui {\n --hover-color: var(--widget-color);\n}\n.lil-dragging * {\n cursor: ew-resize !important;\n}\n.lil-dragging.lil-vertical * {\n cursor: ns-resize !important;\n}\n\n.lil-gui .lil-title {\n height: var(--title-height);\n font-weight: 600;\n padding: 0 var(--padding);\n width: 100%;\n text-align: left;\n background: none;\n text-decoration-skip: objects;\n}\n.lil-gui .lil-title:before {\n font-family: "lil-gui";\n content: "\u25BE";\n padding-right: 2px;\n display: inline-block;\n}\n.lil-gui .lil-title:active {\n background: var(--title-background-color);\n opacity: 0.75;\n}\n@media (hover: hover) {\n body:not(.lil-dragging) .lil-gui .lil-title:hover {\n background: var(--title-background-color);\n opacity: 0.85;\n }\n .lil-gui .lil-title:focus {\n text-decoration: underline var(--focus-color);\n }\n}\n.lil-gui.lil-root > .lil-title:focus {\n text-decoration: none !important;\n}\n.lil-gui.lil-closed > .lil-title:before {\n content: "\u25B8";\n}\n.lil-gui.lil-closed > .lil-children {\n transform: translateY(-7px);\n opacity: 0;\n}\n.lil-gui.lil-closed:not(.lil-transition) > .lil-children {\n display: none;\n}\n.lil-gui.lil-transition > .lil-children {\n transition-duration: 300ms;\n transition-property: height, opacity, transform;\n transition-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1);\n overflow: hidden;\n pointer-events: none;\n}\n.lil-gui .lil-children:empty:before {\n content: "Empty";\n padding: 0 var(--padding);\n margin: var(--spacing) 0;\n display: block;\n height: var(--widget-height);\n font-style: italic;\n line-height: var(--widget-height);\n opacity: 0.5;\n}\n.lil-gui.lil-root > .lil-children > .lil-gui > .lil-title {\n border: 0 solid var(--widget-color);\n border-width: 1px 0;\n transition: border-color 300ms;\n}\n.lil-gui.lil-root > .lil-children > .lil-gui.lil-closed > .lil-title {\n border-bottom-color: transparent;\n}\n.lil-gui + .lil-controller {\n border-top: 1px solid var(--widget-color);\n margin-top: 0;\n padding-top: var(--spacing);\n}\n.lil-gui .lil-gui .lil-gui > .lil-title {\n border: none;\n}\n.lil-gui .lil-gui .lil-gui > .lil-children {\n border: none;\n margin-left: var(--folder-indent);\n border-left: 2px solid var(--widget-color);\n}\n.lil-gui .lil-gui .lil-controller {\n border: none;\n}\n\n.lil-gui label, .lil-gui input, .lil-gui button {\n -webkit-tap-highlight-color: transparent;\n}\n.lil-gui input {\n border: 0;\n outline: none;\n font-family: var(--font-family);\n font-size: var(--input-font-size);\n border-radius: var(--widget-border-radius);\n height: var(--widget-height);\n background: var(--widget-color);\n color: var(--text-color);\n width: 100%;\n}\n@media (hover: hover) {\n .lil-gui input:hover {\n background: var(--hover-color);\n }\n .lil-gui input:active {\n background: var(--focus-color);\n }\n}\n.lil-gui input:disabled {\n opacity: 1;\n}\n.lil-gui input[type=text],\n.lil-gui input[type=number] {\n padding: var(--widget-padding);\n -moz-appearance: textfield;\n}\n.lil-gui input[type=text]:focus,\n.lil-gui input[type=number]:focus {\n background: var(--focus-color);\n}\n.lil-gui input[type=checkbox] {\n appearance: none;\n width: var(--checkbox-size);\n height: var(--checkbox-size);\n border-radius: var(--widget-border-radius);\n text-align: center;\n cursor: pointer;\n}\n.lil-gui input[type=checkbox]:checked:before {\n font-family: "lil-gui";\n content: "\u2713";\n font-size: var(--checkbox-size);\n line-height: var(--checkbox-size);\n}\n@media (hover: hover) {\n .lil-gui input[type=checkbox]:focus {\n box-shadow: inset 0 0 0 1px var(--focus-color);\n }\n}\n.lil-gui button {\n outline: none;\n cursor: pointer;\n font-family: var(--font-family);\n font-size: var(--font-size);\n color: var(--text-color);\n width: 100%;\n border: none;\n}\n.lil-gui .lil-controller button {\n height: var(--widget-height);\n text-transform: none;\n background: var(--widget-color);\n border-radius: var(--widget-border-radius);\n}\n@media (hover: hover) {\n .lil-gui .lil-controller button:hover {\n background: var(--hover-color);\n }\n .lil-gui .lil-controller button:focus {\n box-shadow: inset 0 0 0 1px var(--focus-color);\n }\n}\n.lil-gui .lil-controller button:active {\n background: var(--focus-color);\n}\n\n@font-face {\n font-family: "lil-gui";\n src: url("data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAAALkAAsAAAAABtQAAAKVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDMgqBBIEbATYCJAMUCwwABCAFhAoHgQQbHAbIDiUFEYVARAAAYQTVWNmz9MxhEgodq49wYRUFKE8GWNiUBxI2LBRaVnc51U83Gmhs0Q7JXWMiz5eteLwrKwuxHO8VFxUX9UpZBs6pa5ABRwHA+t3UxUnH20EvVknRerzQgX6xC/GH6ZUvTcAjAv122dF28OTqCXrPuyaDER30YBA1xnkVutDDo4oCi71Ca7rrV9xS8dZHbPHefsuwIyCpmT7j+MnjAH5X3984UZoFFuJ0yiZ4XEJFxjagEBeqs+e1iyK8Xf/nOuwF+vVK0ur765+vf7txotUi0m3N0m/84RGSrBCNrh8Ee5GjODjF4gnWP+dJrH/Lk9k4oT6d+gr6g/wssA2j64JJGP6cmx554vUZnpZfn6ZfX2bMwPPrlANsB86/DiHjhl0OP+c87+gaJo/gY084s3HoYL/ZkWHTRfBXvvoHnnkHvngKun4KBE/ede7tvq3/vQOxDXB1/fdNz6XbPdcr0Vhpojj9dG+owuSKFsslCi1tgEjirjXdwMiov2EioadxmqTHUCIwo8NgQaeIasAi0fTYSPTbSmwbMOFduyh9wvBrESGY0MtgRjtgQR8Q1bRPohn2UoCRZf9wyYANMXFeJTysqAe0I4mrherOekFdKMrYvJjLvOIUM9SuwYB5DVZUwwVjJJOaUnZCmcEkIZZrKqNvRGRMvmFZsmhP4VMKCSXBhSqUBxgMS7h0cZvEd71AWkEhGWaeMFcNnpqyJkyXgYL7PQ1MoSq0wDAkRtJIijkZSmqYTiSImfLiSWXIZwhRh3Rug2X0kk1Dgj+Iu43u5p98ghopcpSo0Uyc8SnjlYX59WUeaMoDqmVD2TOWD9a4pCRAzf2ECgwGcrHjPOWY9bNxq/OL3I/QjwEAAAA=") format("woff2");\n}\n';
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
|
|
2968
|
+
// src/viewer.js
|
|
2969
|
+
var viewer_exports = {};
|
|
2970
|
+
__export(viewer_exports, {
|
|
2971
|
+
adopt: () => adopt,
|
|
2972
|
+
viewer: () => viewer
|
|
2973
|
+
});
|
|
2974
|
+
function safeStringify(arg) {
|
|
2975
|
+
if (typeof arg === "string") return arg;
|
|
2976
|
+
if (arg instanceof Error) return arg.stack || arg.message;
|
|
2977
|
+
try {
|
|
2978
|
+
return JSON.stringify(arg);
|
|
2979
|
+
} catch {
|
|
2980
|
+
return String(arg);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
function appendDebugLine(text, isError) {
|
|
2984
|
+
if (!debugLogEl) return;
|
|
2985
|
+
const line = document.createElement("div");
|
|
2986
|
+
if (isError) line.className = "err";
|
|
2987
|
+
line.textContent = `[${(/* @__PURE__ */ new Date()).toISOString().slice(11, 23)}] ${text}`;
|
|
2988
|
+
debugLogEl.appendChild(line);
|
|
2989
|
+
while (debugLogEl.childElementCount > MAX_DEBUG_LINES) {
|
|
2990
|
+
debugLogEl.removeChild(debugLogEl.firstElementChild);
|
|
2991
|
+
}
|
|
2992
|
+
debugLogEl.scrollTop = debugLogEl.scrollHeight;
|
|
2993
|
+
}
|
|
2994
|
+
function trace(...args) {
|
|
2995
|
+
appendDebugLine(args.map(safeStringify).join(" "), false);
|
|
2996
|
+
if (traceToConsole) console.log(...args);
|
|
2997
|
+
}
|
|
2998
|
+
function fail(...args) {
|
|
2999
|
+
appendDebugLine(args.map(safeStringify).join(" "), true);
|
|
3000
|
+
console.error(...args);
|
|
3001
|
+
}
|
|
3002
|
+
function setMode(id) {
|
|
3003
|
+
if (currentMode === id) return;
|
|
3004
|
+
currentMode = id;
|
|
3005
|
+
for (const [modeId, btn] of modeButtons) {
|
|
3006
|
+
btn.classList.toggle("mode-active", modeId === id);
|
|
3007
|
+
}
|
|
3008
|
+
modulePromise.then((Module) => {
|
|
3009
|
+
Module.setMeasureMode(id === "measure");
|
|
3010
|
+
refreshRulerRow(Module);
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
function refreshRulerRow(Module) {
|
|
3014
|
+
const count = Module.measurementCount();
|
|
3015
|
+
rulerRow.style.display = count > 0 ? "" : "none";
|
|
3016
|
+
rulerClearBtn.textContent = `Clear ${count}`;
|
|
3017
|
+
}
|
|
3018
|
+
function setFileChip(controller, name, { idleLabel, idleTitle, unloadTitle, onUnload }) {
|
|
3019
|
+
const existingX = controller.domElement.querySelector(".lyp-unload");
|
|
3020
|
+
if (existingX) existingX.remove();
|
|
3021
|
+
controller.domElement.classList.toggle("lyp-loaded", !!name);
|
|
3022
|
+
if (!name) {
|
|
3023
|
+
controller.name(idleLabel);
|
|
3024
|
+
controller.domElement.title = idleTitle;
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
controller.name(name);
|
|
3028
|
+
controller.domElement.title = `${name} \u2014 click to replace, \u2715 to unload`;
|
|
3029
|
+
const x = document.createElement("span");
|
|
3030
|
+
x.className = "lyp-unload";
|
|
3031
|
+
x.textContent = "\u2715";
|
|
3032
|
+
x.title = unloadTitle;
|
|
3033
|
+
x.addEventListener("click", (event) => {
|
|
3034
|
+
event.stopPropagation();
|
|
3035
|
+
onUnload();
|
|
3036
|
+
});
|
|
3037
|
+
controller.domElement.appendChild(x);
|
|
3038
|
+
}
|
|
3039
|
+
function setLypChip(name) {
|
|
3040
|
+
setFileChip(lypController, name, {
|
|
3041
|
+
idleLabel: "Load .lyp File",
|
|
3042
|
+
idleTitle: "Load a .lyp layer-properties file",
|
|
3043
|
+
unloadTitle: "Unload .lyp",
|
|
3044
|
+
onUnload: () => {
|
|
3045
|
+
modulePromise.then((Module) => {
|
|
3046
|
+
Module.loadLypText("");
|
|
3047
|
+
renderLayerList(Module.getLayers());
|
|
3048
|
+
});
|
|
3049
|
+
hostCall("unloadLyp");
|
|
3050
|
+
setLypChip(null);
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
function setMarkerChip(name) {
|
|
3055
|
+
setFileChip(markerController, name, {
|
|
3056
|
+
idleLabel: "Load Marker File (.lyrdb / DRC)",
|
|
3057
|
+
idleTitle: "Load a .lyrdb report database or ASCII DRC results database",
|
|
3058
|
+
unloadTitle: "Unload marker file",
|
|
3059
|
+
onUnload: () => {
|
|
3060
|
+
modulePromise.then((Module) => Module.clearMarkers());
|
|
3061
|
+
hostCall("unloadMarkers");
|
|
3062
|
+
removeMarkerBrowser();
|
|
3063
|
+
currentMarkers = null;
|
|
3064
|
+
setMarkerChip(null);
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
function layerTag(item) {
|
|
3069
|
+
return `${item.layer}/${item.datatype}`;
|
|
3070
|
+
}
|
|
3071
|
+
function fmtCount(n) {
|
|
3072
|
+
if (n < 1e3) return String(n);
|
|
3073
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
3074
|
+
return `${(n / 1e6).toFixed(n < 1e7 ? 1 : 0)}M`;
|
|
3075
|
+
}
|
|
3076
|
+
function tintBorder(el, color) {
|
|
3077
|
+
if (el) el.style.borderLeft = `4px solid ${color}`;
|
|
3078
|
+
}
|
|
3079
|
+
function setRowVisible(Module, row, visible) {
|
|
3080
|
+
if (row.state.visible === visible) return;
|
|
3081
|
+
row.state.visible = visible;
|
|
3082
|
+
row.controller.updateDisplay();
|
|
3083
|
+
Module.setLayerVisible(row.item.layer, row.item.datatype, visible);
|
|
3084
|
+
}
|
|
3085
|
+
function syncLayerRowsFromModule(Module) {
|
|
3086
|
+
const visibleByTag = /* @__PURE__ */ new Map();
|
|
3087
|
+
for (const layer of Module.getLayers()) {
|
|
3088
|
+
visibleByTag.set(`${layer.layer}/${layer.datatype}`, layer.visible);
|
|
3089
|
+
}
|
|
3090
|
+
for (const row of layerRows) {
|
|
3091
|
+
const visible = visibleByTag.get(layerTag(row.item));
|
|
3092
|
+
if (visible === void 0 || row.state.visible === visible) continue;
|
|
3093
|
+
row.state.visible = visible;
|
|
3094
|
+
row.controller.updateDisplay();
|
|
3095
|
+
}
|
|
3096
|
+
syncCategoryChecks();
|
|
3097
|
+
}
|
|
3098
|
+
function syncCategoryChecks() {
|
|
3099
|
+
for (const category of layerCategories) {
|
|
3100
|
+
const all = category.rows.every((row) => row.state.visible);
|
|
3101
|
+
if (category.allState.visible !== all) {
|
|
3102
|
+
category.allState.visible = all;
|
|
3103
|
+
category.allController.updateDisplay();
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
function forgetSolo() {
|
|
3108
|
+
if (soloTag === null) return;
|
|
3109
|
+
soloTag = null;
|
|
3110
|
+
soloRestore = null;
|
|
3111
|
+
markSoloRow();
|
|
3112
|
+
}
|
|
3113
|
+
function markSoloRow() {
|
|
3114
|
+
for (const row of layerRows) {
|
|
3115
|
+
row.controller.domElement.classList.toggle("layer-soloed", layerTag(row.item) === soloTag);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
function toggleSolo(item) {
|
|
3119
|
+
const tag = layerTag(item);
|
|
3120
|
+
const restore = soloTag === tag ? soloRestore : null;
|
|
3121
|
+
if (restore) {
|
|
3122
|
+
soloTag = null;
|
|
3123
|
+
soloRestore = null;
|
|
3124
|
+
} else {
|
|
3125
|
+
soloRestore = new Map(layerRows.map((row) => [layerTag(row.item), row.state.visible]));
|
|
3126
|
+
soloTag = tag;
|
|
3127
|
+
}
|
|
3128
|
+
markSoloRow();
|
|
3129
|
+
modulePromise.then((Module) => {
|
|
3130
|
+
for (const row of layerRows) {
|
|
3131
|
+
const rowTag = layerTag(row.item);
|
|
3132
|
+
setRowVisible(Module, row, restore ? restore.get(rowTag) !== false : rowTag === tag);
|
|
3133
|
+
}
|
|
3134
|
+
syncCategoryChecks();
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
function applyBulkVisibility(kind) {
|
|
3138
|
+
forgetSolo();
|
|
3139
|
+
const rows = layerRows.filter((row) => row.matches);
|
|
3140
|
+
modulePromise.then((Module) => {
|
|
3141
|
+
for (const row of rows) {
|
|
3142
|
+
setRowVisible(Module, row, kind === "invert" ? !row.state.visible : kind === "all");
|
|
3143
|
+
}
|
|
3144
|
+
syncCategoryChecks();
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
function applyLayerFilter(text) {
|
|
3148
|
+
const query = text.trim().toLowerCase();
|
|
3149
|
+
if (query && !layerFilterActive) {
|
|
3150
|
+
for (const category of layerCategories) {
|
|
3151
|
+
category.wasOpen = !category.folder.domElement.classList.contains("lil-closed");
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
layerFilterActive = !!query;
|
|
3155
|
+
for (const row of layerRows) {
|
|
3156
|
+
row.matches = !query || row.haystack.includes(query);
|
|
3157
|
+
row.controller.domElement.style.display = row.matches ? "" : "none";
|
|
3158
|
+
}
|
|
3159
|
+
for (const category of layerCategories) {
|
|
3160
|
+
const matched = category.rows.reduce((n, row) => n + (row.matches ? 1 : 0), 0);
|
|
3161
|
+
category.folder.domElement.style.display = matched > 0 ? "" : "none";
|
|
3162
|
+
category.folder.title(query ? `${category.name} (${matched} of ${category.rows.length})` : `${category.name} (${category.rows.length})`);
|
|
3163
|
+
if (query) category.folder.open();
|
|
3164
|
+
else if (!category.wasOpen) category.folder.close();
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
function addLayerRow(parent, item, onSync) {
|
|
3168
|
+
const label = item.name ? `${item.layer}/${item.datatype} \u2013 ${item.name}` : `${item.layer}/${item.datatype}`;
|
|
3169
|
+
const shapes = item.polygonCount || 0;
|
|
3170
|
+
const labels = item.labelCount || 0;
|
|
3171
|
+
const state = { visible: item.visible };
|
|
3172
|
+
const controller = parent.add(state, "visible").name(label).onChange((visible) => {
|
|
3173
|
+
forgetSolo();
|
|
3174
|
+
modulePromise.then((Module) => Module.setLayerVisible(item.layer, item.datatype, visible));
|
|
3175
|
+
if (onSync) onSync();
|
|
3176
|
+
});
|
|
3177
|
+
tintBorder(controller.domElement, item.frameColor);
|
|
3178
|
+
controller.domElement.classList.add("layer-row");
|
|
3179
|
+
controller.domElement.title = [
|
|
3180
|
+
label,
|
|
3181
|
+
`${shapes.toLocaleString()} shape${shapes === 1 ? "" : "s"}, ${labels.toLocaleString()} label${labels === 1 ? "" : "s"}`
|
|
3182
|
+
].join("\n");
|
|
3183
|
+
const count = document.createElement("span");
|
|
3184
|
+
count.className = "layer-count";
|
|
3185
|
+
count.textContent = shapes > 0 ? fmtCount(shapes) : labels > 0 ? `T${fmtCount(labels)}` : "0";
|
|
3186
|
+
const solo = document.createElement("span");
|
|
3187
|
+
solo.className = "layer-solo";
|
|
3188
|
+
solo.textContent = "S";
|
|
3189
|
+
solo.title = "Solo \u2014 hide every other layer (click again to restore them)";
|
|
3190
|
+
solo.addEventListener("click", (event) => {
|
|
3191
|
+
event.preventDefault();
|
|
3192
|
+
event.stopPropagation();
|
|
3193
|
+
toggleSolo(item);
|
|
3194
|
+
});
|
|
3195
|
+
controller.domElement.append(count, solo);
|
|
3196
|
+
return { controller, state, item, matches: true, haystack: `${label} ${item.group || ""}`.toLowerCase() };
|
|
3197
|
+
}
|
|
3198
|
+
function addLayerListControls(folder) {
|
|
3199
|
+
const filterRow = document.createElement("div");
|
|
3200
|
+
filterRow.className = "lil-controller layer-filter-row";
|
|
3201
|
+
const filterName = document.createElement("div");
|
|
3202
|
+
filterName.className = "lil-name";
|
|
3203
|
+
filterName.textContent = "Filter";
|
|
3204
|
+
const filterWidget = document.createElement("div");
|
|
3205
|
+
filterWidget.className = "lil-widget";
|
|
3206
|
+
const filterInput = document.createElement("input");
|
|
3207
|
+
filterInput.type = "text";
|
|
3208
|
+
filterInput.placeholder = "number, name or group";
|
|
3209
|
+
filterInput.addEventListener("input", () => applyLayerFilter(filterInput.value));
|
|
3210
|
+
filterWidget.appendChild(filterInput);
|
|
3211
|
+
filterRow.append(filterName, filterWidget);
|
|
3212
|
+
filterRow.title = "Show only layers whose number, datatype, name or group contains this";
|
|
3213
|
+
const bulkRow = document.createElement("div");
|
|
3214
|
+
bulkRow.className = "lil-controller mode-row layer-bulk-row";
|
|
3215
|
+
const bulkName = document.createElement("div");
|
|
3216
|
+
bulkName.className = "lil-name";
|
|
3217
|
+
bulkName.textContent = "Show";
|
|
3218
|
+
const bulkWidget = document.createElement("div");
|
|
3219
|
+
bulkWidget.className = "lil-widget mode-widget";
|
|
3220
|
+
const BULK = [
|
|
3221
|
+
{ id: "all", label: "All", title: "Show every layer the filter is showing" },
|
|
3222
|
+
{ id: "none", label: "None", title: "Hide every layer the filter is showing" },
|
|
3223
|
+
{ id: "invert", label: "Invert", title: "Flip every filtered layer's visibility" }
|
|
3224
|
+
];
|
|
3225
|
+
for (const action of BULK) {
|
|
3226
|
+
const btn = document.createElement("button");
|
|
3227
|
+
btn.type = "button";
|
|
3228
|
+
btn.textContent = action.label;
|
|
3229
|
+
btn.title = action.title;
|
|
3230
|
+
btn.addEventListener("click", () => applyBulkVisibility(action.id));
|
|
3231
|
+
bulkWidget.appendChild(btn);
|
|
3232
|
+
}
|
|
3233
|
+
bulkRow.append(bulkName, bulkWidget);
|
|
3234
|
+
folder.$children.append(filterRow, bulkRow);
|
|
3235
|
+
}
|
|
3236
|
+
function renderLayerList(layers) {
|
|
3237
|
+
if (layersFolder) {
|
|
3238
|
+
layersFolder.destroy();
|
|
3239
|
+
}
|
|
3240
|
+
layerRows = [];
|
|
3241
|
+
layerCategories = [];
|
|
3242
|
+
layerFilterActive = false;
|
|
3243
|
+
soloTag = null;
|
|
3244
|
+
soloRestore = null;
|
|
3245
|
+
layersFolder = gui.addFolder("Layers");
|
|
3246
|
+
layersFolder.close();
|
|
3247
|
+
addLayerListControls(layersFolder);
|
|
3248
|
+
const OTHER = "Other layers";
|
|
3249
|
+
const categories = /* @__PURE__ */ new Map();
|
|
3250
|
+
for (const layer of layers) {
|
|
3251
|
+
const key = layer.group || OTHER;
|
|
3252
|
+
if (!categories.has(key)) categories.set(key, []);
|
|
3253
|
+
categories.get(key).push(layer);
|
|
3254
|
+
}
|
|
3255
|
+
for (const [category, items] of categories) {
|
|
3256
|
+
const folder = layersFolder.addFolder(`${category} (${items.length})`);
|
|
3257
|
+
folder.close();
|
|
3258
|
+
tintBorder(folder.domElement, items[0].frameColor);
|
|
3259
|
+
const children = [];
|
|
3260
|
+
const syncCategory = () => {
|
|
3261
|
+
const all = children.every((c) => c.state.visible);
|
|
3262
|
+
if (allState.visible !== all) {
|
|
3263
|
+
allState.visible = all;
|
|
3264
|
+
allController.updateDisplay();
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
const allState = { visible: items.every((it) => it.visible) };
|
|
3268
|
+
const allController = folder.add(allState, "visible").name("\u25FC all").onChange((visible) => {
|
|
3269
|
+
forgetSolo();
|
|
3270
|
+
modulePromise.then((Module) => {
|
|
3271
|
+
for (const c of children) setRowVisible(Module, c, visible);
|
|
3272
|
+
});
|
|
3273
|
+
});
|
|
3274
|
+
allController.domElement.title = `Toggle all ${items.length} layers in ${category}`;
|
|
3275
|
+
const shapeTotal = items.reduce((n, it) => n + (it.polygonCount || 0), 0);
|
|
3276
|
+
folder.$title.title = `${category}: ${items.length} layer${items.length === 1 ? "" : "s"}, ${shapeTotal.toLocaleString()} shape${shapeTotal === 1 ? "" : "s"}`;
|
|
3277
|
+
for (const item of items) {
|
|
3278
|
+
children.push(addLayerRow(folder, item, syncCategory));
|
|
3279
|
+
}
|
|
3280
|
+
layerRows.push(...children);
|
|
3281
|
+
layerCategories.push({ name: category, folder, rows: children, allState, allController, wasOpen: false });
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
function composeXform(outer, inner) {
|
|
3285
|
+
return [
|
|
3286
|
+
outer[0] * inner[0] + outer[1] * inner[2],
|
|
3287
|
+
outer[0] * inner[1] + outer[1] * inner[3],
|
|
3288
|
+
outer[2] * inner[0] + outer[3] * inner[2],
|
|
3289
|
+
outer[2] * inner[1] + outer[3] * inner[3],
|
|
3290
|
+
outer[0] * inner[4] + outer[1] * inner[5] + outer[4],
|
|
3291
|
+
outer[2] * inner[4] + outer[3] * inner[5] + outer[5]
|
|
3292
|
+
];
|
|
3293
|
+
}
|
|
3294
|
+
function transformBox(m, box) {
|
|
3295
|
+
const corners = [[box.minX, box.minY], [box.maxX, box.minY], [box.minX, box.maxY], [box.maxX, box.maxY]];
|
|
3296
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
3297
|
+
for (const [x, y] of corners) {
|
|
3298
|
+
const wx = m[0] * x + m[1] * y + m[4];
|
|
3299
|
+
const wy = m[2] * x + m[3] * y + m[5];
|
|
3300
|
+
minX = Math.min(minX, wx);
|
|
3301
|
+
maxX = Math.max(maxX, wx);
|
|
3302
|
+
minY = Math.min(minY, wy);
|
|
3303
|
+
maxY = Math.max(maxY, wy);
|
|
3304
|
+
}
|
|
3305
|
+
return { minX, maxX, minY, maxY };
|
|
3306
|
+
}
|
|
3307
|
+
function hierarchyBoxes(node, cell, parentXform, spanningBox) {
|
|
3308
|
+
const placements = node.placements;
|
|
3309
|
+
if (!placements || placements.length < 6 || !cell.bbox) {
|
|
3310
|
+
return spanningBox ? [spanningBox] : [];
|
|
3311
|
+
}
|
|
3312
|
+
const boxes = [];
|
|
3313
|
+
for (let i = 0; i + 5 < placements.length; i += 6) {
|
|
3314
|
+
const xform = composeXform(parentXform, [
|
|
3315
|
+
placements[i],
|
|
3316
|
+
placements[i + 1],
|
|
3317
|
+
placements[i + 2],
|
|
3318
|
+
placements[i + 3],
|
|
3319
|
+
placements[i + 4],
|
|
3320
|
+
placements[i + 5]
|
|
3321
|
+
]);
|
|
3322
|
+
boxes.push(transformBox(xform, cell.bbox));
|
|
3323
|
+
}
|
|
3324
|
+
return boxes;
|
|
3325
|
+
}
|
|
3326
|
+
function setHierarchyOpen(open, byUser) {
|
|
3327
|
+
if (byUser) hierarchyUserChoice = open;
|
|
3328
|
+
if (hierarchyPanel) hierarchyPanel.classList.toggle("hidden", !open);
|
|
3329
|
+
rootEl.classList.toggle("hierarchy-open", open);
|
|
3330
|
+
if (hierarchyShowBtn) hierarchyShowBtn.setAttribute("aria-expanded", String(open));
|
|
3331
|
+
if (hierarchyHide) hierarchyHide.setAttribute("aria-expanded", String(open));
|
|
3332
|
+
syncCellHighlight();
|
|
3333
|
+
}
|
|
3334
|
+
function syncCellHighlight() {
|
|
3335
|
+
const open = hierarchyPanel && !hierarchyPanel.classList.contains("hidden");
|
|
3336
|
+
const boxes = open ? hierarchySelectedBoxes : null;
|
|
3337
|
+
modulePromise.then((Module) => {
|
|
3338
|
+
if (!boxes || boxes.length === 0) {
|
|
3339
|
+
Module.clearCellHighlight();
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
const flat = [];
|
|
3343
|
+
for (const box of boxes) flat.push(box.minX, box.minY, box.maxX, box.maxY);
|
|
3344
|
+
Module.setCellHighlight(flat);
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
function hierarchySelect(row, path, boxes) {
|
|
3348
|
+
if (hierarchySelectedRow) hierarchySelectedRow.classList.remove("hier-selected");
|
|
3349
|
+
hierarchySelectedRow = row;
|
|
3350
|
+
hierarchySelectedPath = path;
|
|
3351
|
+
hierarchySelectedBoxes = boxes || [];
|
|
3352
|
+
row.classList.add("hier-selected");
|
|
3353
|
+
syncCellHighlight();
|
|
3354
|
+
}
|
|
3355
|
+
function hierarchyDeselect() {
|
|
3356
|
+
if (hierarchySelectedRow) hierarchySelectedRow.classList.remove("hier-selected");
|
|
3357
|
+
hierarchySelectedRow = null;
|
|
3358
|
+
hierarchySelectedPath = null;
|
|
3359
|
+
hierarchySelectedBoxes = [];
|
|
3360
|
+
syncCellHighlight();
|
|
3361
|
+
}
|
|
3362
|
+
function hierarchyTooltip(cell, node, box, boxes) {
|
|
3363
|
+
const lines = [cell.name];
|
|
3364
|
+
if (node.count > 1) {
|
|
3365
|
+
const each = boxes.length > 1 ? "each outlined" : "outlined as one box, too many to mark separately";
|
|
3366
|
+
lines.push(`${node.count} placements here, ${each} (expanding follows the first)`);
|
|
3367
|
+
}
|
|
3368
|
+
lines.push(`${cell.polygons} own shape${cell.polygons === 1 ? "" : "s"}, ${cell.labels} label${cell.labels === 1 ? "" : "s"}, ${cell.refs.length} child cell${cell.refs.length === 1 ? "" : "s"}`);
|
|
3369
|
+
if (box) {
|
|
3370
|
+
lines.push(`${fmtCoord(box.maxX - box.minX)} \xD7 ${fmtCoord(box.maxY - box.minY)} \xB5m at (${fmtCoord((box.minX + box.maxX) / 2)}, ${fmtCoord((box.minY + box.maxY) / 2)}) \u2014 click to zoom to it and outline it (Esc clears)`);
|
|
3371
|
+
} else {
|
|
3372
|
+
lines.push("empty \u2014 no geometry to zoom to");
|
|
3373
|
+
}
|
|
3374
|
+
return lines.join("\n");
|
|
3375
|
+
}
|
|
3376
|
+
function addHierarchyRows(container, nodes, depth, parentPath, parentXform) {
|
|
3377
|
+
for (const node of nodes) {
|
|
3378
|
+
let setExpanded = function(open) {
|
|
3379
|
+
if (!expandable) return;
|
|
3380
|
+
if (open && !built) {
|
|
3381
|
+
built = true;
|
|
3382
|
+
addHierarchyRows(children, cell.refs, depth + 1, path, childXform);
|
|
3383
|
+
}
|
|
3384
|
+
children.classList.toggle("hidden", !open);
|
|
3385
|
+
twisty.textContent = open ? "\u25BE" : "\u25B8";
|
|
3386
|
+
row.setAttribute("aria-expanded", String(open));
|
|
3387
|
+
if (open) hierarchyExpanded.add(path);
|
|
3388
|
+
else hierarchyExpanded.delete(path);
|
|
3389
|
+
};
|
|
3390
|
+
const cell = hierarchyModel.cells[node.cell];
|
|
3391
|
+
if (!cell) continue;
|
|
3392
|
+
const path = parentPath ? `${parentPath}/${cell.name}` : cell.name;
|
|
3393
|
+
const box = node.bbox ? transformBox(parentXform, node.bbox) : null;
|
|
3394
|
+
const boxes = hierarchyBoxes(node, cell, parentXform, box);
|
|
3395
|
+
const childXform = composeXform(parentXform, node.xform);
|
|
3396
|
+
const expandable = cell.refs.length > 0 && depth + 1 < HIERARCHY_MAX_DEPTH;
|
|
3397
|
+
const row = document.createElement("div");
|
|
3398
|
+
row.className = "hier-row";
|
|
3399
|
+
row.style.paddingLeft = `${6 + depth * 12}px`;
|
|
3400
|
+
if (!box) row.classList.add("hier-boxless");
|
|
3401
|
+
row.setAttribute("role", "treeitem");
|
|
3402
|
+
row.setAttribute("aria-level", String(depth + 1));
|
|
3403
|
+
if (expandable) row.setAttribute("aria-expanded", "false");
|
|
3404
|
+
const twisty = document.createElement("span");
|
|
3405
|
+
twisty.className = "hier-twisty";
|
|
3406
|
+
twisty.textContent = expandable ? "\u25B8" : "";
|
|
3407
|
+
const name = document.createElement("span");
|
|
3408
|
+
name.className = "hier-name";
|
|
3409
|
+
name.textContent = cell.name;
|
|
3410
|
+
const count = document.createElement("span");
|
|
3411
|
+
count.className = "hier-count";
|
|
3412
|
+
if (node.count > 1) count.textContent = `\xD7${node.count}`;
|
|
3413
|
+
row.append(twisty, name, count);
|
|
3414
|
+
row.title = hierarchyTooltip(cell, node, box, boxes);
|
|
3415
|
+
const children = document.createElement("div");
|
|
3416
|
+
children.className = "hier-children hidden";
|
|
3417
|
+
children.setAttribute("role", "group");
|
|
3418
|
+
container.append(row, children);
|
|
3419
|
+
let built = false;
|
|
3420
|
+
twisty.addEventListener("click", (event) => {
|
|
3421
|
+
event.stopPropagation();
|
|
3422
|
+
setExpanded(children.classList.contains("hidden"));
|
|
3423
|
+
});
|
|
3424
|
+
row.addEventListener("click", () => {
|
|
3425
|
+
hierarchySelect(row, path, boxes);
|
|
3426
|
+
if (!box) return;
|
|
3427
|
+
modulePromise.then((Module) => Module.zoomToBox(box.minX, box.minY, box.maxX, box.maxY));
|
|
3428
|
+
});
|
|
3429
|
+
if (path === hierarchySelectedPath) hierarchySelect(row, path, boxes);
|
|
3430
|
+
if (path === hierarchyRevealPath) {
|
|
3431
|
+
row.scrollIntoView({ block: "center" });
|
|
3432
|
+
if (box) modulePromise.then((Module) => Module.zoomToBox(box.minX, box.minY, box.maxX, box.maxY));
|
|
3433
|
+
}
|
|
3434
|
+
if (hierarchyExpanded.has(path)) setExpanded(true);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
function rebuildHierarchyRows() {
|
|
3438
|
+
if (!hierarchyTree || !hierarchyModel) return;
|
|
3439
|
+
hierarchySelectedRow = null;
|
|
3440
|
+
hierarchySelectedBoxes = [];
|
|
3441
|
+
hierarchyTree.textContent = "";
|
|
3442
|
+
const cells = hierarchyModel.cells || [];
|
|
3443
|
+
const roots = (hierarchyModel.roots || []).filter((index) => cells[index]);
|
|
3444
|
+
const rootNodes = roots.map((index) => ({
|
|
3445
|
+
cell: index,
|
|
3446
|
+
count: 1,
|
|
3447
|
+
bbox: cells[index].bbox,
|
|
3448
|
+
xform: HIERARCHY_IDENTITY
|
|
3449
|
+
}));
|
|
3450
|
+
addHierarchyRows(hierarchyTree, rootNodes, 0, "", HIERARCHY_IDENTITY);
|
|
3451
|
+
}
|
|
3452
|
+
function hierarchyPathToCell(target2) {
|
|
3453
|
+
if (!hierarchyModel) return null;
|
|
3454
|
+
return cellPathToTarget(
|
|
3455
|
+
hierarchyModel.cells || [],
|
|
3456
|
+
hierarchyModel.roots || [],
|
|
3457
|
+
target2,
|
|
3458
|
+
HIERARCHY_MAX_DEPTH
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
function revealCell(target2) {
|
|
3462
|
+
const path = hierarchyPathToCell(target2);
|
|
3463
|
+
if (!path) return false;
|
|
3464
|
+
const names = path.map((index) => hierarchyModel.cells[index].name);
|
|
3465
|
+
for (let i = 1; i < names.length; i++) {
|
|
3466
|
+
hierarchyExpanded.add(names.slice(0, i).join("/"));
|
|
3467
|
+
}
|
|
3468
|
+
hierarchySelectedPath = names.join("/");
|
|
3469
|
+
hierarchyRevealPath = hierarchySelectedPath;
|
|
3470
|
+
try {
|
|
3471
|
+
rebuildHierarchyRows();
|
|
3472
|
+
} finally {
|
|
3473
|
+
hierarchyRevealPath = null;
|
|
3474
|
+
}
|
|
3475
|
+
return true;
|
|
3476
|
+
}
|
|
3477
|
+
function renderHierarchy(model) {
|
|
3478
|
+
if (!hierarchyTree) return;
|
|
3479
|
+
hierarchyModel = model;
|
|
3480
|
+
hierarchySelectedRow = null;
|
|
3481
|
+
hierarchySelectedBoxes = [];
|
|
3482
|
+
syncCellHighlight();
|
|
3483
|
+
hierarchyTree.textContent = "";
|
|
3484
|
+
const cells = model && model.cells || [];
|
|
3485
|
+
const roots = (model && model.roots || []).filter((index) => cells[index]);
|
|
3486
|
+
const cellCount = model ? model.cellCount : 0;
|
|
3487
|
+
rootEl.classList.toggle("hierarchy-available", cellCount > 0);
|
|
3488
|
+
if (!model || cellCount === 0) {
|
|
3489
|
+
if (hierarchyCount) hierarchyCount.textContent = "";
|
|
3490
|
+
if (hierarchyShowBtn) hierarchyShowBtn.classList.add("hidden");
|
|
3491
|
+
refreshFind(true);
|
|
3492
|
+
setHierarchyOpen(false);
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
if (hierarchyShowBtn) hierarchyShowBtn.classList.remove("hidden");
|
|
3496
|
+
if (hierarchyCount) hierarchyCount.textContent = `${cellCount} cell${cellCount === 1 ? "" : "s"}`;
|
|
3497
|
+
const rootKey = roots.map((i) => cells[i].name).join(" ");
|
|
3498
|
+
const sameDesign = rootKey === hierarchyRootKey;
|
|
3499
|
+
if (!sameDesign) {
|
|
3500
|
+
hierarchyRootKey = rootKey;
|
|
3501
|
+
hierarchyExpanded.clear();
|
|
3502
|
+
hierarchySelectedPath = null;
|
|
3503
|
+
}
|
|
3504
|
+
refreshFind(!sameDesign);
|
|
3505
|
+
if (model.omitted) {
|
|
3506
|
+
const note = document.createElement("div");
|
|
3507
|
+
note.className = "hier-note";
|
|
3508
|
+
note.textContent = `This design has ${cellCount} cells \u2014 too many to browse as a tree, so it isn't built.`;
|
|
3509
|
+
hierarchyTree.append(note);
|
|
3510
|
+
setHierarchyOpen(hierarchyUserChoice === true);
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
if (hierarchyExpanded.size === 0 && roots.length > 0) {
|
|
3514
|
+
hierarchyExpanded.add(cells[roots[0]].name);
|
|
3515
|
+
}
|
|
3516
|
+
rebuildHierarchyRows();
|
|
3517
|
+
setHierarchyOpen(hierarchyUserChoice === true);
|
|
3518
|
+
}
|
|
3519
|
+
function toggleHierarchy() {
|
|
3520
|
+
if (!hierarchyPanel || !hierarchyModel || !hierarchyModel.cellCount) return;
|
|
3521
|
+
setHierarchyOpen(hierarchyPanel.classList.contains("hidden"), true);
|
|
3522
|
+
}
|
|
3523
|
+
function setFindOpen(open) {
|
|
3524
|
+
if (!hierarchySearchBox) return;
|
|
3525
|
+
const changed = open !== findIsOpen();
|
|
3526
|
+
hierarchySearchBox.classList.toggle("hidden", !open);
|
|
3527
|
+
if (hierarchyFindTwisty) hierarchyFindTwisty.textContent = open ? "\u25BE" : "\u25B8";
|
|
3528
|
+
if (hierarchyFindToggle) hierarchyFindToggle.setAttribute("aria-expanded", String(open));
|
|
3529
|
+
if (open) {
|
|
3530
|
+
if (hierarchySearchInput) hierarchySearchInput.focus();
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
if (!changed) return;
|
|
3534
|
+
if (hierarchySearchInput) {
|
|
3535
|
+
hierarchySearchInput.value = "";
|
|
3536
|
+
hierarchySearchInput.blur();
|
|
3537
|
+
}
|
|
3538
|
+
runSearch();
|
|
3539
|
+
}
|
|
3540
|
+
function findIsOpen() {
|
|
3541
|
+
return !!(hierarchySearchBox && !hierarchySearchBox.classList.contains("hidden"));
|
|
3542
|
+
}
|
|
3543
|
+
function setFindResultsOpen(open) {
|
|
3544
|
+
if (!hierarchyResults || !hierarchyTree) return;
|
|
3545
|
+
hierarchyResults.classList.toggle("hidden", !open);
|
|
3546
|
+
hierarchyTree.classList.toggle("hidden", open);
|
|
3547
|
+
}
|
|
3548
|
+
function setFindCount(text) {
|
|
3549
|
+
if (hierarchySearchCount) hierarchySearchCount.textContent = text;
|
|
3550
|
+
}
|
|
3551
|
+
function clearFindRows() {
|
|
3552
|
+
if (hierarchyResults) hierarchyResults.textContent = "";
|
|
3553
|
+
findRows = [];
|
|
3554
|
+
findActiveIndex = -1;
|
|
3555
|
+
}
|
|
3556
|
+
function findNote(text) {
|
|
3557
|
+
if (!hierarchyResults) return;
|
|
3558
|
+
const note = document.createElement("div");
|
|
3559
|
+
note.className = "find-note";
|
|
3560
|
+
note.textContent = text;
|
|
3561
|
+
hierarchyResults.append(note);
|
|
3562
|
+
}
|
|
3563
|
+
function addFindRow(name, meta, title, activate) {
|
|
3564
|
+
if (!hierarchyResults) return;
|
|
3565
|
+
const element = document.createElement("div");
|
|
3566
|
+
element.className = "find-row";
|
|
3567
|
+
const nameEl = document.createElement("span");
|
|
3568
|
+
nameEl.className = "find-name";
|
|
3569
|
+
nameEl.textContent = name;
|
|
3570
|
+
const metaEl = document.createElement("span");
|
|
3571
|
+
metaEl.className = "find-meta";
|
|
3572
|
+
metaEl.textContent = meta;
|
|
3573
|
+
element.append(nameEl, metaEl);
|
|
3574
|
+
element.title = title;
|
|
3575
|
+
const index = findRows.length;
|
|
3576
|
+
element.addEventListener("click", () => activateFindRow(index));
|
|
3577
|
+
hierarchyResults.append(element);
|
|
3578
|
+
findRows.push({ element, activate });
|
|
3579
|
+
}
|
|
3580
|
+
function setFindActive(index) {
|
|
3581
|
+
const previous = findRows[findActiveIndex];
|
|
3582
|
+
if (previous) previous.element.classList.remove("find-active");
|
|
3583
|
+
findActiveIndex = index;
|
|
3584
|
+
const row = findRows[index];
|
|
3585
|
+
if (!row) return;
|
|
3586
|
+
row.element.classList.add("find-active");
|
|
3587
|
+
row.element.scrollIntoView({ block: "nearest" });
|
|
3588
|
+
}
|
|
3589
|
+
function activateFindRow(index) {
|
|
3590
|
+
const row = findRows[index];
|
|
3591
|
+
if (!row || !row.activate) return;
|
|
3592
|
+
setFindActive(index);
|
|
3593
|
+
row.activate(row);
|
|
3594
|
+
}
|
|
3595
|
+
function stepFindRow(direction) {
|
|
3596
|
+
if (findRows.length === 0) return;
|
|
3597
|
+
const from = findActiveIndex < 0 ? direction > 0 ? -1 : 0 : findActiveIndex;
|
|
3598
|
+
setFindActive((from + direction + findRows.length) % findRows.length);
|
|
3599
|
+
}
|
|
3600
|
+
function runSearch() {
|
|
3601
|
+
if (!hierarchySearchInput) return;
|
|
3602
|
+
const query = hierarchySearchInput.value.trim();
|
|
3603
|
+
findQuery = query;
|
|
3604
|
+
if (!query) {
|
|
3605
|
+
clearFindRows();
|
|
3606
|
+
setFindResultsOpen(false);
|
|
3607
|
+
setFindCount("");
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
setFindResultsOpen(true);
|
|
3611
|
+
if (findScope === "labels") runLabelSearch(query);
|
|
3612
|
+
else renderCellResults(query);
|
|
3613
|
+
}
|
|
3614
|
+
function renderCellResults(query) {
|
|
3615
|
+
clearFindRows();
|
|
3616
|
+
if (hierarchyModel && hierarchyModel.omitted) {
|
|
3617
|
+
setFindCount("");
|
|
3618
|
+
findNote(`This design's ${hierarchyModel.cellCount} cells are too many for the viewer to hold as a tree, so it has no cell names to search. Labels still work.`);
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
const cells = hierarchyModel && hierarchyModel.cells || [];
|
|
3622
|
+
const matches = rankCellMatches(cells, query);
|
|
3623
|
+
setFindCount(matches.length === 0 ? "no match" : `${Math.min(matches.length, MAX_FIND_ROWS)} of ${matches.length} cell${matches.length === 1 ? "" : "s"}`);
|
|
3624
|
+
for (const index of matches.slice(0, MAX_FIND_ROWS)) {
|
|
3625
|
+
const cell = cells[index];
|
|
3626
|
+
const meta = cell.polygons > 0 ? fmtCount(cell.polygons) : cell.labels > 0 ? `T${fmtCount(cell.labels)}` : "0";
|
|
3627
|
+
const title = [
|
|
3628
|
+
cell.name,
|
|
3629
|
+
`${cell.polygons.toLocaleString()} own shape${cell.polygons === 1 ? "" : "s"}, ${cell.labels.toLocaleString()} label${cell.labels === 1 ? "" : "s"}, ${cell.refs.length} child cell${cell.refs.length === 1 ? "" : "s"}`,
|
|
3630
|
+
"Click to open the tree down to it, frame it and outline every placement"
|
|
3631
|
+
].join("\n");
|
|
3632
|
+
addFindRow(cell.name, meta, title, (row) => chooseCell(index, row));
|
|
3633
|
+
}
|
|
3634
|
+
if (matches.length > MAX_FIND_ROWS) {
|
|
3635
|
+
findNote(`\u2026 ${matches.length - MAX_FIND_ROWS} more \u2014 narrow the query`);
|
|
3636
|
+
}
|
|
3637
|
+
if (matches.length === 0) findNote(`No cell name contains \u201C${query}\u201D.`);
|
|
3638
|
+
}
|
|
3639
|
+
function chooseCell(index, row) {
|
|
3640
|
+
setFindResultsOpen(false);
|
|
3641
|
+
if (revealCell(index)) return;
|
|
3642
|
+
setFindResultsOpen(true);
|
|
3643
|
+
row.activate = null;
|
|
3644
|
+
row.element.classList.add("find-unreachable");
|
|
3645
|
+
row.element.title = `${hierarchyModel.cells[index].name}
|
|
3646
|
+
No top cell places this one, so the tree has no branch that reaches it.`;
|
|
3647
|
+
}
|
|
3648
|
+
function runLabelSearch(query) {
|
|
3649
|
+
modulePromise.then((Module) => {
|
|
3650
|
+
const result = Module.findLabels(query, MAX_FIND_ROWS);
|
|
3651
|
+
if (query !== findQuery || findScope !== "labels") return;
|
|
3652
|
+
renderLabelResults(query, result);
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
function renderLabelResults(query, result) {
|
|
3656
|
+
clearFindRows();
|
|
3657
|
+
const hits = result.hits || [];
|
|
3658
|
+
const total = result.total || 0;
|
|
3659
|
+
setFindCount(total === 0 ? "no match" : `${hits.length} of ${total.toLocaleString()} label${total === 1 ? "" : "s"}`);
|
|
3660
|
+
for (const hit of hits) {
|
|
3661
|
+
const tag = hit.name ? `${hit.layer}/${hit.datatype} ${hit.name}` : `${hit.layer}/${hit.datatype}`;
|
|
3662
|
+
const meta = hit.visible ? tag : `${tag} \xB7 hidden`;
|
|
3663
|
+
const title = [
|
|
3664
|
+
hit.text,
|
|
3665
|
+
`on layer ${tag}${hit.visible ? "" : " \u2014 currently hidden, but the label is still marked"}`,
|
|
3666
|
+
`at (${fmtCoord(hit.x)}, ${fmtCoord(hit.y)}) \xB5m \u2014 click to pan there and mark it`
|
|
3667
|
+
].join("\n");
|
|
3668
|
+
addFindRow(hit.text, meta, title, () => goToLabel(hit));
|
|
3669
|
+
}
|
|
3670
|
+
if (total > hits.length) {
|
|
3671
|
+
findNote(`\u2026 ${(total - hits.length).toLocaleString()} more \u2014 narrow the query`);
|
|
3672
|
+
}
|
|
3673
|
+
if (total === 0) findNote(`No label text contains \u201C${query}\u201D.`);
|
|
3674
|
+
}
|
|
3675
|
+
function goToLabel(hit) {
|
|
3676
|
+
modulePromise.then((Module) => {
|
|
3677
|
+
Module.goToPoint(hit.x, hit.y);
|
|
3678
|
+
if (!actions.showText) textController.setValue(true);
|
|
3679
|
+
markLabelHit(Module, hit);
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
function markLabelHit(Module, hit) {
|
|
3683
|
+
const zoom = Module.getCamera().zoom;
|
|
3684
|
+
const half = LABEL_MARK_PX / (zoom > 0 ? zoom : 1);
|
|
3685
|
+
if (hierarchySelectedRow) hierarchySelectedRow.classList.remove("hier-selected");
|
|
3686
|
+
hierarchySelectedRow = null;
|
|
3687
|
+
hierarchySelectedPath = null;
|
|
3688
|
+
hierarchySelectedBoxes = [{
|
|
3689
|
+
minX: hit.x - half,
|
|
3690
|
+
minY: hit.y - half,
|
|
3691
|
+
maxX: hit.x + half,
|
|
3692
|
+
maxY: hit.y + half
|
|
3693
|
+
}];
|
|
3694
|
+
syncCellHighlight();
|
|
3695
|
+
}
|
|
3696
|
+
function setFindScope(scope) {
|
|
3697
|
+
if (findScope === scope) return;
|
|
3698
|
+
findScope = scope;
|
|
3699
|
+
updateFindScope();
|
|
3700
|
+
runSearch();
|
|
3701
|
+
}
|
|
3702
|
+
function updateFindScope() {
|
|
3703
|
+
const cellsAvailable = !!(hierarchyModel && !hierarchyModel.omitted && hierarchyModel.cells && hierarchyModel.cells.length > 0);
|
|
3704
|
+
if (hierarchyScopeCells) {
|
|
3705
|
+
hierarchyScopeCells.classList.toggle("scope-active", findScope === "cells");
|
|
3706
|
+
hierarchyScopeCells.setAttribute("aria-pressed", String(findScope === "cells"));
|
|
3707
|
+
hierarchyScopeCells.disabled = !cellsAvailable;
|
|
3708
|
+
hierarchyScopeCells.title = cellsAvailable ? "Search the design's cell names" : "This design's cell names aren't held in the viewer \u2014 see the note in the panel";
|
|
3709
|
+
}
|
|
3710
|
+
if (hierarchyScopeLabels) {
|
|
3711
|
+
hierarchyScopeLabels.classList.toggle("scope-active", findScope === "labels");
|
|
3712
|
+
hierarchyScopeLabels.setAttribute("aria-pressed", String(findScope === "labels"));
|
|
3713
|
+
}
|
|
3714
|
+
if (hierarchySearchInput) {
|
|
3715
|
+
hierarchySearchInput.placeholder = findScope === "labels" ? "label text" : "cell name";
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
function refreshFind(reset) {
|
|
3719
|
+
if (reset && hierarchySearchInput) hierarchySearchInput.value = "";
|
|
3720
|
+
if (hierarchyModel && hierarchyModel.omitted) findScope = "labels";
|
|
3721
|
+
updateFindScope();
|
|
3722
|
+
runSearch();
|
|
3723
|
+
}
|
|
3724
|
+
function focusFindBox() {
|
|
3725
|
+
if (!hierarchySearchInput || !hierarchyModel || !hierarchyModel.cellCount) return;
|
|
3726
|
+
if (hierarchyPanel && hierarchyPanel.classList.contains("hidden")) setHierarchyOpen(true, true);
|
|
3727
|
+
setFindOpen(true);
|
|
3728
|
+
hierarchySearchInput.select();
|
|
3729
|
+
}
|
|
3730
|
+
function removeMarkerBrowser() {
|
|
3731
|
+
if (markersFolder) {
|
|
3732
|
+
markersFolder.destroy();
|
|
3733
|
+
markersFolder = null;
|
|
3734
|
+
}
|
|
3735
|
+
markerItemRows.clear();
|
|
3736
|
+
selectedMarkerRow = null;
|
|
3737
|
+
selectedMarkerId = -1;
|
|
3738
|
+
}
|
|
3739
|
+
function selectMarker(Module, item) {
|
|
3740
|
+
if (selectedMarkerRow) selectedMarkerRow.classList.remove("marker-selected");
|
|
3741
|
+
selectedMarkerRow = markerItemRows.get(item.id) || null;
|
|
3742
|
+
if (selectedMarkerRow) selectedMarkerRow.classList.add("marker-selected");
|
|
3743
|
+
selectedMarkerId = item.id;
|
|
3744
|
+
Module.setSelectedMarker(item.id);
|
|
3745
|
+
if (item.bbox) {
|
|
3746
|
+
Module.zoomToBox(item.bbox.minX, item.bbox.minY, item.bbox.maxX, item.bbox.maxY);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
function fmtCoord(v) {
|
|
3750
|
+
return Number(v.toPrecision(4)).toString();
|
|
3751
|
+
}
|
|
3752
|
+
function renderMarkerBrowser(model) {
|
|
3753
|
+
const keepSelectedId = selectedMarkerId;
|
|
3754
|
+
removeMarkerBrowser();
|
|
3755
|
+
selectedMarkerId = keepSelectedId;
|
|
3756
|
+
const totalItems = model.categories.reduce((n, c) => n + c.items.length, 0);
|
|
3757
|
+
markersFolder = gui.addFolder(`Markers (${totalItems})`);
|
|
3758
|
+
markersFolder.open();
|
|
3759
|
+
if (model.warnings.length > 0) {
|
|
3760
|
+
fail("[GDS] marker warnings:", model.warnings.join(" | "));
|
|
3761
|
+
const row = markersFolder.add({ w: () => {
|
|
3762
|
+
} }, "w").name(`\u26A0 ${model.warnings.length} warning${model.warnings.length === 1 ? "" : "s"}`);
|
|
3763
|
+
row.domElement.title = model.warnings.join("\n");
|
|
3764
|
+
}
|
|
3765
|
+
const opacityController = markersFolder.add(markerUiState, "opacity", 0, 1, 0.05).name("Opacity").onChange((value) => modulePromise.then((Module) => Module.setMarkerOpacity(value)));
|
|
3766
|
+
opacityController.domElement.title = "Opacity of the whole marker overlay";
|
|
3767
|
+
const emptyCount = model.categories.filter((c) => c.items.length === 0).length;
|
|
3768
|
+
const hideEmptyController = markersFolder.add(markerUiState, "hideEmpty").name("Hide empty categories").onChange(() => renderMarkerBrowser(model));
|
|
3769
|
+
hideEmptyController.domElement.title = `Hide categories with 0 violations (currently ${emptyCount} of ${model.categories.length})`;
|
|
3770
|
+
model.categories.forEach((cat, categoryIndex) => {
|
|
3771
|
+
if (markerUiState.hideEmpty && cat.items.length === 0) return;
|
|
3772
|
+
const folder = markersFolder.addFolder(`${cat.name} (${cat.items.length})`);
|
|
3773
|
+
folder.close();
|
|
3774
|
+
if (cat.description) folder.domElement.title = cat.description;
|
|
3775
|
+
if (cat.uiVisible === void 0) cat.uiVisible = false;
|
|
3776
|
+
const visState = { visible: cat.uiVisible };
|
|
3777
|
+
const visController = folder.add(visState, "visible").name("\u25FC visible").onChange((visible) => {
|
|
3778
|
+
cat.uiVisible = visible;
|
|
3779
|
+
modulePromise.then((Module) => Module.setMarkerCategoryVisible(categoryIndex, visible));
|
|
3780
|
+
});
|
|
3781
|
+
visController.domElement.title = `Show/hide all ${cat.items.length} markers in ${cat.name}`;
|
|
3782
|
+
for (const item of cat.items.slice(0, MAX_MARKER_ROWS_PER_CATEGORY)) {
|
|
3783
|
+
const label = item.bbox ? `#${item.label} (${fmtCoord((item.bbox.minX + item.bbox.maxX) / 2)}, ${fmtCoord((item.bbox.minY + item.bbox.maxY) / 2)})` : `#${item.label}`;
|
|
3784
|
+
const controller = folder.add({ go: () => modulePromise.then((Module) => selectMarker(Module, item)) }, "go").name(label);
|
|
3785
|
+
controller.domElement.title = [item.note, cat.description].filter(Boolean).join("\n") || label;
|
|
3786
|
+
markerItemRows.set(item.id, controller.domElement);
|
|
3787
|
+
}
|
|
3788
|
+
if (cat.items.length > MAX_MARKER_ROWS_PER_CATEGORY) {
|
|
3789
|
+
const more = folder.add({ m: () => {
|
|
3790
|
+
} }, "m").name(`\u2026 ${cat.items.length - MAX_MARKER_ROWS_PER_CATEGORY} more (press [ or ] to step)`);
|
|
3791
|
+
more.domElement.classList.add("marker-more-row");
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
selectedMarkerRow = markerItemRows.get(selectedMarkerId) || null;
|
|
3795
|
+
if (selectedMarkerRow) selectedMarkerRow.classList.add("marker-selected");
|
|
3796
|
+
}
|
|
3797
|
+
function stepMarker(direction) {
|
|
3798
|
+
if (!currentMarkers) return;
|
|
3799
|
+
let items = [];
|
|
3800
|
+
for (const cat of currentMarkers.categories) {
|
|
3801
|
+
if (cat.uiVisible === false) continue;
|
|
3802
|
+
items.push(...cat.items);
|
|
3803
|
+
}
|
|
3804
|
+
if (items.length === 0) {
|
|
3805
|
+
items = currentMarkers.categories.flatMap((cat) => cat.items);
|
|
3806
|
+
}
|
|
3807
|
+
if (items.length === 0) return;
|
|
3808
|
+
let idx = items.findIndex((it) => it.id === selectedMarkerId);
|
|
3809
|
+
idx = idx < 0 ? direction > 0 ? 0 : items.length - 1 : (idx + direction + items.length) % items.length;
|
|
3810
|
+
modulePromise.then((Module) => selectMarker(Module, items[idx]));
|
|
3811
|
+
}
|
|
3812
|
+
function showCopyToast(text) {
|
|
3813
|
+
if (!copyToastEl) return;
|
|
3814
|
+
copyToastEl.textContent = text;
|
|
3815
|
+
copyToastEl.classList.remove("hidden");
|
|
3816
|
+
clearTimeout(copyToastTimer);
|
|
3817
|
+
copyToastTimer = setTimeout(() => copyToastEl.classList.add("hidden"), 1800);
|
|
3818
|
+
}
|
|
3819
|
+
function hideCanvasMenu() {
|
|
3820
|
+
if (canvasMenuEl) canvasMenuEl.classList.add("hidden");
|
|
3821
|
+
}
|
|
3822
|
+
function showCanvasMenu(clientX, clientY, text) {
|
|
3823
|
+
if (!canvasMenuEl) return;
|
|
3824
|
+
canvasMenuText = text;
|
|
3825
|
+
if (canvasMenuValueEl) canvasMenuValueEl.textContent = text;
|
|
3826
|
+
canvasMenuEl.classList.remove("hidden");
|
|
3827
|
+
const margin = 4;
|
|
3828
|
+
const maxLeft = window.innerWidth - canvasMenuEl.offsetWidth - margin;
|
|
3829
|
+
const maxTop = window.innerHeight - canvasMenuEl.offsetHeight - margin;
|
|
3830
|
+
canvasMenuEl.style.left = Math.max(margin, Math.min(clientX, maxLeft)) + "px";
|
|
3831
|
+
canvasMenuEl.style.top = Math.max(margin, Math.min(clientY, maxTop)) + "px";
|
|
3832
|
+
if (canvasMenuCopyEl) canvasMenuCopyEl.focus();
|
|
3833
|
+
}
|
|
3834
|
+
function showStaleBanner(show, text) {
|
|
3835
|
+
if (!staleBanner) return;
|
|
3836
|
+
if (text) staleText.textContent = text;
|
|
3837
|
+
staleBanner.classList.toggle("hidden", !show);
|
|
3838
|
+
}
|
|
3839
|
+
function captureViewState(Module) {
|
|
3840
|
+
const layers = Module.getLayers();
|
|
3841
|
+
if (layers.length === 0) return null;
|
|
3842
|
+
const visibility = {};
|
|
3843
|
+
for (const layer of layers) {
|
|
3844
|
+
visibility[`${layer.layer}/${layer.datatype}`] = layer.visible;
|
|
3845
|
+
}
|
|
3846
|
+
return { camera: Module.getCamera(), visibility };
|
|
3847
|
+
}
|
|
3848
|
+
function restoreViewState(Module, saved) {
|
|
3849
|
+
if (!saved) return;
|
|
3850
|
+
for (const layer of Module.getLayers()) {
|
|
3851
|
+
const wasVisible = saved.visibility[`${layer.layer}/${layer.datatype}`];
|
|
3852
|
+
if (wasVisible !== void 0 && wasVisible !== layer.visible) {
|
|
3853
|
+
Module.setLayerVisible(layer.layer, layer.datatype, wasVisible);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
Module.setCamera(saved.camera.zoom, saved.camera.panX, saved.camera.panY);
|
|
3857
|
+
}
|
|
3858
|
+
function persistNamedViews() {
|
|
3859
|
+
hostCall("saveViews", namedViews);
|
|
3860
|
+
}
|
|
3861
|
+
function requestSaveView() {
|
|
3862
|
+
modulePromise.then((Module) => {
|
|
3863
|
+
const captured = captureViewState(Module);
|
|
3864
|
+
if (!captured) return;
|
|
3865
|
+
pendingViewCapture = captured;
|
|
3866
|
+
Promise.resolve(hostCall("promptViewName", namedViews.map((view) => view.name))).then((name) => {
|
|
3867
|
+
if (name) saveNamedView(name);
|
|
3868
|
+
else pendingViewCapture = null;
|
|
3869
|
+
});
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
function saveNamedView(name) {
|
|
3873
|
+
const captured = pendingViewCapture;
|
|
3874
|
+
pendingViewCapture = null;
|
|
3875
|
+
if (!captured || !name) return;
|
|
3876
|
+
const view = {
|
|
3877
|
+
name,
|
|
3878
|
+
camera: {
|
|
3879
|
+
zoom: captured.camera.zoom,
|
|
3880
|
+
panX: captured.camera.panX,
|
|
3881
|
+
panY: captured.camera.panY
|
|
3882
|
+
},
|
|
3883
|
+
visibility: captured.visibility
|
|
3884
|
+
};
|
|
3885
|
+
const at = namedViews.findIndex((existing) => existing.name.toLowerCase() === name.toLowerCase());
|
|
3886
|
+
if (at >= 0) namedViews[at] = view;
|
|
3887
|
+
else namedViews.push(view);
|
|
3888
|
+
renderNamedViews();
|
|
3889
|
+
persistNamedViews();
|
|
3890
|
+
}
|
|
3891
|
+
function deleteNamedView(view) {
|
|
3892
|
+
namedViews = namedViews.filter((existing) => existing !== view);
|
|
3893
|
+
renderNamedViews();
|
|
3894
|
+
persistNamedViews();
|
|
3895
|
+
}
|
|
3896
|
+
function restoreNamedView(view) {
|
|
3897
|
+
modulePromise.then((Module) => {
|
|
3898
|
+
forgetSolo();
|
|
3899
|
+
restoreViewState(Module, view);
|
|
3900
|
+
syncLayerRowsFromModule(Module);
|
|
3901
|
+
});
|
|
3902
|
+
}
|
|
3903
|
+
function renderNamedViews() {
|
|
3904
|
+
for (const controller of viewControllers) controller.destroy();
|
|
3905
|
+
viewControllers = [];
|
|
3906
|
+
viewsFolder.title(namedViews.length > 0 ? `Views (${namedViews.length})` : "Views");
|
|
3907
|
+
for (const view of namedViews) {
|
|
3908
|
+
const controller = viewsFolder.add({ go: () => restoreNamedView(view) }, "go").name(view.name);
|
|
3909
|
+
controller.domElement.classList.add("view-row");
|
|
3910
|
+
controller.domElement.title = `${view.name} \u2014 click to put the camera and layer visibility back, \u2715 to delete`;
|
|
3911
|
+
const remove = document.createElement("span");
|
|
3912
|
+
remove.className = "view-delete";
|
|
3913
|
+
remove.textContent = "\u2715";
|
|
3914
|
+
remove.title = `Delete "${view.name}"`;
|
|
3915
|
+
remove.addEventListener("click", (event) => {
|
|
3916
|
+
event.stopPropagation();
|
|
3917
|
+
deleteNamedView(view);
|
|
3918
|
+
});
|
|
3919
|
+
controller.domElement.appendChild(remove);
|
|
3920
|
+
viewControllers.push(controller);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
function setNamedViews(views) {
|
|
3924
|
+
namedViews = (Array.isArray(views) ? views : []).filter((view) => view && typeof view.name === "string" && view.name.length > 0 && view.camera && typeof view.camera.zoom === "number" && typeof view.camera.panX === "number" && typeof view.camera.panY === "number");
|
|
3925
|
+
renderNamedViews();
|
|
3926
|
+
}
|
|
3927
|
+
function showFatalError(message) {
|
|
3928
|
+
fail("[GDS] load failed:", message);
|
|
3929
|
+
loadError.textContent = "Could not open this layout\n\n" + message;
|
|
3930
|
+
endProgress();
|
|
3931
|
+
renderHierarchy(null);
|
|
3932
|
+
modulePromise.then((Module) => {
|
|
3933
|
+
Module.showLoadError(message);
|
|
3934
|
+
renderLayerList(Module.getLayers());
|
|
3935
|
+
}).catch(() => {
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
function clearFatalError() {
|
|
3939
|
+
loadError.textContent = "";
|
|
3940
|
+
}
|
|
3941
|
+
function beginProgress(inline) {
|
|
3942
|
+
progressInline = inline;
|
|
3943
|
+
loadingOverlay.classList.toggle("hidden", inline);
|
|
3944
|
+
reloadProgress.classList.toggle("hidden", !inline);
|
|
3945
|
+
updateProgress("parsing", 0, 1);
|
|
3946
|
+
}
|
|
3947
|
+
function endProgress() {
|
|
3948
|
+
loadingOverlay.classList.add("hidden");
|
|
3949
|
+
reloadProgress.classList.add("hidden");
|
|
3950
|
+
}
|
|
3951
|
+
function updateProgress(phase, current, total) {
|
|
3952
|
+
const label = phaseLabels[phase] || phase;
|
|
3953
|
+
const fraction = total > 0 ? current / total : 0;
|
|
3954
|
+
const percent = Math.round(fraction * 100);
|
|
3955
|
+
const detail = phase === "triangulating" ? `Layer ${current}/${total}` : `${percent}%`;
|
|
3956
|
+
if (progressInline) {
|
|
3957
|
+
reloadBarFill.style.width = `${percent}%`;
|
|
3958
|
+
reloadLabel.textContent = `Reloading \u2014 ${label} ${detail}`;
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
loadingPhase.textContent = label;
|
|
3962
|
+
loadingBarFill.style.width = `${percent}%`;
|
|
3963
|
+
loadingPercent.textContent = detail;
|
|
3964
|
+
}
|
|
3965
|
+
function detectLightTheme() {
|
|
3966
|
+
if (hostCan("isLightTheme")) return !!host.isLightTheme();
|
|
3967
|
+
if (rootEl.classList.contains("theme-light")) return true;
|
|
3968
|
+
return lightMediaQuery.matches;
|
|
3969
|
+
}
|
|
3970
|
+
function applyTheme() {
|
|
3971
|
+
const light = detectLightTheme();
|
|
3972
|
+
if (light === lightTheme) return;
|
|
3973
|
+
lightTheme = light;
|
|
3974
|
+
rootEl.classList.toggle("theme-light", light);
|
|
3975
|
+
modulePromise.then((Module) => {
|
|
3976
|
+
Module.setTheme(light);
|
|
3977
|
+
const layers = Module.getLayers();
|
|
3978
|
+
if (layers.length > 0) renderLayerList(layers);
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
function createParseWorker() {
|
|
3982
|
+
if (hostCan("createWorker")) return host.createWorker();
|
|
3983
|
+
if (workerBundle) {
|
|
3984
|
+
trace("[GDS] building worker from the bundled script");
|
|
3985
|
+
const blob = new Blob([workerBundle.text()], { type: "text/javascript" });
|
|
3986
|
+
return new Worker(URL.createObjectURL(blob), { type: workerBundle.type });
|
|
3987
|
+
}
|
|
3988
|
+
const url = (name) => JSON.stringify(new URL(name, document.baseURI).href);
|
|
3989
|
+
const bootstrap = `self.gdsLensScriptBase = ${url(".")};
|
|
3990
|
+
importScripts(${url("gds-lens-engine.js")}, ${url("gds-lens-worker.js")});`;
|
|
3991
|
+
trace("[GDS] building worker from document-relative script URLs");
|
|
3992
|
+
return new Worker(URL.createObjectURL(new Blob([bootstrap], { type: "application/javascript" })));
|
|
3993
|
+
}
|
|
3994
|
+
function adopt(element) {
|
|
3995
|
+
if (!element || element === hostElement) return;
|
|
3996
|
+
const next = element.shadowRoot || element.attachShadow({ mode: "open" });
|
|
3997
|
+
for (const name of hostElement.classList) element.classList.add(name);
|
|
3998
|
+
while (shadow.firstChild) next.appendChild(shadow.firstChild);
|
|
3999
|
+
hostElement = element;
|
|
4000
|
+
rootEl = element;
|
|
4001
|
+
shadow = next;
|
|
4002
|
+
viewerRoot = next;
|
|
4003
|
+
if (resolvedModule) resolvedModule.gdsLensRoot = next;
|
|
4004
|
+
else modulePromise.then((Module) => {
|
|
4005
|
+
Module.gdsLensRoot = next;
|
|
4006
|
+
}).catch(() => {
|
|
4007
|
+
});
|
|
4008
|
+
for (const callback of adoptCallbacks) {
|
|
4009
|
+
try {
|
|
4010
|
+
callback(element);
|
|
4011
|
+
} catch (err) {
|
|
4012
|
+
fail("[GDS] a host's onAdopt callback threw:", err);
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
trace("[GDS] moved the viewer into a new <gds-lens>");
|
|
4016
|
+
}
|
|
4017
|
+
function asBytes(source) {
|
|
4018
|
+
if (source instanceof Uint8Array) return source;
|
|
4019
|
+
if (source instanceof ArrayBuffer) return new Uint8Array(source);
|
|
4020
|
+
if (source && source.buffer instanceof ArrayBuffer) {
|
|
4021
|
+
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
4022
|
+
}
|
|
4023
|
+
return source;
|
|
4024
|
+
}
|
|
4025
|
+
async function loadLayout(source, { reload = false } = {}) {
|
|
4026
|
+
const bytes = asBytes(source);
|
|
4027
|
+
trace(
|
|
4028
|
+
"[GDS] init payload: fileData byteLength =",
|
|
4029
|
+
bytes && bytes.byteLength,
|
|
4030
|
+
"reload:",
|
|
4031
|
+
!!reload
|
|
4032
|
+
);
|
|
4033
|
+
if (activeWorker) {
|
|
4034
|
+
trace("[GDS] superseding an in-flight load");
|
|
4035
|
+
activeWorker.terminate();
|
|
4036
|
+
activeWorker = null;
|
|
4037
|
+
}
|
|
4038
|
+
showStaleBanner(false);
|
|
4039
|
+
pendingViewState = null;
|
|
4040
|
+
if (reload && resolvedModule) {
|
|
4041
|
+
try {
|
|
4042
|
+
pendingViewState = captureViewState(resolvedModule);
|
|
4043
|
+
} catch (err) {
|
|
4044
|
+
fail("[GDS] could not capture view state, reloading framed:", err);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
beginProgress(pendingViewState !== null);
|
|
4048
|
+
let parseBytes = bytes;
|
|
4049
|
+
if (looksGzipped(bytes)) {
|
|
4050
|
+
updateProgress("decompressing", 0, 0);
|
|
4051
|
+
const decoded = await decodeLayoutBytes(bytes, MAX_LAYOUT_BYTES);
|
|
4052
|
+
if (!decoded.ok) {
|
|
4053
|
+
fail(`[GDS] gzip expansion failed (${decoded.reason}):`, decoded.detail);
|
|
4054
|
+
showFatalError(describeDecodeFailure(decoded));
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
trace("[GDS] expanded gzip:", bytes.byteLength, "->", decoded.bytes.byteLength, "bytes");
|
|
4058
|
+
parseBytes = decoded.bytes;
|
|
4059
|
+
}
|
|
4060
|
+
let worker;
|
|
4061
|
+
try {
|
|
4062
|
+
worker = createParseWorker();
|
|
4063
|
+
trace("[GDS] new Worker() constructor returned OK");
|
|
4064
|
+
} catch (err) {
|
|
4065
|
+
fail("[GDS] failed to build/start worker:", err);
|
|
4066
|
+
showFatalError(`Failed to create worker: ${err.message || err}`);
|
|
4067
|
+
return;
|
|
4068
|
+
}
|
|
4069
|
+
startWorker(worker, parseBytes);
|
|
4070
|
+
}
|
|
4071
|
+
function applyLyp(name, text) {
|
|
4072
|
+
modulePromise.then((Module) => {
|
|
4073
|
+
Module.loadLypText(text);
|
|
4074
|
+
renderLayerList(Module.getLayers());
|
|
4075
|
+
});
|
|
4076
|
+
setLypChip(name || null);
|
|
4077
|
+
}
|
|
4078
|
+
function applyMarkers(name, text) {
|
|
4079
|
+
modulePromise.then((Module) => {
|
|
4080
|
+
let model;
|
|
4081
|
+
try {
|
|
4082
|
+
model = parseMarkerFile(text, DOMParser);
|
|
4083
|
+
} catch (err) {
|
|
4084
|
+
fail("[GDS] marker parse failed:", err);
|
|
4085
|
+
removeMarkerBrowser();
|
|
4086
|
+
currentMarkers = null;
|
|
4087
|
+
Module.clearMarkers();
|
|
4088
|
+
setMarkerChip(name || null);
|
|
4089
|
+
markerController.domElement.title = `Failed to parse ${name}: ${err.message || err}`;
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
currentMarkers = model;
|
|
4093
|
+
Module.setMarkers(flattenMarkerModel(model));
|
|
4094
|
+
Module.setMarkerOpacity(markerUiState.opacity);
|
|
4095
|
+
selectedMarkerId = -1;
|
|
4096
|
+
renderMarkerBrowser(model);
|
|
4097
|
+
setMarkerChip(name || null);
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4100
|
+
function goToPointFromHost(x, y) {
|
|
4101
|
+
return modulePromise.then((Module) => {
|
|
4102
|
+
const onScreen = Module.goToPoint(x, y);
|
|
4103
|
+
Module.flashPoint(x, y);
|
|
4104
|
+
hostCall("onGotoResult", { ok: !!onScreen, x, y });
|
|
4105
|
+
return !!onScreen;
|
|
4106
|
+
});
|
|
4107
|
+
}
|
|
4108
|
+
function toggleDebug() {
|
|
4109
|
+
rootEl.classList.toggle("debug");
|
|
4110
|
+
}
|
|
4111
|
+
function startWorker(worker, fileData) {
|
|
4112
|
+
activeWorker = worker;
|
|
4113
|
+
worker.onerror = (err) => {
|
|
4114
|
+
fail("[GDS] worker.onerror fired:", err.message, "at", err.filename + ":" + err.lineno + ":" + err.colno, err.error);
|
|
4115
|
+
showFatalError(`Worker failed to start: ${err.message || err}`);
|
|
4116
|
+
};
|
|
4117
|
+
worker.onmessageerror = (err) => {
|
|
4118
|
+
fail("[GDS] worker.onmessageerror fired (structured-clone failure):", err);
|
|
4119
|
+
showFatalError("Worker message failed to deserialize -- see devtools console");
|
|
4120
|
+
};
|
|
4121
|
+
worker.onmessage = (workerEvent) => {
|
|
4122
|
+
const workerMessage = workerEvent.data;
|
|
4123
|
+
if (workerMessage.type === "gdsLog") {
|
|
4124
|
+
appendDebugLine("[worker] " + workerMessage.text, workerMessage.level === "error");
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
trace("[GDS] main thread received worker message:", workerMessage.type);
|
|
4128
|
+
if (workerMessage.type === "gdsProgress") {
|
|
4129
|
+
updateProgress(workerMessage.phase, workerMessage.current, workerMessage.total);
|
|
4130
|
+
} else if (workerMessage.type === "gdsResult") {
|
|
4131
|
+
worker.terminate();
|
|
4132
|
+
if (activeWorker === worker) activeWorker = null;
|
|
4133
|
+
if (!workerMessage.ok) {
|
|
4134
|
+
showFatalError(workerMessage.error);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
trace("[GDS] load succeeded, layer count:", workerMessage.layers.length);
|
|
4138
|
+
modulePromise.then((Module) => {
|
|
4139
|
+
try {
|
|
4140
|
+
Module.uploadLayers(workerMessage.layers, workerMessage.instanceGroups, workerMessage.bbox);
|
|
4141
|
+
} catch (err) {
|
|
4142
|
+
showFatalError(describeLoadFailure(err));
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
clearFatalError();
|
|
4146
|
+
if (pendingViewState) {
|
|
4147
|
+
try {
|
|
4148
|
+
restoreViewState(Module, pendingViewState);
|
|
4149
|
+
} catch (err) {
|
|
4150
|
+
fail("[GDS] could not restore view state:", err);
|
|
4151
|
+
}
|
|
4152
|
+
pendingViewState = null;
|
|
4153
|
+
}
|
|
4154
|
+
renderLayerList(Module.getLayers());
|
|
4155
|
+
renderHierarchy(workerMessage.hierarchy);
|
|
4156
|
+
viewsFolder.show();
|
|
4157
|
+
refreshRulerRow(Module);
|
|
4158
|
+
endProgress();
|
|
4159
|
+
trace("[GDS] done, progress hidden");
|
|
4160
|
+
}, (err) => {
|
|
4161
|
+
showFatalError(`WebAssembly module failed to load: ${err && err.message ? err.message : err}`);
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
};
|
|
4165
|
+
trace("[GDS] posting 'parse' message to worker...");
|
|
4166
|
+
let transfer;
|
|
4167
|
+
if (fileData instanceof ArrayBuffer) {
|
|
4168
|
+
transfer = fileData;
|
|
4169
|
+
} else if (fileData.byteOffset === 0 && fileData.byteLength === fileData.buffer.byteLength) {
|
|
4170
|
+
transfer = fileData.buffer;
|
|
4171
|
+
} else {
|
|
4172
|
+
transfer = fileData.slice().buffer;
|
|
4173
|
+
}
|
|
4174
|
+
worker.postMessage(
|
|
4175
|
+
{ type: "parse", fileData: transfer },
|
|
4176
|
+
[transfer]
|
|
4177
|
+
);
|
|
4178
|
+
trace("[GDS] worker.postMessage('parse') call returned");
|
|
4179
|
+
}
|
|
4180
|
+
var hostElement, shadow, viewerRoot, rootEl, els, debugLogEl, MAX_DEBUG_LINES, debugRequested, traceToConsole, debugPanelEl, debugToggleBtn, debugCopyBtn, host, hostCan, hostCall, gui, actions, displayFolder, textController, gridController, lypController, markerController, MODES, currentMode, modeButtons, modeRow, modeName, modeWidget, rulerRow, rulerName, rulerWidget, rulerClearBtn, glCanvas, layersFolder, layerRows, layerCategories, layerFilterActive, soloTag, soloRestore, hierarchyPanel, hierarchyTree, hierarchyCount, hierarchyHide, hierarchyShowBtn, hierarchyModel, hierarchyExpanded, hierarchySelectedPath, hierarchySelectedRow, hierarchySelectedBoxes, hierarchyRootKey, hierarchyUserChoice, HIERARCHY_MAX_DEPTH, HIERARCHY_IDENTITY, hierarchyRevealPath, hierarchyFindToggle, hierarchyFindTwisty, hierarchySearchBox, hierarchySearchInput, hierarchySearchCount, hierarchyResults, hierarchyScopeCells, hierarchyScopeLabels, MAX_FIND_ROWS, LABEL_MARK_PX, findScope, findQuery, findRows, findActiveIndex, currentMarkers, markersFolder, selectedMarkerId, selectedMarkerRow, markerItemRows, markerUiState, MAX_MARKER_ROWS_PER_CATEGORY, copyToastEl, copyToastTimer, canvasMenuEl, canvasMenuCopyEl, canvasMenuValueEl, canvasMenuText, staleBanner, staleText, staleReloadBtn, staleAlwaysBtn, staleDismiss, viewsFolder, namedViews, viewControllers, pendingViewCapture, saveViewController, loadingOverlay, loadingBarFill, loadingPhase, loadingPercent, reloadProgress, reloadBarFill, reloadLabel, loadError, phaseLabels, progressInline, resolvedModule, activeWorker, pendingViewState, modulePromise, lightMediaQuery, lightTheme, adoptCallbacks, MAX_LAYOUT_BYTES, viewer, controllerRow;
|
|
4181
|
+
var init_viewer2 = __esm({
|
|
4182
|
+
"src/viewer.js"() {
|
|
4183
|
+
init_cell_search();
|
|
4184
|
+
init_marker_parsers();
|
|
4185
|
+
init_load_errors();
|
|
4186
|
+
init_layout_bytes();
|
|
4187
|
+
init_engine_source();
|
|
4188
|
+
init_mount_target();
|
|
4189
|
+
init_viewer_shell();
|
|
4190
|
+
init_viewer();
|
|
4191
|
+
init_lil_gui_esm();
|
|
4192
|
+
init_lil_gui();
|
|
4193
|
+
hostElement = takeMountTarget() || document.querySelector("gds-lens") || document.body.appendChild(document.createElement("gds-lens"));
|
|
4194
|
+
shadow = hostElement.shadowRoot || hostElement.attachShadow({ mode: "open" });
|
|
4195
|
+
shadow.innerHTML = `<style>${lil_gui_default}</style><style>${viewer_default}</style>${viewer_shell_default}`;
|
|
4196
|
+
viewerRoot = shadow;
|
|
4197
|
+
rootEl = hostElement;
|
|
4198
|
+
els = Object.fromEntries(
|
|
4199
|
+
Array.from(viewerRoot.querySelectorAll("[id]"), (el) => [el.id, el])
|
|
4200
|
+
);
|
|
4201
|
+
debugLogEl = els.debugLog;
|
|
4202
|
+
MAX_DEBUG_LINES = 500;
|
|
4203
|
+
debugRequested = () => {
|
|
4204
|
+
if (rootEl && rootEl.hasAttribute && rootEl.hasAttribute("debug")) return true;
|
|
4205
|
+
try {
|
|
4206
|
+
return new URLSearchParams(location.search).get("gdsDebug") === "1";
|
|
4207
|
+
} catch {
|
|
4208
|
+
return false;
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
traceToConsole = debugRequested();
|
|
4212
|
+
debugPanelEl = els.debugPanel;
|
|
4213
|
+
debugToggleBtn = els.debugToggleBtn;
|
|
4214
|
+
if (debugToggleBtn && debugPanelEl) {
|
|
4215
|
+
debugToggleBtn.addEventListener("click", () => {
|
|
4216
|
+
const open = debugPanelEl.classList.toggle("hidden") === false;
|
|
4217
|
+
debugToggleBtn.setAttribute("aria-expanded", String(open));
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
debugCopyBtn = els.debugCopyBtn;
|
|
4221
|
+
if (debugCopyBtn) {
|
|
4222
|
+
debugCopyBtn.addEventListener("click", () => {
|
|
4223
|
+
const text = debugLogEl ? debugLogEl.innerText : "";
|
|
4224
|
+
navigator.clipboard.writeText(text).then(
|
|
4225
|
+
() => trace("[GDS] debug log copied to clipboard"),
|
|
4226
|
+
(err) => {
|
|
4227
|
+
fail("[GDS] clipboard write failed, select-all instead:", err);
|
|
4228
|
+
if (!debugLogEl) return;
|
|
4229
|
+
const range = document.createRange();
|
|
4230
|
+
range.selectNodeContents(debugLogEl);
|
|
4231
|
+
const sel = window.getSelection();
|
|
4232
|
+
sel.removeAllRanges();
|
|
4233
|
+
sel.addRange(range);
|
|
4234
|
+
}
|
|
4235
|
+
);
|
|
4236
|
+
});
|
|
4237
|
+
}
|
|
4238
|
+
trace("[GDS] viewer.js starting to execute");
|
|
4239
|
+
window.onerror = (msg, url, line, col, err) => {
|
|
4240
|
+
fail("[GDS] window.onerror:", msg, "at", url + ":" + line + ":" + col, err && err.stack);
|
|
4241
|
+
};
|
|
4242
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
4243
|
+
fail("[GDS] unhandled promise rejection on main thread:", event.reason);
|
|
4244
|
+
});
|
|
4245
|
+
host = typeof window !== "undefined" && window.gdsLensHost || {};
|
|
4246
|
+
if (!window.gdsLensHost) {
|
|
4247
|
+
fail(
|
|
4248
|
+
"[GDS] no window.gdsLensHost -- gds-lens-host.js did not load or did not run. Nothing can drive the viewer, so no layout will ever appear."
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
hostCan = (name) => typeof host[name] === "function";
|
|
4252
|
+
hostCall = (name, ...args) => hostCan(name) ? host[name](...args) : void 0;
|
|
4253
|
+
trace("[GDS] host ready; Worker:", typeof Worker, "Blob:", typeof Blob, "bundled worker:", !!workerBundle);
|
|
4254
|
+
gui = new GUI({ width: 260, container: viewerRoot.getElementById("guiHost"), injectStyles: false });
|
|
4255
|
+
actions = {
|
|
4256
|
+
// Clicking the row always opens the file dialog (load, or replace the
|
|
4257
|
+
// current file); the injected ✕ (see setFileChip) handles unloading.
|
|
4258
|
+
loadLypFile: () => Promise.resolve(hostCall("pickLyp")).then((picked) => {
|
|
4259
|
+
if (picked) applyLyp(picked.name, picked.text);
|
|
4260
|
+
}),
|
|
4261
|
+
loadMarkerFile: () => Promise.resolve(hostCall("pickMarkers")).then((picked) => {
|
|
4262
|
+
if (picked) applyMarkers(picked.name, picked.text);
|
|
4263
|
+
}),
|
|
4264
|
+
resetView: () => modulePromise.then((Module) => Module.resetView()),
|
|
4265
|
+
showInfill: false,
|
|
4266
|
+
showText: false,
|
|
4267
|
+
mergeOverlaps: false,
|
|
4268
|
+
// On by default -- matches g_show_grid in renderer.cpp, which is the
|
|
4269
|
+
// renderer's own initial state (nothing pushes this value down at startup).
|
|
4270
|
+
showGrid: true
|
|
4271
|
+
};
|
|
4272
|
+
displayFolder = gui.addFolder("Display");
|
|
4273
|
+
displayFolder.close();
|
|
4274
|
+
displayFolder.add(actions, "showInfill").name("Infill").onChange((show) => modulePromise.then((Module) => Module.setShowInfill(show)));
|
|
4275
|
+
textController = displayFolder.add(actions, "showText").name("Text").onChange((show) => modulePromise.then((Module) => Module.setShowText(show)));
|
|
4276
|
+
textController.domElement.title = "Show layout text labels, drawn in their layer's color";
|
|
4277
|
+
displayFolder.add(actions, "mergeOverlaps").name("Merge Overlaps").onChange((on) => modulePromise.then((Module) => Module.setMergeMode(on)));
|
|
4278
|
+
gridController = displayFolder.add(actions, "showGrid").name("Grid").onChange((show) => modulePromise.then((Module) => Module.setShowGrid(show)));
|
|
4279
|
+
gridController.domElement.title = "Show the background grid, spaced at a round step that follows the zoom";
|
|
4280
|
+
lypController = displayFolder.add(actions, "loadLypFile").name("Load .lyp File");
|
|
4281
|
+
markerController = displayFolder.add(actions, "loadMarkerFile").name("Load Marker File (.lyrdb / DRC)");
|
|
4282
|
+
displayFolder.add(actions, "resetView").name("Reset View");
|
|
4283
|
+
MODES = [
|
|
4284
|
+
{ id: "pan", label: "Pan", title: "Drag to pan the view, wheel to zoom" },
|
|
4285
|
+
{
|
|
4286
|
+
id: "measure",
|
|
4287
|
+
label: "Measure",
|
|
4288
|
+
title: "Click two points to measure between them. Snaps to nearby vertices and edges (Alt to place freely), Shift constrains to horizontal/vertical, Esc cancels."
|
|
4289
|
+
}
|
|
4290
|
+
];
|
|
4291
|
+
currentMode = "pan";
|
|
4292
|
+
modeButtons = /* @__PURE__ */ new Map();
|
|
4293
|
+
modeRow = document.createElement("div");
|
|
4294
|
+
modeRow.className = "lil-controller mode-row";
|
|
4295
|
+
modeName = document.createElement("div");
|
|
4296
|
+
modeName.className = "lil-name";
|
|
4297
|
+
modeName.textContent = "Mode";
|
|
4298
|
+
modeWidget = document.createElement("div");
|
|
4299
|
+
modeWidget.className = "lil-widget mode-widget";
|
|
4300
|
+
for (const mode of MODES) {
|
|
4301
|
+
const btn = document.createElement("button");
|
|
4302
|
+
btn.type = "button";
|
|
4303
|
+
btn.textContent = mode.label;
|
|
4304
|
+
btn.title = mode.title;
|
|
4305
|
+
btn.addEventListener("click", () => setMode(mode.id));
|
|
4306
|
+
modeWidget.appendChild(btn);
|
|
4307
|
+
modeButtons.set(mode.id, btn);
|
|
4308
|
+
}
|
|
4309
|
+
modeRow.appendChild(modeName);
|
|
4310
|
+
modeRow.appendChild(modeWidget);
|
|
4311
|
+
gui.$children.prepend(modeRow);
|
|
4312
|
+
modeButtons.get(currentMode).classList.add("mode-active");
|
|
4313
|
+
rulerRow = document.createElement("div");
|
|
4314
|
+
rulerRow.className = "lil-controller mode-row ruler-row";
|
|
4315
|
+
rulerRow.style.display = "none";
|
|
4316
|
+
rulerName = document.createElement("div");
|
|
4317
|
+
rulerName.className = "lil-name";
|
|
4318
|
+
rulerName.textContent = "Rulers";
|
|
4319
|
+
rulerWidget = document.createElement("div");
|
|
4320
|
+
rulerWidget.className = "lil-widget mode-widget";
|
|
4321
|
+
rulerClearBtn = document.createElement("button");
|
|
4322
|
+
rulerClearBtn.type = "button";
|
|
4323
|
+
rulerClearBtn.title = "Remove every measurement on the canvas (also Esc, once nothing is being placed)";
|
|
4324
|
+
rulerClearBtn.addEventListener("click", () => {
|
|
4325
|
+
modulePromise.then((Module) => {
|
|
4326
|
+
Module.clearMeasurements();
|
|
4327
|
+
refreshRulerRow(Module);
|
|
4328
|
+
});
|
|
4329
|
+
});
|
|
4330
|
+
rulerWidget.appendChild(rulerClearBtn);
|
|
4331
|
+
rulerRow.append(rulerName, rulerWidget);
|
|
4332
|
+
modeRow.after(rulerRow);
|
|
4333
|
+
glCanvas = els.glCanvas;
|
|
4334
|
+
if (glCanvas) {
|
|
4335
|
+
glCanvas.addEventListener("mousedown", () => {
|
|
4336
|
+
if (currentMode !== "measure") return;
|
|
4337
|
+
setTimeout(() => modulePromise.then(refreshRulerRow), 0);
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
setLypChip(null);
|
|
4341
|
+
setMarkerChip(null);
|
|
4342
|
+
layersFolder = null;
|
|
4343
|
+
layerRows = [];
|
|
4344
|
+
layerCategories = [];
|
|
4345
|
+
layerFilterActive = false;
|
|
4346
|
+
soloTag = null;
|
|
4347
|
+
soloRestore = null;
|
|
4348
|
+
hierarchyPanel = els.hierarchyPanel;
|
|
4349
|
+
hierarchyTree = els.hierarchyTree;
|
|
4350
|
+
hierarchyCount = els.hierarchyCount;
|
|
4351
|
+
hierarchyHide = els.hierarchyHide;
|
|
4352
|
+
hierarchyShowBtn = els.hierarchyShowBtn;
|
|
4353
|
+
hierarchyModel = null;
|
|
4354
|
+
hierarchyExpanded = /* @__PURE__ */ new Set();
|
|
4355
|
+
hierarchySelectedPath = null;
|
|
4356
|
+
hierarchySelectedRow = null;
|
|
4357
|
+
hierarchySelectedBoxes = [];
|
|
4358
|
+
hierarchyRootKey = null;
|
|
4359
|
+
hierarchyUserChoice = null;
|
|
4360
|
+
HIERARCHY_MAX_DEPTH = 256;
|
|
4361
|
+
HIERARCHY_IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
4362
|
+
hierarchyRevealPath = null;
|
|
4363
|
+
if (hierarchyHide) {
|
|
4364
|
+
hierarchyHide.addEventListener("click", () => setHierarchyOpen(false, true));
|
|
4365
|
+
}
|
|
4366
|
+
if (hierarchyShowBtn) {
|
|
4367
|
+
hierarchyShowBtn.addEventListener("click", () => setHierarchyOpen(true, true));
|
|
4368
|
+
}
|
|
4369
|
+
hierarchyFindToggle = els.hierarchyFindToggle;
|
|
4370
|
+
hierarchyFindTwisty = els.hierarchyFindTwisty;
|
|
4371
|
+
hierarchySearchBox = els.hierarchySearch;
|
|
4372
|
+
hierarchySearchInput = els.hierarchySearchInput;
|
|
4373
|
+
hierarchySearchCount = els.hierarchySearchCount;
|
|
4374
|
+
hierarchyResults = els.hierarchyResults;
|
|
4375
|
+
hierarchyScopeCells = els.hierarchyScopeCells;
|
|
4376
|
+
hierarchyScopeLabels = els.hierarchyScopeLabels;
|
|
4377
|
+
MAX_FIND_ROWS = 200;
|
|
4378
|
+
LABEL_MARK_PX = 14;
|
|
4379
|
+
findScope = "cells";
|
|
4380
|
+
findQuery = "";
|
|
4381
|
+
findRows = [];
|
|
4382
|
+
findActiveIndex = -1;
|
|
4383
|
+
if (hierarchySearchInput) {
|
|
4384
|
+
hierarchySearchInput.addEventListener("input", runSearch);
|
|
4385
|
+
hierarchySearchInput.addEventListener("focus", () => {
|
|
4386
|
+
if (findQuery) setFindResultsOpen(true);
|
|
4387
|
+
});
|
|
4388
|
+
hierarchySearchInput.addEventListener("keydown", (event) => {
|
|
4389
|
+
if (event.key === "Escape") {
|
|
4390
|
+
if (hierarchySearchInput.value) {
|
|
4391
|
+
hierarchySearchInput.value = "";
|
|
4392
|
+
runSearch();
|
|
4393
|
+
} else {
|
|
4394
|
+
setFindOpen(false);
|
|
4395
|
+
}
|
|
4396
|
+
event.stopPropagation();
|
|
4397
|
+
} else if (event.key === "Enter") {
|
|
4398
|
+
activateFindRow(findActiveIndex < 0 ? 0 : findActiveIndex);
|
|
4399
|
+
} else if (event.key === "ArrowDown") {
|
|
4400
|
+
event.preventDefault();
|
|
4401
|
+
stepFindRow(1);
|
|
4402
|
+
} else if (event.key === "ArrowUp") {
|
|
4403
|
+
event.preventDefault();
|
|
4404
|
+
stepFindRow(-1);
|
|
4405
|
+
}
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
if (hierarchyScopeCells) hierarchyScopeCells.addEventListener("click", () => setFindScope("cells"));
|
|
4409
|
+
if (hierarchyScopeLabels) hierarchyScopeLabels.addEventListener("click", () => setFindScope("labels"));
|
|
4410
|
+
if (hierarchyFindToggle) hierarchyFindToggle.addEventListener("click", () => setFindOpen(!findIsOpen()));
|
|
4411
|
+
updateFindScope();
|
|
4412
|
+
currentMarkers = null;
|
|
4413
|
+
markersFolder = null;
|
|
4414
|
+
selectedMarkerId = -1;
|
|
4415
|
+
selectedMarkerRow = null;
|
|
4416
|
+
markerItemRows = /* @__PURE__ */ new Map();
|
|
4417
|
+
markerUiState = { opacity: 1, hideEmpty: false };
|
|
4418
|
+
MAX_MARKER_ROWS_PER_CATEGORY = 200;
|
|
4419
|
+
copyToastEl = els.copyToast;
|
|
4420
|
+
copyToastTimer = 0;
|
|
4421
|
+
canvasMenuEl = els.canvasMenu;
|
|
4422
|
+
canvasMenuCopyEl = els.canvasMenuCopy;
|
|
4423
|
+
canvasMenuValueEl = canvasMenuEl && canvasMenuEl.querySelector(".menu-value");
|
|
4424
|
+
canvasMenuText = "";
|
|
4425
|
+
if (glCanvas && canvasMenuEl && canvasMenuCopyEl) {
|
|
4426
|
+
glCanvas.addEventListener("contextmenu", (event) => {
|
|
4427
|
+
if (!resolvedModule) return;
|
|
4428
|
+
event.preventDefault();
|
|
4429
|
+
showCanvasMenu(
|
|
4430
|
+
event.clientX,
|
|
4431
|
+
event.clientY,
|
|
4432
|
+
resolvedModule.getCoordinateTextAt(event.clientX, event.clientY)
|
|
4433
|
+
);
|
|
4434
|
+
});
|
|
4435
|
+
canvasMenuCopyEl.addEventListener("click", () => {
|
|
4436
|
+
const text = canvasMenuText;
|
|
4437
|
+
hideCanvasMenu();
|
|
4438
|
+
navigator.clipboard.writeText(text).then(
|
|
4439
|
+
() => showCopyToast("Copied \u2014 " + text),
|
|
4440
|
+
(err) => {
|
|
4441
|
+
fail("[GDS] clipboard write failed for coordinate:", err);
|
|
4442
|
+
showCopyToast("Couldn't copy \u2014 " + text);
|
|
4443
|
+
}
|
|
4444
|
+
);
|
|
4445
|
+
});
|
|
4446
|
+
window.addEventListener("pointerdown", (event) => {
|
|
4447
|
+
if (!canvasMenuEl.contains(event.target)) hideCanvasMenu();
|
|
4448
|
+
}, true);
|
|
4449
|
+
window.addEventListener("wheel", hideCanvasMenu, { passive: true });
|
|
4450
|
+
window.addEventListener("resize", hideCanvasMenu);
|
|
4451
|
+
window.addEventListener("blur", hideCanvasMenu);
|
|
4452
|
+
}
|
|
4453
|
+
window.addEventListener("keydown", (event) => {
|
|
4454
|
+
const t = event.target;
|
|
4455
|
+
const tag = t && t.tagName;
|
|
4456
|
+
if (tag === "TEXTAREA" || tag === "INPUT" && t.type !== "checkbox") return;
|
|
4457
|
+
if (event.key === "[") stepMarker(-1);
|
|
4458
|
+
else if (event.key === "]") stepMarker(1);
|
|
4459
|
+
else if (event.key === "m" || event.key === "M") setMode(currentMode === "measure" ? "pan" : "measure");
|
|
4460
|
+
else if (event.key === "Escape") {
|
|
4461
|
+
if (canvasMenuEl && !canvasMenuEl.classList.contains("hidden")) {
|
|
4462
|
+
hideCanvasMenu();
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
modulePromise.then((Module) => {
|
|
4466
|
+
if (!Module.escapeMeasure()) setMode("pan");
|
|
4467
|
+
refreshRulerRow(Module);
|
|
4468
|
+
});
|
|
4469
|
+
hierarchyDeselect();
|
|
4470
|
+
} else if (event.key === "h" || event.key === "H") toggleHierarchy();
|
|
4471
|
+
else if (event.key === "/") {
|
|
4472
|
+
event.preventDefault();
|
|
4473
|
+
focusFindBox();
|
|
4474
|
+
}
|
|
4475
|
+
}, true);
|
|
4476
|
+
staleBanner = els.staleBanner;
|
|
4477
|
+
staleText = els.staleText;
|
|
4478
|
+
staleReloadBtn = els.staleReloadBtn;
|
|
4479
|
+
staleAlwaysBtn = els.staleAlwaysBtn;
|
|
4480
|
+
staleDismiss = els.staleDismiss;
|
|
4481
|
+
if (staleReloadBtn) {
|
|
4482
|
+
staleReloadBtn.addEventListener("click", () => {
|
|
4483
|
+
showStaleBanner(false);
|
|
4484
|
+
hostCall("requestReload");
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
if (staleAlwaysBtn) {
|
|
4488
|
+
staleAlwaysBtn.addEventListener("click", () => {
|
|
4489
|
+
showStaleBanner(false);
|
|
4490
|
+
hostCall("setAutoReload", true);
|
|
4491
|
+
hostCall("requestReload");
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
if (staleDismiss) {
|
|
4495
|
+
staleDismiss.addEventListener("click", () => showStaleBanner(false));
|
|
4496
|
+
}
|
|
4497
|
+
viewsFolder = gui.addFolder("Views");
|
|
4498
|
+
viewsFolder.close();
|
|
4499
|
+
viewsFolder.hide();
|
|
4500
|
+
namedViews = [];
|
|
4501
|
+
viewControllers = [];
|
|
4502
|
+
pendingViewCapture = null;
|
|
4503
|
+
saveViewController = viewsFolder.add({ save: () => requestSaveView() }, "save").name("Save Current View");
|
|
4504
|
+
saveViewController.domElement.title = "Name the current camera and layer visibility, and keep it with this layout";
|
|
4505
|
+
loadingOverlay = els.loadingOverlay;
|
|
4506
|
+
loadingBarFill = els.loadingBarFill;
|
|
4507
|
+
loadingPhase = els.loadingPhase;
|
|
4508
|
+
loadingPercent = els.loadingPercent;
|
|
4509
|
+
reloadProgress = els.reloadProgress;
|
|
4510
|
+
reloadBarFill = els.reloadBarFill;
|
|
4511
|
+
reloadLabel = els.reloadLabel;
|
|
4512
|
+
loadError = els.loadError;
|
|
4513
|
+
phaseLabels = {
|
|
4514
|
+
decompressing: "Decompressing layout...",
|
|
4515
|
+
parsing: "Parsing layout file...",
|
|
4516
|
+
flattening: "Flattening hierarchy...",
|
|
4517
|
+
triangulating: "Triangulating geometry..."
|
|
4518
|
+
};
|
|
4519
|
+
progressInline = false;
|
|
4520
|
+
trace("[GDS] resolving the wasm factory on the main thread...");
|
|
4521
|
+
resolvedModule = null;
|
|
4522
|
+
activeWorker = null;
|
|
4523
|
+
pendingViewState = null;
|
|
4524
|
+
modulePromise = loadGdstkFactory().then((createGdstkModule) => {
|
|
4525
|
+
if (typeof createGdstkModule !== "function") {
|
|
4526
|
+
throw new Error(
|
|
4527
|
+
"gds-lens-engine.js did not load: createGdstkModule is not defined. In the served payload it is a classic <script> that must come before gds-lens.js (see gds-lens.html)."
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
return createGdstkModule({
|
|
4531
|
+
// Read by dom_root() in renderer.cpp for its own element lookups.
|
|
4532
|
+
// Passed in the instantiation object so it is in place before main()
|
|
4533
|
+
// runs -- and re-pointed by adopt() if the viewer later moves.
|
|
4534
|
+
gdsLensRoot: viewerRoot,
|
|
4535
|
+
preRun: [(Module) => {
|
|
4536
|
+
Module.specialHTMLTargets["!gdsLensCanvas"] = viewerRoot.getElementById("glCanvas");
|
|
4537
|
+
}]
|
|
4538
|
+
});
|
|
4539
|
+
});
|
|
4540
|
+
modulePromise.then(
|
|
4541
|
+
(Module) => {
|
|
4542
|
+
resolvedModule = Module;
|
|
4543
|
+
trace("[GDS] main-thread createGdstkModule() resolved OK");
|
|
4544
|
+
},
|
|
4545
|
+
(err) => {
|
|
4546
|
+
fail("[GDS] main-thread createGdstkModule() REJECTED:", err);
|
|
4547
|
+
showFatalError(`WebAssembly module failed to load: ${describeLoadFailure(err)}`);
|
|
4548
|
+
}
|
|
4549
|
+
);
|
|
4550
|
+
lightMediaQuery = window.matchMedia("(prefers-color-scheme: light)");
|
|
4551
|
+
lightTheme = null;
|
|
4552
|
+
new MutationObserver(applyTheme).observe(rootEl, { attributes: true, attributeFilter: ["class"] });
|
|
4553
|
+
lightMediaQuery.addEventListener("change", applyTheme);
|
|
4554
|
+
applyTheme();
|
|
4555
|
+
adoptCallbacks = /* @__PURE__ */ new Set();
|
|
4556
|
+
MAX_LAYOUT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
4557
|
+
viewer = {
|
|
4558
|
+
// The element the viewer is mounted in. A host needs it to scope anything
|
|
4559
|
+
// it binds to the viewer's own surface -- drag-and-drop above all, which
|
|
4560
|
+
// on `window` would preventDefault every drag in the embedding page and
|
|
4561
|
+
// quietly break the host's own drop targets.
|
|
4562
|
+
get element() {
|
|
4563
|
+
return hostElement;
|
|
4564
|
+
},
|
|
4565
|
+
// Called with the new element when the viewer moves (see adopt). Returns a
|
|
4566
|
+
// function that unregisters. A host that binds anything to `element` needs
|
|
4567
|
+
// this, or its listeners stay on an element that is no longer in the page.
|
|
4568
|
+
onAdopt(callback) {
|
|
4569
|
+
adoptCallbacks.add(callback);
|
|
4570
|
+
return () => adoptCallbacks.delete(callback);
|
|
4571
|
+
},
|
|
4572
|
+
load: loadLayout,
|
|
4573
|
+
showError: showFatalError,
|
|
4574
|
+
setLyp: applyLyp,
|
|
4575
|
+
setMarkers: applyMarkers,
|
|
4576
|
+
showStale: (text) => showStaleBanner(true, text),
|
|
4577
|
+
goToPoint: goToPointFromHost,
|
|
4578
|
+
toggleDebug,
|
|
4579
|
+
// For a host whose theme can change after load, to re-ask isLightTheme().
|
|
4580
|
+
applyTheme,
|
|
4581
|
+
// For a host whose stored views can change after open (another editor on
|
|
4582
|
+
// the same layout saving one, say) rather than only being read once.
|
|
4583
|
+
setNamedViews
|
|
4584
|
+
};
|
|
4585
|
+
controllerRow = (controller) => controller.domElement.closest(".lil-controller");
|
|
4586
|
+
if (!hostCan("pickLyp")) controllerRow(lypController)?.remove();
|
|
4587
|
+
if (!hostCan("pickMarkers")) controllerRow(markerController)?.remove();
|
|
4588
|
+
if (!hostCan("saveViews") && !hostCan("promptViewName")) {
|
|
4589
|
+
controllerRow(saveViewController)?.remove();
|
|
4590
|
+
}
|
|
4591
|
+
Promise.resolve(hostCall("loadViews")).then((views) => {
|
|
4592
|
+
if (views) setNamedViews(views);
|
|
4593
|
+
});
|
|
4594
|
+
hostCall("connect", viewer);
|
|
4595
|
+
}
|
|
4596
|
+
});
|
|
4597
|
+
|
|
4598
|
+
// src/gds-lens.js
|
|
4599
|
+
init_mount_target();
|
|
4600
|
+
var INLINE_WASM = false;
|
|
4601
|
+
function warnIfNotUtf8() {
|
|
4602
|
+
if (!INLINE_WASM) return;
|
|
4603
|
+
const encoding = document.characterSet || document.charset;
|
|
4604
|
+
if (encoding && encoding.toUpperCase() !== "UTF-8") {
|
|
4605
|
+
console.error(
|
|
4606
|
+
`[GDS] this document is ${encoding}, not UTF-8. This build of gds-lens embeds its WebAssembly binary in a script that must be decoded as UTF-8; it will fail to load. Add <meta charset="UTF-8"> to the page, serve the scripts as text/javascript; charset=utf-8, or use the default build, which keeps the binary in a separate file.`
|
|
4607
|
+
);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
var mounted = null;
|
|
4611
|
+
var engine = null;
|
|
4612
|
+
var ElementBase = typeof HTMLElement === "undefined" ? class {
|
|
4613
|
+
} : HTMLElement;
|
|
4614
|
+
var GdsLens = class extends ElementBase {
|
|
4615
|
+
#ready = null;
|
|
4616
|
+
connectedCallback() {
|
|
4617
|
+
if (mounted && mounted !== this) {
|
|
4618
|
+
this.#refuse();
|
|
4619
|
+
return;
|
|
4620
|
+
}
|
|
4621
|
+
if (this.#ready) {
|
|
4622
|
+
mounted = this;
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
mounted = this;
|
|
4626
|
+
warnIfNotUtf8();
|
|
4627
|
+
this.#ready = this.#mount();
|
|
4628
|
+
const src = this.getAttribute("src");
|
|
4629
|
+
if (src) this.#ready.then(() => this.load(src));
|
|
4630
|
+
}
|
|
4631
|
+
async #mount() {
|
|
4632
|
+
if (engine) {
|
|
4633
|
+
engine.adopt(this);
|
|
4634
|
+
return engine.viewer;
|
|
4635
|
+
}
|
|
4636
|
+
setMountTarget(this);
|
|
4637
|
+
engine = await Promise.resolve().then(() => (init_viewer2(), viewer_exports));
|
|
4638
|
+
return engine.viewer;
|
|
4639
|
+
}
|
|
4640
|
+
#refuse() {
|
|
4641
|
+
console.error(
|
|
4642
|
+
"[GDS] only one <gds-lens> can be active at a time: the renderer keeps its state in module-scope globals, so a second one would fight the first. Remove the first from the DOM and this one will take over its engine."
|
|
4643
|
+
);
|
|
4644
|
+
this.textContent = "Only one <gds-lens> is supported at a time.";
|
|
4645
|
+
}
|
|
4646
|
+
// Resolves once the engine has mounted, so callers can await readiness
|
|
4647
|
+
// without racing the dynamic import.
|
|
4648
|
+
get ready() {
|
|
4649
|
+
return this.#ready || Promise.reject(new Error("<gds-lens> is not connected"));
|
|
4650
|
+
}
|
|
4651
|
+
// Accepts what a caller is likely to already have: a URL to fetch, or the
|
|
4652
|
+
// bytes themselves in either of the two shapes they usually arrive in.
|
|
4653
|
+
async load(source, options) {
|
|
4654
|
+
const viewer2 = await this.ready;
|
|
4655
|
+
if (typeof source === "string") {
|
|
4656
|
+
const response = await fetch(source);
|
|
4657
|
+
if (!response.ok) throw new Error(`${source}: HTTP ${response.status}`);
|
|
4658
|
+
return viewer2.load(new Uint8Array(await response.arrayBuffer()), options);
|
|
4659
|
+
}
|
|
4660
|
+
return viewer2.load(source, options);
|
|
4661
|
+
}
|
|
4662
|
+
async goToPoint(x, y) {
|
|
4663
|
+
return (await this.ready).goToPoint(x, y);
|
|
4664
|
+
}
|
|
4665
|
+
async setLyp(name, text) {
|
|
4666
|
+
return (await this.ready).setLyp(name, text);
|
|
4667
|
+
}
|
|
4668
|
+
async setMarkers(name, text) {
|
|
4669
|
+
return (await this.ready).setMarkers(name, text);
|
|
4670
|
+
}
|
|
4671
|
+
async showError(message) {
|
|
4672
|
+
return (await this.ready).showError(message);
|
|
4673
|
+
}
|
|
4674
|
+
// Releases the claim without tearing anything down. The element is very
|
|
4675
|
+
// often coming straight back -- React and friends recreate the node on
|
|
4676
|
+
// re-render -- and discarding the wasm instance and GL context only to
|
|
4677
|
+
// rebuild them a tick later would be far worse than holding them. The
|
|
4678
|
+
// engine stays up; the next element to connect adopts it.
|
|
4679
|
+
disconnectedCallback() {
|
|
4680
|
+
if (mounted === this) mounted = null;
|
|
4681
|
+
}
|
|
4682
|
+
static get observedAttributes() {
|
|
4683
|
+
return ["src"];
|
|
4684
|
+
}
|
|
4685
|
+
attributeChangedCallback(name, previous, current) {
|
|
4686
|
+
if (name === "src" && current && current !== previous && this.#ready) {
|
|
4687
|
+
this.load(current);
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
};
|
|
4691
|
+
if (typeof customElements !== "undefined" && !customElements.get("gds-lens")) {
|
|
4692
|
+
customElements.define("gds-lens", GdsLens);
|
|
4693
|
+
}
|
|
4694
|
+
})();
|
|
4695
|
+
/*! Bundled license information:
|
|
4696
|
+
|
|
4697
|
+
lil-gui/dist/lil-gui.esm.js:
|
|
4698
|
+
(**
|
|
4699
|
+
* lil-gui
|
|
4700
|
+
* https://lil-gui.georgealways.com
|
|
4701
|
+
* @version 0.21.0
|
|
4702
|
+
* @author George Michael Brower
|
|
4703
|
+
* @license MIT
|
|
4704
|
+
*)
|
|
4705
|
+
*/
|