@liquidcars/atlas 0.1.25 → 0.1.27

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
@@ -180,9 +180,11 @@ El modelo configura cada instancia sin conocer la implementación del paquete:
180
180
  Cada definición puede declarar parámetros tipados, una paleta local,
181
181
  proporciones canónicas y anchors. Las piezas declarativas admiten primitivas,
182
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`.
183
+ URL/data URI; los SVG también pueden proporcionarse inline. Para planos con
184
+ imagen, `imageFit` admite `cover`, `contain` y `stretch`. El renderer mantiene Three.js encapsulado para evitar que los packs
185
+ dependan de su versión interna. Las relaciones parten del centro por defecto;
186
+ `fromAnchor` y `toAnchor` quedan como overrides explícitos para casos
187
+ excepcionales.
186
188
 
187
189
  Si un namespace no está registrado, Atlas conserva el modelo renderizable con
188
190
  una caja roja de fallback y emite `lc-warning` con el código
@@ -200,6 +202,13 @@ gestionan relaciones. Cada operación reconstruye la geometría necesaria,
200
202
  conserva la cámara y la selección cuando sigue siendo válida, y emite
201
203
  `lc-model-change` con el tipo de operación y el modelo resultante.
202
204
 
205
+ `getRenderStats()` devuelve una instantánea de entidades, relaciones, objetos
206
+ animados, draw calls y contadores de geometrías, texturas y programas del
207
+ renderer. Resulta útil para comprobar que una secuencia repetida de mutaciones
208
+ vuelve a una línea base estable. Al reconstruir o desconectar el componente se
209
+ liberan geometrías, materiales, texturas, cargas de imagen pendientes y el
210
+ contexto WebGL.
211
+
203
212
  ## Distribución
204
213
 
205
214
  El archivo `dist/liquidcars-atlas.js` es un módulo ESM autocontenido: Three.js se incluye dentro del bundle, por lo que el consumidor no necesita instalar otra dependencia para probarlo. El bundle no contiene ningún modelo de negocio ni datos de LiquidCars Atlas; los modelos se suministran aparte mediante `model`, `src` o un `<script type="application/json">`. Para una distribución privada se puede servir ese archivo desde un CDN propio o desde el mismo sitio. Para publicar en npm, después de revisar el API estable:
@@ -213,14 +222,17 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
213
222
  Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
214
223
 
215
224
  ```bash
