@liquidcars/atlas 0.1.38 → 0.1.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,50 +16,156 @@ const RELATION_RENDER_ORDER = 15;
16
16
 
17
17
  const GEOMETRY_DEFINITIONS = new Map();
18
18
  const GEOMETRY_ALIASES = new Map();
19
+ const GEOMETRY_PACKS = new Map();
20
+
21
+ function cloneGeometryMetadata(value) {
22
+ if (Array.isArray(value)) return value.map(cloneGeometryMetadata);
23
+ if (!value || typeof value !== "object") return value;
24
+ return Object.fromEntries(
25
+ Object.entries(value)
26
+ .filter(([, item]) => typeof item !== "function" && item !== undefined)
27
+ .map(([key, item]) => [key, cloneGeometryMetadata(item)])
28
+ );
29
+ }
30
+
31
+ function normalizeGeometryAuthors(value) {
32
+ const authors = value == null ? [] : Array.isArray(value) ? value : [value];
33
+ return authors
34
+ .map(author => typeof author === "string" ? { name: author } : cloneGeometryMetadata(author))
35
+ .filter(author => author && String(author.name || "").trim())
36
+ .map(author => ({ ...author, name: String(author.name).trim() }));
37
+ }
38
+
39
+ function normalizeGeometryStringList(value) {
40
+ const values = value == null ? [] : Array.isArray(value) ? value : [value];
41
+ return [...new Set(values.map(item => String(item || "").trim()).filter(Boolean))];
42
+ }
43
+
44
+ function normalizeGeometryLicense(value) {
45
+ if (!value) return null;
46
+ return typeof value === "string" ? { spdx: value } : cloneGeometryMetadata(value);
47
+ }
48
+
49
+ function normalizeGeometrySource(source, index) {
50
+ const value = cloneGeometryMetadata(source || {});
51
+ const id = String(value.id || `source-${index + 1}`).trim();
52
+ if (!id) throw new TypeError("Geometry pack sources require an id.");
53
+ return {
54
+ ...value,
55
+ id,
56
+ authors: normalizeGeometryAuthors(value.authors ?? value.author),
57
+ license: normalizeGeometryLicense(value.license)
58
+ };
59
+ }
60
+
61
+ function normalizeGeometryResource(resource, index, sourceIds) {
62
+ const value = cloneGeometryMetadata(resource || {});
63
+ const id = String(value.id || "").trim();
64
+ if (!id) throw new TypeError(`Geometry pack resource at index ${index} requires an id.`);
65
+ const source = value.source == null ? null : String(value.source).trim();
66
+ if (source && !sourceIds.has(source)) {
67
+ throw new TypeError(`Geometry pack resource '${id}' references unknown source '${source}'.`);
68
+ }
69
+ return {
70
+ ...value,
71
+ id,
72
+ kind: String(value.kind || value.type || "asset"),
73
+ source,
74
+ authors: normalizeGeometryAuthors(value.authors ?? value.author),
75
+ license: normalizeGeometryLicense(value.license)
76
+ };
77
+ }
78
+
79
+ function normalizeGeometryPackMetadata(pack, namespace) {
80
+ const sources = (Array.isArray(pack.sources) ? pack.sources : []).map(normalizeGeometrySource);
81
+ const sourceIds = new Set(sources.map(source => source.id));
82
+ const resources = (Array.isArray(pack.resources) ? pack.resources : [])
83
+ .map((resource, index) => normalizeGeometryResource(resource, index, sourceIds));
84
+ if (new Set(resources.map(resource => resource.id)).size !== resources.length) {
85
+ throw new TypeError(`Geometry pack '${namespace}' contains duplicate resource ids.`);
86
+ }
87
+ return {
88
+ namespace,
89
+ name: String(pack.name || namespace),
90
+ version: String(pack.version || "0.0.0"),
91
+ description: String(pack.description || ""),
92
+ category: String(pack.category || pack.name || namespace),
93
+ homepage: pack.homepage ? String(pack.homepage) : "",
94
+ repository: cloneGeometryMetadata(pack.repository || null),
95
+ authors: normalizeGeometryAuthors(pack.authors ?? pack.author),
96
+ license: normalizeGeometryLicense(pack.license),
97
+ attribution: String(pack.attribution || ""),
98
+ collections: cloneGeometryMetadata(Array.isArray(pack.collections) ? pack.collections : []),
99
+ sources,
100
+ resources,
101
+ geometries: []
102
+ };
103
+ }
19
104
 
