@liquidcars/atlas-layout 0.1.8 → 0.1.10
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 +11 -2
- package/package.json +1 -1
- package/spec/ATLAS-LAYOUT-SPEC-v1.md +15 -4
- package/src/index.js +83 -1
- package/src/spec.js +17 -3
package/README.md
CHANGED
|
@@ -60,6 +60,13 @@ render:
|
|
|
60
60
|
zoom: 1.35
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Filtered projections
|
|
64
|
+
|
|
65
|
+
`compileAtlasProjection(source, visibleIds, options)` compiles a temporary model
|
|
66
|
+
containing only the requested entities and their required ancestors. Relations,
|
|
67
|
+
constraints and virtual layout groups are pruned consistently. The source object is
|
|
68
|
+
never mutated, making the helper suitable for selection-driven `render.focus` reflow.
|
|
69
|
+
|
|
63
70
|
Las coordenadas son coordenadas del modelo. `zoom` es un multiplicador de
|
|
64
71
|
encuadre independiente del tamaño del viewport y se conserva al compilar YAML
|
|
65
72
|
o Markdown.
|
|
@@ -127,13 +134,15 @@ relations:
|
|
|
127
134
|
from: source
|
|
128
135
|
to: rules
|
|
129
136
|
label: feeds
|
|
137
|
+
text: Sends normalized source data to the rules engine.
|
|
138
|
+
url: https://example.com/relations/source-to-rules
|
|
130
139
|
mode: bidirectional
|
|
131
140
|
- from: rules
|
|
132
141
|
to: output
|
|
133
142
|
mode: broken
|
|
134
143
|
```
|
|
135
144
|
|
|
136
|
-
`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`.
|
|
145
|
+
`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`.
|
|
137
146
|
|
|
138
147
|
En un documento Markdown dividido, el bloque de relaciones contiene una lista YAML:
|
|
139
148
|
|
|
@@ -187,4 +196,4 @@ El resultado mantiene `palette`, `theme`, `entities` y `relations`, con `p` y `s
|
|
|
187
196
|
|
|
188
197
|
El espacio de una celda y el tamaño de la geometría son conceptos distintos. Cuando el layout asigna una celda rectangular a una geometría sin `size` explícito, la figura se centra y se escala uniformemente para caber en el menor volumen compatible; no se estira por separado en `x`, `y` y `z`. Por eso una esfera conserva `s[0] === s[1] === s[2]`, aunque su celda sea rectangular. Un `size` explícito sigue teniendo prioridad y permite al autor solicitar una proporción concreta.
|
|
189
198
|
|
|
190
|
-
`row`, `grid`/`masonry` y `volume` producen posiciones deterministas. Las constraints de tamaño y alineación, el empaquetado compacto, los grupos virtuales y la distribución `justify` forman parte del contrato estable de autoría. El algoritmo `graph` sigue reservado: por ahora emite `GRAPH_LAYOUT_FALLBACK` y usa `grid`. La proyección y el enrutado avanzado de relaciones entre contenedores permanecen como líneas de desarrollo posteriores.
|
|
199
|
+
`row`, `grid`/`masonry` y `volume` producen posiciones deterministas. `variant: masonry` desplaza las filas alternas media celda sin exigir una sección `stagger`; `stagger.offset` queda disponible como override explícito. Para conservar legibilidad frontal se recomienda `plane: xy`, y para una composición lateral, `plane: yz`. Las constraints de tamaño y alineación, el empaquetado compacto, los grupos virtuales y la distribución `justify` forman parte del contrato estable de autoría. El algoritmo `graph` sigue reservado: por ahora emite `GRAPH_LAYOUT_FALLBACK` y usa `grid`. La proyección y el enrutado avanzado de relaciones entre contenedores permanecen como líneas de desarrollo posteriores.
|
package/package.json
CHANGED
|
@@ -47,7 +47,7 @@ subject to sizeMode and diagnostics.
|
|
|
47
47
|
|
|
48
48
|
Required: id.
|
|
49
49
|
|
|
50
|
-
Common fields: name, sub, text, type, parent, children, geometry, geometryOptions,
|
|
50
|
+
Common fields: name, sub, text, url, type, parent, children, geometry, geometryOptions,
|
|
51
51
|
geometryPalette, style, layout, weight, span, size, sizeMode, p, s, c and radius.
|
|
52
52
|
|
|
53
53
|
An entity with children is a container and must explicitly declare `type: container`.
|
|
@@ -73,7 +73,10 @@ fit permits assisted layout to use the geometry's canonical size.
|
|
|
73
73
|
|
|
74
74
|
layout.algorithm accepts row, grid, volume or graph. direction accepts x, y or z.
|
|
75
75
|
Grid layouts also accept variant uniform or masonry, optional columns and rows, and
|
|
76
|
-
a plane of xz (the default), xy or yz.
|
|
76
|
+
a plane of xz (the default), xy or yz. Masonry offsets alternate rows by half a cell
|
|
77
|
+
unless `stagger.offset` explicitly overrides that distance. Use plane xy for layouts
|
|
78
|
+
intended to remain legible from the natural frontal camera, or yz for a lateral view.
|
|
79
|
+
Volume layouts additionally accept layers.
|
|
77
80
|
gap and padding are absolute numbers. justify accepts start, center, end,
|
|
78
81
|
space-between, space-around or space-evenly. These modes become distinct when a
|
|
79
82
|
row receives an explicit or constrained size. Distributed modes treat gap as the
|
|
@@ -145,13 +148,21 @@ constraints:
|
|
|
145
148
|
|
|
146
149
|
Relations require from and to entity ids. mode is directed, bidirectional or broken.
|
|
147
150
|
The aliases visual, render, type and direction are accepted for compatibility.
|
|
148
|
-
Optional fields include id, label, fromAnchor, toAnchor, priority and layout.
|
|
151
|
+
Optional fields include id, label, text, url, fromAnchor, toAnchor, priority and layout.
|
|
152
|
+
`label` is the compact caption associated with the connection; `text` is its longer
|
|
153
|
+
description and `url` links to external documentation when the relation is selected
|
|
154
|
+
in the information panel.
|
|
149
155
|
|
|
150
156
|
## Rendering
|
|
151
157
|
|
|
152
158
|
palette maps tokens to CSS colours. style.color or c can reference a token or direct
|
|
153
159
|
CSS colour. render can additionally set relationMode, selectionMode, toolbar,
|
|
154
|
-
shellLabels (the text-label contrast outline) and camera.
|
|
160
|
+
shellLabels (the text-label contrast outline), focus and camera. `render.focus`
|
|
161
|
+
accepts `mode: all|connected`, `effect: dim|hide`, `layout: preserve|reflow`
|
|
162
|
+
and an optional dim `opacity`. Connected focus retains the selection, its directly
|
|
163
|
+
related entities and the ancestor containers required to preserve context. Preserve
|
|
164
|
+
keeps spatial memory; reflow compiles a temporary projection and never mutates the
|
|
165
|
+
source model. An initial camera snapshot contains model-space
|
|
155
166
|
position and target vectors plus a positive, dimensionless zoom value. Position
|
|
156
167
|
and target determine the viewing direction; zoom preserves the intended framing
|
|
157
168
|
across viewport sizes. theme contains background, lighting and ui settings.
|
package/src/index.js
CHANGED
|
@@ -242,6 +242,8 @@ function normalizeRelations(input, validIds, diagnostics) {
|
|
|
242
242
|
index
|
|
243
243
|
};
|
|
244
244
|
if (relation.id != null) normalized.id = String(relation.id);
|
|
245
|
+
if (relation.text != null) normalized.text = String(relation.text);
|
|
246
|
+
if (relation.url != null) normalized.url = String(relation.url);
|
|
245
247
|
if (relation.fromAnchor != null || relation.from_anchor != null) normalized.fromAnchor = String(relation.fromAnchor ?? relation.from_anchor);
|
|
246
248
|
if (relation.toAnchor != null || relation.to_anchor != null) normalized.toAnchor = String(relation.toAnchor ?? relation.to_anchor);
|
|
247
249
|
return [normalized];
|
|
@@ -890,7 +892,12 @@ function packedGridLayout(items, spec, diagnostics, dimension = 2) {
|
|
|
890
892
|
for (const item of placements) {
|
|
891
893
|
const columnSize = item.span[0] * columnCell + (item.span[0] - 1) * gap;
|
|
892
894
|
const rowSize = item.span[1] * rowCell + (item.span[1] - 1) * gap;
|
|
893
|
-
const
|
|
895
|
+
const staggerOffset = spec.stagger?.offset;
|
|
896
|
+
const offset = spec.variant === "masonry" && item.cell[1] % 2 === 1
|
|
897
|
+
? (staggerOffset == null || staggerOffset === "half-cell"
|
|
898
|
+
? (columnCell + gap) / 2
|
|
899
|
+
: scalar(staggerOffset, 0))
|
|
900
|
+
: 0;
|
|
894
901
|
const slotSize = [0, 0, 0];
|
|
895
902
|
slotSize[columnAxis] = columnSize;
|
|
896
903
|
slotSize[rowAxis] = rowSize;
|
|
@@ -964,6 +971,81 @@ function normalizeRoot(input) {
|
|
|
964
971
|
return input || {};
|
|
965
972
|
}
|
|
966
973
|
|
|
974
|
+
function projectedAtlasSource(input, visibleIds) {
|
|
975
|
+
const source = structuredClone(normalizeRoot(input));
|
|
976
|
+
const requested = new Set(Array.from(visibleIds || []).filter(Boolean));
|
|
977
|
+
const records = new Map();
|
|
978
|
+
|
|
979
|
+
function collect(items, nestedParent = null) {
|
|
980
|
+
for (const entity of Array.isArray(items) ? items : []) {
|
|
981
|
+
if (!entity?.id) continue;
|
|
982
|
+
const parent = nestedParent ?? entity.parent ?? null;
|
|
983
|
+
records.set(entity.id, { entity, parent });
|
|
984
|
+
collect(entity.children, entity.id);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
collect(source.entities);
|
|
988
|
+
for (const id of [...requested]) {
|
|
989
|
+
let parent = records.get(id)?.parent || null;
|
|
990
|
+
while (parent && !requested.has(parent)) {
|
|
991
|
+
requested.add(parent);
|
|
992
|
+
parent = records.get(parent)?.parent || null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function filterEntities(items) {
|
|
997
|
+
return (Array.isArray(items) ? items : []).flatMap(entity => {
|
|
998
|
+
if (!entity?.id || !requested.has(entity.id)) return [];
|
|
999
|
+
const copy = { ...entity };
|
|
1000
|
+
if (Array.isArray(entity.children)) copy.children = filterEntities(entity.children);
|
|
1001
|
+
return [copy];
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
source.entities = filterEntities(source.entities);
|
|
1005
|
+
source.relations = (source.relations || []).filter(relation => requested.has(relation.from) && requested.has(relation.to));
|
|
1006
|
+
|
|
1007
|
+
const childrenByParent = new Map();
|
|
1008
|
+
function index(items, nestedParent = null) {
|
|
1009
|
+
for (const entity of items || []) {
|
|
1010
|
+
const parent = nestedParent ?? entity.parent ?? null;
|
|
1011
|
+
if (!childrenByParent.has(parent)) childrenByParent.set(parent, []);
|
|
1012
|
+
childrenByParent.get(parent).push(entity.id);
|
|
1013
|
+
index(entity.children, entity.id);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
index(source.entities);
|
|
1017
|
+
|
|
1018
|
+
function pruneOwner(owner, ownerId = null) {
|
|
1019
|
+
const siblingIds = new Set(childrenByParent.get(ownerId) || []);
|
|
1020
|
+
const layout = owner.layout ? { ...owner.layout } : null;
|
|
1021
|
+
if (layout?.groups) {
|
|
1022
|
+
layout.groups = layout.groups.flatMap(group => {
|
|
1023
|
+
const members = (group.members || []).filter(id => siblingIds.has(id));
|
|
1024
|
+
return members.length ? [{ ...group, members }] : [];
|
|
1025
|
+
});
|
|
1026
|
+
if (!layout.groups.length) delete layout.groups;
|
|
1027
|
+
owner.layout = layout;
|
|
1028
|
+
}
|
|
1029
|
+
const candidates = new Set([...siblingIds, ...(layout?.groups || []).map(group => group.id)]);
|
|
1030
|
+
if (Array.isArray(owner.constraints)) {
|
|
1031
|
+
owner.constraints = owner.constraints.flatMap(constraint => {
|
|
1032
|
+
const key = Array.isArray(constraint.align) ? "align" : Array.isArray(constraint.equal) ? "equal" : null;
|
|
1033
|
+
if (!key) return [constraint];
|
|
1034
|
+
const ids = constraint[key].filter(id => candidates.has(id));
|
|
1035
|
+
return ids.length >= 2 ? [{ ...constraint, [key]: ids }] : [];
|
|
1036
|
+
});
|
|
1037
|
+
if (!owner.constraints.length) delete owner.constraints;
|
|
1038
|
+
}
|
|
1039
|
+
for (const entity of ownerId === null ? source.entities : owner.children || []) pruneOwner(entity, entity.id);
|
|
1040
|
+
}
|
|
1041
|
+
pruneOwner(source, null);
|
|
1042
|
+
return source;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export function compileAtlasProjection(input, visibleIds, options = {}) {
|
|
1046
|
+
return compileAtlasModel(projectedAtlasSource(input, visibleIds), options);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
967
1049
|
export function compileAtlasModel(input, options = {}) {
|
|
968
1050
|
const diagnostics = [];
|
|
969
1051
|
const source = normalizeRoot(input);
|
package/src/spec.js
CHANGED
|
@@ -30,6 +30,7 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
30
30
|
name: field("Visible title.", null, { type: "string" }),
|
|
31
31
|
sub: field("Visible subtitle.", null, { type: "string" }),
|
|
32
32
|
text: field("Description shown by the inspector.", null, { type: "string" }),
|
|
33
|
+
url: field("External documentation opened from the inspector.", null, { type: "url" }),
|
|
33
34
|
type: field("Structural role. Use container when declaring children; omit it for a leaf item.", ["container"]),
|
|
34
35
|
parent: field("Parent entity id.", null, { type: "string", authoring: "absolute" }),
|
|
35
36
|
children: field("Nested entities.", null, { context: "entity", sequence: true }),
|
|
@@ -56,6 +57,8 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
56
57
|
from: field("Source entity id.", null, { type: "string", required: true }),
|
|
57
58
|
to: field("Target entity id.", null, { type: "string", required: true }),
|
|
58
59
|
label: field("Text shown on the relation.", null, { type: "string" }),
|
|
60
|
+
text: field("Description shown by the inspector.", null, { type: "string" }),
|
|
61
|
+
url: field("External documentation opened from the inspector.", null, { type: "url" }),
|
|
59
62
|
mode: field("Canonical visual relation mode.", ["directed", "bidirectional", "broken"]),
|
|
60
63
|
visual: field("Historical alias for relation mode.", ["directed", "bidirectional", "broken"]),
|
|
61
64
|
fromAnchor: field("Named source anchor.", null, { type: "string" }),
|
|
@@ -69,7 +72,7 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
69
72
|
properties: freeze({
|
|
70
73
|
algorithm: field("Spatial strategy.", ["row", "grid", "volume", "graph"]),
|
|
71
74
|
direction: field("Primary semantic axis.", ["x", "y", "z"]),
|
|
72
|
-
variant: field("Regular or staggered
|
|
75
|
+
variant: field("Regular cells or masonry rows staggered by half a cell by default.", ["uniform", "masonry"]),
|
|
73
76
|
plane: field("Axes used by a two-dimensional grid.", ["xz", "xy", "yz"]),
|
|
74
77
|
packing: field("Preserve initial slots or compact free space after constraints.", ["preserve", "compact"]),
|
|
75
78
|
groups: field("Invisible sibling groups used only during layout.", null, { context: "layoutGroup", sequence: true }),
|
|
@@ -79,7 +82,7 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
79
82
|
gap: field("Spacing between children.", null, { type: "number" }),
|
|
80
83
|
padding: field("Spacing to the container boundary.", null, { type: "number" }),
|
|
81
84
|
justify: field("Row alignment or distribution within an assigned size.", ["start", "center", "end", "space-between", "space-around", "space-evenly"]),
|
|
82
|
-
stagger: field("Optional masonry offset.", null, { context: "stagger" }),
|
|
85
|
+
stagger: field("Optional override for the masonry row offset.", null, { context: "stagger" }),
|
|
83
86
|
constraints: field("Equal-size and edge-alignment rules for this layout's siblings.", null, { context: "constraint", sequence: true })
|
|
84
87
|
})
|
|
85
88
|
}),
|
|
@@ -137,9 +140,19 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
137
140
|
selectionMode: field("Selection ownership.", ["internal", "event"]),
|
|
138
141
|
toolbar: field("Built-in toolbar visibility.", [true, false]),
|
|
139
142
|
shellLabels: field("Text-label contrast outline.", [true, false]),
|
|
143
|
+
focus: field("Selection-driven visual filtering.", null, { context: "focus" }),
|
|
140
144
|
camera: field("Initial camera snapshot in model-space coordinates.", null, { context: "camera" })
|
|
141
145
|
})
|
|
142
146
|
}),
|
|
147
|
+
focus: freeze({
|
|
148
|
+
label: "Visual focus",
|
|
149
|
+
properties: freeze({
|
|
150
|
+
mode: field("Entity set retained by the visual filter.", ["all", "connected"]),
|
|
151
|
+
effect: field("Treatment of entities outside the focused set.", ["dim", "hide"]),
|
|
152
|
+
layout: field("Keep spatial memory or reflow the focused projection.", ["preserve", "reflow"]),
|
|
153
|
+
opacity: field("Opacity applied to dimmed entities.", null, { type: "number" })
|
|
154
|
+
})
|
|
155
|
+
}),
|
|
143
156
|
camera: freeze({
|
|
144
157
|
label: "Initial camera",
|
|
145
158
|
properties: freeze({
|
|
@@ -159,7 +172,7 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
|
|
|
159
172
|
stagger: freeze({
|
|
160
173
|
label: "Stagger",
|
|
161
174
|
properties: freeze({
|
|
162
|
-
offset: field("Masonry row offset.", ["half-cell"], { type: "string" })
|
|
175
|
+
offset: field("Masonry row offset; defaults to half-cell.", ["half-cell"], { type: "string" })
|
|
163
176
|
})
|
|
164
177
|
}),
|
|
165
178
|
label: freeze({ label: "Label appearance", properties: freeze({ color: field("Label colour.", null, { valueSource: "palette" }), opacity: field("Label opacity.", null, { type: "number" }) }) }),
|
|
@@ -186,6 +199,7 @@ export function atlasLayoutContextForPath(path = []) {
|
|
|
186
199
|
["palette", "palette"],
|
|
187
200
|
["theme", "theme"],
|
|
188
201
|
["render", "render"],
|
|
202
|
+
["focus", "focus"],
|
|
189
203
|
["camera", "camera"],
|
|
190
204
|
["span", "span"],
|
|
191
205
|
["stagger", "stagger"],
|