@liquidcars/atlas 0.1.23 → 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
@@ -96,7 +96,7 @@ principales también están expuestos como variables CSS (`--lc-bg`,
96
96
  por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
97
97
  Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
98
98
 
99
- 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`,
100
100
  `lc-relation-mode` y `lc-disconnect`. `lc-select` usa un detalle discriminado:
101
101
  las entidades incluyen `{ kind: 'entity', id, entity, source }` y las relaciones
102
102
  `{ kind: 'relation', id, index, relation, source }`. Los campos históricos `id`
@@ -127,6 +127,68 @@ como `setClickBehavior()`).
127
127
 
128
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.
129
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
+
130
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.
131
193
 
132
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.
@@ -151,12 +213,14 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
151
213
  Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
152
214
 
153
215
  ```bash
154
- npm install ./liquidcars-atlas-0.1.22.tgz
216
+ npm install ./liquidcars-atlas-0.1.24.tgz
155
217
  ```
156
218
 
157
219
  ## Estado
158
220
 
159
- `0.1.23` añade desplazamiento lateral de cámara mediante ratón, teclado
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
160
224
  modificador y gestos táctiles, además de la API pública `panBy()` y `panTo()`.
161
225
  Se conserva la selección contextual de relaciones introducida en `0.1.22`: el usuario puede
162
226
  seleccionar un tubo directamente, el componente lo realza, muestra sus extremos
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" },
@@ -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" });
@@ -29616,15 +29811,20 @@ var LiquidCarsAtlas = class extends HTMLElement {
29616
29811
  this.ready = false;
29617
29812
  this._toolbarVisible = this.getAttribute("toolbar") !== "off";
29618
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
+ };
29619
29817
  this._bindControls();
29620
29818
  this.resizeObserver = new ResizeObserver(() => this.resize());
29621
29819
  this.resizeObserver.observe(this.canvas);
29622
29820
  }
29623
29821
  connectedCallback() {
29822
+ globalThis.addEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29624
29823
  if (this.hasAttribute("height")) this.style.setProperty("--lc-height", this.getAttribute("height"));
29625
29824
  this._loadInitial();
29626
29825
  }
29627
29826
  disconnectedCallback() {
29827
+ globalThis.removeEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29628
29828
  this.resizeObserver.disconnect();
29629
29829
  if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
29630
29830
  if (this.renderer) this.renderer.dispose();
@@ -30042,10 +30242,16 @@ var LiquidCarsAtlas = class extends HTMLElement {
30042
30242
  _clearObjects() {
30043
30243
  for (const node of this.nodes.values()) {
30044
30244
  node.group?.traverse((object) => {
30045
- if (!object.isMesh && !object.isSprite) return;
30245
+ if (!object.isMesh && !object.isSprite && !object.isLineSegments) return;
30046
30246
  object.geometry?.dispose();
30047
- if (Array.isArray(object.material)) object.material.forEach((material) => material.dispose());
30048
- 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
+ }
30049
30255
  });
30050
30256
  this.scene.remove(node.group);
30051
30257
  }
@@ -30066,6 +30272,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
30066
30272
  const color = this.modelData.palette[node.c] || node.c || this.modelData.palette.component;
30067
30273
  if (node.material) node.material.color.set(color);
30068
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
+ }
30069
30279
  }
30070
30280
  }
30071
30281
  _build(options = {}) {
@@ -30080,24 +30290,57 @@ var LiquidCarsAtlas = class extends HTMLElement {
30080
30290
  node.group = new Group();
30081
30291
  node.group.position.copy(this._worldPosition(node));
30082
30292
  const geometryKind = entity.geometry || "box";
30083
- const baked = geometryKind === "box" && Number(entity.radius) > 0;
30084
- const geometry = makeGeometry(geometryKind, entity.radius, entity.segments, entity.s);
30085
- if (!baked) normalizeGeometry(geometry);
30086
- const color = this.modelData.palette[entity.c] || entity.c || this.modelData.palette.component;
30087
- node.material = new MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.06, transparent: true, opacity: entity.type === "shell" ? 0.96 : 1 });
30088
- node.mesh = new Mesh(geometry, node.material);
30089
- node.mesh.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30090
- if (baked) {
30091
- geometry.computeBoundingBox();
30092
- node.labelSize = geometry.boundingBox.getSize(new Vector3());
30093
- } 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
+ }
30094
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);
30095
30343
  }
30096
- node.mesh.userData.node = node;
30097
- node.group.add(node.mesh);
30098
- node.edge = new LineSegments(new EdgesGeometry(geometry, 22), new LineBasicMaterial({ color, transparent: true, opacity: 0.7 }));
30099
- node.edge.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30100
- node.group.add(node.edge);
30101
30344
  node.label = makeLabel(entity.name, color, node.labelSize, entity.type);
30102
30345
  node.label.userData.node = node;
30103
30346
  node.label.traverse((labelFace) => {
@@ -30123,18 +30366,19 @@ var LiquidCarsAtlas = class extends HTMLElement {
30123
30366
  }
30124
30367
  return true;
30125
30368
  }
30126
- _endpoint(node, other, isSource = false) {
30369
+ _endpoint(node, other, isSource = false, relation = null) {
30127
30370
  const center = this._worldPosition(node);
30128
30371
  const mode = node.anchor || "center";
30129
- if (mode === "center") return center;
30130
- if (mode === "port") {
30372
+ const requestedAnchor = relation?.[isSource ? "fromAnchor" : "toAnchor"] || relation?.[isSource ? "from_anchor" : "to_anchor"];
30373
+ if (mode === "port" || requestedAnchor || node.geometryDefinition?.anchors) {
30131
30374
  const ports = node.ports || {};
30132
- const port = ports[isSource ? "output" : "input"] || ports.default;
30375
+ const port = ports[requestedAnchor] || ports[isSource ? "output" : "input"] || ports.default;
30133
30376
  if (Array.isArray(port) && port.length >= 3) {
30134
30377
  const half2 = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
30135
30378
  return center.add(new Vector3(port[0] * half2.x, port[1] * half2.y, port[2] * half2.z));
30136
30379
  }
30137
30380
  }
30381
+ if (mode === "center") return center;
30138
30382
  const direction = this._worldPosition(other).sub(center);
30139
30383
  if (direction.lengthSq() < 1e-8) return center;
30140
30384
  const half = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
@@ -30158,8 +30402,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
30158
30402
  const from = this.nodes.get(relation.from);
30159
30403
  const to = this.nodes.get(relation.to);
30160
30404
  if (!from || !to) continue;
30161
- const start = this._endpoint(from, to, true);
30162
- 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);
30163
30407
  const delta = end.clone().sub(start);
30164
30408
  if (delta.lengthSq() < 0.01) continue;
30165
30409
  const visualMode = relationVisualMode(relation);
@@ -30380,13 +30624,21 @@ var LiquidCarsAtlas = class extends HTMLElement {
30380
30624
  const visible = this._isVisible(node);
30381
30625
  node.group.visible = visible;
30382
30626
  const alpha = node.type === "shell" ? this.open.has(node.id) ? 0.12 : 1 : 1;
30383
- node.material.opacity = alpha;
30384
30627
  const transparent = alpha < 0.999;
30385
- node.material.depthWrite = !transparent;
30386
30628
  node.mesh.renderOrder = transparent ? 10 - (node.parent ? 1 : 0) : 0;
30387
- node.material.emissive?.set(node === this.selected ? node.material.color : 0);
30388
- node.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30389
- 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;
30390
30642
  const opened = node.type === "shell" && this.open.has(node.id);
30391
30643
  const transparentLabel = node.type === "shell" && (opened || alpha < 0.65);
30392
30644
  const displayTransparentLabel = transparentLabel && this.showShellLabels;
@@ -30465,7 +30717,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30465
30717
  if (!(node.type === "shell" && this.open.has(node.id))) meshes.push(node.mesh);
30466
30718
  if (node.label?.visible) meshes.push(...node.label.children.filter((face) => face.visible));
30467
30719
  }
30468
- const nodeHit = this.raycaster.intersectObjects(meshes, false)[0] || null;
30720
+ const nodeHit = this.raycaster.intersectObjects(meshes, true)[0] || null;
30469
30721
  const relationTargets = this.relationGroup.children.filter((object) => object.userData.relationPick);
30470
30722
  const relationHit = this.raycaster.intersectObjects(relationTargets, false)[0] || null;
30471
30723
  if (nodeHit && (!relationHit || nodeHit.distance <= relationHit.distance + 0.12)) return { kind: "entity", node: nodeHit.object.userData.node };
@@ -30571,5 +30823,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
30571
30823
  if (!customElements.get("liquidcars-atlas")) customElements.define("liquidcars-atlas", LiquidCarsAtlas);
30572
30824
  export {
30573
30825
  LiquidCarsAtlas,
30826
+ getGeometryCatalog,
30827
+ registerGeometryPack,
30828
+ unregisterGeometryPack,
30574
30829
  normalizeModel
30575
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.23",
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",