@liquidcars/atlas 0.1.22 → 0.1.24
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 +72 -4
- package/demo/index.html +2 -2
- package/dist/liquidcars-atlas.js +352 -41
- package/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ atlas.setRelationMode('all'); // 'all' | 'selected' | 'none'
|
|
|
55
55
|
atlas.setToolbarVisible(true); // también se puede usar toolbar="on" | "off"
|
|
56
56
|
atlas.zoomIn();
|
|
57
57
|
atlas.zoomOut();
|
|
58
|
+
atlas.panBy(-1.5, 0); // desplazamiento horizontal/vertical en el plano de cámara
|
|
59
|
+
atlas.panTo(0, 0, 0); // centra la cámara en un punto del mundo
|
|
58
60
|
atlas.fit();
|
|
59
61
|
atlas.frontView();
|
|
60
62
|
atlas.setShellLabels(true);
|
|
@@ -94,7 +96,7 @@ principales también están expuestos como variables CSS (`--lc-bg`,
|
|
|
94
96
|
por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
|
|
95
97
|
Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
|
|
96
98
|
|
|
97
|
-
Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-model-change`,
|
|
99
|
+
Eventos disponibles: `lc-ready`, `lc-error`, `lc-warning`, `lc-select`, `lc-model-change`,
|
|
98
100
|
`lc-relation-mode` y `lc-disconnect`. `lc-select` usa un detalle discriminado:
|
|
99
101
|
las entidades incluyen `{ kind: 'entity', id, entity, source }` y las relaciones
|
|
100
102
|
`{ kind: 'relation', id, index, relation, source }`. Los campos históricos `id`
|
|
@@ -125,9 +127,71 @@ como `setClickBehavior()`).
|
|
|
125
127
|
|
|
126
128
|
Geometrías incluidas en esta primera versión: `box`, `cylinder`, `sphere`, `cone`, `frustum-cone`, `frustum-pyramid`, `frustum-triangle`, `prism`, `pyramid-square`, `pyramid-triangle`, `octahedron`, `dodecahedron` e `icosahedron`. Las variantes `frustum-*` son sólidos truncados con dos caras finitas paralelas; las dimensiones `s` se aplican sobre su caja final normalizada.
|
|
127
129
|
|
|
130
|
+
## Geometry packs
|
|
131
|
+
|
|
132
|
+
Las geometrías pueden distribuirse en paquetes independientes. El componente
|
|
133
|
+
expone `registerGeometryPack()`, `unregisterGeometryPack()` y
|
|
134
|
+
`getGeometryCatalog()`; los mismos métodos están disponibles como estáticos en
|
|
135
|
+
`LiquidCarsAtlas`. Los identificadores usan un namespace (`infra.server`,
|
|
136
|
+
`architecture.application`, etc.) y las primitivas integradas también se
|
|
137
|
+
publican como `geo.box`, `geo.sphere`, etc. Los nombres históricos sin
|
|
138
|
+
namespace siguen siendo compatibles.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { registerGeometryPack } from "@liquidcars/atlas";
|
|
142
|
+
|
|
143
|
+
registerGeometryPack({
|
|
144
|
+
namespace: "infra",
|
|
145
|
+
name: "Infrastructure",
|
|
146
|
+
version: "0.1.0",
|
|
147
|
+
palette: { chassis: "#587386", accent: "#72dbc9" },
|
|
148
|
+
geometries: [{
|
|
149
|
+
id: "server",
|
|
150
|
+
name: "Server",
|
|
151
|
+
canonicalSize: [2.7, 1.8, 2.2],
|
|
152
|
+
parameters: {
|
|
153
|
+
units: { type: "number", min: 1, max: 8, default: 3 },
|
|
154
|
+
icon: { type: "icon", default: "⚙" },
|
|
155
|
+
image: { type: "image", default: "" }
|
|
156
|
+
},
|
|
157
|
+
anchors: { input: [-1, 0, 0], output: [1, 0, 0], network: [0, 0, 1] },
|
|
158
|
+
create({ parameters }) {
|
|
159
|
+
return { parts: [
|
|
160
|
+
{ shape: "box", size: [1, 1, 1], color: "chassis" },
|
|
161
|
+
{ shape: "plane", size: [.42, .42, 1], position: [0, 0, .51],
|
|
162
|
+
color: "accent", icon: parameters.icon, image: parameters.image,
|
|
163
|
+
edges: false, doubleSided: true }
|
|
164
|
+
] };
|
|
165
|
+
}
|
|
166
|
+
}]
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
El modelo configura cada instancia sin conocer la implementación del paquete:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"geometry": "infra.server",
|
|
175
|
+
"geometryOptions": { "units": 4, "icon": "DB" },
|
|
176
|
+
"geometryPalette": { "accent": "#ffd166" }
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Cada definición puede declarar parámetros tipados, una paleta local,
|
|
181
|
+
proporciones canónicas y anchors. Las piezas declarativas admiten primitivas,
|
|
182
|
+
posición, rotación, escala, materiales, iconos de texto o emoji e imágenes por
|
|
183
|
+
URL/data URI. El renderer mantiene Three.js encapsulado para evitar que los
|
|
184
|
+
packs dependan de su versión interna. Las relaciones pueden seleccionar anchors
|
|
185
|
+
con `fromAnchor` y `toAnchor`.
|
|
186
|
+
|
|
187
|
+
Si un namespace no está registrado, Atlas conserva el modelo renderizable con
|
|
188
|
+
una caja roja de fallback y emite `lc-warning` con el código
|
|
189
|
+
`GEOMETRY_NOT_REGISTERED`. Si la factoría del pack falla se usa el código
|
|
190
|
+
`GEOMETRY_BUILD_FAILED`.
|
|
191
|
+
|
|
128
192
|
La paleta puede cambiarse después de cargar el modelo mediante `atlas.setPalette({ nombre: color })` o asignando `atlas.palette = { ... }`. El cambio actualiza las piezas, aristas y relaciones sin reconstruir el modelo.
|
|
129
193
|
|
|
130
|
-
Las acciones de navegación y exploración
|
|
194
|
+
Las acciones de navegación y exploración también están disponibles para la página anfitriona: `zoomIn()`, `zoomOut()`, `panBy()`, `panTo()`, `fit()`, `frontView()`, `setShellLabels()`, `toggleShellLabels()`, `openAllContainers()` y `closeAllContainers()`. El usuario puede desplazar la vista con el botón derecho, con `Shift` + arrastre o mediante dos dedos en una pantalla táctil. Así se puede construir una barra de controles propia sin depender de la toolbar interna.
|
|
131
195
|
|
|
132
196
|
La escena también se puede editar en caliente. `updateModel(patch)` reemplaza
|
|
133
197
|
las secciones indicadas; `addEntity`, `updateEntity` y `removeEntity` gestionan
|
|
@@ -149,12 +213,16 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
|
|
|
149
213
|
Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
|
|
150
214
|
|
|
151
215
|
```bash
|
|
152
|
-
npm install ./liquidcars-atlas-0.1.
|
|
216
|
+
npm install ./liquidcars-atlas-0.1.24.tgz
|
|
153
217
|
```
|
|
154
218
|
|
|
155
219
|
## Estado
|
|
156
220
|
|
|
157
|
-
`0.1.
|
|
221
|
+
`0.1.24` incorpora geometry packs independientes, namespaces, parámetros
|
|
222
|
+
personalizados, paletas locales, anchors e imágenes/iconos declarativos. La
|
|
223
|
+
versión `0.1.23` añadió desplazamiento lateral de cámara mediante ratón, teclado
|
|
224
|
+
modificador y gestos táctiles, además de la API pública `panBy()` y `panTo()`.
|
|
225
|
+
Se conserva la selección contextual de relaciones introducida en `0.1.22`: el usuario puede
|
|
158
226
|
seleccionar un tubo directamente, el componente lo realza, muestra sus extremos
|
|
159
227
|
y modo en el panel y emite el mismo evento `lc-select` con `kind: 'relation'`.
|
|
160
228
|
La API pública añade `selectRelation()`, `selectedRelation` y `selection`, y la
|
package/demo/index.html
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
liquidcars-atlas { display: block; width: 100%; max-width: 1480px; margin: 0 auto; }
|
|
15
15
|
code { color: #85e5d3; }
|
|
16
16
|
</style>
|
|
17
|
-
<script type="module" src="../dist/liquidcars-atlas.js?v=0.1.
|
|
17
|
+
<script type="module" src="../dist/liquidcars-atlas.js?v=0.1.24"></script>
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<header>
|
|
21
21
|
<h1>LiquidCars Atlas</h1>
|
|
22
|
-
<p>Prueba de distribución del componente <code><liquidcars-atlas></code> · v0.1.
|
|
22
|
+
<p>Prueba de distribución del componente <code><liquidcars-atlas></code> · v0.1.24</p>
|
|
23
23
|
</header>
|
|
24
24
|
<main>
|
|
25
25
|
<liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
|
package/dist/liquidcars-atlas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! LiquidCars Atlas 0.1.
|
|
1
|
+
/*! LiquidCars Atlas 0.1.24 | MIT */
|
|
2
2
|
|
|
3
3
|
// src/three.core.js
|
|
4
4
|
/**
|
|
@@ -29344,6 +29344,86 @@ var DEFAULT_PALETTE = {
|
|
|
29344
29344
|
component: "#8ae4d0",
|
|
29345
29345
|
connection: "#f4edba"
|
|
29346
29346
|
};
|
|
29347
|
+
var GEOMETRY_DEFINITIONS = /* @__PURE__ */ new Map();
|
|
29348
|
+
var GEOMETRY_ALIASES = /* @__PURE__ */ new Map();
|
|
29349
|
+
function publicGeometryDefinition(definition) {
|
|
29350
|
+
const { create, ...metadata } = definition;
|
|
29351
|
+
return {
|
|
29352
|
+
...metadata,
|
|
29353
|
+
canonicalSize: [...definition.canonicalSize],
|
|
29354
|
+
parameters: { ...definition.parameters },
|
|
29355
|
+
palette: { ...definition.palette },
|
|
29356
|
+
anchors: { ...definition.anchors }
|
|
29357
|
+
};
|
|
29358
|
+
}
|
|
29359
|
+
function registerGeometryPack(pack) {
|
|
29360
|
+
if (!pack || typeof pack !== "object") throw new TypeError("A geometry pack must be an object.");
|
|
29361
|
+
const namespace = String(pack.namespace || "").trim();
|
|
29362
|
+
if (!/^[a-z][a-z0-9-]*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid namespace.");
|
|
29363
|
+
if (!Array.isArray(pack.geometries) || !pack.geometries.length) throw new TypeError("A geometry pack requires at least one geometry.");
|
|
29364
|
+
const registered = [];
|
|
29365
|
+
for (const source of pack.geometries) {
|
|
29366
|
+
if (!source || typeof source !== "object" || !source.id) throw new TypeError(`Geometry pack '${namespace}' contains an invalid geometry.`);
|
|
29367
|
+
const localId = String(source.id).includes(".") ? String(source.id).split(".").pop() : String(source.id);
|
|
29368
|
+
const id = String(source.id).includes(".") ? String(source.id) : `${namespace}.${localId}`;
|
|
29369
|
+
const definition = {
|
|
29370
|
+
...source,
|
|
29371
|
+
id,
|
|
29372
|
+
localId,
|
|
29373
|
+
namespace,
|
|
29374
|
+
pack: pack.name || namespace,
|
|
29375
|
+
packVersion: pack.version || "0.0.0",
|
|
29376
|
+
category: source.category || pack.category || pack.name || namespace,
|
|
29377
|
+
canonicalSize: Array.isArray(source.canonicalSize) && source.canonicalSize.length === 3 ? source.canonicalSize.map(Number) : [2.5, 1.5, 2.5],
|
|
29378
|
+
parameters: source.parameters && typeof source.parameters === "object" ? source.parameters : {},
|
|
29379
|
+
palette: { ...pack.palette || {}, ...source.palette || {} },
|
|
29380
|
+
anchors: { ...pack.anchors || {}, ...source.anchors || {} }
|
|
29381
|
+
};
|
|
29382
|
+
GEOMETRY_DEFINITIONS.set(id, definition);
|
|
29383
|
+
GEOMETRY_ALIASES.set(id, id);
|
|
29384
|
+
for (const alias of source.aliases || []) GEOMETRY_ALIASES.set(String(alias), id);
|
|
29385
|
+
registered.push(id);
|
|
29386
|
+
}
|
|
29387
|
+
if (typeof globalThis.dispatchEvent === "function" && typeof CustomEvent === "function") {
|
|
29388
|
+
globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered } }));
|
|
29389
|
+
}
|
|
29390
|
+
return { namespace, geometries: registered };
|
|
29391
|
+
}
|
|
29392
|
+
function unregisterGeometryPack(namespace) {
|
|
29393
|
+
const removed = [];
|
|
29394
|
+
for (const [id, definition] of GEOMETRY_DEFINITIONS) {
|
|
29395
|
+
if (definition.namespace !== namespace) continue;
|
|
29396
|
+
GEOMETRY_DEFINITIONS.delete(id);
|
|
29397
|
+
removed.push(id);
|
|
29398
|
+
}
|
|
29399
|
+
for (const [alias, id] of GEOMETRY_ALIASES) if (removed.includes(id)) GEOMETRY_ALIASES.delete(alias);
|
|
29400
|
+
return removed;
|
|
29401
|
+
}
|
|
29402
|
+
function getGeometryCatalog() {
|
|
29403
|
+
return [...GEOMETRY_DEFINITIONS.values()].map(publicGeometryDefinition);
|
|
29404
|
+
}
|
|
29405
|
+
function geometryDefinition(kind) {
|
|
29406
|
+
const id = GEOMETRY_ALIASES.get(String(kind || "box")) || String(kind || "box");
|
|
29407
|
+
return GEOMETRY_DEFINITIONS.get(id) || null;
|
|
29408
|
+
}
|
|
29409
|
+
var CORE_GEOMETRIES = [
|
|
29410
|
+
"box", "cylinder", "sphere", "cone", "frustum-cone", "frustum-pyramid", "frustum-triangle",
|
|
29411
|
+
"prism", "pyramid-square", "pyramid-triangle", "octahedron", "dodecahedron", "icosahedron"
|
|
29412
|
+
];
|
|
29413
|
+
registerGeometryPack({
|
|
29414
|
+
namespace: "geo",
|
|
29415
|
+
name: "Geometric primitives",
|
|
29416
|
+
version: "1",
|
|
29417
|
+
category: "Geometric primitives",
|
|
29418
|
+
geometries: CORE_GEOMETRIES.map((id) => ({
|
|
29419
|
+
id,
|
|
29420
|
+
primitive: id,
|
|
29421
|
+
aliases: [id],
|
|
29422
|
+
name: id.split("-").map((word) => word[0].toUpperCase() + word.slice(1)).join(" "),
|
|
29423
|
+
canonicalSize: id === "sphere" || ["octahedron", "dodecahedron", "icosahedron"].includes(id) ? [2.2, 2.2, 2.2] : [2.5, 1.5, 2.5],
|
|
29424
|
+
parameters: id === "box" ? { radius: { type: "number", label: "Radius", min: 0, max: 0.72, step: 0.01, default: 0 } } : {}
|
|
29425
|
+
}))
|
|
29426
|
+
});
|
|
29347
29427
|
var DEFAULT_THEME = {
|
|
29348
29428
|
background: { color: "#0a111a", glow: "#1c2b39", fade: "#0a111a" },
|
|
29349
29429
|
panel: { background: "linear-gradient(140deg,#182633,#0e1925 65%)", border: "#2a3a48", text: "#b5c6d2", title: "#e9f0f5" },
|
|
@@ -29371,7 +29451,7 @@ var CSS = [
|
|
|
29371
29451
|
":host{display:block;position:relative;height:var(--lc-height,680px);min-height:420px;color:var(--lc-panel-title,#e9f0f5);font:14px Inter,system-ui,-apple-system,sans-serif;background:var(--lc-bg,#0a111a);border:1px solid #294252;border-radius:12px;overflow:hidden;contain:content}",
|
|
29372
29452
|
"*{box-sizing:border-box}.atlas{display:grid;grid-template-columns:minmax(0,1fr) 300px;height:100%}.atlas[data-panel=hidden]{grid-template-columns:1fr}.viewport{position:relative;min-width:0;overflow:hidden;background:radial-gradient(ellipse at 50% 45%,var(--lc-bg-glow,#1c2b39),var(--lc-bg-fade,#0a111a) 82%)}canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:none}.toolbar{position:absolute;z-index:10;bottom:28px;left:50%;transform:translateX(-50%);display:flex;gap:3px;padding:6px;background:var(--lc-toolbar-bg,#15232fe8);border:1px solid var(--lc-toolbar-border,#3a5062);border-radius:8px;white-space:nowrap}.toolbar[data-hidden=true],.toolbar[hidden],.panel[data-hidden=true],.panel[hidden],.crumbs[data-hidden=true],.crumbs[hidden],.hint[data-hidden=true],.hint[hidden]{display:none!important}.toolbar button{height:30px;padding:0 9px;border:0;border-radius:5px;background:none;color:var(--lc-toolbar-text,#dbe7ec);cursor:pointer}.toolbar button:hover,.toolbar button.active{background:var(--lc-toolbar-active,#1f3d4b);color:var(--lc-toolbar-active-text,#c5f2e9)}.toolbar i{display:block;width:1px;margin:2px 3px;background:#38515f}.hint{position:absolute;z-index:9;top:8px;bottom:auto;left:0;right:0;text-align:center;padding:2px 8px;font-size:11px;line-height:16px;color:var(--lc-hint,#8da6b8);pointer-events:none}.panel{overflow:auto;padding:24px 20px;background:var(--lc-panel-bg,linear-gradient(140deg,#182633,#0e1925 65%));border-left:1px solid var(--lc-panel-border,#2a3a48)}.panel h2{font-size:26px;line-height:1.12;font-weight:470;margin:12px 0;color:var(--lc-panel-title,#e9f0f5)}.panel h3{font-size:11px;letter-spacing:1.4px;color:#91aabd;margin:24px 0 10px}.panel p{font-size:14px;line-height:1.55;color:var(--lc-panel-text,#b5c6d2)}.panel .eyebrow{font-size:11px;letter-spacing:1.7px;color:#6ad7c0}.panel .sub{font-size:13px;color:#8fa8bd;margin-bottom:16px}.panel button{display:block;width:100%;padding:10px 0;text-align:left;background:none;border:0;border-bottom:1px solid var(--lc-panel-border,#2a3a48);color:#d9e7eb;cursor:pointer}.panel button:hover{color:#6ad7c0}.panel .relation{padding:10px 0;border-bottom:1px solid var(--lc-panel-border,#2a3a48)}.panel .relation strong{display:block;font-weight:550}.panel .relation small{display:block;color:#86a6b9;font-size:12px;margin-top:4px}.panel .back{color:#9db7c8;font-size:13px}.panel .crumbs{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:0 0 12px}.panel .crumbs button{display:inline;width:auto;padding:3px 7px;border:0;border-radius:4px;background:var(--lc-crumb-bg,#142733);color:var(--lc-crumb-text,#91b6c7);font-size:12px}.panel .crumbs button.current{background:var(--lc-crumb-active,#1c4a4d);color:var(--lc-crumb-active-text,#c5f2e9)}.panel .note{border-top:1px solid var(--lc-panel-border,#2a3a48);padding-top:16px;margin-top:22px;font-size:12px;color:#7692a6}.error{position:absolute;inset:20%;padding:18px;background:#172b3b;border:1px solid #7896a9;line-height:1.5;z-index:4}@media(max-width:900px){.toolbar{gap:0}.toolbar button{padding:0 6px;font-size:12px}}@media(max-width:760px){:host{height:680px}.atlas{grid-template-columns:1fr;grid-template-rows:minmax(420px,1fr) auto}.panel{max-height:250px;border-left:0;border-top:1px solid var(--lc-panel-border,#2a3a48);padding:18px}.toolbar{bottom:24px}.hint{top:8px;bottom:auto;display:block}}"
|
|
29373
29453
|
].join("");
|
|
29374
|
-
var TEMPLATE = '<div class="atlas"><section class="viewport"><canvas tabindex="0" aria-label="Modelo 3D interactivo"></canvas><div class="toolbar"><button data-action="minus" aria-label="Alejar">\u2212</button><button data-action="plus" aria-label="Acercar">+</button><i></i><button data-action="fit">\u2316 Encuadrar</button><button data-action="front">\u25B1 Frontal</button><button data-action="labels" class="active" aria-pressed="true">\u25A7 Etiquetas</button><i></i><button data-action="relations-all" aria-pressed="false">Todas</button><button data-action="relations-selected" class="active" aria-pressed="true">Selecci\xF3n</button><button data-action="relations-none" aria-pressed="false">Ocultar</button></div><div class="hint">
|
|
29454
|
+
var TEMPLATE = '<div class="atlas"><section class="viewport"><canvas tabindex="0" aria-label="Modelo 3D interactivo"></canvas><div class="toolbar"><button data-action="minus" aria-label="Alejar">\u2212</button><button data-action="plus" aria-label="Acercar">+</button><i></i><button data-action="fit">\u2316 Encuadrar</button><button data-action="front">\u25B1 Frontal</button><button data-action="labels" class="active" aria-pressed="true">\u25A7 Etiquetas</button><i></i><button data-action="relations-all" aria-pressed="false">Todas</button><button data-action="relations-selected" class="active" aria-pressed="true">Selecci\xF3n</button><button data-action="relations-none" aria-pressed="false">Ocultar</button></div><div class="hint">Gira: arrastra \xB7 Pan: \u21E7+arrastra / 2 dedos \xB7 Zoom: rueda o pellizco \xB7 Selecciona: clic</div></section><aside class="panel" aria-live="polite"></aside></div>';
|
|
29375
29455
|
function escapeHtml(value) {
|
|
29376
29456
|
return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
29377
29457
|
}
|
|
@@ -29500,6 +29580,112 @@ function normalizeGeometry(geometry) {
|
|
|
29500
29580
|
geometry.computeBoundingBox();
|
|
29501
29581
|
return geometry;
|
|
29502
29582
|
}
|
|
29583
|
+
function resolveGeometryParameters(entity, definition) {
|
|
29584
|
+
const supplied = entity.geometryOptions || entity.geometry_options || entity.parameters || {};
|
|
29585
|
+
const resolved = {};
|
|
29586
|
+
for (const [name, schema] of Object.entries(definition?.parameters || {})) {
|
|
29587
|
+
if (schema && Object.hasOwn(schema, "default")) resolved[name] = schema.default;
|
|
29588
|
+
}
|
|
29589
|
+
return { ...resolved, ...supplied && typeof supplied === "object" ? supplied : {} };
|
|
29590
|
+
}
|
|
29591
|
+
function resolveGeometryColor(value, palette, fallback) {
|
|
29592
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
29593
|
+
return palette[value] || value;
|
|
29594
|
+
}
|
|
29595
|
+
function iconTexture(value, foreground = "#ffffff", background = "transparent") {
|
|
29596
|
+
const bitmap = document.createElement("canvas");
|
|
29597
|
+
bitmap.width = 256;
|
|
29598
|
+
bitmap.height = 256;
|
|
29599
|
+
const context = bitmap.getContext("2d");
|
|
29600
|
+
context.clearRect(0, 0, 256, 256);
|
|
29601
|
+
if (background !== "transparent") {
|
|
29602
|
+
context.fillStyle = background;
|
|
29603
|
+
context.fillRect(0, 0, 256, 256);
|
|
29604
|
+
}
|
|
29605
|
+
context.fillStyle = foreground;
|
|
29606
|
+
context.font = "600 150px system-ui, sans-serif";
|
|
29607
|
+
context.textAlign = "center";
|
|
29608
|
+
context.textBaseline = "middle";
|
|
29609
|
+
context.fillText(String(value || ""), 128, 137, 220);
|
|
29610
|
+
const texture = new CanvasTexture(bitmap);
|
|
29611
|
+
texture.colorSpace = SRGBColorSpace;
|
|
29612
|
+
return texture;
|
|
29613
|
+
}
|
|
29614
|
+
function partGeometry(part) {
|
|
29615
|
+
if (part.shape === "plane") return new PlaneGeometry(1, 1);
|
|
29616
|
+
return makeGeometry(part.shape || part.geometry || "box", part.radius, part.segments, part.size);
|
|
29617
|
+
}
|
|
29618
|
+
function createGeometryContent(definition, entity, modelPalette, invalidate) {
|
|
29619
|
+
const parameters = resolveGeometryParameters(entity, definition);
|
|
29620
|
+
const localPalette = {
|
|
29621
|
+
...modelPalette,
|
|
29622
|
+
...definition.palette || {},
|
|
29623
|
+
...entity.geometryPalette || entity.geometry_palette || {}
|
|
29624
|
+
};
|
|
29625
|
+
let descriptor = typeof definition.create === "function" ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity } }) : { parts: definition.parts || [] };
|
|
29626
|
+
if (Array.isArray(descriptor)) descriptor = { parts: descriptor };
|
|
29627
|
+
const parts = Array.isArray(descriptor?.parts) ? descriptor.parts : [];
|
|
29628
|
+
if (!parts.length) throw new Error(`Geometry '${definition.id}' did not produce any parts.`);
|
|
29629
|
+
const root = new Group();
|
|
29630
|
+
const colorBindings = [];
|
|
29631
|
+
const meshes = [];
|
|
29632
|
+
for (const part of parts) {
|
|
29633
|
+
const geometry = partGeometry(part);
|
|
29634
|
+
normalizeGeometry(geometry);
|
|
29635
|
+
const colorValue = part.color || "component";
|
|
29636
|
+
const color = resolveGeometryColor(colorValue, localPalette, modelPalette.component || DEFAULT_PALETTE.component);
|
|
29637
|
+
const material = new MeshStandardMaterial({
|
|
29638
|
+
color,
|
|
29639
|
+
roughness: Number.isFinite(Number(part.roughness)) ? Number(part.roughness) : 0.72,
|
|
29640
|
+
metalness: Number.isFinite(Number(part.metalness)) ? Number(part.metalness) : 0.06,
|
|
29641
|
+
transparent: part.transparent === true || Number(part.opacity) < 1,
|
|
29642
|
+
opacity: Number.isFinite(Number(part.opacity)) ? Number(part.opacity) : 1,
|
|
29643
|
+
side: part.doubleSided ? DoubleSide : FrontSide
|
|
29644
|
+
});
|
|
29645
|
+
const icon = part.icon;
|
|
29646
|
+
const image = part.image;
|
|
29647
|
+
if (typeof image === "string" && image) {
|
|
29648
|
+
const bitmap = new Image();
|
|
29649
|
+
bitmap.crossOrigin = "anonymous";
|
|
29650
|
+
material.map = new Texture(bitmap);
|
|
29651
|
+
bitmap.addEventListener("load", () => {
|
|
29652
|
+
material.map.needsUpdate = true;
|
|
29653
|
+
invalidate?.();
|
|
29654
|
+
});
|
|
29655
|
+
bitmap.addEventListener("error", () => invalidate?.());
|
|
29656
|
+
bitmap.src = image;
|
|
29657
|
+
material.map.colorSpace = SRGBColorSpace;
|
|
29658
|
+
material.needsUpdate = true;
|
|
29659
|
+
} else if (icon != null && icon !== "") {
|
|
29660
|
+
material.map = iconTexture(icon, resolveGeometryColor(part.iconColor || "#ffffff", localPalette, "#ffffff"), resolveGeometryColor(part.iconBackground || "transparent", localPalette, "transparent"));
|
|
29661
|
+
material.transparent = true;
|
|
29662
|
+
material.needsUpdate = true;
|
|
29663
|
+
}
|
|
29664
|
+
const mesh = new Mesh(geometry, material);
|
|
29665
|
+
mesh.position.fromArray(Array.isArray(part.position) ? part.position : [0, 0, 0]);
|
|
29666
|
+
mesh.scale.fromArray(Array.isArray(part.size) ? part.size : [1, 1, 1]);
|
|
29667
|
+
if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
|
|
29668
|
+
root.add(mesh);
|
|
29669
|
+
meshes.push(mesh);
|
|
29670
|
+
colorBindings.push({ material, value: colorValue, palette: localPalette });
|
|
29671
|
+
if (part.edges !== false && part.shape !== "plane") {
|
|
29672
|
+
const edgeMaterial = new LineBasicMaterial({ color, transparent: true, opacity: Number.isFinite(Number(part.edgeOpacity)) ? Number(part.edgeOpacity) : 0.55 });
|
|
29673
|
+
const edge = new LineSegments(new EdgesGeometry(geometry, 22), edgeMaterial);
|
|
29674
|
+
edge.position.copy(mesh.position);
|
|
29675
|
+
edge.rotation.copy(mesh.rotation);
|
|
29676
|
+
edge.scale.copy(mesh.scale);
|
|
29677
|
+
root.add(edge);
|
|
29678
|
+
colorBindings.push({ material: edgeMaterial, value: part.edgeColor || colorValue, palette: localPalette });
|
|
29679
|
+
}
|
|
29680
|
+
}
|
|
29681
|
+
root.updateMatrixWorld(true);
|
|
29682
|
+
const bounds = new Box3().setFromObject(root);
|
|
29683
|
+
const contentSize = bounds.getSize(new Vector3());
|
|
29684
|
+
const center = bounds.getCenter(new Vector3());
|
|
29685
|
+
root.children.forEach((child) => child.position.sub(center));
|
|
29686
|
+
root.scale.set(1 / Math.max(contentSize.x, 1e-4), 1 / Math.max(contentSize.y, 1e-4), 1 / Math.max(contentSize.z, 1e-4));
|
|
29687
|
+
return { root, meshes, colorBindings, parameters, descriptor };
|
|
29688
|
+
}
|
|
29503
29689
|
function makeLabel(text, color, dimensions, type = "") {
|
|
29504
29690
|
const bitmap = document.createElement("canvas");
|
|
29505
29691
|
const context = bitmap.getContext("2d");
|
|
@@ -29580,6 +29766,15 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29580
29766
|
static get observedAttributes() {
|
|
29581
29767
|
return ["height", "relation-mode", "shell-labels", "toolbar", "selection-mode"];
|
|
29582
29768
|
}
|
|
29769
|
+
static registerGeometryPack(pack) {
|
|
29770
|
+
return registerGeometryPack(pack);
|
|
29771
|
+
}
|
|
29772
|
+
static unregisterGeometryPack(namespace) {
|
|
29773
|
+
return unregisterGeometryPack(namespace);
|
|
29774
|
+
}
|
|
29775
|
+
static getGeometryCatalog() {
|
|
29776
|
+
return getGeometryCatalog();
|
|
29777
|
+
}
|
|
29583
29778
|
constructor() {
|
|
29584
29779
|
super();
|
|
29585
29780
|
this.attachShadow({ mode: "open" });
|
|
@@ -29608,6 +29803,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29608
29803
|
this.focus = new Vector3();
|
|
29609
29804
|
this.targetFocus = new Vector3();
|
|
29610
29805
|
this.drag = null;
|
|
29806
|
+
this.pointers = /* @__PURE__ */ new Map();
|
|
29611
29807
|
this.relationGroup = new Group();
|
|
29612
29808
|
this.scene.add(this.relationGroup);
|
|
29613
29809
|
this.relations = [];
|
|
@@ -29615,15 +29811,20 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29615
29811
|
this.ready = false;
|
|
29616
29812
|
this._toolbarVisible = this.getAttribute("toolbar") !== "off";
|
|
29617
29813
|
this.toolbar = this.shadowRoot.querySelector(".toolbar");
|
|
29814
|
+
this._geometryRegistryListener = () => {
|
|
29815
|
+
if (this.ready && this.modelData) this._build({ preserveSelection: this.selected?.id, preserveRelation: this.selectedRelationIndex });
|
|
29816
|
+
};
|
|
29618
29817
|
this._bindControls();
|
|
29619
29818
|
this.resizeObserver = new ResizeObserver(() => this.resize());
|
|
29620
29819
|
this.resizeObserver.observe(this.canvas);
|
|
29621
29820
|
}
|
|
29622
29821
|
connectedCallback() {
|
|
29822
|
+
globalThis.addEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
|
|
29623
29823
|
if (this.hasAttribute("height")) this.style.setProperty("--lc-height", this.getAttribute("height"));
|
|
29624
29824
|
this._loadInitial();
|
|
29625
29825
|
}
|
|
29626
29826
|
disconnectedCallback() {
|
|
29827
|
+
globalThis.removeEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
|
|
29627
29828
|
this.resizeObserver.disconnect();
|
|
29628
29829
|
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
29629
29830
|
if (this.renderer) this.renderer.dispose();
|
|
@@ -29830,6 +30031,27 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29830
30031
|
this.invalidate();
|
|
29831
30032
|
return this;
|
|
29832
30033
|
}
|
|
30034
|
+
panBy(horizontal = 0, vertical = 0) {
|
|
30035
|
+
const direction = this.camera.getWorldDirection(new Vector3());
|
|
30036
|
+
const right = new Vector3().crossVectors(direction, this.camera.up).normalize();
|
|
30037
|
+
const up = new Vector3().crossVectors(right, direction).normalize();
|
|
30038
|
+
this.targetFocus.addScaledVector(right, Number(horizontal) || 0);
|
|
30039
|
+
this.targetFocus.addScaledVector(up, Number(vertical) || 0);
|
|
30040
|
+
this.invalidate();
|
|
30041
|
+
return this;
|
|
30042
|
+
}
|
|
30043
|
+
panTo(x = 0, y = 0, z = 0) {
|
|
30044
|
+
const value = Array.isArray(x) ? x : x && typeof x === "object" ? [x.x, x.y, x.z] : [x, y, z];
|
|
30045
|
+
this.targetFocus.set(Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0);
|
|
30046
|
+
this.invalidate();
|
|
30047
|
+
return this;
|
|
30048
|
+
}
|
|
30049
|
+
_panByPixels(dx, dy) {
|
|
30050
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
30051
|
+
const distance = Math.max(1, this.camera.position.distanceTo(this.focus));
|
|
30052
|
+
const worldPerPixel = 2 * distance * Math.tan(MathUtils.degToRad(this.camera.fov) / 2) / Math.max(1, rect.height);
|
|
30053
|
+
return this.panBy(-dx * worldPerPixel, dy * worldPerPixel);
|
|
30054
|
+
}
|
|
29833
30055
|
fit() {
|
|
29834
30056
|
const bounds = new Box3();
|
|
29835
30057
|
for (const node of this.nodes.values()) {
|
|
@@ -30020,10 +30242,16 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30020
30242
|
_clearObjects() {
|
|
30021
30243
|
for (const node of this.nodes.values()) {
|
|
30022
30244
|
node.group?.traverse((object) => {
|
|
30023
|
-
if (!object.isMesh && !object.isSprite) return;
|
|
30245
|
+
if (!object.isMesh && !object.isSprite && !object.isLineSegments) return;
|
|
30024
30246
|
object.geometry?.dispose();
|
|
30025
|
-
if (Array.isArray(object.material)) object.material.forEach((material) =>
|
|
30026
|
-
|
|
30247
|
+
if (Array.isArray(object.material)) object.material.forEach((material) => {
|
|
30248
|
+
material.map?.dispose();
|
|
30249
|
+
material.dispose();
|
|
30250
|
+
});
|
|
30251
|
+
else {
|
|
30252
|
+
object.material?.map?.dispose();
|
|
30253
|
+
object.material?.dispose();
|
|
30254
|
+
}
|
|
30027
30255
|
});
|
|
30028
30256
|
this.scene.remove(node.group);
|
|
30029
30257
|
}
|
|
@@ -30044,6 +30272,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30044
30272
|
const color = this.modelData.palette[node.c] || node.c || this.modelData.palette.component;
|
|
30045
30273
|
if (node.material) node.material.color.set(color);
|
|
30046
30274
|
if (node.edge?.material) node.edge.material.color.set(color);
|
|
30275
|
+
for (const binding of node.colorBindings || []) {
|
|
30276
|
+
const palette = { ...this.modelData.palette, ...node.geometryPalette || {}, ...node.geometryPaletteOverrides || {} };
|
|
30277
|
+
binding.material.color.set(resolveGeometryColor(binding.value, palette, color));
|
|
30278
|
+
}
|
|
30047
30279
|
}
|
|
30048
30280
|
}
|
|
30049
30281
|
_build(options = {}) {
|
|
@@ -30058,24 +30290,57 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30058
30290
|
node.group = new Group();
|
|
30059
30291
|
node.group.position.copy(this._worldPosition(node));
|
|
30060
30292
|
const geometryKind = entity.geometry || "box";
|
|
30061
|
-
const
|
|
30062
|
-
const
|
|
30063
|
-
|
|
30064
|
-
|
|
30065
|
-
|
|
30066
|
-
|
|
30067
|
-
node.
|
|
30068
|
-
|
|
30069
|
-
|
|
30070
|
-
|
|
30071
|
-
}
|
|
30293
|
+
const definition = geometryDefinition(geometryKind);
|
|
30294
|
+
const missingGeometry = !definition && geometryKind.includes(".");
|
|
30295
|
+
const color = missingGeometry ? "#ed8f98" : this.modelData.palette[entity.c] || entity.c || this.modelData.palette.component;
|
|
30296
|
+
if (missingGeometry) this.dispatchEvent(new CustomEvent("lc-warning", { detail: { code: "GEOMETRY_NOT_REGISTERED", geometry: geometryKind, entityId: entity.id } }));
|
|
30297
|
+
const primitiveKind = definition?.primitive || geometryKind;
|
|
30298
|
+
const isCustom = definition && !definition.primitive && entity.type !== "shell";
|
|
30299
|
+
node.geometryDefinition = definition || null;
|
|
30300
|
+
node.geometryMissing = missingGeometry;
|
|
30301
|
+
node.geometryPalette = definition?.palette || {};
|
|
30302
|
+
node.geometryPaletteOverrides = entity.geometryPalette || entity.geometry_palette || {};
|
|
30303
|
+
node.ports = { ...definition?.anchors || {}, ...entity.ports || {} };
|
|
30304
|
+
if (isCustom) {
|
|
30305
|
+
try {
|
|
30306
|
+
const content = createGeometryContent(definition, entity, this.modelData.palette, () => this.invalidate());
|
|
30307
|
+
content.root.scale.multiply(new Vector3().fromArray(entity.s));
|
|
30308
|
+
content.root.traverse((object) => {
|
|
30309
|
+
if (object.isMesh) object.userData.node = node;
|
|
30310
|
+
});
|
|
30311
|
+
node.mesh = content.root;
|
|
30312
|
+
node.colorBindings = content.colorBindings;
|
|
30313
|
+
node.geometryOptions = content.parameters;
|
|
30314
|
+
node.group.add(content.root);
|
|
30315
|
+
} catch (error) {
|
|
30316
|
+
this.dispatchEvent(new CustomEvent("lc-warning", { detail: { code: "GEOMETRY_BUILD_FAILED", geometry: geometryKind, entityId: entity.id, error } }));
|
|
30317
|
+
const fallback = normalizeGeometry(makeGeometry("box"));
|
|
30318
|
+
node.material = new MeshStandardMaterial({ color: "#ed8f98", roughness: 0.72, metalness: 0.06 });
|
|
30319
|
+
node.mesh = new Mesh(fallback, node.material);
|
|
30320
|
+
node.mesh.scale.fromArray(entity.s);
|
|
30321
|
+
node.mesh.userData.node = node;
|
|
30322
|
+
node.group.add(node.mesh);
|
|
30323
|
+
}
|
|
30072
30324
|
node.labelSize = new Vector3().fromArray(entity.s);
|
|
30325
|
+
} else {
|
|
30326
|
+
const baked = primitiveKind === "box" && Number(entity.radius) > 0;
|
|
30327
|
+
const geometry = makeGeometry(primitiveKind, entity.radius, entity.segments, entity.s);
|
|
30328
|
+
if (!baked) normalizeGeometry(geometry);
|
|
30329
|
+
node.material = new MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.06, transparent: true, opacity: entity.type === "shell" ? 0.96 : 1 });
|
|
30330
|
+
node.mesh = new Mesh(geometry, node.material);
|
|
30331
|
+
node.mesh.scale.fromArray(baked ? [1, 1, 1] : entity.s);
|
|
30332
|
+
if (baked) {
|
|
30333
|
+
geometry.computeBoundingBox();
|
|
30334
|
+
node.labelSize = geometry.boundingBox.getSize(new Vector3());
|
|
30335
|
+
} else {
|
|
30336
|
+
node.labelSize = new Vector3().fromArray(entity.s);
|
|
30337
|
+
}
|
|
30338
|
+
node.mesh.userData.node = node;
|
|
30339
|
+
node.group.add(node.mesh);
|
|
30340
|
+
node.edge = new LineSegments(new EdgesGeometry(geometry, 22), new LineBasicMaterial({ color, transparent: true, opacity: 0.7 }));
|
|
30341
|
+
node.edge.scale.fromArray(baked ? [1, 1, 1] : entity.s);
|
|
30342
|
+
node.group.add(node.edge);
|
|
30073
30343
|
}
|
|
30074
|
-
node.mesh.userData.node = node;
|
|
30075
|
-
node.group.add(node.mesh);
|
|
30076
|
-
node.edge = new LineSegments(new EdgesGeometry(geometry, 22), new LineBasicMaterial({ color, transparent: true, opacity: 0.7 }));
|
|
30077
|
-
node.edge.scale.fromArray(baked ? [1, 1, 1] : entity.s);
|
|
30078
|
-
node.group.add(node.edge);
|
|
30079
30344
|
node.label = makeLabel(entity.name, color, node.labelSize, entity.type);
|
|
30080
30345
|
node.label.userData.node = node;
|
|
30081
30346
|
node.label.traverse((labelFace) => {
|
|
@@ -30101,18 +30366,19 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30101
30366
|
}
|
|
30102
30367
|
return true;
|
|
30103
30368
|
}
|
|
30104
|
-
_endpoint(node, other, isSource = false) {
|
|
30369
|
+
_endpoint(node, other, isSource = false, relation = null) {
|
|
30105
30370
|
const center = this._worldPosition(node);
|
|
30106
30371
|
const mode = node.anchor || "center";
|
|
30107
|
-
|
|
30108
|
-
if (mode === "port") {
|
|
30372
|
+
const requestedAnchor = relation?.[isSource ? "fromAnchor" : "toAnchor"] || relation?.[isSource ? "from_anchor" : "to_anchor"];
|
|
30373
|
+
if (mode === "port" || requestedAnchor || node.geometryDefinition?.anchors) {
|
|
30109
30374
|
const ports = node.ports || {};
|
|
30110
|
-
const port = ports[isSource ? "output" : "input"] || ports.default;
|
|
30375
|
+
const port = ports[requestedAnchor] || ports[isSource ? "output" : "input"] || ports.default;
|
|
30111
30376
|
if (Array.isArray(port) && port.length >= 3) {
|
|
30112
30377
|
const half2 = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
|
|
30113
30378
|
return center.add(new Vector3(port[0] * half2.x, port[1] * half2.y, port[2] * half2.z));
|
|
30114
30379
|
}
|
|
30115
30380
|
}
|
|
30381
|
+
if (mode === "center") return center;
|
|
30116
30382
|
const direction = this._worldPosition(other).sub(center);
|
|
30117
30383
|
if (direction.lengthSq() < 1e-8) return center;
|
|
30118
30384
|
const half = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
|
|
@@ -30136,8 +30402,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30136
30402
|
const from = this.nodes.get(relation.from);
|
|
30137
30403
|
const to = this.nodes.get(relation.to);
|
|
30138
30404
|
if (!from || !to) continue;
|
|
30139
|
-
const start = this._endpoint(from, to, true);
|
|
30140
|
-
const end = this._endpoint(to, from, false);
|
|
30405
|
+
const start = this._endpoint(from, to, true, relation);
|
|
30406
|
+
const end = this._endpoint(to, from, false, relation);
|
|
30141
30407
|
const delta = end.clone().sub(start);
|
|
30142
30408
|
if (delta.lengthSq() < 0.01) continue;
|
|
30143
30409
|
const visualMode = relationVisualMode(relation);
|
|
@@ -30358,13 +30624,21 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30358
30624
|
const visible = this._isVisible(node);
|
|
30359
30625
|
node.group.visible = visible;
|
|
30360
30626
|
const alpha = node.type === "shell" ? this.open.has(node.id) ? 0.12 : 1 : 1;
|
|
30361
|
-
node.material.opacity = alpha;
|
|
30362
30627
|
const transparent = alpha < 0.999;
|
|
30363
|
-
node.material.depthWrite = !transparent;
|
|
30364
30628
|
node.mesh.renderOrder = transparent ? 10 - (node.parent ? 1 : 0) : 0;
|
|
30365
|
-
|
|
30366
|
-
|
|
30367
|
-
|
|
30629
|
+
if (node.material) {
|
|
30630
|
+
node.material.opacity = alpha;
|
|
30631
|
+
node.material.depthWrite = !transparent;
|
|
30632
|
+
node.material.emissive?.set(node === this.selected ? node.material.color : 0);
|
|
30633
|
+
node.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
|
|
30634
|
+
} else {
|
|
30635
|
+
node.mesh.traverse((object) => {
|
|
30636
|
+
if (!object.isMesh || !object.material) return;
|
|
30637
|
+
object.material.emissive?.set(node === this.selected ? object.material.color : 0);
|
|
30638
|
+
object.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
|
|
30639
|
+
});
|
|
30640
|
+
}
|
|
30641
|
+
if (node.edge?.material) node.edge.material.opacity = node === this.selected ? 1 : 0.65;
|
|
30368
30642
|
const opened = node.type === "shell" && this.open.has(node.id);
|
|
30369
30643
|
const transparentLabel = node.type === "shell" && (opened || alpha < 0.65);
|
|
30370
30644
|
const displayTransparentLabel = transparentLabel && this.showShellLabels;
|
|
@@ -30443,7 +30717,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30443
30717
|
if (!(node.type === "shell" && this.open.has(node.id))) meshes.push(node.mesh);
|
|
30444
30718
|
if (node.label?.visible) meshes.push(...node.label.children.filter((face) => face.visible));
|
|
30445
30719
|
}
|
|
30446
|
-
const nodeHit = this.raycaster.intersectObjects(meshes,
|
|
30720
|
+
const nodeHit = this.raycaster.intersectObjects(meshes, true)[0] || null;
|
|
30447
30721
|
const relationTargets = this.relationGroup.children.filter((object) => object.userData.relationPick);
|
|
30448
30722
|
const relationHit = this.raycaster.intersectObjects(relationTargets, false)[0] || null;
|
|
30449
30723
|
if (nodeHit && (!relationHit || nodeHit.distance <= relationHit.distance + 0.12)) return { kind: "entity", node: nodeHit.object.userData.node };
|
|
@@ -30463,28 +30737,60 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30463
30737
|
if (action === "relations-none") this.setRelationMode("none");
|
|
30464
30738
|
if (action) this.invalidate();
|
|
30465
30739
|
});
|
|
30740
|
+
this.canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
|
30466
30741
|
this.canvas.addEventListener("pointerdown", (event) => {
|
|
30467
|
-
if (event.
|
|
30468
|
-
this.
|
|
30742
|
+
if (event.pointerType !== "touch" && ![0, 1, 2].includes(event.button)) return;
|
|
30743
|
+
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
30744
|
+
const mode = event.pointerType === "touch" ? "rotate" : event.shiftKey || event.button !== 0 ? "pan" : "rotate";
|
|
30745
|
+
this.drag = { pointerId: event.pointerId, pointerType: event.pointerType, button: event.button, mode, x: event.clientX, y: event.clientY, moved: false };
|
|
30746
|
+
if (event.pointerType === "touch" && this.pointers.size === 2) {
|
|
30747
|
+
const [a, b] = [...this.pointers.values()];
|
|
30748
|
+
this.drag = { mode: "touch-pan", x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, distance: Math.hypot(a.x - b.x, a.y - b.y), moved: true };
|
|
30749
|
+
}
|
|
30469
30750
|
this.canvas.setPointerCapture(event.pointerId);
|
|
30470
30751
|
this.canvas.style.cursor = "grabbing";
|
|
30471
30752
|
});
|
|
30472
30753
|
this.canvas.addEventListener("pointermove", (event) => {
|
|
30473
|
-
if (!this.drag) return;
|
|
30754
|
+
if (!this.drag || !this.pointers.has(event.pointerId)) return;
|
|
30755
|
+
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
30756
|
+
if (event.pointerType === "touch" && this.pointers.size >= 2) {
|
|
30757
|
+
const [a, b] = [...this.pointers.values()];
|
|
30758
|
+
const x = (a.x + b.x) / 2;
|
|
30759
|
+
const y = (a.y + b.y) / 2;
|
|
30760
|
+
const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
30761
|
+
if (this.drag.mode !== "touch-pan") this.drag = { mode: "touch-pan", x, y, distance, moved: true };
|
|
30762
|
+
else {
|
|
30763
|
+
this._panByPixels(x - this.drag.x, y - this.drag.y);
|
|
30764
|
+
this.targetZoom = Math.min(2.8, Math.max(0.55, this.targetZoom * distance / Math.max(1, this.drag.distance)));
|
|
30765
|
+
Object.assign(this.drag, { x, y, distance, moved: true });
|
|
30766
|
+
}
|
|
30767
|
+
this.invalidate();
|
|
30768
|
+
return;
|
|
30769
|
+
}
|
|
30770
|
+
if (this.drag.pointerId !== event.pointerId) return;
|
|
30474
30771
|
const dx = event.clientX - this.drag.x;
|
|
30475
30772
|
const dy = event.clientY - this.drag.y;
|
|
30476
30773
|
if (Math.abs(dx) + Math.abs(dy) > 3) this.drag.moved = true;
|
|
30477
30774
|
this.drag.x = event.clientX;
|
|
30478
30775
|
this.drag.y = event.clientY;
|
|
30479
|
-
this.
|
|
30480
|
-
|
|
30776
|
+
if (this.drag.mode === "pan" || event.shiftKey) this._panByPixels(dx, dy);
|
|
30777
|
+
else {
|
|
30778
|
+
this.yaw -= dx * 5e-3;
|
|
30779
|
+
this.pitch = Math.max(-1.56, Math.min(1.56, this.pitch + dy * 5e-3));
|
|
30780
|
+
}
|
|
30481
30781
|
this.invalidate();
|
|
30482
30782
|
});
|
|
30483
|
-
|
|
30783
|
+
const endPointer = (event, cancelled = false) => {
|
|
30484
30784
|
const drag = this.drag;
|
|
30785
|
+
this.pointers.delete(event.pointerId);
|
|
30786
|
+
if (this.pointers.size) {
|
|
30787
|
+
const [pointerId, point] = this.pointers.entries().next().value;
|
|
30788
|
+
this.drag = { pointerId, pointerType: "touch", button: 0, mode: "rotate", x: point.x, y: point.y, moved: true };
|
|
30789
|
+
return;
|
|
30790
|
+
}
|
|
30485
30791
|
this.drag = null;
|
|
30486
30792
|
this.canvas.style.cursor = "grab";
|
|
30487
|
-
if (drag && !drag.moved) {
|
|
30793
|
+
if (!cancelled && drag && !drag.moved && drag.button === 0) {
|
|
30488
30794
|
const picked = this._pick(event);
|
|
30489
30795
|
if (picked?.kind === "entity") {
|
|
30490
30796
|
const node = picked.node;
|
|
@@ -30498,7 +30804,9 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30498
30804
|
}
|
|
30499
30805
|
}
|
|
30500
30806
|
this.invalidate();
|
|
30501
|
-
}
|
|
30807
|
+
};
|
|
30808
|
+
this.canvas.addEventListener("pointerup", (event) => endPointer(event));
|
|
30809
|
+
this.canvas.addEventListener("pointercancel", (event) => endPointer(event, true));
|
|
30502
30810
|
this.canvas.addEventListener("wheel", (event) => {
|
|
30503
30811
|
event.preventDefault();
|
|
30504
30812
|
this.targetZoom = Math.min(2.8, Math.max(0.55, this.targetZoom * Math.exp(-event.deltaY * 1e-3)));
|
|
@@ -30515,5 +30823,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30515
30823
|
if (!customElements.get("liquidcars-atlas")) customElements.define("liquidcars-atlas", LiquidCarsAtlas);
|
|
30516
30824
|
export {
|
|
30517
30825
|
LiquidCarsAtlas,
|
|
30826
|
+
getGeometryCatalog,
|
|
30827
|
+
registerGeometryPack,
|
|
30828
|
+
unregisterGeometryPack,
|
|
30518
30829
|
normalizeModel
|
|
30519
30830
|
};
|
package/index.html
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
liquidcars-atlas { display: block; width: 100%; max-width: 1480px; margin: 0 auto; }
|
|
15
15
|
code { color: #85e5d3; }
|
|
16
16
|
</style>
|
|
17
|
-
<script type="module" src="./dist/liquidcars-atlas.js?v=0.1.
|
|
17
|
+
<script type="module" src="./dist/liquidcars-atlas.js?v=0.1.24"></script>
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<header>
|
|
21
21
|
<h1>LiquidCars Atlas</h1>
|
|
22
|
-
<p>Prueba de distribución del componente <code><liquidcars-atlas></code> · v0.1.
|
|
22
|
+
<p>Prueba de distribución del componente <code><liquidcars-atlas></code> · v0.1.24</p>
|
|
23
23
|
</header>
|
|
24
24
|
<main>
|
|
25
25
|
<liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
|