@formicoidea/labre-framework-wardley 0.42.0 → 0.45.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.
- package/dist/commands.js +12 -6
- package/dist/element-renderer.d.ts +1 -1
- package/dist/element-view.d.ts +1 -1
- package/dist/export.d.ts +56 -6
- package/dist/export.js +12 -13
- package/dist/import.d.ts +107 -106
- package/dist/import.js +96 -54
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/interchange.d.ts +13 -18
- package/dist/interchange.js +16 -20
- package/dist/node/node-renderer.d.ts +1 -1
- package/dist/reading.js +4 -0
- package/dist/roles.js +25 -6
- package/dist/svg-import.d.ts +2 -0
- package/dist/svg-import.js +309 -0
- package/dist/svg-read.d.ts +108 -0
- package/dist/svg-read.js +89 -0
- package/dist/svg-recognise-labre.d.ts +2 -0
- package/dist/svg-recognise-labre.js +240 -0
- package/dist/svg-recognise-owm.d.ts +2 -0
- package/dist/svg-recognise-owm.js +303 -0
- package/dist/svg-recognise-renderer.d.ts +2 -0
- package/dist/svg-recognise-renderer.js +385 -0
- package/dist/svg-recognise-shapes.d.ts +2 -0
- package/dist/svg-recognise-shapes.js +240 -0
- package/dist/svg-remarks.d.ts +16 -0
- package/dist/svg-remarks.js +31 -0
- package/dist/toolbar/config.d.ts +2 -2
- package/dist/toolbar/node-config.d.ts +1 -1
- package/dist/toolbar/senior-tool.d.ts +1 -1
- package/dist/translations.js +10 -2
- package/package.json +2 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { BOARD_SVG_MARKER_VERSION } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { matchLabels, owmPlotOf } from './export.js';
|
|
3
|
+
import { WARDLEY_ROLE, WARDLEY_ROLES } from './roles.js';
|
|
4
|
+
import { startDrawnMap } from './svg-read.js';
|
|
5
|
+
import { WARDLEY_SVG_IMPORT_REMARKS } from './svg-remarks.js';
|
|
6
|
+
/**
|
|
7
|
+
* Labre's own board SVG, recognised (ADR 0032 §4.1, §6) — first in detection
|
|
8
|
+
* order, because it is the one producer whose file states roles exactly.
|
|
9
|
+
*
|
|
10
|
+
* The export wraps every element it replays in a `<g>` carrying
|
|
11
|
+
* `data-labre-id`, `-type`, `-role`, `-xywh` (the stored bound, in the file's
|
|
12
|
+
* coordinates), `-source` / `-target` on a connector and `-group` on a grouped
|
|
13
|
+
* element, and stamps the root with `data-labre-svg`. Certain on that root
|
|
14
|
+
* stamp, at a version this reader knows.
|
|
15
|
+
*
|
|
16
|
+
* So nothing is guessed but names: the plot is the map's own (`owmPlotOf` on
|
|
17
|
+
* the board's bound), a node's place is its stored bound, a dependency names
|
|
18
|
+
* its two ends, a change arrow names the twin it points at. A NAME is read off
|
|
19
|
+
* the `<text>` the renderer drew, never off a marker — the markers carry no
|
|
20
|
+
* prose — and matched to its node with the very function the OWM export uses
|
|
21
|
+
* (`matchLabels`), so the export and this import agree on whose name is
|
|
22
|
+
* whose. What the statements have no word for — an area, a Porter's force, a
|
|
23
|
+
* method, another framework's artefact, a role no Wardley table declares —
|
|
24
|
+
* is left to the sketch.
|
|
25
|
+
*/
|
|
26
|
+
/** Roles that are a node of the value chain, and the keyword each lays out as. */
|
|
27
|
+
const NODE_KEYWORD = Object.assign(Object.create(null), {
|
|
28
|
+
[WARDLEY_ROLE.component]: 'component',
|
|
29
|
+
[WARDLEY_ROLE.anchor]: 'anchor',
|
|
30
|
+
[WARDLEY_ROLE.market]: 'market',
|
|
31
|
+
[WARDLEY_ROLE.ecosystem]: 'ecosystem',
|
|
32
|
+
[WARDLEY_ROLE.accelerator]: 'accelerator',
|
|
33
|
+
[WARDLEY_ROLE.decelerator]: 'deaccelerator',
|
|
34
|
+
});
|
|
35
|
+
/** A bound a marker states, as four finite numbers, or `undefined`. */
|
|
36
|
+
function boxOf(raw) {
|
|
37
|
+
if (raw === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(parsed) || parsed.length !== 4)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (!parsed.every(value => typeof value === 'number' && Number.isFinite(value)))
|
|
49
|
+
return undefined;
|
|
50
|
+
const [x, y, w, h] = parsed;
|
|
51
|
+
return { x, y, w, h };
|
|
52
|
+
}
|
|
53
|
+
const centreOf = (box) => ({
|
|
54
|
+
x: box.x + box.w / 2,
|
|
55
|
+
y: box.y + box.h / 2,
|
|
56
|
+
});
|
|
57
|
+
/** The words a group's own `<text>`s draw, line after line. */
|
|
58
|
+
function drawnText(element) {
|
|
59
|
+
return Array.from(element.querySelectorAll('text'))
|
|
60
|
+
.map(text => text.textContent ?? '')
|
|
61
|
+
.join(' ')
|
|
62
|
+
.replace(/\s+/g, ' ')
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
export function recogniseLabreExport(root) {
|
|
66
|
+
if (root.getAttribute('data-labre-svg') !== BOARD_SVG_MARKER_VERSION) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const { drawn, consume: consumeElement } = startDrawnMap({
|
|
70
|
+
producer: `Labre SVG ${BOARD_SVG_MARKER_VERSION}`,
|
|
71
|
+
});
|
|
72
|
+
const consume = (marked) => consumeElement(marked?.element);
|
|
73
|
+
/* Every marked group whose numbers are numbers and whose role is Wardley's. */
|
|
74
|
+
const marked = [];
|
|
75
|
+
for (const element of Array.from(root.querySelectorAll('g[data-labre-id]'))) {
|
|
76
|
+
const id = element.getAttribute('data-labre-id') ?? '';
|
|
77
|
+
const box = boxOf(element.getAttribute('data-labre-xywh'));
|
|
78
|
+
if (id.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
if (!box) {
|
|
81
|
+
drawn.remarks.push({
|
|
82
|
+
kind: 'warning',
|
|
83
|
+
sourceId: id,
|
|
84
|
+
message: WARDLEY_SVG_IMPORT_REMARKS.unreadableCoordinates[1],
|
|
85
|
+
messageKey: WARDLEY_SVG_IMPORT_REMARKS.unreadableCoordinates[0],
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const role = element.getAttribute('data-labre-role') ?? undefined;
|
|
90
|
+
// A role no Wardley table declares — another framework's, or a forged
|
|
91
|
+
// one — is not this reader's to read: that element stays a sketch, and is
|
|
92
|
+
// never a new role (ADR 0032 §7).
|
|
93
|
+
if (role !== undefined && !WARDLEY_ROLES[role])
|
|
94
|
+
continue;
|
|
95
|
+
marked.push({
|
|
96
|
+
element,
|
|
97
|
+
id,
|
|
98
|
+
type: element.getAttribute('data-labre-type') ?? '',
|
|
99
|
+
role,
|
|
100
|
+
box,
|
|
101
|
+
source: element.getAttribute('data-labre-source') ?? undefined,
|
|
102
|
+
target: element.getAttribute('data-labre-target') ?? undefined,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const byId = new Map(marked.map(entry => [entry.id, entry]));
|
|
106
|
+
const withRole = (role) => marked.filter(entry => entry.role === role);
|
|
107
|
+
/* The map, and its plot. */
|
|
108
|
+
const map = withRole(WARDLEY_ROLE.map)[0];
|
|
109
|
+
if (map) {
|
|
110
|
+
const plot = owmPlotOf(map.box);
|
|
111
|
+
drawn.plot = plot;
|
|
112
|
+
consume(map);
|
|
113
|
+
}
|
|
114
|
+
/* Names, matched the way the OWM export matches them. */
|
|
115
|
+
const named = marked.filter(entry => entry.type === 'wardleyNode');
|
|
116
|
+
const labels = withRole(WARDLEY_ROLE.label);
|
|
117
|
+
const names = matchLabels(named.map(entry => ({
|
|
118
|
+
id: entry.id,
|
|
119
|
+
kind: entry.role?.slice('wardley:'.length) ?? '',
|
|
120
|
+
role: entry.role,
|
|
121
|
+
elementBound: entry.box,
|
|
122
|
+
})), labels.map(entry => ({
|
|
123
|
+
text: drawnText(entry.element),
|
|
124
|
+
elementBound: entry.box,
|
|
125
|
+
})));
|
|
126
|
+
/* Change arrows first: their targets are twins, not nodes. */
|
|
127
|
+
const twins = new Set();
|
|
128
|
+
for (const arrow of withRole(WARDLEY_ROLE.changeArrow)) {
|
|
129
|
+
const from = arrow.source ? byId.get(arrow.source) : undefined;
|
|
130
|
+
const twin = arrow.target ? byId.get(arrow.target) : undefined;
|
|
131
|
+
if (!from || !twin || !NODE_KEYWORD[from.role ?? ''])
|
|
132
|
+
continue;
|
|
133
|
+
twins.add(twin.id);
|
|
134
|
+
drawn.evolutions.push({
|
|
135
|
+
of: from.id,
|
|
136
|
+
name: names.get(twin.id) ?? names.get(from.id) ?? '',
|
|
137
|
+
x: centreOf(twin.box).x,
|
|
138
|
+
});
|
|
139
|
+
consume(arrow);
|
|
140
|
+
consume(twin);
|
|
141
|
+
}
|
|
142
|
+
/* Nodes. */
|
|
143
|
+
for (const entry of named) {
|
|
144
|
+
const keyword = NODE_KEYWORD[entry.role ?? ''];
|
|
145
|
+
if (!keyword || twins.has(entry.id))
|
|
146
|
+
continue;
|
|
147
|
+
drawn.nodes.push({
|
|
148
|
+
id: entry.id,
|
|
149
|
+
kind: keyword,
|
|
150
|
+
name: names.get(entry.id) ?? '',
|
|
151
|
+
...centreOf(entry.box),
|
|
152
|
+
});
|
|
153
|
+
consume(entry);
|
|
154
|
+
}
|
|
155
|
+
/* Pipelines stand alone; a handle is redrawn with its body. */
|
|
156
|
+
const handleOf = new Map();
|
|
157
|
+
const bodies = withRole(WARDLEY_ROLE.pipeline);
|
|
158
|
+
for (const body of bodies) {
|
|
159
|
+
drawn.pipelines.push({
|
|
160
|
+
id: body.id,
|
|
161
|
+
name: names.get(body.id) ?? '',
|
|
162
|
+
top: body.box.y,
|
|
163
|
+
x1: body.box.x,
|
|
164
|
+
x2: body.box.x + body.box.w,
|
|
165
|
+
});
|
|
166
|
+
consume(body);
|
|
167
|
+
}
|
|
168
|
+
for (const handle of withRole(WARDLEY_ROLE.handle)) {
|
|
169
|
+
const { x, y } = centreOf(handle.box);
|
|
170
|
+
const body = bodies.find(candidate => x >= candidate.box.x &&
|
|
171
|
+
x <= candidate.box.x + candidate.box.w &&
|
|
172
|
+
Math.abs(y - candidate.box.y) <= handle.box.h);
|
|
173
|
+
if (!body)
|
|
174
|
+
continue;
|
|
175
|
+
// The layout names the handle it mints after its pipeline.
|
|
176
|
+
handleOf.set(handle.id, `${body.id} handle`);
|
|
177
|
+
consume(handle);
|
|
178
|
+
}
|
|
179
|
+
/* Dependencies, by the two ids they name. */
|
|
180
|
+
const nodeIds = new Set(drawn.nodes.map(node => node.id));
|
|
181
|
+
const end = (id) => id === undefined ? undefined : nodeIds.has(id) ? id : handleOf.get(id);
|
|
182
|
+
for (const link of withRole(WARDLEY_ROLE.dependency)) {
|
|
183
|
+
const from = end(link.source);
|
|
184
|
+
const to = end(link.target);
|
|
185
|
+
if (from === undefined || to === undefined) {
|
|
186
|
+
drawn.remarks.push({
|
|
187
|
+
kind: 'warning',
|
|
188
|
+
sourceId: link.id,
|
|
189
|
+
element: 'link',
|
|
190
|
+
message: WARDLEY_SVG_IMPORT_REMARKS.danglingLink[1],
|
|
191
|
+
messageKey: WARDLEY_SVG_IMPORT_REMARKS.danglingLink[0],
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
drawn.links.push({ from, to });
|
|
196
|
+
consume(link);
|
|
197
|
+
}
|
|
198
|
+
/* Inertia bars, and free texts — an OWM note is a free text on the map. */
|
|
199
|
+
for (const bar of withRole(WARDLEY_ROLE.inertia)) {
|
|
200
|
+
drawn.inertias.push(centreOf(bar.box));
|
|
201
|
+
consume(bar);
|
|
202
|
+
}
|
|
203
|
+
for (const text of marked.filter(entry => entry.type === 'text' && entry.role === undefined)) {
|
|
204
|
+
const words = drawnText(text.element);
|
|
205
|
+
if (words.length === 0)
|
|
206
|
+
continue;
|
|
207
|
+
drawn.notes.push({ text: words, ...centreOf(text.box) });
|
|
208
|
+
consume(text);
|
|
209
|
+
}
|
|
210
|
+
/* A glyph's own wiring (a market's dots and triangle), and the groups. */
|
|
211
|
+
const wiring = new Set(marked
|
|
212
|
+
.filter(entry => entry.type === 'wardleyNode' && entry.role === undefined)
|
|
213
|
+
.map(entry => entry.id));
|
|
214
|
+
for (const entry of marked) {
|
|
215
|
+
const glyph = wiring.has(entry.id) ||
|
|
216
|
+
(entry.type === 'connector' &&
|
|
217
|
+
entry.role === undefined &&
|
|
218
|
+
wiring.has(entry.source ?? '') &&
|
|
219
|
+
wiring.has(entry.target ?? ''));
|
|
220
|
+
if (glyph || entry.type === 'group')
|
|
221
|
+
consume(entry);
|
|
222
|
+
}
|
|
223
|
+
// The clip paths svgcanvas hoists to the root draw nothing by themselves.
|
|
224
|
+
for (const defs of Array.from(root.children).filter(child => child.localName === 'defs')) {
|
|
225
|
+
drawn.consumed.add(defs);
|
|
226
|
+
}
|
|
227
|
+
// A label goes when it named something now drawn natively — the layout
|
|
228
|
+
// writes that name again. One that named nothing, or named an artefact left
|
|
229
|
+
// to the sketch (a method, a force), stays visible beside it.
|
|
230
|
+
const redrawn = new Set([
|
|
231
|
+
...drawn.nodes.map(node => node.id),
|
|
232
|
+
...twins,
|
|
233
|
+
...bodies.map(body => body.id),
|
|
234
|
+
].flatMap(id => (names.has(id) ? [names.get(id)] : [])));
|
|
235
|
+
for (const label of labels) {
|
|
236
|
+
if (redrawn.has(drawnText(label.element)))
|
|
237
|
+
consume(label);
|
|
238
|
+
}
|
|
239
|
+
return drawn;
|
|
240
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { svgFrameOf } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { finiteAttr, idIndex, originOf, startDrawnMap, textOf, } from './svg-read.js';
|
|
3
|
+
import { WARDLEY_SVG_IMPORT_REMARKS } from './svg-remarks.js';
|
|
4
|
+
/**
|
|
5
|
+
* An OnlineWardleyMaps "Export SVG", recognised (ADR 0032 §4.3).
|
|
6
|
+
*
|
|
7
|
+
* Read against the export the tool actually writes (`MapEnvironment.tsx`
|
|
8
|
+
* `downloadMapAsSVG`, and the corpus files rendered from its own components),
|
|
9
|
+
* which is NOT quite what its source suggests at a glance:
|
|
10
|
+
*
|
|
11
|
+
* - every artefact is a `modern_movable_element_<id>` group translated to its
|
|
12
|
+
* centre, holding an `element_circle_<id>` (a component), an
|
|
13
|
+
* `element_square_<id>` (a component inside a pipeline), and for a market
|
|
14
|
+
* or an ecosystem a `market_circle_<id>` / `ecosystem_circle_<id>` on top;
|
|
15
|
+
* its name is the `<id>-text` text, drawn in a sibling group;
|
|
16
|
+
* - `<id>` is the component's line in the DSL, so it is a number — but it is
|
|
17
|
+
* read as an opaque string, whatever a hostile file puts there;
|
|
18
|
+
* - an anchor is a `modern_movable_anchor__<id>_` group with its name inside;
|
|
19
|
+
* - a link is `modern_link_<from>_<to>`, drawn TWICE when one end is an anchor
|
|
20
|
+
* (once among the links, once among the anchor links);
|
|
21
|
+
* - an evolved twin is the `<id>_evolved` artefact, its arrow
|
|
22
|
+
* `modern_evolving_link_<id>_<id>_evolved`;
|
|
23
|
+
* - inertia is a `line.inertia-symbol`; a pipeline is `pipeline_box_<line>`
|
|
24
|
+
* with its two ends `modern_movable_pipeline_x1_<line>` / `x2_`, under the
|
|
25
|
+
* component whose square sits on its top edge;
|
|
26
|
+
* - a note is `modern_movable_modern_note_<line>` around
|
|
27
|
+
* `modern_note_text_<line>`; a climate arrow is
|
|
28
|
+
* `modern_movable_accelerator_element_<line>`, turned by `rotate(180…)` when
|
|
29
|
+
* it is a decelerator, and drawn with NO name;
|
|
30
|
+
* - the plot is `rect#fillArea`, the title `text#mapTitle`.
|
|
31
|
+
*
|
|
32
|
+
* Methods (`method_<id>`), annotations and the PST boxes have no native
|
|
33
|
+
* artefact here and are left to the sketch, as is anything a person added.
|
|
34
|
+
*
|
|
35
|
+
* ## Certain, with or without the plot
|
|
36
|
+
*
|
|
37
|
+
* ADR 0032 §4 names `fillArea` plus one element marker as certain, and §5 then
|
|
38
|
+
* reads an OWM file "whose `fillArea` was stripped" as certain too. Both hold
|
|
39
|
+
* here: certainty comes from an element marker INSIDE OWM's own movable
|
|
40
|
+
* wrapper — two structures only this tool writes together — and `fillArea`
|
|
41
|
+
* decides whether the plot is read or estimated.
|
|
42
|
+
*/
|
|
43
|
+
const MOVABLE_ELEMENT = 'modern_movable_element_';
|
|
44
|
+
const MOVABLE_ANCHOR = /^modern_movable_anchor__(.*)_$/;
|
|
45
|
+
const MOVABLE_ACCELERATOR = 'modern_movable_accelerator_element_';
|
|
46
|
+
const MOVABLE_NOTE = 'modern_movable_modern_note_';
|
|
47
|
+
const PIPELINE_BOX = 'pipeline_box_';
|
|
48
|
+
const LINK = 'modern_link_';
|
|
49
|
+
const EVOLVING_LINK = 'modern_evolving_link_';
|
|
50
|
+
const EVOLVED = '_evolved';
|
|
51
|
+
/** The chrome every export carries, drawn by the native board instead. */
|
|
52
|
+
const CHROME_IDS = ['grid', 'fillArea', 'valueChain', 'Evolution', 'mapTitle'];
|
|
53
|
+
/** How far a pipeline's owner may sit from the box's top edge, in file units. */
|
|
54
|
+
const PIPELINE_OWNER_REACH = 15;
|
|
55
|
+
const after = (id, prefix) => id.startsWith(prefix) ? id.slice(prefix.length) : undefined;
|
|
56
|
+
export function recogniseOnlineWardleyMaps(root) {
|
|
57
|
+
const ids = idIndex(root);
|
|
58
|
+
/* Certain: an OWM element marker inside OWM's own movable wrapper. */
|
|
59
|
+
const certain = [...ids.keys()].some(id => {
|
|
60
|
+
const own = after(id, MOVABLE_ELEMENT);
|
|
61
|
+
return (own !== undefined &&
|
|
62
|
+
[
|
|
63
|
+
'element_circle_',
|
|
64
|
+
'element_square_',
|
|
65
|
+
'market_circle_',
|
|
66
|
+
'ecosystem_circle_',
|
|
67
|
+
]
|
|
68
|
+
.map(prefix => ids.get(`${prefix}${own}`))
|
|
69
|
+
.some(marker => marker !== undefined && ids.get(id).contains(marker)));
|
|
70
|
+
});
|
|
71
|
+
if (!certain)
|
|
72
|
+
return undefined;
|
|
73
|
+
const { drawn, consume } = startDrawnMap({
|
|
74
|
+
producer: 'OnlineWardleyMaps SVG',
|
|
75
|
+
});
|
|
76
|
+
const unreadable = (sourceId) => {
|
|
77
|
+
drawn.remarks.push({
|
|
78
|
+
kind: 'warning',
|
|
79
|
+
sourceId,
|
|
80
|
+
message: WARDLEY_SVG_IMPORT_REMARKS.unreadableCoordinates[1],
|
|
81
|
+
messageKey: WARDLEY_SVG_IMPORT_REMARKS.unreadableCoordinates[0],
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
/* The plot, in the coordinates of its parent group. */
|
|
85
|
+
const fillArea = ids.get('fillArea');
|
|
86
|
+
if (fillArea) {
|
|
87
|
+
const frame = svgFrameOf(fillArea);
|
|
88
|
+
const [x, y, w, h] = ['x', 'y', 'width', 'height'].map(name => finiteAttr(fillArea, name));
|
|
89
|
+
if (w !== undefined && h !== undefined && w > 0 && h > 0) {
|
|
90
|
+
const plot = {
|
|
91
|
+
x0: frame.ox + (x ?? 0) * frame.s,
|
|
92
|
+
y0: frame.oy + (y ?? 0) * frame.s,
|
|
93
|
+
width: w * frame.s,
|
|
94
|
+
height: h * frame.s,
|
|
95
|
+
};
|
|
96
|
+
drawn.plot = plot;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const title = textOf(ids.get('mapTitle'));
|
|
100
|
+
if (title.length > 0)
|
|
101
|
+
drawn.title = title;
|
|
102
|
+
/* The chrome: drawn again by the native board, so never sketched. */
|
|
103
|
+
for (const id of CHROME_IDS)
|
|
104
|
+
consume(ids.get(id));
|
|
105
|
+
// The pan-and-zoom canvas's own backdrops: the plain rects beside the
|
|
106
|
+
// plot's ancestors, between it and the root (never the root's own children,
|
|
107
|
+
// which is where anything somebody added to the file lands).
|
|
108
|
+
for (let at = fillArea?.parentElement; at && at !== root && at.parentElement !== root; at = at.parentElement) {
|
|
109
|
+
for (const sibling of Array.from(at.parentElement?.children ?? [])) {
|
|
110
|
+
if (sibling.localName === 'rect' && !sibling.hasAttribute('id')) {
|
|
111
|
+
consume(sibling);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Hover animations and gradient definitions draw nothing by themselves.
|
|
116
|
+
for (const element of Array.from(root.querySelectorAll('defs, style'))) {
|
|
117
|
+
consume(element);
|
|
118
|
+
}
|
|
119
|
+
/* The artefacts. */
|
|
120
|
+
const byId = new Map();
|
|
121
|
+
const twins = [];
|
|
122
|
+
for (const [id, group] of ids) {
|
|
123
|
+
const own = after(id, MOVABLE_ELEMENT);
|
|
124
|
+
if (own === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
if (own.endsWith(EVOLVED)) {
|
|
127
|
+
twins.push({ of: own.slice(0, -EVOLVED.length), group });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const kind = ids.has(`market_circle_${own}`)
|
|
131
|
+
? 'market'
|
|
132
|
+
: ids.has(`ecosystem_circle_${own}`)
|
|
133
|
+
? 'ecosystem'
|
|
134
|
+
: ids.has(`element_circle_${own}`) || ids.has(`element_square_${own}`)
|
|
135
|
+
? 'component'
|
|
136
|
+
: undefined;
|
|
137
|
+
if (kind === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
const at = originOf(group);
|
|
140
|
+
if (!at) {
|
|
141
|
+
unreadable(own);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const label = ids.get(`${own}-text`);
|
|
145
|
+
const node = { id: own, kind, name: textOf(label), x: at[0], y: at[1] };
|
|
146
|
+
byId.set(own, node);
|
|
147
|
+
drawn.nodes.push(node);
|
|
148
|
+
consume(group);
|
|
149
|
+
consume(ids.get(`modern_movable_${own}-text-movable`));
|
|
150
|
+
}
|
|
151
|
+
for (const [id, group] of ids) {
|
|
152
|
+
const own = MOVABLE_ANCHOR.exec(id)?.[1];
|
|
153
|
+
if (own === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
const at = originOf(group);
|
|
156
|
+
if (!at) {
|
|
157
|
+
unreadable(own);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const node = {
|
|
161
|
+
id: own,
|
|
162
|
+
kind: 'anchor',
|
|
163
|
+
name: textOf(ids.get(`${own}-text`)),
|
|
164
|
+
x: at[0],
|
|
165
|
+
y: at[1],
|
|
166
|
+
};
|
|
167
|
+
byId.set(own, node);
|
|
168
|
+
drawn.nodes.push(node);
|
|
169
|
+
consume(group);
|
|
170
|
+
}
|
|
171
|
+
for (const [id, group] of ids) {
|
|
172
|
+
const own = after(id, MOVABLE_ACCELERATOR);
|
|
173
|
+
if (own === undefined)
|
|
174
|
+
continue;
|
|
175
|
+
const at = originOf(group);
|
|
176
|
+
if (!at) {
|
|
177
|
+
unreadable(own);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const arrow = ids.get(`accelerator_circle_${own}`);
|
|
181
|
+
const turned = /rotate\(\s*180/.test(arrow?.getAttribute('transform') ?? '');
|
|
182
|
+
drawn.nodes.push({
|
|
183
|
+
id: `accelerator ${own}`,
|
|
184
|
+
kind: turned ? 'deaccelerator' : 'accelerator',
|
|
185
|
+
name: '',
|
|
186
|
+
x: at[0],
|
|
187
|
+
y: at[1],
|
|
188
|
+
});
|
|
189
|
+
consume(group);
|
|
190
|
+
}
|
|
191
|
+
for (const { of, group } of twins) {
|
|
192
|
+
const base = byId.get(of);
|
|
193
|
+
const at = originOf(group);
|
|
194
|
+
if (!base || !at) {
|
|
195
|
+
if (!at)
|
|
196
|
+
unreadable(`${of}${EVOLVED}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
drawn.evolutions.push({
|
|
200
|
+
of,
|
|
201
|
+
name: textOf(ids.get(`${of}${EVOLVED}-text`)) || base.name,
|
|
202
|
+
x: at[0],
|
|
203
|
+
});
|
|
204
|
+
consume(group);
|
|
205
|
+
consume(ids.get(`modern_movable_${of}${EVOLVED}-text-movable`));
|
|
206
|
+
consume(ids.get(`${EVOLVING_LINK}${of}_${of}${EVOLVED}`));
|
|
207
|
+
}
|
|
208
|
+
/* Links, bound by the two ids in their own id. */
|
|
209
|
+
const known = (end) => byId.has(end)
|
|
210
|
+
? end
|
|
211
|
+
: end.endsWith(EVOLVED) && byId.has(end.slice(0, -EVOLVED.length))
|
|
212
|
+
? end.slice(0, -EVOLVED.length)
|
|
213
|
+
: undefined;
|
|
214
|
+
const linked = new Set();
|
|
215
|
+
for (const element of Array.from(root.querySelectorAll('[id]'))) {
|
|
216
|
+
const rest = after(element.getAttribute('id') ?? '', LINK);
|
|
217
|
+
if (rest === undefined)
|
|
218
|
+
continue;
|
|
219
|
+
let ends;
|
|
220
|
+
for (let at = rest.indexOf('_'); at >= 0; at = rest.indexOf('_', at + 1)) {
|
|
221
|
+
const from = known(rest.slice(0, at));
|
|
222
|
+
const to = known(rest.slice(at + 1));
|
|
223
|
+
if (from !== undefined && to !== undefined) {
|
|
224
|
+
ends = [from, to];
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!ends) {
|
|
229
|
+
drawn.remarks.push({
|
|
230
|
+
kind: 'warning',
|
|
231
|
+
sourceId: rest,
|
|
232
|
+
element: 'link',
|
|
233
|
+
message: WARDLEY_SVG_IMPORT_REMARKS.danglingLink[1],
|
|
234
|
+
messageKey: WARDLEY_SVG_IMPORT_REMARKS.danglingLink[0],
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
consume(element);
|
|
239
|
+
// Drawn twice when an end is an anchor; one dependency all the same.
|
|
240
|
+
const key = JSON.stringify(ends);
|
|
241
|
+
if (linked.has(key))
|
|
242
|
+
continue;
|
|
243
|
+
linked.add(key);
|
|
244
|
+
drawn.links.push({ from: ends[0], to: ends[1] });
|
|
245
|
+
}
|
|
246
|
+
/* Inertia. */
|
|
247
|
+
for (const line of Array.from(root.querySelectorAll('line.inertia-symbol'))) {
|
|
248
|
+
const [x1, y1, x2, y2] = ['x1', 'y1', 'x2', 'y2'].map(name => finiteAttr(line, name));
|
|
249
|
+
if ([x1, y1, x2, y2].some(value => value === undefined)) {
|
|
250
|
+
unreadable('inertia');
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const frame = svgFrameOf(line);
|
|
254
|
+
drawn.inertias.push({
|
|
255
|
+
x: frame.ox + ((x1 + x2) / 2) * frame.s,
|
|
256
|
+
y: frame.oy + ((y1 + y2) / 2) * frame.s,
|
|
257
|
+
});
|
|
258
|
+
consume(line);
|
|
259
|
+
}
|
|
260
|
+
/* Pipelines, hung under the component sitting on their top edge. */
|
|
261
|
+
for (const [id, box] of ids) {
|
|
262
|
+
const own = after(id, PIPELINE_BOX);
|
|
263
|
+
if (own === undefined)
|
|
264
|
+
continue;
|
|
265
|
+
const top = originOf(box);
|
|
266
|
+
const left = originOf(ids.get(`modern_movable_pipeline_x1_${own}`) ?? box);
|
|
267
|
+
const right = ids.get(`modern_movable_pipeline_x2_${own}`);
|
|
268
|
+
const end = right ? originOf(right) : undefined;
|
|
269
|
+
if (!top || !left || !end) {
|
|
270
|
+
unreadable(id);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const owner = drawn.nodes
|
|
274
|
+
.filter(node => node.kind === 'component' &&
|
|
275
|
+
Math.abs(node.y - top[1]) <= PIPELINE_OWNER_REACH &&
|
|
276
|
+
node.x >= Math.min(left[0], end[0]) - PIPELINE_OWNER_REACH &&
|
|
277
|
+
node.x <= Math.max(left[0], end[0]) + PIPELINE_OWNER_REACH)
|
|
278
|
+
.sort((a, b) => Math.abs(a.y - top[1]) - Math.abs(b.y - top[1]))[0];
|
|
279
|
+
if (!owner)
|
|
280
|
+
continue;
|
|
281
|
+
drawn.pipelines.push({ of: owner.id, x1: left[0], x2: end[0] });
|
|
282
|
+
consume(box);
|
|
283
|
+
consume(ids.get(`modern_movable_pipeline_x1_${own}`));
|
|
284
|
+
consume(right);
|
|
285
|
+
}
|
|
286
|
+
/* Notes. */
|
|
287
|
+
for (const [id, group] of ids) {
|
|
288
|
+
const own = after(id, MOVABLE_NOTE);
|
|
289
|
+
if (own === undefined)
|
|
290
|
+
continue;
|
|
291
|
+
const at = originOf(group);
|
|
292
|
+
if (!at) {
|
|
293
|
+
unreadable(id);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const text = textOf(ids.get(`modern_note_text_${own}`) ?? group);
|
|
297
|
+
if (text.length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
drawn.notes.push({ text, x: at[0], y: at[1] });
|
|
300
|
+
consume(group);
|
|
301
|
+
}
|
|
302
|
+
return drawn;
|
|
303
|
+
}
|