@liquidcars/atlas 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/liquidcars-atlas.js +82 -36
- package/dist/liquidcars-atlas.js.map +1 -1
- package/package.json +1 -1
- package/src/diagram-svg.js +1 -1
- package/src/diagram-view.js +38 -11
- package/src/liquidcars-atlas.js +51 -8
package/package.json
CHANGED
package/src/diagram-svg.js
CHANGED
|
@@ -539,7 +539,7 @@ export function renderDiagramSvg(atlas, options = {}) {
|
|
|
539
539
|
if (hiddenByFocus(box.node.id)) return "";
|
|
540
540
|
const baseColor = box.node.material?.color || new THREE.Color("#58788d");
|
|
541
541
|
const color = `#${baseColor.getHexString()}`;
|
|
542
|
-
const ink = `#${baseColor.clone().lerp(new THREE.Color("#254253"), .48).getHexString()}`;
|
|
542
|
+
const ink = `#${baseColor.clone().lerp(new THREE.Color(options.containerInk || "#254253"), options.containerInk ? .65 : .48).getHexString()}`;
|
|
543
543
|
let parent = box.node.parent, guard = 0;
|
|
544
544
|
while (parent && !boxBackdrops.has(parent) && guard++ < atlas.nodes.size) parent = atlas.nodes.get(parent)?.parent;
|
|
545
545
|
const parentBackdrop = boxBackdrops.get(parent) || canvasColor;
|
package/src/diagram-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** The document's 2D presentation. The SVG is also the source of SVG/PNG exports. */
|
|
2
|
+
import * as THREE from "./three.module.js";
|
|
2
3
|
const escapeHtml = value => String(value ?? "").replace(/[&<>"']/g, char => ({
|
|
3
4
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
4
5
|
})[char]);
|
|
@@ -6,7 +7,8 @@ const escapeHtml = value => String(value ?? "").replace(/[&<>"']/g, char => ({
|
|
|
6
7
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
7
8
|
|
|
8
9
|
export const DIAGRAM_VIEW_CSS = `
|
|
9
|
-
.
|
|
10
|
+
.toolbar button[hidden],.toolbar-menu[hidden]{display:none!important}
|
|
11
|
+
.diagram-view{position:absolute;inset:0;overflow:auto;background-color:var(--lc-diagram-background,#f5f5f5);background-image:radial-gradient(circle,var(--lc-diagram-dot,#b3b3b3) .8px,transparent 1px);background-size:16px 16px;color:#172c3a;cursor:grab;touch-action:none}
|
|
10
12
|
.diagram-view[hidden],:host(:not([data-view-mode="2d"])) .diagram-view{display:none!important}
|
|
11
13
|
:host([data-view-mode="2d"]) .viewport>canvas,:host([data-view-mode="2d"]) .atlas-jump{visibility:hidden;pointer-events:none}
|
|
12
14
|
:host([data-view-mode="2d"]) .toolbar-group[data-toolbar-group="camera"],
|
|
@@ -37,7 +39,7 @@ export const DIAGRAM_VIEW_CSS = `
|
|
|
37
39
|
.diagram-orientation-cube circle{fill:#132b3b;stroke:#50758b}
|
|
38
40
|
.diagram-toolbar .diagram-orbit,.diagram-toolbar .diagram-camera-zoom{display:block;padding:6px 9px;color:#dbe7ec;font-size:12px}
|
|
39
41
|
.diagram-toolbar .diagram-orbit select,.diagram-toolbar .diagram-camera-zoom input{display:block;width:100%;margin-top:4px}
|
|
40
|
-
:host([data-view-mode="2d"]) .hint{color
|
|
42
|
+
:host([data-view-mode="2d"]) .hint{color:var(--lc-diagram-hint,#527286)}
|
|
41
43
|
@media print{:host([data-view-mode="2d"]) .diagram-view{display:block!important;overflow:hidden}.diagram-orientation-cube{display:none!important}}
|
|
42
44
|
`;
|
|
43
45
|
|
|
@@ -56,6 +58,7 @@ export const DIAGRAM_TOOLBAR = `<div class="toolbar-group diagram-toolbar" data-
|
|
|
56
58
|
const DEFAULT_VIEW = Object.freeze({
|
|
57
59
|
cameraView: { position: [0, 0, 30], target: [0, 0, 0] },
|
|
58
60
|
cameraZoom: 1, gap: 20, labels: true, relations: "all", showOrientationCube: true,
|
|
61
|
+
theme: "auto", grid: true,
|
|
59
62
|
focus: { mode: "all", effect: "dim", layout: "preserve", opacity: .2 }
|
|
60
63
|
});
|
|
61
64
|
|
|
@@ -69,6 +72,8 @@ export function normalizedDiagramView(value = {}) {
|
|
|
69
72
|
cameraZoom: clamp(Number(view.cameraZoom) || 1, .5, 5),
|
|
70
73
|
gap: clamp(Number(view.gap ?? 20) || 0, 0, 150),
|
|
71
74
|
labels: view.labels !== false,
|
|
75
|
+
theme: ["auto", "light", "dark"].includes(view.theme) ? view.theme : "auto",
|
|
76
|
+
grid: view.grid !== false,
|
|
72
77
|
showOrientationCube: view.showOrientationCube !== false,
|
|
73
78
|
relations: ["all", "within", "selected", "callers", "called", "none"].includes(view.relations) ? view.relations : "all",
|
|
74
79
|
focus: {
|
|
@@ -188,13 +193,22 @@ export class DiagramView {
|
|
|
188
193
|
renderOptions(extra = {}) {
|
|
189
194
|
const state = this.config;
|
|
190
195
|
const focused = this._focusedIds();
|
|
196
|
+
const background = state.background || (state.theme === "dark" ? "#2d2f31" : state.theme === "light" ? "#f5f5f5" : null)
|
|
197
|
+
|| globalThis.getComputedStyle?.(this.root).backgroundColor || "#f5f5f5";
|
|
198
|
+
const color = new THREE.Color(background);
|
|
199
|
+
const dark = color.r * .2126 + color.g * .7152 + color.b * .0722 < .32;
|
|
200
|
+
// Computed CSS colours are rgb(...); SVG backdrop blending requires #rrggbb.
|
|
201
|
+
const canvasColor = `#${color.getHexString()}`;
|
|
191
202
|
return {
|
|
192
203
|
cameraView: state.cameraView, cameraFrame: { zoom: state.cameraZoom, aspect: state.frameAspect || this.root.clientWidth / Math.max(1, this.root.clientHeight) },
|
|
193
204
|
includeIds: state.includeIds, relationGroups: state.relationGroups,
|
|
194
205
|
relations: state.relations, relationIndexes: this._relationIndexes(),
|
|
195
206
|
focus: focused ? { ids: [...focused], ...state.focus } : null,
|
|
196
207
|
gap: state.gap, labels: state.labels, placements: state.placements,
|
|
197
|
-
background:
|
|
208
|
+
background: canvasColor, canvasColor,
|
|
209
|
+
labelColor: dark ? "#e9f0f5" : "#172c3a",
|
|
210
|
+
connectionColor: dark ? "#b1c7d3" : "#58788d",
|
|
211
|
+
...(dark ? { containerInk: "#e9f0f5" } : {}), ...extra
|
|
198
212
|
};
|
|
199
213
|
}
|
|
200
214
|
|
|
@@ -250,9 +264,23 @@ export class DiagramView {
|
|
|
250
264
|
this._refreshQueued = false;
|
|
251
265
|
if (this.atlas.viewMode !== "2d" || !this.atlas.ready || !this.root.clientWidth) return false;
|
|
252
266
|
this._syncControls();
|
|
267
|
+
this.root.style.backgroundColor = this.config.background || (this.config.theme === "dark" ? "#2d2f31" : this.config.theme === "light" ? "#f5f5f5" : "");
|
|
268
|
+
this.root.style.backgroundImage = this.config.grid ? "" : "none";
|
|
269
|
+
if (this.config.theme === "auto") this.root.style.removeProperty("--lc-diagram-dot");
|
|
270
|
+
else this.root.style.setProperty("--lc-diagram-dot", this.config.theme === "dark" ? "#73777c" : "#b3b3b3");
|
|
253
271
|
const options = this.renderOptions({ faceCache: new Map(), background: null, maskGrid: true });
|
|
254
272
|
const viewport = { width: this.root.clientWidth, height: this.root.clientHeight };
|
|
255
|
-
const
|
|
273
|
+
const toolbar = this.atlas.toolbar;
|
|
274
|
+
const insets = {
|
|
275
|
+
top: this.atlas.shadowRoot.querySelector(".hint")?.hidden ? 16 : 34,
|
|
276
|
+
right: 16,
|
|
277
|
+
bottom: this.atlas.toolbarVisible && !toolbar.hidden ? toolbar.offsetHeight + 44 : 16,
|
|
278
|
+
left: this.cube.hidden ? 16 : 132
|
|
279
|
+
};
|
|
280
|
+
this.content.style.padding = `${insets.top}px ${insets.right}px ${insets.bottom}px ${insets.left}px`;
|
|
281
|
+
const fit = result => Math.min(1,
|
|
282
|
+
(viewport.width - insets.left - insets.right) / result.width,
|
|
283
|
+
(viewport.height - insets.top - insets.bottom) / result.height);
|
|
256
284
|
const initial = this.atlas.exportDiagramSvg({ ...options, measureOnly: true });
|
|
257
285
|
let zoom = this.zoom === "fit" ? fit(initial) : this.zoom;
|
|
258
286
|
for (let i = 0; i < (this.zoom === "fit" ? 8 : 1); i += 1) {
|
|
@@ -264,6 +292,7 @@ export class DiagramView {
|
|
|
264
292
|
zoom = next;
|
|
265
293
|
}
|
|
266
294
|
const result = this.atlas.exportDiagramSvg({ ...options, observerScale: zoom });
|
|
295
|
+
if (this.zoom === "fit") this._lastFitZoom = zoom;
|
|
267
296
|
this.result = result;
|
|
268
297
|
const previousX = this.root.scrollLeft, previousY = this.root.scrollTop;
|
|
269
298
|
this.content.innerHTML = result.svg;
|
|
@@ -339,8 +368,7 @@ export class DiagramView {
|
|
|
339
368
|
|
|
340
369
|
focusSelection() {
|
|
341
370
|
if (this.zoom === "fit" && this.result) {
|
|
342
|
-
const fit =
|
|
343
|
-
(this.root.clientHeight - 32) / this.result.height);
|
|
371
|
+
const fit = this._lastFitZoom || 1;
|
|
344
372
|
this.zoom = clamp(Math.max(.75, fit * 1.8), .08, 4);
|
|
345
373
|
this.refresh();
|
|
346
374
|
}
|
|
@@ -358,8 +386,8 @@ export class DiagramView {
|
|
|
358
386
|
if (!action) return false;
|
|
359
387
|
if (action === "diagram-fit") { this.zoom = "fit"; this.refresh(); return true; }
|
|
360
388
|
if (action === "diagram-plus" || action === "diagram-minus") {
|
|
361
|
-
const current = this.zoom === "fit" ?
|
|
362
|
-
this.zoom = clamp(current * (action === "diagram-plus" ? 1.
|
|
389
|
+
const current = this.zoom === "fit" ? this._lastFitZoom || 1 : this.zoom;
|
|
390
|
+
this.zoom = clamp(current * (action === "diagram-plus" ? 1.15 : 1 / 1.15), .08, 4);
|
|
363
391
|
this.refresh(); return true;
|
|
364
392
|
}
|
|
365
393
|
if (action === "labels") { this.setConfig({ labels: !this.config.labels }, { layout: true }); return true; }
|
|
@@ -479,8 +507,7 @@ export class DiagramView {
|
|
|
479
507
|
const remainingY = dy + this.root.scrollTop - oldY;
|
|
480
508
|
if ((remainingX || remainingY) && this.config.cameraZoom > 1) {
|
|
481
509
|
const angles = cameraAngles(this.config.cameraView);
|
|
482
|
-
const zoom = this.zoom === "fit" ?
|
|
483
|
-
this.root.clientHeight / (this.result?.height || 1)) : this.zoom;
|
|
510
|
+
const zoom = this.zoom === "fit" ? this._lastFitZoom || 1 : this.zoom;
|
|
484
511
|
const target = panTarget(this.config.cameraView.target, angles, remainingX, remainingY, 80 * Math.max(.08, zoom));
|
|
485
512
|
this.setConfig({ cameraView: cameraView(target, angles.yaw, angles.pitch) }, { layout: true, defer: true });
|
|
486
513
|
}
|
|
@@ -494,7 +521,7 @@ export class DiagramView {
|
|
|
494
521
|
this.root.addEventListener("wheel", event => {
|
|
495
522
|
if (!event.altKey && !event.ctrlKey) return;
|
|
496
523
|
event.preventDefault();
|
|
497
|
-
const zoom = this.zoom === "fit" ?
|
|
524
|
+
const zoom = this.zoom === "fit" ? this._lastFitZoom || 1 : this.zoom;
|
|
498
525
|
this.zoom = clamp(zoom * (event.deltaY < 0 ? 1.15 : 1 / 1.15), .08, 4);
|
|
499
526
|
this.scheduleRefresh();
|
|
500
527
|
}, { passive: false });
|
package/src/liquidcars-atlas.js
CHANGED
|
@@ -944,9 +944,11 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
944
944
|
let cachedImage = null;
|
|
945
945
|
if (typeof image === "string" && image) {
|
|
946
946
|
cachedImage = cachedGeometryImage(image);
|
|
947
|
-
material.map =
|
|
947
|
+
material.map = cachedImage
|
|
948
|
+
? new THREE.CanvasTexture(cachedImage.canvas)
|
|
949
|
+
: part.fallbackIcon ? iconTexture(part.fallbackIcon) : new THREE.Texture(new Image());
|
|
948
950
|
if (!cachedImage) {
|
|
949
|
-
const bitmap = material.map.image;
|
|
951
|
+
const bitmap = part.fallbackIcon ? new Image() : material.map.image;
|
|
950
952
|
bitmap.crossOrigin = "anonymous";
|
|
951
953
|
let cancelled = false;
|
|
952
954
|
let objectUrl = null;
|
|
@@ -964,6 +966,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
964
966
|
const onLoad = () => {
|
|
965
967
|
removeListeners();
|
|
966
968
|
if (cancelled || !material.map) { releaseObjectUrl(); return; }
|
|
969
|
+
let loadedImage = bitmap;
|
|
967
970
|
if (svgSource) {
|
|
968
971
|
try {
|
|
969
972
|
const aspect = (bitmap.naturalWidth || 1) / Math.max(1, bitmap.naturalHeight || 1);
|
|
@@ -975,16 +978,27 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
975
978
|
if (!context) throw new Error("Canvas 2D is unavailable");
|
|
976
979
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
977
980
|
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
978
|
-
|
|
981
|
+
loadedImage = canvas;
|
|
979
982
|
rememberGeometryImage(image, canvas, bitmap.naturalWidth / Math.max(1, bitmap.naturalHeight));
|
|
980
983
|
} catch (error) {
|
|
981
984
|
reportWarning?.({ code: "SVG_RASTERIZE_FAILED", geometry: definition.id, entityId: entity.id, image, error });
|
|
985
|
+
if (part.fallbackIcon) loadedImage = null;
|
|
982
986
|
}
|
|
983
987
|
}
|
|
984
988
|
releaseObjectUrl();
|
|
985
989
|
const imageAspect = bitmap.naturalWidth / Math.max(1, bitmap.naturalHeight);
|
|
986
|
-
|
|
987
|
-
|
|
990
|
+
if (loadedImage) {
|
|
991
|
+
// A 2D projection may have read the fallback map already. Swap the
|
|
992
|
+
// complete texture so WebGL also uploads the decoded image.
|
|
993
|
+
const previousMap = material.map;
|
|
994
|
+
const nextMap = loadedImage === bitmap ? new THREE.Texture(bitmap) : new THREE.CanvasTexture(loadedImage);
|
|
995
|
+
nextMap.colorSpace = THREE.SRGBColorSpace;
|
|
996
|
+
fitGeometryImage(part, mesh, nextMap, imageAspect);
|
|
997
|
+
nextMap.needsUpdate = true;
|
|
998
|
+
material.map = nextMap;
|
|
999
|
+
material.needsUpdate = true;
|
|
1000
|
+
previousMap.dispose();
|
|
1001
|
+
}
|
|
988
1002
|
invalidate?.();
|
|
989
1003
|
};
|
|
990
1004
|
const onError = () => {
|
|
@@ -1658,7 +1672,8 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1658
1672
|
}
|
|
1659
1673
|
|
|
1660
1674
|
_syncDocumentToolbar() {
|
|
1661
|
-
const canNavigate = Boolean(this.documentResolver) && this._visitIndex >= 0
|
|
1675
|
+
const canNavigate = Boolean(this.documentResolver) && this._visitIndex >= 0
|
|
1676
|
+
&& (!this._toolbarControls || this._toolbarControls.has("navigation"));
|
|
1662
1677
|
const group = this.navigationToolbar?.querySelector('[data-toolbar-group="navigation"]');
|
|
1663
1678
|
if (group) group.hidden = !canNavigate;
|
|
1664
1679
|
if (this.navigationToolbar) this.navigationToolbar.hidden = !canNavigate || !this.toolbarVisible;
|
|
@@ -2147,9 +2162,37 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2147
2162
|
return this.setFocusFilter({ mode: "all" }, options);
|
|
2148
2163
|
}
|
|
2149
2164
|
|
|
2165
|
+
_setToolbarControls(value) {
|
|
2166
|
+
const controls = Array.isArray(value) ? new Set(value.map(item => String(item).trim())) : null;
|
|
2167
|
+
this._toolbarControls = controls;
|
|
2168
|
+
if (!this.toolbar) return;
|
|
2169
|
+
const allowed = (...names) => !controls || controls.has("all") || names.some(name => controls.has(name));
|
|
2170
|
+
const group = this.toolbar;
|
|
2171
|
+
const buttons = {
|
|
2172
|
+
"view-mode": ["mode", "view-mode"],
|
|
2173
|
+
minus: ["zoom", "minus"], plus: ["zoom", "plus"], fit: ["fit"], front: ["front"],
|
|
2174
|
+
"diagram-minus": ["zoom", "diagram-minus"], "diagram-plus": ["zoom", "diagram-plus"],
|
|
2175
|
+
"diagram-fit": ["fit", "diagram-fit"]
|
|
2176
|
+
};
|
|
2177
|
+
for (const button of group.querySelectorAll('.toolbar-group > button[data-action]')) {
|
|
2178
|
+
button.hidden = !allowed(...(buttons[button.dataset.action] || [button.dataset.action]));
|
|
2179
|
+
}
|
|
2180
|
+
for (const menu of group.querySelectorAll('.toolbar-group > .toolbar-menu')) {
|
|
2181
|
+
const name = menu.dataset.toolbarMenu;
|
|
2182
|
+
menu.hidden = !allowed(name, name?.replace(/^diagram-/, ""));
|
|
2183
|
+
if (menu.hidden) menu.open = false;
|
|
2184
|
+
}
|
|
2185
|
+
for (const section of group.querySelectorAll('.toolbar-group:not([data-toolbar-group="navigation"])')) {
|
|
2186
|
+
section.hidden = !section.querySelector(':scope > button[data-action]:not([hidden]), :scope > .toolbar-menu:not([hidden])');
|
|
2187
|
+
}
|
|
2188
|
+
this._syncDocumentToolbar();
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2150
2191
|
_applyModelRenderOptions() {
|
|
2151
2192
|
const render = { ...this.modelData?.render, ...this.modelData?.render?.views?.["3d"] };
|
|
2152
2193
|
if (!render || typeof render !== "object") return this;
|
|
2194
|
+
const toolbar = this.modelData?.render?.ui?.toolbar ?? render.toolbar;
|
|
2195
|
+
this._setToolbarControls(Array.isArray(toolbar) ? toolbar : this.modelData?.render?.ui?.controls);
|
|
2153
2196
|
if (RELATION_DISPLAY_MODES.includes(render.relationMode) && render.relationMode !== this.relationMode) this.setRelationMode(render.relationMode);
|
|
2154
2197
|
if (normalizeRelationMarker(render.relationMarker) !== this.relationMarker) this.setRelationMarker(render.relationMarker, { emit: false, source: "model" });
|
|
2155
2198
|
if (Array.isArray(render.visibleRelationGroups)) this.setVisibleRelationGroups(render.visibleRelationGroups, { emit: false, source: "model" });
|
|
@@ -2157,7 +2200,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2157
2200
|
if (Object.hasOwn(render, "selectionMode") && render.selectionMode !== this.selectionMode) this.setSelectionMode(render.selectionMode);
|
|
2158
2201
|
const selectionNavigation = render.navigation?.onSelect ?? render.selectionNavigation;
|
|
2159
2202
|
if (selectionNavigation != null && selectionNavigation !== this.selectionNavigation) this.setSelectionNavigation(selectionNavigation);
|
|
2160
|
-
if (
|
|
2203
|
+
if (toolbar !== undefined && (toolbar !== false) !== this.toolbarVisible) this.setToolbarVisible(toolbar !== false);
|
|
2161
2204
|
if (Object.hasOwn(render, "shellLabels") && (render.shellLabels !== false) !== this.showShellLabels) this.setShellLabels(render.shellLabels !== false);
|
|
2162
2205
|
if (Object.hasOwn(render, "itemFacing") && normalizeItemFacing(render.itemFacing) !== this.itemFacing) this.setItemFacing(render.itemFacing, { emit: false, source: "model" });
|
|
2163
2206
|
if (render.focus) this.setFocusFilter(render.focus, { emit: false });
|
|
@@ -2676,7 +2719,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
2676
2719
|
if (atlas) atlas.dataset.panel = this.panelVisible ? "visible" : "hidden";
|
|
2677
2720
|
const toolbarVisible = this.toolbarVisible;
|
|
2678
2721
|
const panelVisible = this.panelVisible;
|
|
2679
|
-
const hintVisible = theme.ui.hint !== false;
|
|
2722
|
+
const hintVisible = (this.modelData?.render?.ui?.hint ?? theme.ui.hint) !== false;
|
|
2680
2723
|
if (toolbar) {
|
|
2681
2724
|
toolbar.dataset.hidden = String(!toolbarVisible);
|
|
2682
2725
|
toolbar.hidden = !toolbarVisible;
|