216
- npm install ./liquidcars-atlas-0.1.25.tgz
225
+ npm install ./liquidcars-atlas-0.1.27.tgz
217
226
  ```
218
227
 
219
228
  ## Estado
220
229
 
221
- `0.1.25` mantiene el centro como anclaje implícito de toda relación y utiliza
230
+ `0.1.27` añade carga automática de imágenes SVG remotas, SVG mediante data URI
231
+ y SVG inline, manteniendo las mismas opciones de cara y ajuste que las imágenes
232
+ raster. `0.1.26` mantiene el centro como anclaje implícito de toda relación y utiliza
222
233
  los puertos de geometrías externas únicamente cuando el modelo los solicita.
223
- También evita recorrer los materiales de geometrías compuestas en cada frame.
234
+ También evita recorrer los materiales de geometrías compuestas en cada frame,
235
+ refuerza la liberación de recursos y expone `getRenderStats()`.
224
236
  `0.1.24` incorporó geometry packs independientes, namespaces, parámetros
225
237
  personalizados, paletas locales, anchors e imágenes/iconos declarativos. La
226
238
  versión `0.1.23` añadió desplazamiento lateral de cámara mediante ratón, teclado
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.25"></script>
17
+ <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.27"></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.25</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.27</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.25 | MIT */
1
+ /*! LiquidCars Atlas 0.1.27 | MIT */
2
2
 
3
3
  // src/three.core.js
4
4
  /**
@@ -29642,18 +29642,90 @@ function createGeometryContent(definition, entity, modelPalette, invalidate) {
29642
29642
  opacity: Number.isFinite(Number(part.opacity)) ? Number(part.opacity) : 1,
29643
29643
  side: part.doubleSided ? DoubleSide : FrontSide
29644
29644
  });
29645
+ let mesh;
29645
29646
  const icon = part.icon;
29646
29647
  const image = part.image;
29647
29648
  if (typeof image === "string" && image) {
29648
29649
  const bitmap = new Image();
29649
29650
  bitmap.crossOrigin = "anonymous";
29650
29651
  material.map = new Texture(bitmap);
29651
- bitmap.addEventListener("load", () => {
29652
+ let cancelled = false;
29653
+ let objectUrl = null;
29654
+ const abortController = new AbortController();
29655
+ const releaseObjectUrl = () => {
29656
+ if (!objectUrl) return;
29657
+ URL.revokeObjectURL(objectUrl);
29658
+ objectUrl = null;
29659
+ };
29660
+ const removeListeners = () => {
29661
+ bitmap.removeEventListener("load", onLoad);
29662
+ bitmap.removeEventListener("error", onError);
29663
+ };
29664
+ const onLoad = () => {
29665
+ removeListeners();
29666
+ releaseObjectUrl();
29667
+ if (cancelled || !material.map) return;
29668
+ const fit = part.imageFit || part.fit || "stretch";
29669
+ const imageAspect = bitmap.naturalWidth / Math.max(1, bitmap.naturalHeight);
29670
+ const baseWidth = Math.abs(Number(part.size?.[0])) || 1;
29671
+ const baseHeight = Math.abs(Number(part.size?.[1])) || 1;
29672
+ const faceAspect = baseWidth / baseHeight;
29673
+ if (fit === "cover") {
29674
+ if (imageAspect > faceAspect) {
29675
+ material.map.repeat.set(faceAspect / imageAspect, 1);
29676
+ material.map.offset.set((1 - material.map.repeat.x) / 2, 0);
29677
+ } else {
29678
+ material.map.repeat.set(1, imageAspect / faceAspect);
29679
+ material.map.offset.set(0, (1 - material.map.repeat.y) / 2);
29680
+ }
29681
+ } else if (fit === "contain" && mesh) {
29682
+ if (imageAspect > faceAspect) mesh.scale.y = baseWidth / imageAspect;
29683
+ else mesh.scale.x = baseHeight * imageAspect;
29684
+ }
29652
29685
  material.map.needsUpdate = true;
29653
29686
  invalidate?.();
29654
- });
29655
- bitmap.addEventListener("error", () => invalidate?.());
29656
- bitmap.src = image;
29687
+ };
29688
+ const onError = () => {
29689
+ removeListeners();
29690
+ releaseObjectUrl();
29691
+ if (!cancelled) invalidate?.();
29692
+ };
29693
+ material.userData.atlasImageCleanup = () => {
29694
+ cancelled = true;
29695
+ abortController.abort();
29696
+ removeListeners();
29697
+ releaseObjectUrl();
29698
+ bitmap.src = "";
29699
+ };
29700
+ bitmap.addEventListener("load", onLoad);
29701
+ bitmap.addEventListener("error", onError);
29702
+ const assignImageSource = async () => {
29703
+ const inlineSvg = /^\s*<svg(?:\s|>)/i.test(image);
29704
+ if (inlineSvg) {
29705
+ objectUrl = URL.createObjectURL(new Blob([image], { type: "image/svg+xml" }));
29706
+ bitmap.src = objectUrl;
29707
+ return;
29708
+ }
29709
+ if (/^(?:data|blob):/i.test(image)) {
29710
+ bitmap.src = image;
29711
+ return;
29712
+ }
29713
+ try {
29714
+ const response = await fetch(image, { mode: "cors", signal: abortController.signal });
29715
+ if (!response.ok) throw new Error(`Image request failed with ${response.status}`);
29716
+ let blob = await response.blob();
29717
+ const contentType = response.headers.get("content-type") || blob.type;
29718
+ const looksLikeSvg = /image\/svg\+xml/i.test(contentType) || /\.svg(?:[?#]|$)/i.test(image);
29719
+ if (looksLikeSvg && blob.type !== "image/svg+xml") blob = new Blob([await blob.text()], { type: "image/svg+xml" });
29720
+ if (cancelled) return;
29721
+ objectUrl = URL.createObjectURL(blob);
29722
+ bitmap.src = objectUrl;
29723
+ } catch (error) {
29724
+ if (cancelled || error?.name === "AbortError") return;
29725
+ bitmap.src = image;
29726
+ }
29727
+ };
29728
+ assignImageSource();
29657
29729
  material.map.colorSpace = SRGBColorSpace;
29658
29730
  material.needsUpdate = true;
29659
29731
  } else if (icon != null && icon !== "") {
@@ -29661,7 +29733,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate) {
29661
29733
  material.transparent = true;
29662
29734
  material.needsUpdate = true;
29663
29735
  }
29664
- const mesh = new Mesh(geometry, material);
29736
+ mesh = new Mesh(geometry, material);
29665
29737
  mesh.position.fromArray(Array.isArray(part.position) ? part.position : [0, 0, 0]);
29666
29738
  mesh.scale.fromArray(Array.isArray(part.size) ? part.size : [1, 1, 1]);
29667
29739
  if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
@@ -29820,6 +29892,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29820
29892
  }
29821
29893
  connectedCallback() {
29822
29894
  globalThis.addEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29895
+ this.resizeObserver.observe(this.canvas);
29823
29896
  if (this.hasAttribute("height")) this.style.setProperty("--lc-height", this.getAttribute("height"));
29824
29897
  this._loadInitial();
29825
29898
  }
@@ -29827,7 +29900,13 @@ var LiquidCarsAtlas = class extends HTMLElement {
29827
29900
  globalThis.removeEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29828
29901
  this.resizeObserver.disconnect();
29829
29902
  if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
29830
- if (this.renderer) this.renderer.dispose();
29903
+ this._clearObjects();
29904
+ if (this.renderer) {
29905
+ this.renderer.dispose();
29906
+ this.renderer.forceContextLoss?.();
29907
+ this.renderer = null;
29908
+ }
29909
+ this.ready = false;
29831
29910
  this.dispatchEvent(new CustomEvent("lc-disconnect"));
29832
29911
  }
29833
29912
  attributeChangedCallback(name, previous, value) {
@@ -29861,6 +29940,28 @@ var LiquidCarsAtlas = class extends HTMLElement {
29861
29940
  const relation = this.selectedRelation;
29862
29941
  return relation ? { kind: "relation", id: relation.id || null, index: relation.index, relation } : null;
29863
29942
  }
29943
+ getRenderStats() {
29944
+ const info = this.renderer?.info;
29945
+ return {
29946
+ ready: this.ready,
29947
+ nodes: this.nodes.size,
29948
+ relations: this.relations.length,
29949
+ relationObjects: this.relationGroup.children.length,
29950
+ animatedRelationObjects: this.relationGroup.children.filter((object) => object.userData?.start).length,
29951
+ memory: {
29952
+ geometries: info?.memory?.geometries || 0,
29953
+ textures: info?.memory?.textures || 0,
29954
+ programs: info?.programs?.length || 0
29955
+ },
29956
+ render: {
29957
+ frame: info?.render?.frame || 0,
29958
+ calls: info?.render?.calls || 0,
29959
+ triangles: info?.render?.triangles || 0,
29960
+ lines: info?.render?.lines || 0,
29961
+ points: info?.render?.points || 0
29962
+ }
29963
+ };
29964
+ }
29864
29965
  get clickBehavior() {
29865
29966
  return this.selectionMode;
29866
29967
  }
@@ -30240,17 +30341,26 @@ var LiquidCarsAtlas = class extends HTMLElement {
30240
30341
  return parent ? this._worldPosition(parent, trail).add(position) : position;
30241
30342
  }
30242
30343
  _clearObjects() {
30344
+ const geometries = /* @__PURE__ */ new Set();
30345
+ const materials = /* @__PURE__ */ new Set();
30346
+ const textures2 = /* @__PURE__ */ new Set();
30243
30347
  for (const node of this.nodes.values()) {
30244
30348
  node.group?.traverse((object) => {
30245
30349
  if (!object.isMesh && !object.isSprite && !object.isLineSegments) return;
30246
- object.geometry?.dispose();
30247
- if (Array.isArray(object.material)) object.material.forEach((material) => {
30248
- material.map?.dispose();
30350
+ if (object.geometry && !geometries.has(object.geometry)) {
30351
+ geometries.add(object.geometry);
30352
+ object.geometry.dispose();
30353
+ }
30354
+ const objectMaterials = Array.isArray(object.material) ? object.material : [object.material];
30355
+ for (const material of objectMaterials.filter(Boolean)) {
30356
+ if (materials.has(material)) continue;
30357
+ materials.add(material);
30358
+ material.userData?.atlasImageCleanup?.();
30359
+ if (material.map && !textures2.has(material.map)) {
30360
+ textures2.add(material.map);
30361
+ material.map.dispose();
30362
+ }
30249
30363
  material.dispose();
30250
- });
30251
- else {
30252
- object.material?.map?.dispose();
30253
- object.material?.dispose();
30254
30364
  }
30255
30365
  });
30256
30366
  this.scene.remove(node.group);
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.25"></script>
17
+ <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.27"></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.25</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.27</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.25",
3
+ "version": "0.1.27",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",