20
105
  function publicGeometryDefinition(definition) {
21
- const { create, ...metadata } = definition;
106
+ const { create, resourceMap, ...metadata } = definition;
22
107
  return {
23
- ...metadata,
108
+ ...cloneGeometryMetadata(metadata),
24
109
  canonicalSize: [...definition.canonicalSize],
25
- parameters: { ...definition.parameters },
26
- palette: { ...definition.palette },
27
- anchors: { ...definition.anchors }
110
+ parameters: cloneGeometryMetadata(definition.parameters),
111
+ palette: cloneGeometryMetadata(definition.palette),
112
+ anchors: cloneGeometryMetadata(definition.anchors)
28
113
  };
29
114
  }
30
115
 
116
+ function publicGeometryPack(pack) {
117
+ return cloneGeometryMetadata(pack);
118
+ }
119
+
31
120
  export function registerGeometryPack(pack) {
32
121
  if (!pack || typeof pack !== "object") throw new TypeError("A geometry pack must be an object.");
33
122
  const namespace = String(pack.namespace || "").trim();
34
- if (!/^[a-z][a-z0-9-]*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid namespace.");
123
+ if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid dot-separated namespace.");
35
124
  if (!Array.isArray(pack.geometries) || !pack.geometries.length) throw new TypeError("A geometry pack requires at least one geometry.");
36
- const registered = [];
125
+
126
+ const metadata = normalizeGeometryPackMetadata(pack, namespace);
127
+ const resourceMap = Object.freeze(Object.fromEntries(
128
+ metadata.resources.map(resource => [resource.id, Object.freeze(cloneGeometryMetadata(resource))])
129
+ ));
130
+ const definitions = [];
37
131
  for (const source of pack.geometries) {
38
132
  if (!source || typeof source !== "object" || !source.id) throw new TypeError(`Geometry pack '${namespace}' contains an invalid geometry.`);
39
133
  const localId = String(source.id).includes(".") ? String(source.id).split(".").pop() : String(source.id);
40
134
  const id = String(source.id).includes(".") ? String(source.id) : `${namespace}.${localId}`;
41
- const definition = {
135
+ definitions.push({
42
136
  ...source,
43
137
  id,
44
138
  localId,
45
139
  namespace,
46
- pack: pack.name || namespace,
47
- packVersion: pack.version || "0.0.0",
48
- category: source.category || pack.category || pack.name || namespace,
140
+ pack: metadata.name,
141
+ packVersion: metadata.version,
142
+ category: source.category || metadata.category,
143
+ collection: source.collection == null ? "" : String(source.collection).trim(),
144
+ tags: normalizeGeometryStringList(source.tags),
145
+ keywords: normalizeGeometryStringList(source.keywords),
49
146
  canonicalSize: Array.isArray(source.canonicalSize) && source.canonicalSize.length === 3 ? source.canonicalSize.map(Number) : [2.5, 1.5, 2.5],
50
147
  parameters: source.parameters && typeof source.parameters === "object" ? source.parameters : {},
51
148
  palette: { ...(pack.palette || {}), ...(source.palette || {}) },
52
- anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) }
53
- };
54
- GEOMETRY_DEFINITIONS.set(id, definition);
55
- GEOMETRY_ALIASES.set(id, id);
56
- for (const alias of source.aliases || []) GEOMETRY_ALIASES.set(String(alias), id);
57
- registered.push(id);
149
+ anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) },
150
+ resourceMap
151
+ });
58
152
  }
153
+
154
+ unregisterGeometryPack(namespace);
155
+ const registered = [];
156
+ for (const definition of definitions) {
157
+ GEOMETRY_DEFINITIONS.set(definition.id, definition);
158
+ GEOMETRY_ALIASES.set(definition.id, definition.id);
159
+ for (const alias of definition.aliases || []) GEOMETRY_ALIASES.set(String(alias), definition.id);
160
+ registered.push(definition.id);
161
+ }
162
+ metadata.geometries = [...registered];
163
+ GEOMETRY_PACKS.set(namespace, metadata);
164
+ const registeredPack = publicGeometryPack(metadata);
59
165
  if (typeof globalThis.dispatchEvent === "function" && typeof CustomEvent === "function") {
60
- globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered } }));
166
+ globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered, pack: registeredPack } }));
61
167
  }
