@oxeo/visual 0.0.0-stage → 0.2.0

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.
Files changed (48) hide show
  1. package/README.md +99 -2
  2. package/bin/oxeo-visual.js +2 -0
  3. package/dist/build.d.ts +23 -0
  4. package/dist/build.js +58 -0
  5. package/dist/cli/build-command.d.ts +5 -0
  6. package/dist/cli/build-command.js +57 -0
  7. package/dist/cli/dev.d.ts +6 -0
  8. package/dist/cli/dev.js +274 -0
  9. package/dist/cli/index.d.ts +1 -0
  10. package/dist/cli/index.js +104 -0
  11. package/dist/cli/open.d.ts +1 -0
  12. package/dist/cli/open.js +21 -0
  13. package/dist/cli/output.d.ts +4 -0
  14. package/dist/cli/output.js +13 -0
  15. package/dist/cli/run.d.ts +7 -0
  16. package/dist/cli/run.js +55 -0
  17. package/dist/cli/worker.d.ts +20 -0
  18. package/dist/cli/worker.js +94 -0
  19. package/dist/config.d.ts +33 -0
  20. package/dist/config.js +26 -0
  21. package/dist/extract.d.ts +8 -0
  22. package/dist/extract.js +300 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +24 -0
  25. package/dist/layout/cards.d.ts +43 -0
  26. package/dist/layout/cards.js +121 -0
  27. package/dist/layout/place.d.ts +40 -0
  28. package/dist/layout/place.js +197 -0
  29. package/dist/layout/route.d.ts +30 -0
  30. package/dist/layout/route.js +192 -0
  31. package/dist/model.d.ts +60 -0
  32. package/dist/model.js +2 -0
  33. package/dist/project.d.ts +14 -0
  34. package/dist/project.js +11 -0
  35. package/dist/render/client.js +871 -0
  36. package/dist/render/html.d.ts +8 -0
  37. package/dist/render/html.js +211 -0
  38. package/dist/render/palette.d.ts +4 -0
  39. package/dist/render/palette.js +66 -0
  40. package/dist/render/svg.d.ts +20 -0
  41. package/dist/render/svg.js +222 -0
  42. package/dist/scheme-meta.d.ts +90 -0
  43. package/dist/scheme-meta.js +2 -0
  44. package/dist/text.d.ts +2 -0
  45. package/dist/text.js +12 -0
  46. package/dist/types.d.ts +15 -0
  47. package/dist/types.js +143 -0
  48. package/package.json +36 -3
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relationRows = exports.rowCenter = exports.buildCard = exports.ownFieldsFirst = exports.textInset = exports.boxInset = exports.rowLevel = exports.textWidth = exports.BOX_PAD_AFTER = exports.BOX_GAP_BEFORE = exports.BOX_STEP = exports.HEADER_FONT = exports.ROW_FONT = exports.MONO_ADVANCE = void 0;
4
+ exports.MONO_ADVANCE = 0.6;
5
+ exports.ROW_FONT = 11;
6
+ exports.HEADER_FONT = 13;
7
+ exports.BOX_STEP = 6;
8
+ exports.BOX_GAP_BEFORE = 5;
9
+ exports.BOX_PAD_AFTER = 6;
10
+ const textWidth = (text, size) => Math.ceil([...text].length * size * exports.MONO_ADVANCE);
11
+ exports.textWidth = textWidth;
12
+ const rowLevel = (row) => (row.kind === 'block' ? row.depth + 1 : row.depth);
13
+ exports.rowLevel = rowLevel;
14
+ const boxInset = (level) => level * exports.BOX_STEP;
15
+ exports.boxInset = boxInset;
16
+ const textInset = (level, spacing) => level === 0 ? spacing.cardPadX : (0, exports.boxInset)(level) + 8;
17
+ exports.textInset = textInset;
18
+ const truncate = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
19
+ const detailOf = (field) => {
20
+ switch (field.kind) {
21
+ case 'id':
22
+ return `PK · ${field.type}`;
23
+ case 'ref':
24
+ return `→ ${field.target}`;
25
+ case 'set':
26
+ return `⇉ ${field.target}[]`;
27
+ case 'own':
28
+ return `◆ ${field.target}[]`;
29
+ case 'owner':
30
+ return `◆ ${field.target}`;
31
+ case 'embedded':
32
+ return field.type ?? 'embedded';
33
+ }
34
+ if (field.values?.length)
35
+ return truncate(field.values.join(' | '), 34);
36
+ const ts = field.tsType?.replace(/\s*\|\s*null$/, '');
37
+ return truncate(ts && ts !== field.type ? ts : (field.type ?? ''), 34);
38
+ };
39
+ const ownFieldsFirst = (fields) => [
40
+ ...fields.filter((f) => f.kind !== 'embedded'),
41
+ ...fields.filter((f) => f.kind === 'embedded')
42
+ ];
43
+ exports.ownFieldsFirst = ownFieldsFirst;
44
+ const buildCard = (entity, spacing) => {
45
+ const rows = [];
46
+ let y = spacing.cardHeader;
47
+ const push = (row) => {
48
+ rows.push({ ...row, y, h: spacing.cardRow });
49
+ y += spacing.cardRow;
50
+ };
51
+ const walk = (fields, depth, variant) => {
52
+ for (const field of (0, exports.ownFieldsFirst)(fields)) {
53
+ const row = {
54
+ path: field.path,
55
+ label: field.name,
56
+ detail: detailOf(field),
57
+ kind: field.kind === 'embedded' ? 'block' : field.kind,
58
+ depth,
59
+ target: field.target,
60
+ onDelete: field.onDelete,
61
+ nullable: field.nullable,
62
+ variant
63
+ };
64
+ if (field.kind !== 'embedded') {
65
+ push(row);
66
+ continue;
67
+ }
68
+ y += exports.BOX_GAP_BEFORE;
69
+ const index = rows.length;
70
+ push(row);
71
+ walk(field.fields ?? [], depth + 1, variant);
72
+ y += exports.BOX_PAD_AFTER;
73
+ rows[index].boxBottom = y;
74
+ }
75
+ };
76
+ walk(entity.fields, 0);
77
+ for (const v of entity.variants) {
78
+ y += exports.BOX_GAP_BEFORE;
79
+ push({
80
+ path: `@${v.value}`,
81
+ label: `${entity.discriminatorKey} = ${v.value}`,
82
+ detail: v.model,
83
+ kind: 'variant',
84
+ depth: 0,
85
+ variant: v.value
86
+ });
87
+ walk(v.fields, 0, v.value);
88
+ }
89
+ const rowWidth = (row) => (0, exports.textInset)((0, exports.rowLevel)(row), spacing) * 2 +
90
+ (0, exports.textWidth)(row.label + (row.nullable ? '?' : ''), exports.ROW_FONT) +
91
+ 18 +
92
+ (0, exports.textWidth)(row.detail, exports.ROW_FONT);
93
+ const width = Math.max(spacing.cardMinWidth, (0, exports.textWidth)(entity.key, exports.HEADER_FONT) + 40, ...rows.map(rowWidth));
94
+ return {
95
+ key: entity.key,
96
+ entity,
97
+ rows,
98
+ width: Math.ceil(width / 2) * 2,
99
+ height: y + 4
100
+ };
101
+ };
102
+ exports.buildCard = buildCard;
103
+ const rowCenter = (card, path, spacing) => {
104
+ const row = card.rows.find((r) => r.path === path);
105
+ return row ? row.y + row.h / 2 : spacing.cardHeader + spacing.cardRow / 2;
106
+ };
107
+ exports.rowCenter = rowCenter;
108
+ const relationRows = (relation, cards) => {
109
+ const source = cards.get(relation.source);
110
+ const target = cards.get(relation.target);
111
+ if (relation.kind === 'own') {
112
+ return {
113
+ source,
114
+ sourcePath: relation.field,
115
+ target,
116
+ targetPath: relation.ownerField ?? target.entity.idKey
117
+ };
118
+ }
119
+ return { source, sourcePath: relation.field, target, targetPath: target.entity.idKey };
120
+ };
121
+ exports.relationRows = relationRows;
@@ -0,0 +1,40 @@
1
+ import type { Spacing, VisualConfig, ZoneConfig } from '../config';
2
+ import type { SchemeModel } from '../model';
3
+ import { type Card } from './cards';
4
+ export interface Box {
5
+ x: number;
6
+ y: number;
7
+ w: number;
8
+ h: number;
9
+ }
10
+ export interface PlacedCard extends Box {
11
+ card: Card;
12
+ zone: string;
13
+ }
14
+ export interface FileBox extends Box {
15
+ file: string;
16
+ namespace?: string;
17
+ zone: string;
18
+ }
19
+ export interface ZoneBox extends Box {
20
+ zone: ZoneConfig;
21
+ meta: string;
22
+ }
23
+ export interface LabelBox extends Box {
24
+ kind: 'zone' | 'file';
25
+ }
26
+ export interface Placement {
27
+ width: number;
28
+ height: number;
29
+ cards: Map<string, PlacedCard>;
30
+ files: FileBox[];
31
+ zones: ZoneBox[];
32
+ labels: LabelBox[];
33
+ }
34
+ export declare const FALLBACK_ZONE = "?";
35
+ export declare const ZONE_TITLE_FONT = 20;
36
+ export declare const ZONE_META_FONT = 12;
37
+ export declare const FILE_LABEL_FONT = 11;
38
+ export declare const fileLabel: (file: string, namespace?: string) => string;
39
+ export declare const uiTextWidth: (text: string, size: number) => number;
40
+ export declare const place: (model: SchemeModel, cards: Map<string, Card>, config: VisualConfig, spacing: Spacing) => Placement;
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.place = exports.uiTextWidth = exports.fileLabel = exports.FILE_LABEL_FONT = exports.ZONE_META_FONT = exports.ZONE_TITLE_FONT = exports.FALLBACK_ZONE = void 0;
4
+ const text_1 = require("../text");
5
+ const cards_1 = require("./cards");
6
+ exports.FALLBACK_ZONE = '?';
7
+ exports.ZONE_TITLE_FONT = 20;
8
+ exports.ZONE_META_FONT = 12;
9
+ exports.FILE_LABEL_FONT = 11;
10
+ const fileLabel = (file, namespace) => namespace ? `${file} · ${namespace}` : file;
11
+ exports.fileLabel = fileLabel;
12
+ const uiTextWidth = (text, size) => Math.ceil([...text].length * size * 0.62);
13
+ exports.uiTextWidth = uiTextWidth;
14
+ const offset = (block, dx, dy) => block.items.map((item) => ({ key: item.key, x: item.x + dx, y: item.y + dy }));
15
+ const layoutFile = (keys, model, cards, spacing, aspect) => {
16
+ const inFile = new Set(keys);
17
+ const children = new Map();
18
+ const owned = new Set();
19
+ for (const relation of model.relations) {
20
+ if (relation.kind !== 'own' || !inFile.has(relation.source) || !inFile.has(relation.target))
21
+ continue;
22
+ if (owned.has(relation.target) || relation.source === relation.target)
23
+ continue;
24
+ owned.add(relation.target);
25
+ children.set(relation.source, [...(children.get(relation.source) ?? []), relation.target]);
26
+ }
27
+ const tree = (key) => {
28
+ const card = cards.get(key);
29
+ const kids = (children.get(key) ?? []).map(tree);
30
+ if (!kids.length)
31
+ return { w: card.width, h: card.height, items: [{ key, x: 0, y: 0 }] };
32
+ const x0 = card.width + spacing.cardGapX;
33
+ const items = [{ key, x: 0, y: 0 }];
34
+ let y = 0;
35
+ for (const kid of kids) {
36
+ items.push(...offset(kid, x0, y));
37
+ y += kid.h + spacing.cardGapY;
38
+ }
39
+ const kidsHeight = y - spacing.cardGapY;
40
+ return { w: x0 + Math.max(...kids.map((k) => k.w)), h: Math.max(card.height, kidsHeight), items };
41
+ };
42
+ const roots = keys.filter((key) => !owned.has(key)).map(tree);
43
+ const columns = (limit) => {
44
+ const placed = [];
45
+ let x = 0;
46
+ let y = 0;
47
+ let columnWidth = 0;
48
+ let width = 0;
49
+ let height = 0;
50
+ for (const block of roots) {
51
+ if (y > 0 && y + block.h > limit) {
52
+ x += columnWidth + spacing.cardGapX;
53
+ y = 0;
54
+ columnWidth = 0;
55
+ }
56
+ placed.push(...offset(block, x, y));
57
+ columnWidth = Math.max(columnWidth, block.w);
58
+ height = Math.max(height, y + block.h);
59
+ width = Math.max(width, x + block.w);
60
+ y += block.h + spacing.cardGapY;
61
+ }
62
+ return { w: width, h: height, items: placed };
63
+ };
64
+ const tallest = Math.max(...roots.map((b) => b.h));
65
+ const total = roots.reduce((sum, b) => sum + b.h + spacing.cardGapY, 0);
66
+ let best = null;
67
+ for (let limit = tallest; limit <= total + 1; limit += 20) {
68
+ const candidate = columns(limit);
69
+ const score = Math.abs(Math.log(candidate.w / candidate.h / aspect));
70
+ const bestScore = best ? Math.abs(Math.log(best.w / best.h / aspect)) : Infinity;
71
+ if (score < bestScore - 1e-6)
72
+ best = candidate;
73
+ }
74
+ return best ?? columns(total);
75
+ };
76
+ const place = (model, cards, config, spacing) => {
77
+ const byFile = new Map();
78
+ for (const entity of model.entities)
79
+ byFile.set(entity.file, [...(byFile.get(entity.file) ?? []), entity.key]);
80
+ const zones = config.zones.map((zone) => ({
81
+ ...zone,
82
+ files: zone.files.map((row) => row.filter((f) => byFile.has(f)))
83
+ }));
84
+ const assigned = new Set(zones.flatMap((z) => z.files.flat()));
85
+ const rest = model.files.map((f) => f.file).filter((file) => !assigned.has(file));
86
+ const bands = config.bands.map((band) => [...band]);
87
+ if (rest.length) {
88
+ console.warn(` ! файлы без зоны в конфиге: ${rest.join(', ')}`);
89
+ zones.push({ id: exports.FALLBACK_ZONE, title: 'Без зоны', color: '#6b6b76', files: [rest] });
90
+ bands.push([exports.FALLBACK_ZONE]);
91
+ }
92
+ const namespaceOf = new Map(model.files.map((f) => [f.file, f.namespace]));
93
+ const zoneLayouts = new Map(zones.map((zone) => {
94
+ const rows = zone.files
95
+ .filter((row) => row.length)
96
+ .map((row) => row.map((file) => {
97
+ const block = layoutFile(byFile.get(file), model, cards, spacing, 1.5);
98
+ const label = (0, exports.fileLabel)(file, namespaceOf.get(file));
99
+ const w = Math.max(block.w, (0, cards_1.textWidth)(label, exports.FILE_LABEL_FONT) + 8) + spacing.filePadX * 2;
100
+ const h = block.h + spacing.filePadTop + spacing.filePadBottom;
101
+ return { file, block, w, h };
102
+ }));
103
+ const rowWidth = (row) => row.reduce((s, f) => s + f.w, 0) + spacing.fileGap * (row.length - 1);
104
+ const contentW = Math.max(...rows.map(rowWidth), (0, exports.uiTextWidth)(`${zone.id} · ${zone.title}`, exports.ZONE_TITLE_FONT));
105
+ const contentH = rows.reduce((s, row) => s + Math.max(...row.map((f) => f.h)), 0) +
106
+ spacing.fileGap * (rows.length - 1);
107
+ return [
108
+ zone.id,
109
+ {
110
+ zone,
111
+ rows,
112
+ rowWidth,
113
+ w: contentW + spacing.zonePadX * 2,
114
+ h: contentH + spacing.zonePadTop + spacing.zonePadBottom
115
+ }
116
+ ];
117
+ }));
118
+ const bandWidth = (band) => band.reduce((s, id) => s + zoneLayouts.get(id).w, 0) + spacing.zoneGap * (band.length - 1);
119
+ const validBands = bands
120
+ .map((band) => band.filter((id) => zoneLayouts.has(id)))
121
+ .filter((band) => band.length);
122
+ const mapWidth = Math.max(...validBands.map(bandWidth));
123
+ const placement = {
124
+ width: 0,
125
+ height: 0,
126
+ cards: new Map(),
127
+ files: [],
128
+ zones: [],
129
+ labels: []
130
+ };
131
+ let y = spacing.margin;
132
+ for (const band of validBands) {
133
+ const natural = bandWidth(band);
134
+ const naturalZones = band.reduce((s, id) => s + zoneLayouts.get(id).w, 0);
135
+ const bandHeight = Math.max(...band.map((id) => zoneLayouts.get(id).h));
136
+ let x = spacing.margin;
137
+ for (const id of band) {
138
+ const layout = zoneLayouts.get(id);
139
+ const w = layout.w + ((mapWidth - natural) * layout.w) / naturalZones;
140
+ const files = layout.rows.flat();
141
+ const count = files.reduce((s, f) => s + f.block.items.length, 0);
142
+ const meta = `${(0, text_1.plural)(files.length, ['файл', 'файла', 'файлов'])} · ${(0, text_1.plural)(count, ['коллекция', 'коллекции', 'коллекций'])}`;
143
+ placement.zones.push({ zone: layout.zone, meta, x, y, w, h: bandHeight });
144
+ placement.labels.push({
145
+ kind: 'zone',
146
+ x: x + spacing.zonePadX - 6,
147
+ y: y + 12,
148
+ w: (0, exports.uiTextWidth)(`${layout.zone.id} · ${layout.zone.title}`, exports.ZONE_TITLE_FONT) +
149
+ 24 +
150
+ (0, exports.uiTextWidth)(meta, exports.ZONE_META_FONT),
151
+ h: 36
152
+ });
153
+ const contentWidth = Math.max(...layout.rows.map(layout.rowWidth));
154
+ let fy = y + spacing.zonePadTop;
155
+ for (const row of layout.rows) {
156
+ let fx = x + (w - contentWidth) / 2;
157
+ for (const file of row) {
158
+ placement.files.push({
159
+ file: file.file,
160
+ namespace: namespaceOf.get(file.file),
161
+ zone: id,
162
+ x: fx,
163
+ y: fy,
164
+ w: file.w,
165
+ h: file.h
166
+ });
167
+ placement.labels.push({
168
+ kind: 'file',
169
+ x: fx + spacing.filePadX - 4,
170
+ y: fy - 12,
171
+ w: (0, cards_1.textWidth)((0, exports.fileLabel)(file.file, namespaceOf.get(file.file)), exports.FILE_LABEL_FONT) + 8,
172
+ h: 38
173
+ });
174
+ for (const item of file.block.items) {
175
+ const card = cards.get(item.key);
176
+ placement.cards.set(item.key, {
177
+ card,
178
+ zone: id,
179
+ x: fx + spacing.filePadX + item.x,
180
+ y: fy + spacing.filePadTop + item.y,
181
+ w: card.width,
182
+ h: card.height
183
+ });
184
+ }
185
+ fx += file.w + spacing.fileGap;
186
+ }
187
+ fy += Math.max(...row.map((f) => f.h)) + spacing.fileGap;
188
+ }
189
+ x += w + spacing.zoneGap;
190
+ }
191
+ y += bandHeight + spacing.bandGap;
192
+ }
193
+ placement.width = mapWidth + spacing.margin * 2;
194
+ placement.height = y - spacing.bandGap + spacing.margin;
195
+ return placement;
196
+ };
197
+ exports.place = place;
@@ -0,0 +1,30 @@
1
+ import type { Spacing } from '../config';
2
+ import type { Relation } from '../model';
3
+ import { type Card } from './cards';
4
+ import type { Placement } from './place';
5
+ export type Point = [number, number];
6
+ export interface Route {
7
+ relation: Relation;
8
+ points: Point[];
9
+ }
10
+ export interface RoutingMetrics {
11
+ routes: number;
12
+ failed: number;
13
+ hugging: number;
14
+ cleared: number;
15
+ cardHits: number;
16
+ crossings: number;
17
+ length: number;
18
+ ms: number;
19
+ }
20
+ export interface RoutingOptions {
21
+ shapeBuffer: number;
22
+ nudging: number;
23
+ segmentPenalty: number;
24
+ crossingPenalty: number;
25
+ }
26
+ export declare const DEFAULT_ROUTING: RoutingOptions;
27
+ export declare const routeRelations: (relations: Relation[], placement: Placement, cards: Map<string, Card>, spacing: Spacing, options?: RoutingOptions) => Promise<{
28
+ routes: Route[];
29
+ metrics: RoutingMetrics;
30
+ }>;
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routeRelations = exports.DEFAULT_ROUTING = void 0;
4
+ const cards_1 = require("./cards");
5
+ exports.DEFAULT_ROUTING = {
6
+ shapeBuffer: 10,
7
+ nudging: 6,
8
+ segmentPenalty: 60,
9
+ crossingPenalty: 80
10
+ };
11
+ const importEsm = new Function('specifier', 'return import(specifier)');
12
+ let avoidPromise = null;
13
+ const loadAvoid = () => {
14
+ avoidPromise ??= importEsm('libavoid-js').then(async ({ AvoidLib }) => {
15
+ await AvoidLib.load();
16
+ return AvoidLib.getInstance();
17
+ });
18
+ return avoidPromise;
19
+ };
20
+ const segmentsOf = (points) => points.slice(1).map((p, i) => [points[i], p]);
21
+ const hitsBox = ([a, b], box) => {
22
+ const inset = 1;
23
+ const x1 = Math.min(a[0], b[0]);
24
+ const x2 = Math.max(a[0], b[0]);
25
+ const y1 = Math.min(a[1], b[1]);
26
+ const y2 = Math.max(a[1], b[1]);
27
+ return (x2 > box.x + inset && x1 < box.x + box.w - inset && y2 > box.y + inset && y1 < box.y + box.h - inset);
28
+ };
29
+ const crosses = ([a, b], [c, d]) => {
30
+ const horizontal = a[1] === b[1];
31
+ const otherHorizontal = c[1] === d[1];
32
+ if (horizontal === otherHorizontal)
33
+ return false;
34
+ const [h1, h2, v1, v2] = horizontal ? [a, b, c, d] : [c, d, a, b];
35
+ const x = v1[0];
36
+ const y = h1[1];
37
+ const inside = (value, p, q) => value > Math.min(p, q) + 0.5 && value < Math.max(p, q) - 0.5;
38
+ return inside(x, h1[0], h2[0]) && inside(y, v1[1], v2[1]);
39
+ };
40
+ const BORDER_GAP = 8;
41
+ const overlap = (a1, a2, b1, b2) => Math.min(Math.max(a1, a2), Math.max(b1, b2)) - Math.max(Math.min(a1, a2), Math.min(b1, b2));
42
+ const bordersOf = (placement) => [...placement.zones, ...placement.files].flatMap((b) => [
43
+ { horizontal: true, at: b.y, from: b.x, to: b.x + b.w },
44
+ { horizontal: true, at: b.y + b.h, from: b.x, to: b.x + b.w },
45
+ { horizontal: false, at: b.x, from: b.y, to: b.y + b.h },
46
+ { horizontal: false, at: b.x + b.w, from: b.y, to: b.y + b.h }
47
+ ]);
48
+ const clearBorders = (routes, placement, nudging) => {
49
+ const borders = bordersOf(placement);
50
+ const boxes = [...placement.cards.values()];
51
+ const huggingBorder = (horizontal, at, from, to) => borders.find((b) => b.horizontal === horizontal &&
52
+ Math.abs(b.at - at) < BORDER_GAP - 0.5 &&
53
+ overlap(from, to, b.from, b.to) > 10);
54
+ const tooClose = (route, horizontal, at, from, to) => routes.some((other) => other !== route &&
55
+ segmentsOf(other.points).some(([a, b]) => {
56
+ const parallel = horizontal ? Math.abs(a[1] - b[1]) < 0.5 : Math.abs(a[0] - b[0]) < 0.5;
57
+ if (!parallel)
58
+ return false;
59
+ const otherAt = horizontal ? a[1] : a[0];
60
+ const range = horizontal ? overlap(from, to, a[0], b[0]) : overlap(from, to, a[1], b[1]);
61
+ return Math.abs(otherAt - at) < nudging - 1 && range > 0;
62
+ }));
63
+ let hugging = 0;
64
+ let cleared = 0;
65
+ for (const route of routes) {
66
+ const p = route.points;
67
+ for (let i = 1; i < p.length - 2; i++) {
68
+ const [a, b] = [p[i], p[i + 1]];
69
+ const horizontal = Math.abs(a[1] - b[1]) < 0.5;
70
+ if (!horizontal && Math.abs(a[0] - b[0]) >= 0.5)
71
+ continue;
72
+ const axis = horizontal ? 1 : 0;
73
+ const at = a[axis];
74
+ const from = horizontal ? a[0] : a[1];
75
+ const to = horizontal ? b[0] : b[1];
76
+ const border = huggingBorder(horizontal, at, from, to);
77
+ if (!border)
78
+ continue;
79
+ hugging++;
80
+ const side = at >= border.at ? 1 : -1;
81
+ for (const target of [border.at + side * BORDER_GAP, border.at - side * BORDER_GAP]) {
82
+ const keeps = (q, edge) => Math.sign(at - q[axis]) === Math.sign(target - q[axis]) &&
83
+ Math.abs(target - q[axis]) >= (edge ? 14 : 4);
84
+ if (!keeps(p[i - 1], i === 1) || !keeps(p[i + 2], i + 2 === p.length - 1))
85
+ continue;
86
+ const moved = (q) => (horizontal ? [q[0], target] : [target, q[1]]);
87
+ const changed = [
88
+ [p[i - 1], moved(a)],
89
+ [moved(a), moved(b)],
90
+ [moved(b), p[i + 2]]
91
+ ];
92
+ if (changed.some((segment) => boxes.some((box) => hitsBox(segment, box))))
93
+ continue;
94
+ if (huggingBorder(horizontal, target, from, to) ||
95
+ tooClose(route, horizontal, target, from, to))
96
+ continue;
97
+ p[i] = moved(a);
98
+ p[i + 1] = moved(b);
99
+ cleared++;
100
+ break;
101
+ }
102
+ }
103
+ }
104
+ return { hugging, cleared };
105
+ };
106
+ const routeRelations = async (relations, placement, cards, spacing, options = exports.DEFAULT_ROUTING) => {
107
+ const started = Date.now();
108
+ const A = await loadAvoid();
109
+ const router = new A.Router(A.OrthogonalRouting);
110
+ router.setRoutingParameter(A.shapeBufferDistance, options.shapeBuffer);
111
+ router.setRoutingParameter(A.idealNudgingDistance, options.nudging);
112
+ router.setRoutingParameter(A.segmentPenalty, options.segmentPenalty);
113
+ if (options.crossingPenalty > 0)
114
+ router.setRoutingParameter(A.crossingPenalty, options.crossingPenalty);
115
+ router.setRoutingOption(A.nudgeOrthogonalSegmentsConnectedToShapes, false);
116
+ router.setRoutingOption(A.nudgeSharedPathsWithCommonEndPoint, true);
117
+ router.setRoutingOption(A.nudgeOrthogonalTouchingColinearSegments, true);
118
+ const rect = (box) => new A.ShapeRef(router, new A.Rectangle(new A.Point(box.x, box.y), new A.Point(box.x + box.w, box.y + box.h)));
119
+ const shapes = new Map();
120
+ for (const [key, placed] of placement.cards)
121
+ shapes.set(key, rect(placed));
122
+ for (const label of placement.labels)
123
+ rect(label);
124
+ const pinClasses = new Map();
125
+ const pinFor = (key, path, role) => {
126
+ const id = `${key}|${path}|${role}`;
127
+ const known = pinClasses.get(id);
128
+ if (known)
129
+ return known;
130
+ const classId = pinClasses.size + 1;
131
+ pinClasses.set(id, classId);
132
+ const card = cards.get(key);
133
+ const shape = shapes.get(key);
134
+ const y = (0, cards_1.rowCenter)(card, path, spacing) / card.height;
135
+ const pin = (x, py, dir) => new A.ShapeConnectionPin(shape, classId, x, py, true, 0, dir).setExclusive(false);
136
+ pin(0, y, A.ConnDirLeft);
137
+ pin(1, y, A.ConnDirRight);
138
+ if (role === 'key') {
139
+ pin(0.5, 0, A.ConnDirUp);
140
+ pin(0.5, 1, A.ConnDirDown);
141
+ }
142
+ return classId;
143
+ };
144
+ const connectors = relations
145
+ .filter((r) => placement.cards.has(r.source) && placement.cards.has(r.target))
146
+ .map((relation) => {
147
+ const { source, sourcePath, target, targetPath } = (0, cards_1.relationRows)(relation, cards);
148
+ const from = pinFor(source.key, sourcePath, 'out');
149
+ const to = pinFor(target.key, targetPath, relation.kind === 'own' ? 'in' : 'key');
150
+ const conn = new A.ConnRef(router, new A.ConnEnd(shapes.get(source.key), from), new A.ConnEnd(shapes.get(target.key), to));
151
+ return { relation, conn };
152
+ });
153
+ router.processTransaction();
154
+ const routes = connectors.map(({ relation, conn }) => {
155
+ const line = conn.displayRoute();
156
+ const points = [];
157
+ for (let i = 0; i < line.size(); i++) {
158
+ const p = line.get_ps(i);
159
+ points.push([Math.round(p.x * 10) / 10, Math.round(p.y * 10) / 10]);
160
+ }
161
+ return { relation, points };
162
+ });
163
+ const { hugging, cleared } = clearBorders(routes, placement, options.nudging);
164
+ const boxes = [...placement.cards.values()];
165
+ const segments = routes.map((r) => segmentsOf(r.points));
166
+ let cardHits = 0;
167
+ let crossings = 0;
168
+ segments.forEach((list, i) => {
169
+ for (const segment of list)
170
+ cardHits += boxes.filter((box) => hitsBox(segment, box)).length;
171
+ for (let j = i + 1; j < segments.length; j++) {
172
+ for (const a of list)
173
+ for (const b of segments[j])
174
+ if (crosses(a, b))
175
+ crossings++;
176
+ }
177
+ });
178
+ return {
179
+ routes,
180
+ metrics: {
181
+ routes: routes.length,
182
+ failed: routes.filter((r) => r.points.length < 2).length,
183
+ hugging,
184
+ cleared,
185
+ cardHits,
186
+ crossings,
187
+ length: Math.round(segments.flat().reduce((s, [a, b]) => s + Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]), 0)),
188
+ ms: Date.now() - started
189
+ }
190
+ };
191
+ };
192
+ exports.routeRelations = routeRelations;
@@ -0,0 +1,60 @@
1
+ export type OnDelete = 'cascade' | 'restrict' | 'set-null' | 'unlink';
2
+ export type FieldKind = 'id' | 'prop' | 'embedded' | 'ref' | 'set' | 'own' | 'owner';
3
+ export interface Field {
4
+ name: string;
5
+ path: string;
6
+ kind: FieldKind;
7
+ type?: string;
8
+ tsType?: string;
9
+ decorators?: string[];
10
+ typeRefs?: string[];
11
+ nullable?: boolean;
12
+ values?: (string | number)[];
13
+ target?: string;
14
+ onDelete?: OnDelete;
15
+ ownerField?: string;
16
+ doc?: string;
17
+ fields?: Field[];
18
+ }
19
+ export interface Variant {
20
+ value: string;
21
+ model: string;
22
+ doc?: string;
23
+ decorators?: string[];
24
+ fields: Field[];
25
+ }
26
+ export interface Entity {
27
+ key: string;
28
+ model: string;
29
+ file: string;
30
+ namespace?: string;
31
+ order: number;
32
+ idKey: string;
33
+ idType: string;
34
+ discriminatorKey?: string;
35
+ doc?: string;
36
+ decorators?: string[];
37
+ fields: Field[];
38
+ variants: Variant[];
39
+ }
40
+ export type RelationKind = 'own' | 'ref' | 'set';
41
+ export interface Relation {
42
+ id: string;
43
+ source: string;
44
+ target: string;
45
+ kind: RelationKind;
46
+ field: string;
47
+ ownerField?: string;
48
+ onDelete: OnDelete;
49
+ nullable: boolean;
50
+ variant?: string;
51
+ }
52
+ export interface SchemeModel {
53
+ entities: Entity[];
54
+ relations: Relation[];
55
+ files: {
56
+ file: string;
57
+ namespace?: string;
58
+ }[];
59
+ types: Record<string, string>;
60
+ }
package/dist/model.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ import type { VisualConfig } from './config';
2
+ import type { SchemeSource } from './scheme-meta';
3
+ export interface OxeoVisualConfig extends VisualConfig {
4
+ scheme: SchemeSource;
5
+ sourceDir: string;
6
+ tsconfig?: string;
7
+ watch?: string[];
8
+ output?: {
9
+ dir?: string;
10
+ name?: string;
11
+ };
12
+ }
13
+ export declare const defineConfig: (config: OxeoVisualConfig) => OxeoVisualConfig;
14
+ export declare const CONFIG_FILES: string[];
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONFIG_FILES = exports.defineConfig = void 0;
4
+ const defineConfig = (config) => config;
5
+ exports.defineConfig = defineConfig;
6
+ exports.CONFIG_FILES = [
7
+ 'oxeo-visual.config.ts',
8
+ 'oxeo-visual.config.cts',
9
+ 'oxeo-visual.config.js',
10
+ 'oxeo-visual.config.cjs'
11
+ ];