@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 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 de la toolbar también están disponibles para la página anfitriona: `zoomIn()`, `zoomOut()`, `fit()`, `frontView()`, `setShellLabels()`, `toggleShellLabels()`, `openAllContainers()` y `closeAllContainers()`. Así se puede construir una barra de controles propia sin depender de la toolbar interna.
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.22.tgz
216
+ npm install ./liquidcars-atlas-0.1.24.tgz
153
217
  ```
154
218
 
155
219
  ## Estado
156
220
 
157
- `0.1.22` amplía la selección contextual a las relaciones. El usuario puede
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.21"></script>
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>&lt;liquidcars-atlas&gt;</code> · v0.1.21</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</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>
@@ -1,4 +1,4 @@
1
- /*! LiquidCars Atlas 0.1.22 | MIT */
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">Arrastra para girar \xB7 rueda para zoom \xB7 clic para seleccionar</div></section><aside class="panel" aria-live="polite"></aside></div>';
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[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) => material.dispose());
30026
- else object.material?.dispose();
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 baked = geometryKind === "box" && Number(entity.radius) > 0;
30062
- const geometry = makeGeometry(geometryKind, entity.radius, entity.segments, entity.s);
30063
- if (!baked) normalizeGeometry(geometry);
30064
- const color = this.modelData.palette[entity.c] || entity.c || this.modelData.palette.component;
30065
- node.material = new MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.06, transparent: true, opacity: entity.type === "shell" ? 0.96 : 1 });
30066
- node.mesh = new Mesh(geometry, node.material);
30067
- node.mesh.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30068
- if (baked) {
30069
- geometry.computeBoundingBox();
30070
- node.labelSize = geometry.boundingBox.getSize(new Vector3());
30071
- } else {
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
- if (mode === "center") return center;
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
- node.material.emissive?.set(node === this.selected ? node.material.color : 0);
30366
- node.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30367
- node.edge.material.opacity = node === this.selected ? 1 : 0.65;
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, false)[0] || null;
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.button !== 0) return;
30468
- this.drag = { x: event.clientX, y: event.clientY, moved: false };
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.yaw -= dx * 5e-3;
30480
- this.pitch = Math.max(-1.56, Math.min(1.56, this.pitch + dy * 5e-3));
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
- this.canvas.addEventListener("pointerup", (event) => {
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.21"></script>
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>&lt;liquidcars-atlas&gt;</code> · v0.1.21</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",