@liquidcars/atlas 0.1.24 → 0.1.26

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. Para planos con imagen, `imageFit` admite `cover`, `contain` y
184
+ `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,12 +222,16 @@ 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.24.tgz
225
+ npm install ./liquidcars-atlas-0.1.26.tgz
217
226
  ```
218
227
 
219
228
  ## Estado
220
229
 
221
- `0.1.24` incorpora geometry packs independientes, namespaces, parámetros
230
+ `0.1.26` mantiene el centro como anclaje implícito de toda relación y utiliza
231
+ los puertos de geometrías externas únicamente cuando el modelo los solicita.
232
+ También evita recorrer los materiales de geometrías compuestas en cada frame,
233
+ refuerza la liberación de recursos y expone `getRenderStats()`.
234
+ `0.1.24` incorporó geometry packs independientes, namespaces, parámetros
222
235
  personalizados, paletas locales, anchors e imágenes/iconos declarativos. La
223
236
  versión `0.1.23` añadió desplazamiento lateral de cámara mediante ratón, teclado
224
237
  modificador y gestos táctiles, además de la API pública `panBy()` y `panTo()`.
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.24"></script>
17
+ <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.26"></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.24</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.26</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.24 | MIT */
1
+ /*! LiquidCars Atlas 0.1.26 | MIT */
2
2
 
3
3
  // src/three.core.js
4
4
  /**
@@ -29642,17 +29642,52 @@ 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
+ const removeListeners = () => {
29654
+ bitmap.removeEventListener("load", onLoad);
29655
+ bitmap.removeEventListener("error", onError);
29656
+ };
29657
+ const onLoad = () => {
29658
+ removeListeners();
29659
+ if (cancelled || !material.map) return;
29660
+ const fit = part.imageFit || part.fit || "stretch";
29661
+ const imageAspect = bitmap.naturalWidth / Math.max(1, bitmap.naturalHeight);
29662
+ const baseWidth = Math.abs(Number(part.size?.[0])) || 1;
29663
+ const baseHeight = Math.abs(Number(part.size?.[1])) || 1;
29664
+ const faceAspect = baseWidth / baseHeight;
29665
+ if (fit === "cover") {
29666
+ if (imageAspect > faceAspect) {
29667
+ material.map.repeat.set(faceAspect / imageAspect, 1);
29668
+ material.map.offset.set((1 - material.map.repeat.x) / 2, 0);
29669
+ } else {
29670
+ material.map.repeat.set(1, imageAspect / faceAspect);
29671
+ material.map.offset.set(0, (1 - material.map.repeat.y) / 2);
29672
+ }
29673
+ } else if (fit === "contain" && mesh) {
29674
+ if (imageAspect > faceAspect) mesh.scale.y = baseWidth / imageAspect;
29675
+ else mesh.scale.x = baseHeight * imageAspect;
29676
+ }
29652
29677
  material.map.needsUpdate = true;
29653
29678
  invalidate?.();
29654
- });
29655
- bitmap.addEventListener("error", () => invalidate?.());
29679
+ };
29680
+ const onError = () => {
29681
+ removeListeners();
29682
+ if (!cancelled) invalidate?.();
29683
+ };
29684
+ material.userData.atlasImageCleanup = () => {
29685
+ cancelled = true;
29686
+ removeListeners();
29687
+ bitmap.src = "";
29688
+ };
29689
+ bitmap.addEventListener("load", onLoad);
29690
+ bitmap.addEventListener("error", onError);
29656
29691
  bitmap.src = image;
29657
29692
  material.map.colorSpace = SRGBColorSpace;
29658
29693
  material.needsUpdate = true;
@@ -29661,7 +29696,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate) {
29661
29696
  material.transparent = true;
29662
29697
  material.needsUpdate = true;
29663
29698
  }
29664
- const mesh = new Mesh(geometry, material);
29699
+ mesh = new Mesh(geometry, material);
29665
29700
  mesh.position.fromArray(Array.isArray(part.position) ? part.position : [0, 0, 0]);
29666
29701
  mesh.scale.fromArray(Array.isArray(part.size) ? part.size : [1, 1, 1]);
29667
29702
  if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
@@ -29820,6 +29855,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29820
29855
  }
29821
29856
  connectedCallback() {
29822
29857
  globalThis.addEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29858
+ this.resizeObserver.observe(this.canvas);
29823
29859
  if (this.hasAttribute("height")) this.style.setProperty("--lc-height", this.getAttribute("height"));
29824
29860
  this._loadInitial();
29825
29861
  }
@@ -29827,7 +29863,13 @@ var LiquidCarsAtlas = class extends HTMLElement {
29827
29863
  globalThis.removeEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29828
29864
  this.resizeObserver.disconnect();
29829
29865
  if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
29830
- if (this.renderer) this.renderer.dispose();
29866
+ this._clearObjects();
29867
+ if (this.renderer) {
29868
+ this.renderer.dispose();
29869
+ this.renderer.forceContextLoss?.();
29870
+ this.renderer = null;
29871
+ }
29872
+ this.ready = false;
29831
29873
  this.dispatchEvent(new CustomEvent("lc-disconnect"));
29832
29874
  }
29833
29875
  attributeChangedCallback(name, previous, value) {
@@ -29861,6 +29903,28 @@ var LiquidCarsAtlas = class extends HTMLElement {
29861
29903
  const relation = this.selectedRelation;
29862
29904
  return relation ? { kind: "relation", id: relation.id || null, index: relation.index, relation } : null;
29863
29905
  }
29906
+ getRenderStats() {
29907
+ const info = this.renderer?.info;
29908
+ return {
29909
+ ready: this.ready,
29910
+ nodes: this.nodes.size,
29911
+ relations: this.relations.length,
29912
+ relationObjects: this.relationGroup.children.length,
29913
+ animatedRelationObjects: this.relationGroup.children.filter((object) => object.userData?.start).length,
29914
+ memory: {
29915
+ geometries: info?.memory?.geometries || 0,
29916
+ textures: info?.memory?.textures || 0,
29917
+ programs: info?.programs?.length || 0
29918
+ },
29919
+ render: {
29920
+ frame: info?.render?.frame || 0,
29921
+ calls: info?.render?.calls || 0,
29922
+ triangles: info?.render?.triangles || 0,
29923
+ lines: info?.render?.lines || 0,
29924
+ points: info?.render?.points || 0
29925
+ }
29926
+ };
29927
+ }
29864
29928
  get clickBehavior() {
29865
29929
  return this.selectionMode;
29866
29930
  }
@@ -30240,17 +30304,26 @@ var LiquidCarsAtlas = class extends HTMLElement {
30240
30304
  return parent ? this._worldPosition(parent, trail).add(position) : position;
30241
30305
  }
30242
30306
  _clearObjects() {
30307
+ const geometries = /* @__PURE__ */ new Set();
30308
+ const materials = /* @__PURE__ */ new Set();
30309
+ const textures2 = /* @__PURE__ */ new Set();
30243
30310
  for (const node of this.nodes.values()) {
30244
30311
  node.group?.traverse((object) => {
30245
30312
  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();
30313
+ if (object.geometry && !geometries.has(object.geometry)) {
30314
+ geometries.add(object.geometry);
30315
+ object.geometry.dispose();
30316
+ }
30317
+ const objectMaterials = Array.isArray(object.material) ? object.material : [object.material];
30318
+ for (const material of objectMaterials.filter(Boolean)) {
30319
+ if (materials.has(material)) continue;
30320
+ materials.add(material);
30321
+ material.userData?.atlasImageCleanup?.();
30322
+ if (material.map && !textures2.has(material.map)) {
30323
+ textures2.add(material.map);
30324
+ material.map.dispose();
30325
+ }
30249
30326
  material.dispose();
30250
- });
30251
- else {
30252
- object.material?.map?.dispose();
30253
- object.material?.dispose();
30254
30327
  }
30255
30328
  });
