@liquidcars/atlas-layout 0.1.8 → 0.1.9

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
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas-layout",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Declarative layout compiler for LiquidCars Atlas models",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -151,7 +151,12 @@ Optional fields include id, label, fromAnchor, toAnchor, priority and layout.
151
151
 
152
152
  palette maps tokens to CSS colours. style.color or c can reference a token or direct
153
153
  CSS colour. render can additionally set relationMode, selectionMode, toolbar,
154
- shellLabels (the text-label contrast outline) and camera. An initial camera snapshot contains model-space
154
+ shellLabels (the text-label contrast outline), focus and camera. `render.focus`
155
+ accepts `mode: all|connected`, `effect: dim|hide`, `layout: preserve|reflow`
156
+ and an optional dim `opacity`. Connected focus retains the selection, its directly
157
+ related entities and the ancestor containers required to preserve context. Preserve
158
+ keeps spatial memory; reflow compiles a temporary projection and never mutates the
159
+ source model. An initial camera snapshot contains model-space
155
160
  position and target vectors plus a positive, dimensionless zoom value. Position
156
161
  and target determine the viewing direction; zoom preserves the intended framing
157
162
  across viewport sizes. theme contains background, lighting and ui settings.
package/src/index.js CHANGED
@@ -964,6 +964,81 @@ function normalizeRoot(input) {
964
964
  return input || {};
965
965
  }
966
966
 
967
+ function projectedAtlasSource(input, visibleIds) {
968
+ const source = structuredClone(normalizeRoot(input));
969
+ const requested = new Set(Array.from(visibleIds || []).filter(Boolean));
970
+ const records = new Map();
971
+
972
+ function collect(items, nestedParent = null) {
973
+ for (const entity of Array.isArray(items) ? items : []) {
974
+ if (!entity?.id) continue;
975
+ const parent = nestedParent ?? entity.parent ?? null;
976
+ records.set(entity.id, { entity, parent });
977
+ collect(entity.children, entity.id);
978
+ }
979
+ }
980
+ collect(source.entities);
981
+ for (const id of [...requested]) {
982
+ let parent = records.get(id)?.parent || null;
983
+ while (parent && !requested.has(parent)) {
984
+ requested.add(parent);
985
+ parent = records.get(parent)?.parent || null;
986
+ }
987
+ }
988
+
989
+ function filterEntities(items) {
990
+ return (Array.isArray(items) ? items : []).flatMap(entity => {
991
+ if (!entity?.id || !requested.has(entity.id)) return [];
992
+ const copy = { ...entity };
993
+ if (Array.isArray(entity.children)) copy.children = filterEntities(entity.children);
994
+ return [copy];
995
+ });
996
+ }
997
+ source.entities = filterEntities(source.entities);
998
+ source.relations = (source.relations || []).filter(relation => requested.has(relation.from) && requested.has(relation.to));
999
+
1000
+ const childrenByParent = new Map();
1001
+ function index(items, nestedParent = null) {
1002
+ for (const entity of items || []) {
1003
+ const parent = nestedParent ?? entity.parent ?? null;
1004
+ if (!childrenByParent.has(parent)) childrenByParent.set(parent, []);
1005
+ childrenByParent.get(parent).push(entity.id);
1006
+ index(entity.children, entity.id);
1007
+ }
1008
+ }
1009
+ index(source.entities);
1010
+
1011
+ function pruneOwner(owner, ownerId = null) {
1012
+ const siblingIds = new Set(childrenByParent.get(ownerId) || []);
1013
+ const layout = owner.layout ? { ...owner.layout } : null;
1014
+ if (layout?.groups) {
1015
+ layout.groups = layout.groups.flatMap(group => {
1016
+ const members = (group.members || []).filter(id => siblingIds.has(id));
1017
+ return members.length ? [{ ...group, members }] : [];
1018
+ });
1019
+ if (!layout.groups.length) delete layout.groups;
1020
+ owner.layout = layout;
1021
+ }
1022
+ const candidates = new Set([...siblingIds, ...(layout?.groups || []).map(group => group.id)]);
1023
+ if (Array.isArray(owner.constraints)) {
1024
+ owner.constraints = owner.constraints.flatMap(constraint => {
1025
+ const key = Array.isArray(constraint.align) ? "align" : Array.isArray(constraint.equal) ? "equal" : null;
1026
+ if (!key) return [constraint];
1027
+ const ids = constraint[key].filter(id => candidates.has(id));
1028
+ return ids.length >= 2 ? [{ ...constraint, [key]: ids }] : [];
1029
+ });
1030
+ if (!owner.constraints.length) delete owner.constraints;
1031
+ }
1032
+ for (const entity of ownerId === null ? source.entities : owner.children || []) pruneOwner(entity, entity.id);
1033
+ }
1034
+ pruneOwner(source, null);
1035
+ return source;
1036
+ }
1037
+
1038
+ export function compileAtlasProjection(input, visibleIds, options = {}) {
1039
+ return compileAtlasModel(projectedAtlasSource(input, visibleIds), options);
1040
+ }
1041
+
967
1042
  export function compileAtlasModel(input, options = {}) {
968
1043
  const diagnostics = [];
969
1044
  const source = normalizeRoot(input);
package/src/spec.js CHANGED
@@ -137,9 +137,19 @@ export const ATLAS_LAYOUT_SPEC_V1 = freeze({
137
137
  selectionMode: field("Selection ownership.", ["internal", "event"]),
138
138
  toolbar: field("Built-in toolbar visibility.", [true, false]),
139
139
  shellLabels: field("Text-label contrast outline.", [true, false]),
140
+ focus: field("Selection-driven visual filtering.", null, { context: "focus" }),
140
141
  camera: field("Initial camera snapshot in model-space coordinates.", null, { context: "camera" })
141
142
  })
142
143
  }),
144
+ focus: freeze({
145
+ label: "Visual focus",
146
+ properties: freeze({
147
+ mode: field("Entity set retained by the visual filter.", ["all", "connected"]),
148
+ effect: field("Treatment of entities outside the focused set.", ["dim", "hide"]),
149
+ layout: field("Keep spatial memory or reflow the focused projection.", ["preserve", "reflow"]),
150
+ opacity: field("Opacity applied to dimmed entities.", null, { type: "number" })
151
+ })
152
+ }),
143
153
  camera: freeze({
144
154
  label: "Initial camera",
145
155
  properties: freeze({
@@ -186,6 +196,7 @@ export function atlasLayoutContextForPath(path = []) {
186
196
  ["palette", "palette"],
187
197
  ["theme", "theme"],
188
198
  ["render", "render"],
199
+ ["focus", "focus"],
189
200
  ["camera", "camera"],
190
201
  ["span", "span"],
191
202
  ["stagger", "stagger"],