@formicoidea/labre-framework-wardley 0.34.2 → 0.36.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/actions.d.ts +111 -31
- package/dist/actions.js +184 -98
- package/dist/commands-manifest.js +40 -0
- package/dist/commands.d.ts +18 -1
- package/dist/commands.js +96 -7
- package/dist/export.js +50 -2
- package/dist/import.d.ts +5 -3
- package/dist/import.js +53 -8
- package/dist/index.d.ts +4 -1
- package/dist/index.js +16 -1
- package/dist/legend.d.ts +27 -1
- package/dist/legend.js +277 -3
- package/dist/morph.d.ts +129 -0
- package/dist/morph.js +405 -0
- package/dist/natures.d.ts +31 -0
- package/dist/natures.js +64 -4
- package/dist/node/consts.d.ts +123 -0
- package/dist/node/consts.js +122 -0
- package/dist/node/node-view.d.ts +82 -4
- package/dist/node/node-view.js +108 -4
- package/dist/node/porter-watcher.d.ts +119 -0
- package/dist/node/porter-watcher.js +255 -0
- package/dist/porter-competition.d.ts +36 -0
- package/dist/porter-competition.js +50 -0
- package/dist/presets.d.ts +276 -0
- package/dist/presets.js +554 -0
- package/dist/roles.d.ts +4 -0
- package/dist/roles.js +85 -7
- package/dist/toolbar/icons.d.ts +36 -0
- package/dist/toolbar/icons.js +58 -0
- package/dist/toolbar/node-config.d.ts +20 -0
- package/dist/toolbar/node-config.js +100 -7
- package/dist/view.js +40 -2
- package/package.json +2 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { WardleyNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { ELEMENT_TAGS_FIELD, elementTagValues, InteractivityExtension, setElementTag, } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
import { WARDLEY_COMPETITION_TAG_ID } from '../natures.js';
|
|
4
|
+
import { competitionOfPorterLetter, porterLetterOfCompetition, } from '../porter-competition.js';
|
|
5
|
+
/**
|
|
6
|
+
* Keeps a Porter's-forces glyph saying ONE thing: the letter in the circle and
|
|
7
|
+
* the `wardley:competition` tag on it are two spellings of the same statement.
|
|
8
|
+
*
|
|
9
|
+
* ## Why there are two spellings at all
|
|
10
|
+
*
|
|
11
|
+
* The letter is the NOTATION — an architect reads `L` off the map and knows
|
|
12
|
+
* what it says, and typing another letter is the whole of the edit
|
|
13
|
+
* (`node-view.ts`). The tag is the FACT — the half a rule, a reading or a host
|
|
14
|
+
* report can query, because nothing can ask "which forces does this map name?"
|
|
15
|
+
* of a `Y.Text` holding one character. Dropping either would cost something
|
|
16
|
+
* real, so both exist.
|
|
17
|
+
*
|
|
18
|
+
* ## The invariant: the tag is DERIVED from the letter
|
|
19
|
+
*
|
|
20
|
+
* The two are not symmetric, and the recette of #211 is what made the asymmetry
|
|
21
|
+
* necessary. The letter is what the author writes; the tag is a reading of it,
|
|
22
|
+
* recomputed on EVERY local change of the text and written as a consequence of
|
|
23
|
+
* that change rather than as a step of its own. So `tag = f(text)` holds at
|
|
24
|
+
* every instant, including at every instant an undo or a redo passes through.
|
|
25
|
+
*
|
|
26
|
+
* That is the fix for the defect the recette found. Reconciling only when the
|
|
27
|
+
* editor closed left the two able to disagree: the inline editor writes a
|
|
28
|
+
* select-all-and-retype as several Yjs operations, each its own undo entry,
|
|
29
|
+
* while the tag was one further entry captured after all of them. Undoing then
|
|
30
|
+
* landed on an intermediate text — `RR` — carrying the tag of the text that
|
|
31
|
+
* came after it, and redoing past the tag's own entry re-ran the reconciliation
|
|
32
|
+
* against a text that had already moved on, so the document never returned to
|
|
33
|
+
* where it started.
|
|
34
|
+
*
|
|
35
|
+
* The reading is taken on a MICROTASK after the change, and written UNTRACKED
|
|
36
|
+
* (`Store.withoutTransact`, an origin the undo manager ignores). So the undo
|
|
37
|
+
* stack holds the author's keystrokes and nothing else, and whatever text a
|
|
38
|
+
* step lands on — `E`, the intermediate `RR`, the original `R` — the tag is
|
|
39
|
+
* recomputed from it a tick later. There is no entry to get out of order,
|
|
40
|
+
* because the qualification is not an entry.
|
|
41
|
+
*
|
|
42
|
+
* ## Tag → letter, and why it is the quiet direction
|
|
43
|
+
*
|
|
44
|
+
* A menu pick is the one gesture that starts from the tag, so it — and only it
|
|
45
|
+
* — redraws the letter. Two guards keep that from becoming a second author:
|
|
46
|
+
*
|
|
47
|
+
* - It compares the two SEMANTICALLY (`competitionOfPorterLetter(raw)`), never
|
|
48
|
+
* character by character. A circle reading `e` already says "struggle to
|
|
49
|
+
* establish", so a tag derived from it must not turn round and rewrite it to
|
|
50
|
+
* `E`. Without this the two directions would trade writes on every keystroke.
|
|
51
|
+
* - It never runs while the id is in the EDITING set. That is C4's lesson
|
|
52
|
+
* (`C4TypeLineWatcher`): rewriting the text an author has their caret in
|
|
53
|
+
* throws that caret, and no letter is worth it.
|
|
54
|
+
*
|
|
55
|
+
* The one deliberate asymmetry: a CLEARED tag leaves the circle alone. Removing
|
|
56
|
+
* a value from the menu is a statement about the qualification — "I no longer
|
|
57
|
+
* claim which force this is" — and not about a map somebody is still reading.
|
|
58
|
+
* The tie is re-tied by the next edit of the letter, which is the only gesture
|
|
59
|
+
* that ever asserts one.
|
|
60
|
+
*
|
|
61
|
+
* ## `local`, and always-on
|
|
62
|
+
*
|
|
63
|
+
* Both directions are LOCAL-only, for `EdgyRelationResolver`'s reason: without
|
|
64
|
+
* it every peer on a shared board would derive the same tag the moment a letter
|
|
65
|
+
* synced, giving N−1 of them an entry for a gesture they did not make.
|
|
66
|
+
*
|
|
67
|
+
* Registered by the RENDER half rather than the flag-gated one
|
|
68
|
+
* (`docs/adr/0009`): this creates no element, offers no button and adds no
|
|
69
|
+
* artefact to any map. It keeps an element ALREADY in the document coherent
|
|
70
|
+
* with itself, which a force drawn while the Wardley button was on must stay
|
|
71
|
+
* when the button goes off.
|
|
72
|
+
*/
|
|
73
|
+
export class WardleyPorterWatcher extends InteractivityExtension {
|
|
74
|
+
constructor() {
|
|
75
|
+
super(...arguments);
|
|
76
|
+
this._selection = null;
|
|
77
|
+
this._updated = null;
|
|
78
|
+
this._disposeSurface = null;
|
|
79
|
+
this._editing = new Set();
|
|
80
|
+
this._pending = new Set();
|
|
81
|
+
this._scheduled = false;
|
|
82
|
+
this._mounted = false;
|
|
83
|
+
}
|
|
84
|
+
static { this.key = 'wardley-porter-watcher'; }
|
|
85
|
+
mounted() {
|
|
86
|
+
this._mounted = true;
|
|
87
|
+
// The editing set is tracked for ONE purpose: to know whose caret is where,
|
|
88
|
+
// so the tag→letter direction never rewrites a circle somebody is inside.
|
|
89
|
+
this._selection = this.gfx.selection.slots.updated.subscribe(selections => {
|
|
90
|
+
this._editing = wardleyEditingIds(selections);
|
|
91
|
+
});
|
|
92
|
+
// The surface is a SIGNAL — null at mount on a document whose surface block
|
|
93
|
+
// has not arrived yet, and replaced when it is. Subscribing to the signal
|
|
94
|
+
// rather than reading it once is what `EdgyRelationResolver` does, and for
|
|
95
|
+
// the same reason: a subscription taken against a surface that no longer
|
|
96
|
+
// exists is a subscription to nothing.
|
|
97
|
+
this._disposeSurface = this.gfx.surface$.subscribe(surface => {
|
|
98
|
+
this._updated?.unsubscribe();
|
|
99
|
+
this._updated = null;
|
|
100
|
+
if (!surface)
|
|
101
|
+
return;
|
|
102
|
+
this._updated = surface.elementUpdated.subscribe(({ id, props, local }) => {
|
|
103
|
+
if (!local || !props)
|
|
104
|
+
return;
|
|
105
|
+
// `watchText` bridges the nested `Y.Text` under this key, so this is
|
|
106
|
+
// every change of the letter — a keystroke, an undo, a redo alike.
|
|
107
|
+
if ('text' in props)
|
|
108
|
+
this._scheduleDerive(id);
|
|
109
|
+
// `observeTags` republishes the nested map under the field's own key,
|
|
110
|
+
// so this is the one payload a qualification produces.
|
|
111
|
+
if (ELEMENT_TAGS_FIELD in props)
|
|
112
|
+
this._drawLetterFromTag(id);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
unmounted() {
|
|
117
|
+
this._mounted = false;
|
|
118
|
+
this._pending.clear();
|
|
119
|
+
this._selection?.unsubscribe();
|
|
120
|
+
this._selection = null;
|
|
121
|
+
this._updated?.unsubscribe();
|
|
122
|
+
this._updated = null;
|
|
123
|
+
this._disposeSurface?.();
|
|
124
|
+
this._disposeSurface = null;
|
|
125
|
+
this._editing = new Set();
|
|
126
|
+
super.unmounted();
|
|
127
|
+
}
|
|
128
|
+
/** The porter this id names, when it is one this peer may rewrite. */
|
|
129
|
+
_porter(elementId) {
|
|
130
|
+
// A read-only document is READ: every write below would be refused, and
|
|
131
|
+
// nobody can have typed a letter into it in the first place.
|
|
132
|
+
if (this.std.store.readonly)
|
|
133
|
+
return null;
|
|
134
|
+
const element = this.gfx.surface?.getElementById(elementId);
|
|
135
|
+
if (!(element instanceof WardleyNodeElementModel))
|
|
136
|
+
return null;
|
|
137
|
+
if (element.kind !== 'porter')
|
|
138
|
+
return null;
|
|
139
|
+
if (element.isLocked())
|
|
140
|
+
return null;
|
|
141
|
+
return element;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Queue a re-reading of the letter, for the end of the current task.
|
|
145
|
+
*
|
|
146
|
+
* A MICROTASK, and it is load-bearing rather than cosmetic. `watchText` fires
|
|
147
|
+
* from inside the Yjs transaction that changed the text, and at that instant
|
|
148
|
+
* the element's own `Y.Map` observer may not have run yet — so `element.tags`
|
|
149
|
+
* can still resolve, through `_preserved`, to a nested map the very same
|
|
150
|
+
* transaction has just deleted. A tag written into that map goes nowhere at
|
|
151
|
+
* all: the recette of #211 saw exactly that, as an undo landing on a letter
|
|
152
|
+
* whose force had silently failed to follow it.
|
|
153
|
+
*
|
|
154
|
+
* Deferring past the transaction settles two things at once. The document is
|
|
155
|
+
* consistent when the reading is taken, and the write lands OUTSIDE any
|
|
156
|
+
* tracked transaction — so {@link Store.withoutTransact} really does keep it
|
|
157
|
+
* off the undo stack, and the tag becomes what it claims to be: a pure
|
|
158
|
+
* function of the text, recomputed after every change of it, in either
|
|
159
|
+
* direction of history, and never a step an author has to undo.
|
|
160
|
+
*/
|
|
161
|
+
_scheduleDerive(elementId) {
|
|
162
|
+
this._pending.add(elementId);
|
|
163
|
+
if (this._scheduled)
|
|
164
|
+
return;
|
|
165
|
+
this._scheduled = true;
|
|
166
|
+
queueMicrotask(() => {
|
|
167
|
+
this._scheduled = false;
|
|
168
|
+
const ids = [...this._pending];
|
|
169
|
+
this._pending.clear();
|
|
170
|
+
if (!this._mounted)
|
|
171
|
+
return;
|
|
172
|
+
for (const id of ids)
|
|
173
|
+
this._deriveTagFromText(id);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** The letter as it now stands, written down as the fact it states. */
|
|
177
|
+
_deriveTagFromText(elementId) {
|
|
178
|
+
const element = this._porter(elementId);
|
|
179
|
+
if (!element)
|
|
180
|
+
return;
|
|
181
|
+
const value = competitionOfPorterLetter(element.text?.toString());
|
|
182
|
+
// Anything that is not one of the three — a half-typed `RR`, a stray word,
|
|
183
|
+
// an emptied circle — carries no tag. A glyph nobody can read must not be
|
|
184
|
+
// reported as a force somebody named, and an INTERMEDIATE state an undo
|
|
185
|
+
// stops on is exactly such a glyph.
|
|
186
|
+
const next = value ? [value] : [];
|
|
187
|
+
const current = elementTagValues(element, WARDLEY_COMPETITION_TAG_ID);
|
|
188
|
+
if (current.length === next.length &&
|
|
189
|
+
current.every((v, i) => v === next[i]))
|
|
190
|
+
return;
|
|
191
|
+
// No `captureSync`, and UNTRACKED. The tag is a consequence of the letter,
|
|
192
|
+
// never a step of its own: `withoutTransact` writes with an origin the undo
|
|
193
|
+
// manager ignores, so an author undoes the letter they typed and the
|
|
194
|
+
// reading of it simply follows, in either direction, for ever.
|
|
195
|
+
this.std.store.withoutTransact(() => {
|
|
196
|
+
setElementTag(element, WARDLEY_COMPETITION_TAG_ID, next);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** The value just picked from the menu, drawn as the letter it stands for. */
|
|
200
|
+
_drawLetterFromTag(elementId) {
|
|
201
|
+
const element = this._porter(elementId);
|
|
202
|
+
if (!element)
|
|
203
|
+
return;
|
|
204
|
+
// Never into a circle somebody has their caret in (`C4TypeLineWatcher`).
|
|
205
|
+
if (this._editing.has(elementId))
|
|
206
|
+
return;
|
|
207
|
+
const value = elementTagValues(element, WARDLEY_COMPETITION_TAG_ID)[0];
|
|
208
|
+
const letter = porterLetterOfCompetition(value);
|
|
209
|
+
// A cleared tag — and a value whose def has vanished, which has no letter
|
|
210
|
+
// either — leaves the circle exactly as it is. See the class comment: this
|
|
211
|
+
// is the one deliberate asymmetry.
|
|
212
|
+
if (!letter)
|
|
213
|
+
return;
|
|
214
|
+
const text = element.text;
|
|
215
|
+
if (!text)
|
|
216
|
+
return;
|
|
217
|
+
const raw = text.toString();
|
|
218
|
+
// SEMANTICALLY, not character by character: a circle reading `e` already
|
|
219
|
+
// says this value, and rewriting it to `E` would be this watcher answering
|
|
220
|
+
// its own derivation on every keystroke.
|
|
221
|
+
if (competitionOfPorterLetter(raw) === value)
|
|
222
|
+
return;
|
|
223
|
+
// No `captureSync` either: this only ever runs as a consequence of the tag
|
|
224
|
+
// write that caused it, and it nests into that write's transaction — so a
|
|
225
|
+
// menu pick costs exactly one undo entry, redrawing included.
|
|
226
|
+
//
|
|
227
|
+
// Mutated IN PLACE rather than replaced, as `C4TypeLineWatcher` does: the
|
|
228
|
+
// `Y.Text` instance is what the element's change watcher and any bound
|
|
229
|
+
// editor hold, and swapping it for a fresh one would leave both pointing at
|
|
230
|
+
// a text nobody is editing.
|
|
231
|
+
this.std.store.transact(() => {
|
|
232
|
+
text.delete(0, text.length);
|
|
233
|
+
text.insert(0, letter);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The ids currently being EDITED, out of a selection update.
|
|
239
|
+
*
|
|
240
|
+
* A pure function because it is the whole of what the watcher needs to know
|
|
241
|
+
* about the selection: a text editor holds an editing selection for as long as
|
|
242
|
+
* it is mounted and drops it when it closes, so this set is "whose caret is in
|
|
243
|
+
* something right now" and nothing else. Total over an empty selection, which
|
|
244
|
+
* is what arrives when an editor closes onto bare canvas.
|
|
245
|
+
*/
|
|
246
|
+
export function wardleyEditingIds(selections) {
|
|
247
|
+
const editing = new Set();
|
|
248
|
+
for (const selection of selections) {
|
|
249
|
+
if (!selection.editing)
|
|
250
|
+
continue;
|
|
251
|
+
for (const id of selection.elements)
|
|
252
|
+
editing.add(id);
|
|
253
|
+
}
|
|
254
|
+
return editing;
|
|
255
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The letter and the tag, in both directions — the whole of what makes the
|
|
3
|
+
* drawing and the qualification say the same thing.
|
|
4
|
+
*
|
|
5
|
+
* A pure module on purpose: this is the only place where the notation
|
|
6
|
+
* (`R` / `L` / `E`, one character in a circle) and the queryable fact
|
|
7
|
+
* (`wardley:competition/...`) are related to one another, and it is the part of
|
|
8
|
+
* `WardleyPorterWatcher` worth being wrong about. Nothing here reads an
|
|
9
|
+
* element, a surface or a document.
|
|
10
|
+
*/
|
|
11
|
+
/** The three letters the notation admits, and nothing else. */
|
|
12
|
+
export type WardleyPorterLetter = 'R' | 'L' | 'E';
|
|
13
|
+
/**
|
|
14
|
+
* What a competition value is written as, `null` when it is not one of the
|
|
15
|
+
* three.
|
|
16
|
+
*
|
|
17
|
+
* Total over anything a DOCUMENT can carry, not just over the ids this pack
|
|
18
|
+
* declares: defs are runtime configuration and are never persisted, so a board
|
|
19
|
+
* may legitimately hold a value whose pack was removed or renamed. Such a value
|
|
20
|
+
* has no letter, and the watcher leaves the circle alone rather than guessing
|
|
21
|
+
* one.
|
|
22
|
+
*/
|
|
23
|
+
export declare function porterLetterOfCompetition(valueId: string | null | undefined): WardleyPorterLetter | null;
|
|
24
|
+
/**
|
|
25
|
+
* The competition value a circle's text states, `null` when it states none.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately forgiving in exactly two ways and strict everywhere else. It
|
|
28
|
+
* trims — the shape editor keeps whatever whitespace the author left — and it
|
|
29
|
+
* upper-cases, because `l` is the same force as `L` and refusing it would make
|
|
30
|
+
* the qualification depend on a shift key. It then demands EXACTLY one of the
|
|
31
|
+
* three letters: `RL` is not a force, `X` is not a force, and neither is an
|
|
32
|
+
* empty circle. Anything else clears the tag rather than picking the nearest
|
|
33
|
+
* value, because a glyph nobody can read must not be reported as a force
|
|
34
|
+
* somebody named.
|
|
35
|
+
*/
|
|
36
|
+
export declare function competitionOfPorterLetter(text: string | null | undefined): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WARDLEY_COMPETITION } from './natures.js';
|
|
2
|
+
const LETTER_OF_VALUE = {
|
|
3
|
+
[WARDLEY_COMPETITION.relative]: 'R',
|
|
4
|
+
[WARDLEY_COMPETITION.struggle]: 'L',
|
|
5
|
+
[WARDLEY_COMPETITION.establish]: 'E',
|
|
6
|
+
};
|
|
7
|
+
const VALUE_OF_LETTER = {
|
|
8
|
+
R: WARDLEY_COMPETITION.relative,
|
|
9
|
+
L: WARDLEY_COMPETITION.struggle,
|
|
10
|
+
E: WARDLEY_COMPETITION.establish,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* What a competition value is written as, `null` when it is not one of the
|
|
14
|
+
* three.
|
|
15
|
+
*
|
|
16
|
+
* Total over anything a DOCUMENT can carry, not just over the ids this pack
|
|
17
|
+
* declares: defs are runtime configuration and are never persisted, so a board
|
|
18
|
+
* may legitimately hold a value whose pack was removed or renamed. Such a value
|
|
19
|
+
* has no letter, and the watcher leaves the circle alone rather than guessing
|
|
20
|
+
* one.
|
|
21
|
+
*/
|
|
22
|
+
export function porterLetterOfCompetition(valueId) {
|
|
23
|
+
if (typeof valueId !== 'string')
|
|
24
|
+
return null;
|
|
25
|
+
// A null-prototype lookup would be tidier, but the table is a literal and
|
|
26
|
+
// `Object.hasOwn` is what stops `'toString'` resolving to a letter.
|
|
27
|
+
return Object.hasOwn(LETTER_OF_VALUE, valueId)
|
|
28
|
+
? LETTER_OF_VALUE[valueId]
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The competition value a circle's text states, `null` when it states none.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately forgiving in exactly two ways and strict everywhere else. It
|
|
35
|
+
* trims — the shape editor keeps whatever whitespace the author left — and it
|
|
36
|
+
* upper-cases, because `l` is the same force as `L` and refusing it would make
|
|
37
|
+
* the qualification depend on a shift key. It then demands EXACTLY one of the
|
|
38
|
+
* three letters: `RL` is not a force, `X` is not a force, and neither is an
|
|
39
|
+
* empty circle. Anything else clears the tag rather than picking the nearest
|
|
40
|
+
* value, because a glyph nobody can read must not be reported as a force
|
|
41
|
+
* somebody named.
|
|
42
|
+
*/
|
|
43
|
+
export function competitionOfPorterLetter(text) {
|
|
44
|
+
if (typeof text !== 'string')
|
|
45
|
+
return null;
|
|
46
|
+
const letter = text.trim().toUpperCase();
|
|
47
|
+
return Object.hasOwn(VALUE_OF_LETTER, letter)
|
|
48
|
+
? VALUE_OF_LETTER[letter]
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { type WardleyNodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
/**
|
|
3
|
+
* What a Wardley artefact IS, as props — the ONE description the creation sites
|
|
4
|
+
* and the morph both read.
|
|
5
|
+
*
|
|
6
|
+
* Split out of `actions.ts` when the morph landed, for the reason C4's and
|
|
7
|
+
* BPMN's `presets.ts` give: a kind's appearance is written by the preset of the
|
|
8
|
+
* kind it was CREATED as and nothing else ever rewrites it, so a morph that
|
|
9
|
+
* restated the table would agree with the palette the day it was written and
|
|
10
|
+
* drift on the first restyle. Derived, they cannot — a component morphed into a
|
|
11
|
+
* market and one drawn from the sub-menu are the same element.
|
|
12
|
+
*
|
|
13
|
+
* The three pieces that are not artefacts live here too, and for the same
|
|
14
|
+
* reason: the pipeline's HANDLE, the market's three neutral DOTS and the
|
|
15
|
+
* triangle that wires them are glyph parts, and the morph has to be able to
|
|
16
|
+
* build and unbuild them exactly as `createWardleyMarket` /
|
|
17
|
+
* `createWardleyPipeline` build them once.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Every kind a user can draw — {@link WardleyNodeKind} minus the `handle`.
|
|
21
|
+
*
|
|
22
|
+
* The handle is not an artefact: it is the square that straddles a pipeline's
|
|
23
|
+
* top edge so connectors have somewhere to land (the body itself declares
|
|
24
|
+
* `connectable === false`). Nobody picks one from the palette, nothing may morph
|
|
25
|
+
* into one, and {@link wardleyHandleProps} is how the two composites that need
|
|
26
|
+
* one ask for it.
|
|
27
|
+
*/
|
|
28
|
+
export type WardleyArtefactKind = Exclude<WardleyNodeKind, 'handle'>;
|
|
29
|
+
/**
|
|
30
|
+
* The size a kind is BORN at, which for Wardley is the notation rather than a
|
|
31
|
+
* default.
|
|
32
|
+
*
|
|
33
|
+
* A market is 30 across and a component is 18 because that is what tells a
|
|
34
|
+
* reader which is which — the two are the same white circle otherwise. Same for
|
|
35
|
+
* the ecosystem's 40 and the pipeline's 120×25 bar. This is the table the morph
|
|
36
|
+
* re-centres an artefact on, and the one the palette draws from.
|
|
37
|
+
*/
|
|
38
|
+
export declare const WARDLEY_NODE_SIZE: Record<WardleyArtefactKind, {
|
|
39
|
+
w: number;
|
|
40
|
+
h: number;
|
|
41
|
+
}>;
|
|
42
|
+
/** The two shapes one area kind is drawn as: a plain zone, or one with corners. */
|
|
43
|
+
export type WardleyAreaShape = 'rect' | 'polygon';
|
|
44
|
+
/**
|
|
45
|
+
* The size an area is born at, per shape.
|
|
46
|
+
*
|
|
47
|
+
* Beside {@link WARDLEY_NODE_SIZE} rather than in it, because the table above
|
|
48
|
+
* is keyed by KIND and these two artefacts are one kind: a rectangle 240 × 160
|
|
49
|
+
* covers a band of the map, and the polygon is born square so its default
|
|
50
|
+
* pentagon is regular. `WARDLEY_NODE_SIZE.area` is the rect's, so anything that
|
|
51
|
+
* asks a kind for its canonical box — the morph does — still gets an answer.
|
|
52
|
+
*/
|
|
53
|
+
export declare const WARDLEY_AREA_SIZE: Record<WardleyAreaShape, {
|
|
54
|
+
w: number;
|
|
55
|
+
h: number;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Every kind that is created NEXT TO a name — {@link WardleyArtefactKind} minus
|
|
59
|
+
* the `porter` and the `area`.
|
|
60
|
+
*
|
|
61
|
+
* Those two are the artefacts with no label ELEMENT at all, and narrowing the
|
|
62
|
+
* key type is how that is said rather than an empty string parked in the table
|
|
63
|
+
* below. An empty string would be a name the author has not typed yet, which is
|
|
64
|
+
* exactly what the placeholders ARE and exactly what the morph is allowed to
|
|
65
|
+
* rewrite; a force's letter is the notation and belongs to nobody's vocabulary,
|
|
66
|
+
* and a zone's name is written INSIDE the zone, so a placeholder there would be
|
|
67
|
+
* a word the author has to delete before typing their own.
|
|
68
|
+
*/
|
|
69
|
+
export type WardleyLabelledKind = Exclude<WardleyArtefactKind, 'porter' | 'area'>;
|
|
70
|
+
/**
|
|
71
|
+
* The words a kind is created NEXT TO — the prompt an artefact nobody has named
|
|
72
|
+
* still carries, and the only string a morph is ever allowed to rewrite.
|
|
73
|
+
*/
|
|
74
|
+
export declare const WARDLEY_NODE_LABEL: Record<WardleyLabelledKind, string>;
|
|
75
|
+
/** A box centred on a point — how every Wardley creation site places a node. */
|
|
76
|
+
export declare function wardleyCenteredBox(cx: number, cy: number, w: number, h: number): string;
|
|
77
|
+
/** The canonical box of one kind, centred where the artefact already stands. */
|
|
78
|
+
export declare function wardleyCanonicalBox(kind: WardleyArtefactKind, cx: number, cy: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* Every prop a Wardley node is created with, for one kind and one box.
|
|
81
|
+
*
|
|
82
|
+
* Most of the pack is a native ELLIPSE and differs only in fill and size: the
|
|
83
|
+
* glyph — the anchor's silhouette, the ecosystem's hatched donut, the method's
|
|
84
|
+
* inner circle — is drawn by the renderer from `kind`, off this same model, so
|
|
85
|
+
* stroke and fill stay editable from the shape toolbar exactly like any other
|
|
86
|
+
* shape's. Two families are the exception, and they are the reason `clearOf`
|
|
87
|
+
* exists on this pack: the pipeline and the area are RECTS and the only kinds
|
|
88
|
+
* that write `radius`, and the accelerator / decelerator are POLYGONS — the
|
|
89
|
+
* only kinds that write `vertices` and `isClosed` from here.
|
|
90
|
+
*
|
|
91
|
+
* No `text`: a Wardley label is a separate free-text element grouped with the
|
|
92
|
+
* node (see `addLabel` in `actions.ts`), never words stored on the shape. The
|
|
93
|
+
* porter and the area are the two exceptions, and neither writes it HERE: their
|
|
94
|
+
* inner text is content the author types, added at the creation site.
|
|
95
|
+
*/
|
|
96
|
+
export declare function wardleyNodeProps(kind: WardleyArtefactKind, box: {
|
|
97
|
+
xywh: string;
|
|
98
|
+
}): Record<string, unknown> & {
|
|
99
|
+
type: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* An AREA, as props — one kind, two shapes.
|
|
103
|
+
*
|
|
104
|
+
* The second discriminator this pack needed, and the reason it is a parameter
|
|
105
|
+
* rather than a second `kind`: a rectangular zone and a polygonal one are the
|
|
106
|
+
* same statement about the map ("all of this is one thing"), drawn with a
|
|
107
|
+
* different number of corners. A second kind would have to be kept in step in
|
|
108
|
+
* the role table, the legend, the export and the morph's key union, for a
|
|
109
|
+
* difference the `shapeType` already carries.
|
|
110
|
+
*
|
|
111
|
+
* Everything but the outline comes from {@link wardleyNodeProps}, which is the
|
|
112
|
+
* house rule: the fill, the rim and the role are the area's, wherever it is
|
|
113
|
+
* built from. What is added here is the two shapes and the fact that an area's
|
|
114
|
+
* NAME is its own inner text.
|
|
115
|
+
*
|
|
116
|
+
* ## The text is the name, and the zone never grows to fit it
|
|
117
|
+
*
|
|
118
|
+
* `TextFitMode.Overflow`, exactly as the porter's letter and the inertia bar:
|
|
119
|
+
* an area is a boundary drawn around real components, so a long name must never
|
|
120
|
+
* push that boundary out and swallow a component the author did not mean to
|
|
121
|
+
* include. Top-left, because a zone's name belongs in its corner rather than
|
|
122
|
+
* across the middle of the map it covers — and no `text` at all, so the editor
|
|
123
|
+
* a double-click opens starts on an empty line.
|
|
124
|
+
*/
|
|
125
|
+
export declare function wardleyAreaProps(shape: WardleyAreaShape, box: {
|
|
126
|
+
xywh: string;
|
|
127
|
+
}): Record<string, unknown> & {
|
|
128
|
+
type: string;
|
|
129
|
+
};
|
|
130
|
+
/** Where an area of the given shape is born, centred on a point. */
|
|
131
|
+
export declare function wardleyAreaBox(shape: WardleyAreaShape, cx: number, cy: number): string;
|
|
132
|
+
/**
|
|
133
|
+
* The pipeline's handle: a node-sized square straddling the body's top edge, and
|
|
134
|
+
* the only place a connector may land on a pipeline.
|
|
135
|
+
*
|
|
136
|
+
* It inherits `centerAnchorOnly` from the model like every other Wardley node,
|
|
137
|
+
* and it carries `wardley:handle` — a role of its own, so nothing that judges
|
|
138
|
+
* components ever judges it.
|
|
139
|
+
*/
|
|
140
|
+
export declare function wardleyHandleProps(box: {
|
|
141
|
+
xywh: string;
|
|
142
|
+
}): Record<string, unknown> & {
|
|
143
|
+
type: string;
|
|
144
|
+
};
|
|
145
|
+
/** Where a handle sits, given the body's centre: astride the top edge. */
|
|
146
|
+
export declare function wardleyHandleBox(cx: number, cy: number): string;
|
|
147
|
+
/**
|
|
148
|
+
* One of the market's three inner dots — thicker-bordered, and deliberately
|
|
149
|
+
* ROLE-LESS.
|
|
150
|
+
*
|
|
151
|
+
* They are the GLYPH's own wiring rather than artefacts the user placed, the
|
|
152
|
+
* same reason the triangle between them carries no role: they sit inside the
|
|
153
|
+
* market circle by construction, so a role would make every market composite
|
|
154
|
+
* report an overlap with itself (W3).
|
|
155
|
+
*/
|
|
156
|
+
export declare function wardleyMarketDotProps(box: {
|
|
157
|
+
xywh: string;
|
|
158
|
+
}): Record<string, unknown> & {
|
|
159
|
+
type: string;
|
|
160
|
+
};
|
|
161
|
+
/** The three dot boxes of a market centred on (cx, cy) — an upright triangle. */
|
|
162
|
+
export declare function wardleyMarketDotBoxes(cx: number, cy: number): string[];
|
|
163
|
+
/**
|
|
164
|
+
* One side of the market's triangle: an ATTACHED connector, so it auto-routes
|
|
165
|
+
* centre to centre and follows the dots on move and resize.
|
|
166
|
+
*/
|
|
167
|
+
export declare function wardleyMarketLinkProps(sourceId: string, targetId: string): Record<string, unknown> & {
|
|
168
|
+
type: string;
|
|
169
|
+
};
|
|
170
|
+
/** The pairs the triangle wires, given the three dots in creation order. */
|
|
171
|
+
export declare function wardleyMarketLinkPairs<T>(dots: readonly T[]): [T, T][];
|
|
172
|
+
/** One of the four arrows: its own axis-aligned box, and the outline in it. */
|
|
173
|
+
export interface WardleyPorterArrow {
|
|
174
|
+
/** Serialized `xywh` of the box the polygon is drawn inside. */
|
|
175
|
+
xywh: string;
|
|
176
|
+
/** The outline as normalized [0-1] coordinates of that box, tip first. */
|
|
177
|
+
vertices: number[][];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The four arrows of a Porter's-forces glyph, for a circle of the given radius
|
|
181
|
+
* centred on (cx, cy) — a box and an outline each.
|
|
182
|
+
*
|
|
183
|
+
* The ONE description of the geometry, so the creation site, the map legend and
|
|
184
|
+
* the tests read the same numbers rather than three sets that agreed the day
|
|
185
|
+
* they were written. Each arrow starts `PORTER_ARROW.gap` clear of the rim —
|
|
186
|
+
* the arrows push against the circle, they do not touch it — and runs
|
|
187
|
+
* `PORTER_ARROW.length` further out, tip at the far end.
|
|
188
|
+
*
|
|
189
|
+
* Every distance scales WITH the radius, and that is what makes a legend row the
|
|
190
|
+
* same drawing at another size: a 12-unit glyph carrying four map-sized arrows
|
|
191
|
+
* would not be a small Porter, it would be a circle somebody stabbed.
|
|
192
|
+
*/
|
|
193
|
+
export declare function wardleyPorterArrows(cx: number, cy: number, radius?: number): WardleyPorterArrow[];
|
|
194
|
+
/**
|
|
195
|
+
* One of those arrows as props: a filled POLYGON on a `wardleyNode`, and
|
|
196
|
+
* deliberately ROLE-LESS.
|
|
197
|
+
*
|
|
198
|
+
* A polygon and not a connector, which is the first fix the recette of #210
|
|
199
|
+
* asked for: a connector's triangle head is sized off its stroke width, so the
|
|
200
|
+
* heads arrived longer than the arrows and covered the circle, the letter and
|
|
201
|
+
* the double-click target. A polygon's outline is exactly the seven points
|
|
202
|
+
* above.
|
|
203
|
+
*
|
|
204
|
+
* A `wardleyNode` and not a plain `shape`, which is the second: recette v2
|
|
205
|
+
* found that `ShapeElementView` gives EVERY plain shape a double-click that
|
|
206
|
+
* mounts the inner-text editor, and mounting it grew an arrow from 24 units
|
|
207
|
+
* high to 44 — a deformation that survived Escape. An arrow is not a thing you
|
|
208
|
+
* write in. Posting it as a `wardleyNode` moves it onto `WardleyNodeView`,
|
|
209
|
+
* which opens an editor only on the artefact that HAS a letter; the renderer is
|
|
210
|
+
* unaffected, because a Wardley node draws itself through the native shape
|
|
211
|
+
* renderer and that renderer already dispatches on `shapeType`.
|
|
212
|
+
*
|
|
213
|
+
* Role-less, and `kind: 'porter'` with it, for the reason the market's three
|
|
214
|
+
* inner dots already give — they are `kind: 'component'` and carry no role: this
|
|
215
|
+
* is the glyph's own wiring rather than anything the author drew, so a role
|
|
216
|
+
* would make every composite report an overlap with itself (W3), would offer
|
|
217
|
+
* four arrows to the OWM writer, and would let a stray label bind to one.
|
|
218
|
+
* `undefined` writes no key at all, exactly as it does for those dots.
|
|
219
|
+
*
|
|
220
|
+
* Filled rather than stroked, so nothing about them is sized off a line width
|
|
221
|
+
* ever again.
|
|
222
|
+
*/
|
|
223
|
+
export declare function wardleyPorterArrowProps(arrow: WardleyPorterArrow): Record<string, unknown> & {
|
|
224
|
+
type: string;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* The letter inside the circle, as shape-text props.
|
|
228
|
+
*
|
|
229
|
+
* A plain STRING and not a `Y.Text`: `ShapeElementModel.propsToY` builds the
|
|
230
|
+
* Y type on the way into the document, and this package depends on yjs only in
|
|
231
|
+
* its tests. `TextFitMode.Overflow` is the load-bearing line — the glyph has a
|
|
232
|
+
* canonical size that says "external force" at a glance, so the circle must
|
|
233
|
+
* never grow to fit whatever the author types into it, exactly as the inertia
|
|
234
|
+
* bar never deforms around its own text.
|
|
235
|
+
*/
|
|
236
|
+
export declare function wardleyPorterLetterProps(letter: string, fontSize?: number): Record<string, unknown>;
|
|
237
|
+
/**
|
|
238
|
+
* What a kind is worth to a node that ALREADY EXISTS — {@link wardleyNodeProps}
|
|
239
|
+
* minus the three things a morph has no business touching.
|
|
240
|
+
*
|
|
241
|
+
* The whole preset and not `{kind, role}`, and on this pack it is visibly not a
|
|
242
|
+
* formality: a pipeline is a rect with square corners and a semi-transparent
|
|
243
|
+
* fill, and the other three members of the family are opaque white ellipses. A
|
|
244
|
+
* two-key patch would turn a component into a "pipeline" still drawn as a
|
|
245
|
+
* circle.
|
|
246
|
+
*/
|
|
247
|
+
export declare function wardleyMorphProps(kind: WardleyArtefactKind): Record<string, unknown>;
|
|
248
|
+
/**
|
|
249
|
+
* The kinds that can actually MORPH — the one family `morph.ts` declares.
|
|
250
|
+
*
|
|
251
|
+
* Restated here rather than imported, and that is a circular-import dodge with
|
|
252
|
+
* a real reason behind it: `morph.ts` reads this module for the presets, so
|
|
253
|
+
* this module cannot read it back. `morph.unit.spec.ts` asserts the two lists
|
|
254
|
+
* are the same set, so the restatement cannot drift.
|
|
255
|
+
*
|
|
256
|
+
* It exists because {@link wardleyMorphClears} needs a UNION, and a union over
|
|
257
|
+
* the whole pack is the wrong one. Since the accelerator and the decelerator
|
|
258
|
+
* joined it, "every key any kind writes" includes `vertices` and `isClosed` —
|
|
259
|
+
* keys of two POLYGONS that are in no family and that nothing may ever morph
|
|
260
|
+
* into. Left unrestricted, morphing a market back to a component would emit a
|
|
261
|
+
* delete for `vertices` on an ellipse that never had one: harmless today, and a
|
|
262
|
+
* patch that says something untrue about the pack the day a morphable kind
|
|
263
|
+
* gains an outline.
|
|
264
|
+
*/
|
|
265
|
+
export declare const WARDLEY_MORPHABLE_KINDS: readonly WardleyArtefactKind[];
|
|
266
|
+
/**
|
|
267
|
+
* The fields to DELETE after morphing to `kind` — the keys some other kind
|
|
268
|
+
* writes and this one does not.
|
|
269
|
+
*
|
|
270
|
+
* `radius` for every kind but the pipeline, and that is not a formality either:
|
|
271
|
+
* a patch cannot express absence, so a pipeline morphed back to a component
|
|
272
|
+
* would keep `radius: 0` sitting in the Y.Map, silently in force on an ellipse.
|
|
273
|
+
* Derived rather than listed, so the day a preset gains a conditional key it is
|
|
274
|
+
* covered without anybody remembering to come here.
|
|
275
|
+
*/
|
|
276
|
+
export declare function wardleyMorphClears(kind: WardleyArtefactKind): readonly string[];
|