@liquidcars/atlas-layout 0.1.16 → 0.1.17

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
@@ -173,6 +173,14 @@ relations:
173
173
 
174
174
  `label` es el título corto visible junto a la conexión, `text` permite documentar su significado y `url` enlaza documentación externa desde el panel de información. `mode` es la forma recomendada y el compilador siempre la emite normalizada. Para facilitar la migración se aceptan también `visual`, `render`, `type`, `direction`, `bidirectional: true` y `broken: true`, además de los alias `two-way`, `both` e `interrupted`. La forma abreviada puede incluir el modo como cuarto valor: `[from, to, label, mode]`. Un modo desconocido conserva la relación como `directed` y añade el diagnóstico `UNKNOWN_RELATION_MODE`.
175
175
 
176
+ Las relaciones también pueden declararse en `relations` dentro de cualquier entidad
177
+ `type: container`. El compilador aplana las relaciones raíz y anidadas en un único
178
+ array de runtime. La exportación visual usa una forma canónica: cada relación se
179
+ guarda en el contenedor común más profundo de sus dos extremos; si no existe un
180
+ contenedor común, se guarda en `relations` en la raíz. Así, compilar, editar y
181
+ volver a exportar no depende de dónde se declaró originalmente la conexión.
182
+
183
+
176
184
  En un documento Markdown dividido, el bloque de relaciones contiene una lista YAML:
177
185
 