62
- return { namespace, geometries: registered };
168
+ return registeredPack;
63
169
  }
64
170
 
65
171
  export function unregisterGeometryPack(namespace) {
@@ -70,6 +176,7 @@ export function unregisterGeometryPack(namespace) {
70
176
  removed.push(id);
71
177
  }
72
178
  for (const [alias, id] of GEOMETRY_ALIASES) if (removed.includes(id)) GEOMETRY_ALIASES.delete(alias);
179
+ GEOMETRY_PACKS.delete(namespace);
73
180
  return removed;
74
181
  }
75
182
 
@@ -77,6 +184,10 @@ export function getGeometryCatalog() {
77
184
  return [...GEOMETRY_DEFINITIONS.values()].map(publicGeometryDefinition);
78
185
  }
79
186
 
187
+ export function getGeometryPackCatalog() {
188
+ return [...GEOMETRY_PACKS.values()].map(publicGeometryPack);
189
+ }
190
+
80
191
  function geometryDefinition(kind) {
81
192
  const id = GEOMETRY_ALIASES.get(kind) || kind;
82
193
  return GEOMETRY_DEFINITIONS.get(id) || null;
@@ -122,7 +233,11 @@ registerGeometryPack({
122
233
  namespace: "geo",
123
234
  name: "Geometric primitives",
124
235
  version: "1",
236
+ description: "Built-in Atlas geometric primitives.",
125
237
  category: "Geometric primitives",
238
+ authors: [{ name: "LiquidCars S.L.", url: "https://liquidcars.net" }],
239
+ license: { spdx: "MIT", url: "https://opensource.org/license/mit" },
240
+ homepage: "https://github.com/gaelibus/atlas",
126
241
  geometries: CORE_GEOMETRIES.map(id => ({
127
242
  id,
128
243
  primitive: id,
@@ -389,7 +504,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
389
504
  ...(entity.geometryPalette || entity.geometry_palette || {})
390
505
  };
391
506
  let descriptor = typeof definition.create === "function"
392
- ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity } })
507
+ ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity }, resources: definition.resourceMap || {} })
393
508
  : { parts: definition.parts || [] };
394
509
  if (Array.isArray(descriptor)) descriptor = { parts: descriptor };
395
510
  const parts = Array.isArray(descriptor?.parts) ? descriptor.parts : [];
@@ -691,6 +806,7 @@ class LiquidCarsAtlas extends HTMLElement {
691
806
  static registerGeometryPack(pack) { return registerGeometryPack(pack); }
692
807
  static unregisterGeometryPack(namespace) { return unregisterGeometryPack(namespace); }
693
808
  static getGeometryCatalog() { return getGeometryCatalog(); }
809
+ static getGeometryPackCatalog() { return getGeometryPackCatalog(); }
694
810
 
695
811
  constructor() {
696
812
  super();
@@ -1444,7 +1560,7 @@ class LiquidCarsAtlas extends HTMLElement {
1444
1560
  return this.panBy(-dx * worldPerPixel, dy * worldPerPixel);
1445
1561
  }
1446
1562
 
1447
- fit() {
1563
+ fit(options = {}) {
1448
1564
  const bounds = new THREE.Box3();
1449
1565
  for (const node of this.nodes.values()) {
1450
1566
  const center = this._worldPosition(node);
@@ -1460,6 +1576,10 @@ class LiquidCarsAtlas extends HTMLElement {
1460
1576
  const baseDistance = this._baseCameraDistance();
1461
1577
  this.targetZoom = Math.max(MIN_ZOOM, Math.min(2.8, baseDistance / requiredDistance));
1462
1578
  this.targetFocus.copy(center);
1579
+ if (options.animate === false) {
1580
+ this.zoom = this.targetZoom;
1581
+ this.focus.copy(this.targetFocus);
1582
+ }
1463
1583
  this.invalidate();
1464
1584
  return this;
1465
1585
  }