30256
30329
  this.scene.remove(node.group);
@@ -30305,8 +30378,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
30305
30378
  try {
30306
30379
  const content = createGeometryContent(definition, entity, this.modelData.palette, () => this.invalidate());
30307
30380
  content.root.scale.multiply(new Vector3().fromArray(entity.s));
30381
+ node.appearanceMaterials = [];
30308
30382
  content.root.traverse((object) => {
30309
- if (object.isMesh) object.userData.node = node;
30383
+ if (!object.isMesh) return;
30384
+ object.userData.node = node;
30385
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
30386
+ node.appearanceMaterials.push(...materials.filter(Boolean));
30310
30387
  });
30311
30388
  node.mesh = content.root;
30312
30389
  node.colorBindings = content.colorBindings;
@@ -30370,7 +30447,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
30370
30447
  const center = this._worldPosition(node);
30371
30448
  const mode = node.anchor || "center";
30372
30449
  const requestedAnchor = relation?.[isSource ? "fromAnchor" : "toAnchor"] || relation?.[isSource ? "from_anchor" : "to_anchor"];
30373
- if (mode === "port" || requestedAnchor || node.geometryDefinition?.anchors) {
30450
+ // Geometry packs may advertise available ports, but their mere presence
30451
+ // must not alter the model. The centre remains the default endpoint;
30452
+ // ports are used only when the entity or relation explicitly requests one.
30453
+ if (mode === "port" || requestedAnchor) {
30374
30454
  const ports = node.ports || {};
30375
30455
  const port = ports[requestedAnchor] || ports[isSource ? "output" : "input"] || ports.default;
30376
30456
  if (Array.isArray(port) && port.length >= 3) {
@@ -30631,12 +30711,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
30631
30711
  node.material.depthWrite = !transparent;
30632
30712
  node.material.emissive?.set(node === this.selected ? node.material.color : 0);
30633
30713
  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
- });
30714
+ } else if (node._appearanceSelected !== (node === this.selected)) {
30715
+ for (const material of node.appearanceMaterials || []) {
30716
+ material.emissive?.set(node === this.selected ? material.color : 0);
30717
+ material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30718
+ }
30719
+ node._appearanceSelected = node === this.selected;
30640
30720
  }
30641
30721
  if (node.edge?.material) node.edge.material.opacity = node === this.selected ? 1 : 0.65;
30642
30722
  const opened = node.type === "shell" && this.open.has(node.id);
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.24"></script>
17
+ <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.26"></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.24</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.26</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.24",
3
+ "version": "0.1.26",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",