178
186
  ````markdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas-layout",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Declarative layout compiler for LiquidCars Atlas models",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -127,6 +127,7 @@ function normalizeEntityTypes(flat, geometryCatalog, diagnostics) {
127
127
  const declared = typeof declaredValue === "string" ? declaredValue.trim().toLowerCase() : "";
128
128
  const childIds = flat.childrenById.get(entity.id) || [];
129
129
  const hasChildren = childIds.length > 0 || (Array.isArray(source.children) && source.children.length > 0);
130
+ const hasRelations = Array.isArray(source.relations) && source.relations.length > 0;
130
131
 
131
132
  if (declaredValue != null && typeof declaredValue !== "string") {
132
133
  diagnostic(diagnostics, "fatal", "UNKNOWN_ENTITY_TYPE",
@@ -140,6 +141,10 @@ function normalizeEntityTypes(flat, geometryCatalog, diagnostics) {
140
141
  diagnostic(diagnostics, "fatal", "CONTAINER_TYPE_REQUIRED_FOR_CHILDREN",
141
142
  "Entity '" + entity.id + "' declares children and must set type: container.",
142
143
  { entityId: entity.id });
144
+ } else if (hasRelations && !["container", "shell"].includes(declared)) {
145
+ diagnostic(diagnostics, "fatal", "CONTAINER_TYPE_REQUIRED_FOR_RELATIONS",
146
+ "Entity '" + entity.id + "' declares relations and must set type: container.",
147
+ { entityId: entity.id });
143
148
  }
144
149
 
145
150
  if (declared && !["item", "container", "shell"].includes(declared)) {
@@ -148,7 +153,7 @@ function normalizeEntityTypes(flat, geometryCatalog, diagnostics) {
148
153
  { entityId: entity.id, type: source.type });
149
154
  }
150
155
 
151
- const type = hasChildren || declared === "container" || declared === "shell" ? "container" : "item";
156
+ const type = hasChildren || hasRelations || declared === "container" || declared === "shell" ? "container" : "item";
152
157
  entity.type = type;
153
158
 
154
159
  if (Object.hasOwn(source, "open") && typeof source.open !== "boolean") {
@@ -175,6 +180,7 @@ function flattenEntities(input, diagnostics) {
175
180
  const entities = [];
176
181
  const childrenById = new Map();
177
182
  const sourceById = new Map();
183
+ const relations = [];
178
184
  const seen = new Set();
179
185
 
180
186
  function visit(items, parent = null) {
@@ -193,6 +199,8 @@ function flattenEntities(input, diagnostics) {
193
199
  const entity = { ...raw };
194
200
  delete entity.children;
195
201
  delete entity.layout;
202
+ delete entity.relations;
203
+ if (Array.isArray(raw.relations)) relations.push(...raw.relations);
196
204
  entity.__atlasExplicitSize = Array.isArray(raw.size) && raw.size.length === 3;
197
205
  entity.parent = parent ?? raw.parent ?? null;
198
206
  entity.type = raw.type;
@@ -216,7 +224,7 @@ function flattenEntities(input, diagnostics) {
216
224
  if (!siblings.includes(entity.id)) siblings.push(entity.id);
217
225
  }
218
226
  }
219
- return { entities, childrenById, sourceById };
227
+ return { entities, childrenById, sourceById, relations };
220
228
  }
221
229
 
222
230
  function normalizeRelationMode(relation, diagnostics, index, shorthandMode) {
@@ -1193,16 +1201,25 @@ function projectedAtlasSource(input, visibleIds) {
1193
1201
  }
1194
1202
  }
1195
1203
 
1204
+ const relationIsVisible = relation => {
1205
+ const from = Array.isArray(relation) ? relation[0] : relation?.from;
1206
+ const to = Array.isArray(relation) ? relation[1] : relation?.to;
1207
+ return requested.has(from) && requested.has(to);
1208
+ };
1196
1209
  function filterEntities(items) {
1197
1210
  return (Array.isArray(items) ? items : []).flatMap(entity => {
1198
1211
  if (!entity?.id || !requested.has(entity.id)) return [];
1199
1212
  const copy = { ...entity };
1200
1213
  if (Array.isArray(entity.children)) copy.children = filterEntities(entity.children);
1214
+ if (Array.isArray(entity.relations)) {
1215
+ copy.relations = entity.relations.filter(relationIsVisible);
1216
+ if (!copy.relations.length) delete copy.relations;
1217
+ }
1201
1218
  return [copy];
1202
1219
  });
1203
1220
  }
1204
1221
  source.entities = filterEntities(source.entities);
1205
- source.relations = (source.relations || []).filter(relation => requested.has(relation.from) && requested.has(relation.to));
1222
+ source.relations = (source.relations || []).filter(relationIsVisible);
1206
1223
 
1207
1224
  const childrenByParent = new Map();
1208
1225
  function index(items, nestedParent = null) {
@@ -1849,7 +1866,7 @@ function compileResolvedAtlasModel(input, options = {}) {
1849
1866
  normalizeEntityTypes(flat, options.geometryCatalog || [], diagnostics);
1850
1867
  if (!flat.entities.length) diagnostic(diagnostics, "fatal", "NO_VALID_ENTITIES", "The model does not contain any valid entities.");
1851
1868
  const validIds = new Set(flat.entities.map(entity => entity.id));
1852
- const relations = normalizeRelations(source.relations, validIds, diagnostics);
1869
+ const relations = normalizeRelations([...(source.relations || []), ...flat.relations], validIds, diagnostics);
1853
1870
  const modelStyle = mergeStyle(DEFAULT_MODEL_STYLE, source.style || {});
1854
1871
  if (!Object.keys(modelStyle.edges || {}).length) delete modelStyle.edges;
1855
1872
  resolveStyles(flat.entities, flat.sourceById, flat.childrenById, palette, diagnostics, modelStyle);
package/src/spec.js CHANGED
@@ -36,6 +36,7 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
36
36
  open: field("Initial expansion state for a container; omitted means closed.", [true, false]),
37
37
  parent: field("Parent entity id.", null, { type: "string", authoring: "absolute" }),
38
38
  children: field("Nested entities.", null, { context: "entity", sequence: true }),
39
+ relations: field("Connections canonically owned by this container.", null, { context: "relation", sequence: true }),
39
40
  geometry: field("Geometry id supplied by Atlas or a registered pack.", null, { valueSource: "geometryCatalog" }),
40
41
  geometryOptions: field("Typed parameters supplied by the selected geometry.", null, { context: "geometryOptions" }),
41
42
  geometryPalette: field("Local palette overrides supplied by the selected geometry.", null, { context: "geometryPalette" }),