@formicoidea/labre-framework-edgy 0.31.0 → 0.33.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 +39 -0
- package/dist/actions.js +193 -0
- package/dist/board-renderer.d.ts +12 -0
- package/dist/board-renderer.js +28 -0
- package/dist/board-view.d.ts +11 -0
- package/dist/board-view.js +20 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +103 -0
- package/dist/consts.d.ts +35 -0
- package/dist/consts.js +46 -0
- package/dist/descriptor.d.ts +8 -3
- package/dist/descriptor.js +6 -3
- package/dist/element-renderer.js +24 -13
- package/dist/element-view.js +9 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/label-layout.js +8 -2
- package/dist/legend.d.ts +2 -0
- package/dist/legend.js +109 -0
- package/dist/metamodel.d.ts +96 -0
- package/dist/metamodel.js +128 -0
- package/dist/nudges.d.ts +26 -0
- package/dist/nudges.js +54 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +66 -0
- package/dist/relation-resolver.d.ts +32 -0
- package/dist/relation-resolver.js +101 -0
- package/dist/relation.d.ts +142 -0
- package/dist/relation.js +188 -0
- package/dist/roles.d.ts +108 -0
- package/dist/roles.js +182 -0
- package/dist/rules.d.ts +43 -0
- package/dist/rules.js +153 -0
- package/dist/templates/index.d.ts +22 -1
- package/dist/templates/index.js +303 -52
- package/dist/toolbar/config.d.ts +33 -0
- package/dist/toolbar/config.js +82 -15
- package/dist/toolbar/edgy-menu.d.ts +7 -25
- package/dist/toolbar/edgy-menu.js +7 -186
- package/dist/toolbar/edgy-senior-button.js +8 -2
- package/dist/toolbar/icons.d.ts +12 -0
- package/dist/toolbar/icons.js +31 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/translations.d.ts +24 -0
- package/dist/translations.js +29 -0
- package/dist/view.d.ts +18 -0
- package/dist/view.js +95 -8
- package/package.json +6 -3
package/dist/rules.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { EDGY_DYNAMIC_RELATIONS } from './metamodel.js';
|
|
2
|
+
import { EDGY_ROLE, EDGY_ROLES, EDGY_VERB_ROLE, } from './roles.js';
|
|
3
|
+
/**
|
|
4
|
+
* EDGY validation rules (WS1).
|
|
5
|
+
*
|
|
6
|
+
* DATA owned by the framework, versioned per rule: the engine
|
|
7
|
+
* (`@labre/affine-block-surface`) knows how to evaluate a FAMILY, never a
|
|
8
|
+
* concrete rule. Adding an EDGY rule is adding an entry to this array.
|
|
9
|
+
*
|
|
10
|
+
* Registered from the flag-gated `EdgyViewExtension`, so switching the EDGY
|
|
11
|
+
* flag off removes the rules with the rest of the tooling — boards already
|
|
12
|
+
* drawn keep rendering, they simply stop being checked (`docs/adr/0009`).
|
|
13
|
+
*
|
|
14
|
+
* ## Two rules, and the ones deliberately left out
|
|
15
|
+
*
|
|
16
|
+
* The PO arbitration of 26/08/2026 put EDGY's two JUDGEMENT controls — "every
|
|
17
|
+
* intersection element is linked to both its parent facets" and "every element
|
|
18
|
+
* wears its facet's colour" — in `./nudges.ts` rather than here. Neither is
|
|
19
|
+
* decidable: the first would have to guess which circle an element belongs to
|
|
20
|
+
* from where somebody dropped it, and the second would indict a board whose
|
|
21
|
+
* author uses their own palette. A checklist can ask them; an algorithm cannot
|
|
22
|
+
* answer them.
|
|
23
|
+
*
|
|
24
|
+
* ## Debt: `tree-mixed-kinds`
|
|
25
|
+
*
|
|
26
|
+
* EDGY's own notation has TREES — a capability tree, an organisation tree —
|
|
27
|
+
* whose levels must all be the same kind of element. There is no tree on this
|
|
28
|
+
* canvas yet: nothing draws one, nothing stores one, and no role names one. The
|
|
29
|
+
* rule is therefore deferred rather than written against an artefact that does
|
|
30
|
+
* not exist. When a tree lands, it comes with its own role and this is where its
|
|
31
|
+
* rule goes.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The 24 sanctioned sentences, DERIVED from the metamodel.
|
|
35
|
+
*
|
|
36
|
+
* One triplet per row of `EDGY_DYNAMIC_RELATIONS`, read exactly as
|
|
37
|
+
* `docs/adr/0010` reads a typed edge: source is the subject, target the object.
|
|
38
|
+
* The matrix is never restated — a relation added to the metamodel is drawn by
|
|
39
|
+
* the template, gets a role from `roles.ts` and becomes legal here, all from the
|
|
40
|
+
* one table. `requires` contributes three triplets under one role, which is what
|
|
41
|
+
* it means for a verb to be spoken of three different pairs.
|
|
42
|
+
*/
|
|
43
|
+
export const EDGY_ALLOWED_RELATIONS = EDGY_DYNAMIC_RELATIONS.map(([source, target, verb]) => ({
|
|
44
|
+
source: EDGY_ROLE[source],
|
|
45
|
+
edge: EDGY_VERB_ROLE[verb],
|
|
46
|
+
target: EDGY_ROLE[target],
|
|
47
|
+
}));
|
|
48
|
+
/**
|
|
49
|
+
* **E1** — a relation says something the EDGY metamodel does not.
|
|
50
|
+
*
|
|
51
|
+
* EDGY is a language before it is a diagram: its 24 relations are the sentences
|
|
52
|
+
* the method sanctions, and a link drawn between the wrong two elements — or
|
|
53
|
+
* drawn the right way round between the right two but carrying the wrong verb —
|
|
54
|
+
* is a modelling mistake no amount of dragging fixes. "A journey traverses a
|
|
55
|
+
* channel" is EDGY; "a channel traverses a journey" is a sentence nobody can
|
|
56
|
+
* act on.
|
|
57
|
+
*
|
|
58
|
+
* ## What stays silent
|
|
59
|
+
*
|
|
60
|
+
* Everything the `relation-endpoints` family already keeps quiet about, and one
|
|
61
|
+
* of them matters more here than anywhere: **a plain connector carries no role
|
|
62
|
+
* and is never judged**. Free links are how an EDGY workshop actually runs —
|
|
63
|
+
* somebody draws an arrow to say "these two have something to do with each
|
|
64
|
+
* other" and names it later — and the day the tool starts indicting that
|
|
65
|
+
* gesture is the day it gets switched off. Only the typed relations the toolbox
|
|
66
|
+
* and the templates stamp are read, and only when BOTH ends are official
|
|
67
|
+
* elements: a relation onto a bare sticky, onto a People node or onto an
|
|
68
|
+
* element of another framework is outside the alphabet, hence outside the
|
|
69
|
+
* conversation.
|
|
70
|
+
*
|
|
71
|
+
* Severity `warning` and not `blocking-overridable`: nothing in this library
|
|
72
|
+
* refuses a gesture, so declaring the blocking level would be data claiming an
|
|
73
|
+
* effect that does not exist (the note `wardley/rules.ts` carries at length).
|
|
74
|
+
* The profiles demote it to `audit` on a sketch.
|
|
75
|
+
*/
|
|
76
|
+
const nonCanonicalLink = {
|
|
77
|
+
id: 'edgy.non-canonical-link',
|
|
78
|
+
framework: 'edgy',
|
|
79
|
+
family: 'relation-endpoints',
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
// No `appliesTo`: the subject is a RELATION, and the role that names it is
|
|
82
|
+
// declared where the family reads it — naming one of the three indicted
|
|
83
|
+
// elements here would be data that lies.
|
|
84
|
+
roles: EDGY_ROLES,
|
|
85
|
+
messageKey: 'com.labre.edgy.validation.non-canonical-link',
|
|
86
|
+
messageFallback: 'This relation is not one the EDGY metamodel declares between these two elements.',
|
|
87
|
+
suggestionKey: 'com.labre.edgy.validation.non-canonical-link.suggestion',
|
|
88
|
+
suggestionFallback: 'Read the link out loud — source, verb, target. Reverse it, re-point an end, or use the verb EDGY gives these two elements.',
|
|
89
|
+
version: 1,
|
|
90
|
+
provenance: {
|
|
91
|
+
source: 'recommendation',
|
|
92
|
+
reference: 'EDGY (Enterprise Design) — the 24 relations its metamodel sanctions',
|
|
93
|
+
},
|
|
94
|
+
// Not a frame the rule measures against — a sentence is right or wrong
|
|
95
|
+
// wherever it is written — but the facets diagram or board a finding is
|
|
96
|
+
// ATTRIBUTED to, so the arbitration "ignore this rule on the whole board" has
|
|
97
|
+
// somewhere to live (the `no-overlap` pattern).
|
|
98
|
+
backgroundRole: EDGY_ROLE.background,
|
|
99
|
+
endpoints: {
|
|
100
|
+
// The PARENT role: every verb specialises it, so the rule reads all 22 of
|
|
101
|
+
// them and no free connector.
|
|
102
|
+
edgeRole: EDGY_ROLE.relation,
|
|
103
|
+
allowed: EDGY_ALLOWED_RELATIONS,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* **E2** — two artefacts must not sit on top of each other.
|
|
108
|
+
*
|
|
109
|
+
* A readability rule, the same one Wardley ships and for the same reason: a
|
|
110
|
+
* board you cannot read is useless, while a momentary overlap during a drag is
|
|
111
|
+
* not a mistake. It matters more on an EDGY facets diagram than almost
|
|
112
|
+
* anywhere, because WHERE an element sits is what says which facet it belongs
|
|
113
|
+
* to: an artefact hidden under another has lost its meaning, not just its
|
|
114
|
+
* legibility.
|
|
115
|
+
*
|
|
116
|
+
* ONE declared pair, `element × element`, because `roleIsA` covers the whole
|
|
117
|
+
* subtree — the four kinds and the twelve official elements — from the root.
|
|
118
|
+
* Backgrounds are outside that subtree by construction, so an element sitting on
|
|
119
|
+
* the diagram it belongs to is never a collision.
|
|
120
|
+
*
|
|
121
|
+
* `minPenetration: 4` model units, the calibration Wardley's W3 arrived at on
|
|
122
|
+
* the recette of 01/08/2026: under a twentieth of an EDGY node's 80-unit height
|
|
123
|
+
* and about twice a connector's stroke, so two boxes whose corners share a hair
|
|
124
|
+
* are silent while a box genuinely covering another is not. Verified against
|
|
125
|
+
* every layout this package ships: the twelve nodes of the EDGY dynamic
|
|
126
|
+
* template — the only stamped layout, hence the only one this rule can see —
|
|
127
|
+
* clear each other by 80 model units at the closest, twenty times the
|
|
128
|
+
* threshold.
|
|
129
|
+
*/
|
|
130
|
+
const overlappingArtefacts = {
|
|
131
|
+
id: 'edgy.overlapping-artefacts',
|
|
132
|
+
framework: 'edgy',
|
|
133
|
+
family: 'no-overlap',
|
|
134
|
+
severity: 'warning',
|
|
135
|
+
// No `appliesTo`: the subjects of a `no-overlap` rule are the pairs below.
|
|
136
|
+
roles: EDGY_ROLES,
|
|
137
|
+
messageKey: 'com.labre.edgy.validation.overlapping-artefacts',
|
|
138
|
+
messageFallback: 'These two overlap and make the board harder to read.',
|
|
139
|
+
suggestionKey: 'com.labre.edgy.validation.overlapping-artefacts.suggestion',
|
|
140
|
+
suggestionFallback: 'Move one of them aside — on a facets diagram, where an element sits is what says which facet it belongs to.',
|
|
141
|
+
version: 1,
|
|
142
|
+
provenance: {
|
|
143
|
+
source: 'labre-convention',
|
|
144
|
+
reference: 'Labre readability convention — no EDGY rule speaks of overlapping ink',
|
|
145
|
+
},
|
|
146
|
+
backgroundRole: EDGY_ROLE.background,
|
|
147
|
+
overlap: [[EDGY_ROLE.element, EDGY_ROLE.element]],
|
|
148
|
+
minPenetration: 4,
|
|
149
|
+
};
|
|
150
|
+
export const EDGY_RULES = [
|
|
151
|
+
nonCanonicalLink,
|
|
152
|
+
overlappingArtefacts,
|
|
153
|
+
];
|
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
import { type TemplateCategory } from '@formicoidea/labre-core/gfx/template';
|
|
1
|
+
import { type Template, type TemplateCategory } from '@formicoidea/labre-core/gfx/template';
|
|
2
|
+
/**
|
|
3
|
+
* Background scale (from REF coords to model coords): large enough that the
|
|
4
|
+
* default-size nodes breathe inside each zone. The background is cropped to
|
|
5
|
+
* the circles (`cropToCircles`), so its xywh covers CROP × DYN_SCALE.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DYN_SCALE = 4.8;
|
|
8
|
+
/**
|
|
9
|
+
* The metamodel itself — the 12 elements and the 24 relations — now lives in
|
|
10
|
+
* `../metamodel.ts`, so the role vocabulary can DERIVE from it without this
|
|
11
|
+
* module and that one importing each other. Re-exported here under the names
|
|
12
|
+
* they have always had: nothing that reads them had to change.
|
|
13
|
+
*/
|
|
14
|
+
export { EDGY_DYNAMIC_NODES, EDGY_DYNAMIC_RELATIONS, type EdgyElementName, } from '../metamodel.js';
|
|
15
|
+
/**
|
|
16
|
+
* Reference coords → template model coords: the background element sits at
|
|
17
|
+
* (0,0) and renders the cropped circles box, so a reference point maps to
|
|
18
|
+
* `(p - CROP.origin) × DYN_SCALE`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function dynToModel(refX: number, refY: number): [number, number];
|
|
21
|
+
/** Exported for direct insertion from the EDGY senior toolbar menu. */
|
|
22
|
+
export declare const edgyDynamicTemplate: Template;
|
|
2
23
|
export declare const edgyTemplateCategory: TemplateCategory;
|
package/dist/templates/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { makeTemplateSnapshot, surfaceText, } from '@formicoidea/labre-core/gfx/template';
|
|
2
2
|
import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextAlign, } from '@formicoidea/labre-core/model';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { CROP, CROP_LABELED } from '../consts.js';
|
|
4
|
+
import { EDGY_DYNAMIC_NODES, EDGY_DYNAMIC_RELATIONS, EDGY_ZONE_FILL, edgyElementLabel, } from '../metamodel.js';
|
|
5
|
+
import { EDGY_RELATION_LABEL_DISTANCE, edgyVerbLabelXYWH } from '../relation.js';
|
|
6
|
+
import { EDGY_ROLE, EDGY_VERB_ROLE } from '../roles.js';
|
|
7
|
+
import { ACTIVITY_VERTICES, INNER_FONT_SIZE, NODE_FILL, NODE_SIZE, NODE_STROKE, NODE_STROKE_WIDTH, OUTCOME_RADIUS, } from '../node/consts.js';
|
|
5
8
|
// EDGY facet palette (header / pale sub-card).
|
|
6
9
|
const C = {
|
|
7
10
|
identity: ['#1ec873', '#9fe6c2'],
|
|
@@ -35,7 +38,15 @@ function rect(x, y, w, h, opts = {}) {
|
|
|
35
38
|
}
|
|
36
39
|
return el;
|
|
37
40
|
}
|
|
38
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* An EDGY node (kind drives the native shape): outcome/object box, people,
|
|
43
|
+
* activity chevron.
|
|
44
|
+
*
|
|
45
|
+
* `role` is optional and writes NOTHING when absent: the illustrative templates
|
|
46
|
+
* (journey, blueprint, org chart) stay neutral drawings the engine never looks
|
|
47
|
+
* at, exactly as they were before roles existed. Only the EDGY dynamic template
|
|
48
|
+
* — the one that IS the metamodel — stamps its elements.
|
|
49
|
+
*/
|
|
39
50
|
function enode(kind, x, y, w, h, opts = {}) {
|
|
40
51
|
const el = {
|
|
41
52
|
type: 'edgyNode',
|
|
@@ -52,6 +63,9 @@ function enode(kind, x, y, w, h, opts = {}) {
|
|
|
52
63
|
};
|
|
53
64
|
if (kind === 'activity')
|
|
54
65
|
el.vertices = ACTIVITY_VERTICES;
|
|
66
|
+
// `undefined` writes nothing: a neutral node keeps no `role` key.
|
|
67
|
+
if (opts.role !== undefined)
|
|
68
|
+
el.role = opts.role;
|
|
55
69
|
if (opts.text != null) {
|
|
56
70
|
el.text = surfaceText(opts.text);
|
|
57
71
|
el.color = opts.textColor ?? NODE_STROKE;
|
|
@@ -85,22 +99,76 @@ function line(x1, y1, x2, y2, opts = {}) {
|
|
|
85
99
|
target: { position: [x2, y2] },
|
|
86
100
|
};
|
|
87
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Connector ATTACHED to two template elements (by key): endpoints clip to the
|
|
104
|
+
* element edges and follow moves — unlike `line()`, whose free positions are
|
|
105
|
+
* only right until the user drags something.
|
|
106
|
+
*/
|
|
107
|
+
function attach(src, dst, opts = {}) {
|
|
108
|
+
return {
|
|
109
|
+
type: 'connector',
|
|
110
|
+
mode: opts.mode ?? ConnectorMode.Straight,
|
|
111
|
+
stroke: NODE_STROKE,
|
|
112
|
+
strokeWidth: 2,
|
|
113
|
+
strokeStyle: StrokeStyle.Solid,
|
|
114
|
+
frontEndpointStyle: PointStyle.None,
|
|
115
|
+
rearEndpointStyle: opts.arrow ? PointStyle.Triangle : PointStyle.None,
|
|
116
|
+
source: { id: src },
|
|
117
|
+
target: { id: dst },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
88
120
|
// ── Facets overview (six facets, sub-cards, facets/intersections) ─────
|
|
89
121
|
function facetsOverview() {
|
|
90
122
|
const out = {};
|
|
91
123
|
const tall = (key, x, name, cards) => {
|
|
92
124
|
const [hdr, sub] = C[key];
|
|
93
|
-
out[`${key}F`] = rect(x, 140, 192, 460, {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
125
|
+
out[`${key}F`] = rect(x, 140, 192, 460, {
|
|
126
|
+
fill: hdr,
|
|
127
|
+
stroke: hdr,
|
|
128
|
+
radius: 16,
|
|
129
|
+
});
|
|
130
|
+
out[`${key}N`] = label(x, 168, 192, 28, name, {
|
|
131
|
+
color: '#ffffff',
|
|
132
|
+
fontSize: 18,
|
|
133
|
+
});
|
|
134
|
+
out[`${key}1`] = rect(x + 16, 208, 160, 80, {
|
|
135
|
+
fill: sub,
|
|
136
|
+
stroke: sub,
|
|
137
|
+
radius: 6,
|
|
138
|
+
text: cards[0],
|
|
139
|
+
fontSize: 16,
|
|
140
|
+
});
|
|
141
|
+
out[`${key}2`] = rect(x + 16, 300, 160, 80, {
|
|
142
|
+
fill: sub,
|
|
143
|
+
stroke: sub,
|
|
144
|
+
radius: 6,
|
|
145
|
+
text: cards[1],
|
|
146
|
+
fontSize: 16,
|
|
147
|
+
});
|
|
148
|
+
out[`${key}3`] = enode('activity', x + 16, 392, 160, 84, {
|
|
149
|
+
fill: sub,
|
|
150
|
+
text: cards[2],
|
|
151
|
+
fontSize: 16,
|
|
152
|
+
});
|
|
98
153
|
};
|
|
99
154
|
const low = (key, x, name) => {
|
|
100
155
|
const [hdr, sub] = C[key];
|
|
101
|
-
out[`${key}F`] = rect(x, 300, 192, 424, {
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
out[`${key}F`] = rect(x, 300, 192, 424, {
|
|
157
|
+
fill: hdr,
|
|
158
|
+
stroke: hdr,
|
|
159
|
+
radius: 16,
|
|
160
|
+
});
|
|
161
|
+
out[`${key}1`] = rect(x + 16, 360, 160, 92, {
|
|
162
|
+
fill: sub,
|
|
163
|
+
stroke: sub,
|
|
164
|
+
radius: 6,
|
|
165
|
+
text: name,
|
|
166
|
+
fontSize: 16,
|
|
167
|
+
});
|
|
168
|
+
out[`${key}N`] = label(x, 686, 192, 28, name, {
|
|
169
|
+
color: '#ffffff',
|
|
170
|
+
fontSize: 18,
|
|
171
|
+
});
|
|
104
172
|
};
|
|
105
173
|
// lower facets first (drawn behind the tall ones at the overlaps)
|
|
106
174
|
low('organisation', 240, 'Organisation');
|
|
@@ -117,39 +185,114 @@ function facetsOverview() {
|
|
|
117
185
|
}
|
|
118
186
|
// ── Customer journey ──────────────────────────────────────────────────
|
|
119
187
|
function journey() {
|
|
120
|
-
const step = (i, x) => enode('activity', x, 132, 224, 116, {
|
|
121
|
-
|
|
122
|
-
|
|
188
|
+
const step = (i, x) => enode('activity', x, 132, 224, 116, {
|
|
189
|
+
fill: JOURNEY_PINK,
|
|
190
|
+
text: `Journey step ${i}`,
|
|
191
|
+
textColor: '#ffffff',
|
|
192
|
+
fontSize: 18,
|
|
193
|
+
});
|
|
194
|
+
const ch = (x, n) => enode('object', x, 392, 184, 96, {
|
|
195
|
+
fill: JOURNEY_PINK,
|
|
196
|
+
text: `Channel ${n}`,
|
|
197
|
+
fontSize: 16,
|
|
198
|
+
});
|
|
199
|
+
const tk = (x, n) => enode('object', x, 540, 168, 92, {
|
|
200
|
+
fill: JOURNEY_PINK,
|
|
201
|
+
text: `Task ${n}`,
|
|
202
|
+
fontSize: 16,
|
|
203
|
+
});
|
|
123
204
|
return {
|
|
124
205
|
cust: enode('people', 40, 120, 64, 64, { fill: '#ffffff' }),
|
|
125
206
|
custL: label(20, 192, 104, 24, 'Customer', { fontSize: 14 }),
|
|
126
|
-
band: {
|
|
127
|
-
|
|
207
|
+
band: {
|
|
208
|
+
type: 'shape',
|
|
209
|
+
shapeType: 'polygon',
|
|
210
|
+
vertices: [
|
|
211
|
+
[0, 0],
|
|
212
|
+
[0.86, 0],
|
|
213
|
+
[1, 0.5],
|
|
214
|
+
[0.86, 1],
|
|
215
|
+
[0, 1],
|
|
216
|
+
],
|
|
217
|
+
filled: true,
|
|
218
|
+
fillColor: JOURNEY_PINK,
|
|
219
|
+
strokeColor: JOURNEY_PINK,
|
|
220
|
+
strokeWidth: 1,
|
|
221
|
+
shapeStyle: ShapeStyle.General,
|
|
222
|
+
roughness: 0,
|
|
223
|
+
xywh: '[150,108,860,164]',
|
|
224
|
+
},
|
|
225
|
+
bandL: label(170, 116, 120, 24, 'Journey', {
|
|
226
|
+
color: '#ffffff',
|
|
227
|
+
fontSize: 16,
|
|
228
|
+
align: TextAlign.Left,
|
|
229
|
+
}),
|
|
128
230
|
s1: step(1, 240),
|
|
129
231
|
s2: step(2, 500),
|
|
130
232
|
s3: step(3, 760),
|
|
131
|
-
rightTask: enode('object', 1060, 132, 168, 92, {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
233
|
+
rightTask: enode('object', 1060, 132, 168, 92, {
|
|
234
|
+
text: 'Task',
|
|
235
|
+
fontSize: 16,
|
|
236
|
+
}),
|
|
237
|
+
c1: ch(258, 'X'),
|
|
238
|
+
c2: ch(518, 'Y'),
|
|
239
|
+
c3: ch(778, 'Z'),
|
|
240
|
+
t1: tk(266, 'A'),
|
|
241
|
+
t2: tk(526, 'B'),
|
|
242
|
+
t3: tk(786, 'C'),
|
|
243
|
+
trav1: label(258, 350, 184, 20, 'traverses', {
|
|
244
|
+
fontSize: 13,
|
|
245
|
+
color: '#5f6368',
|
|
246
|
+
}),
|
|
247
|
+
trav2: label(518, 350, 184, 20, 'traverses', {
|
|
248
|
+
fontSize: 13,
|
|
249
|
+
color: '#5f6368',
|
|
250
|
+
}),
|
|
251
|
+
trav3: label(778, 350, 184, 20, 'traverses', {
|
|
252
|
+
fontSize: 13,
|
|
253
|
+
color: '#5f6368',
|
|
254
|
+
}),
|
|
137
255
|
use1: label(258, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
|
|
138
256
|
use2: label(518, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
|
|
139
257
|
use3: label(778, 504, 184, 20, 'uses', { fontSize: 13, color: '#5f6368' }),
|
|
140
|
-
l1:
|
|
141
|
-
|
|
258
|
+
l1: attach('s1', 'c1'),
|
|
259
|
+
l2: attach('s2', 'c2'),
|
|
260
|
+
l3: attach('s3', 'c3'),
|
|
261
|
+
l4: attach('c1', 't1'),
|
|
262
|
+
l5: attach('c2', 't2'),
|
|
263
|
+
l6: attach('c3', 't3'),
|
|
142
264
|
};
|
|
143
265
|
}
|
|
144
266
|
// ── Service blueprint (six swimlanes) ─────────────────────────────────
|
|
145
267
|
function blueprint() {
|
|
146
|
-
const lanes = [
|
|
147
|
-
|
|
268
|
+
const lanes = [
|
|
269
|
+
'Physical Evidence',
|
|
270
|
+
'Customer Actions',
|
|
271
|
+
'On-stage Actions',
|
|
272
|
+
'Back-stage Actions',
|
|
273
|
+
'Support Processes',
|
|
274
|
+
'Support Systems',
|
|
275
|
+
];
|
|
276
|
+
const laneFill = [
|
|
277
|
+
'#fdeef2',
|
|
278
|
+
'#fbd5e0',
|
|
279
|
+
'#dff0fb',
|
|
280
|
+
'#d4e9f8',
|
|
281
|
+
'#d4e9f8',
|
|
282
|
+
'#d4e9f8',
|
|
283
|
+
];
|
|
148
284
|
const out = {};
|
|
149
285
|
lanes.forEach((name, i) => {
|
|
150
286
|
const y = 40 + i * 150;
|
|
151
|
-
out[`lane${i}`] = rect(36, y, 1280, 150, {
|
|
152
|
-
|
|
287
|
+
out[`lane${i}`] = rect(36, y, 1280, 150, {
|
|
288
|
+
fill: laneFill[i],
|
|
289
|
+
stroke: laneFill[i],
|
|
290
|
+
sw: 0,
|
|
291
|
+
});
|
|
292
|
+
out[`laneL${i}`] = label(52, y + 12, 260, 22, name, {
|
|
293
|
+
fontSize: 15,
|
|
294
|
+
align: TextAlign.Left,
|
|
295
|
+
});
|
|
153
296
|
});
|
|
154
297
|
const chev = (x, y, fill, t) => enode('activity', x, y, 200, 60, { fill, text: t, fontSize: 14 });
|
|
155
298
|
const box = (x, y, fill, t) => enode('object', x, y, 200, 60, { fill, text: t, fontSize: 14 });
|
|
@@ -157,19 +300,27 @@ function blueprint() {
|
|
|
157
300
|
Object.assign(out, {
|
|
158
301
|
af: rect(280, 64, 220, 70, { text: 'Admission Form' }),
|
|
159
302
|
cf: rect(1010, 64, 220, 70, { text: 'Confirmation' }),
|
|
160
|
-
sf: chev(280, 214, P, 'Send Form'),
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
303
|
+
sf: chev(280, 214, P, 'Send Form'),
|
|
304
|
+
fs: chev(600, 214, P, 'Follow status'),
|
|
305
|
+
rd: chev(1010, 214, P, 'Receive decision'),
|
|
306
|
+
fh: chev(280, 364, B, 'Form handling'),
|
|
307
|
+
cs: chev(600, 364, B, 'Customer support'),
|
|
308
|
+
ct: chev(1010, 364, B, 'Contact'),
|
|
309
|
+
csv: chev(280, 514, B, 'Customer service'),
|
|
310
|
+
cfu: box(1010, 514, B, 'Customer follow-up'),
|
|
311
|
+
pf: chev(600, 664, B, 'Process Form'),
|
|
312
|
+
dec: chev(900, 664, B, 'Decision [Accept|Reject]'),
|
|
313
|
+
crm: box(280, 814, B, 'CRM Application'),
|
|
314
|
+
cms: box(600, 814, B, 'Case Management'),
|
|
315
|
+
pay: box(900, 814, B, 'Payments System'),
|
|
316
|
+
a1: attach('af', 'sf', { arrow: true }),
|
|
317
|
+
a2: attach('sf', 'fs', { arrow: true }),
|
|
318
|
+
a3: attach('fs', 'rd', { arrow: true }),
|
|
319
|
+
a4: attach('sf', 'fh', { arrow: true }),
|
|
320
|
+
a5: attach('fh', 'csv', { arrow: true }),
|
|
321
|
+
a6: attach('pf', 'dec', { arrow: true }),
|
|
322
|
+
a7: attach('crm', 'cms', { arrow: true }),
|
|
323
|
+
a8: attach('cms', 'pay', { arrow: true }),
|
|
173
324
|
});
|
|
174
325
|
return out;
|
|
175
326
|
}
|
|
@@ -179,20 +330,111 @@ function orgChart() {
|
|
|
179
330
|
const u = (x, y, w, t) => enode('object', x, y, w, 64, { fill: cyan, text: t, fontSize: 16 });
|
|
180
331
|
return {
|
|
181
332
|
org: u(420, 40, 180, 'Organisation'),
|
|
182
|
-
a: u(120, 200, 200, 'Business Unit A'),
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
333
|
+
a: u(120, 200, 200, 'Business Unit A'),
|
|
334
|
+
b: u(410, 200, 200, 'Business Unit B'),
|
|
335
|
+
c: u(700, 200, 200, 'Business Unit C'),
|
|
336
|
+
a1: u(60, 360, 170, 'Group A-1'),
|
|
337
|
+
a2: u(260, 360, 170, 'Group A-2'),
|
|
338
|
+
c1: u(715, 360, 170, 'Group C-1'),
|
|
339
|
+
// Attached orthogonal connectors: the router draws the org-chart elbows
|
|
340
|
+
// and the links follow when units are moved around.
|
|
341
|
+
e1: attach('org', 'a', { mode: ConnectorMode.Orthogonal }),
|
|
342
|
+
e2: attach('org', 'b', { mode: ConnectorMode.Orthogonal }),
|
|
343
|
+
e3: attach('org', 'c', { mode: ConnectorMode.Orthogonal }),
|
|
344
|
+
e4: attach('a', 'a1', { mode: ConnectorMode.Orthogonal }),
|
|
345
|
+
e5: attach('a', 'a2', { mode: ConnectorMode.Orthogonal }),
|
|
346
|
+
e6: attach('c', 'c1', { mode: ConnectorMode.Orthogonal }),
|
|
189
347
|
};
|
|
190
348
|
}
|
|
191
|
-
|
|
349
|
+
// ── EDGY dynamic (the relational metamodel, on the facets background) ──
|
|
350
|
+
/**
|
|
351
|
+
* Background scale (from REF coords to model coords): large enough that the
|
|
352
|
+
* default-size nodes breathe inside each zone. The background is cropped to
|
|
353
|
+
* the circles (`cropToCircles`), so its xywh covers CROP × DYN_SCALE.
|
|
354
|
+
*/
|
|
355
|
+
export const DYN_SCALE = 4.8;
|
|
356
|
+
/**
|
|
357
|
+
* The metamodel itself — the 12 elements and the 24 relations — now lives in
|
|
358
|
+
* `../metamodel.ts`, so the role vocabulary can DERIVE from it without this
|
|
359
|
+
* module and that one importing each other. Re-exported here under the names
|
|
360
|
+
* they have always had: nothing that reads them had to change.
|
|
361
|
+
*/
|
|
362
|
+
export { EDGY_DYNAMIC_NODES, EDGY_DYNAMIC_RELATIONS, } from '../metamodel.js';
|
|
363
|
+
/**
|
|
364
|
+
* Reference coords → template model coords: the background element sits at
|
|
365
|
+
* (0,0) and renders the cropped circles box, so a reference point maps to
|
|
366
|
+
* `(p - CROP.origin) × DYN_SCALE`.
|
|
367
|
+
*/
|
|
368
|
+
export function dynToModel(refX, refY) {
|
|
369
|
+
return [(refX - CROP.x) * DYN_SCALE, (refY - CROP.y) * DYN_SCALE];
|
|
370
|
+
}
|
|
371
|
+
function dynamic() {
|
|
372
|
+
const out = {
|
|
373
|
+
bg: {
|
|
374
|
+
type: 'edgy',
|
|
375
|
+
showLabels: false,
|
|
376
|
+
showPictos: false,
|
|
377
|
+
cropToCircles: true,
|
|
378
|
+
// The frame a finding is attributed to. Stamped here and nowhere else in
|
|
379
|
+
// this template: the background is what makes the board an EDGY board.
|
|
380
|
+
role: EDGY_ROLE.facets,
|
|
381
|
+
xywh: `[0,0,${CROP.w * DYN_SCALE},${CROP.h * DYN_SCALE}]`,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
for (const [key, { kind, cx, cy, w, zone }] of Object.entries(EDGY_DYNAMIC_NODES)) {
|
|
385
|
+
const nw = w ?? NODE_SIZE[kind].w;
|
|
386
|
+
const nh = NODE_SIZE[kind].h;
|
|
387
|
+
const [mx, my] = dynToModel(cx, cy);
|
|
388
|
+
const name = edgyElementLabel(key);
|
|
389
|
+
out[key] = enode(kind, mx - nw / 2, my - nh / 2, nw, nh, {
|
|
390
|
+
text: name,
|
|
391
|
+
fill: EDGY_ZONE_FILL[zone],
|
|
392
|
+
// The OFFICIAL element, not just its kind: this template IS the
|
|
393
|
+
// metamodel, so its Purpose is a Purpose and not merely an outcome.
|
|
394
|
+
role: EDGY_ROLE[key],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
EDGY_DYNAMIC_RELATIONS.forEach(([src, dst, verb, t], i) => {
|
|
398
|
+
out[`rel${i}`] = {
|
|
399
|
+
type: 'connector',
|
|
400
|
+
mode: ConnectorMode.Straight,
|
|
401
|
+
stroke: NODE_STROKE,
|
|
402
|
+
strokeWidth: 2,
|
|
403
|
+
strokeStyle: StrokeStyle.Solid,
|
|
404
|
+
frontEndpointStyle: PointStyle.None,
|
|
405
|
+
rearEndpointStyle: PointStyle.None,
|
|
406
|
+
source: { id: src },
|
|
407
|
+
target: { id: dst },
|
|
408
|
+
// The verb, as a ROLE — one per canonical verb, derived from this very
|
|
409
|
+
// table (`../roles.ts`). Source is the subject and target the object
|
|
410
|
+
// (`docs/adr/0010` tier 1), which is exactly how the row above reads, so
|
|
411
|
+
// `edgy.non-canonical-link` finds all 24 of these sentences legal.
|
|
412
|
+
role: EDGY_VERB_ROLE[verb],
|
|
413
|
+
// Native connector label: the verb travels with the link. The x/y are
|
|
414
|
+
// re-centred on the path at the first layout, but the w/h ARE the label
|
|
415
|
+
// box — size it to the verb so the text lays out on one line. The
|
|
416
|
+
// distance slides the verb along the link like the reference diagram.
|
|
417
|
+
text: surfaceText(verb),
|
|
418
|
+
labelXYWH: edgyVerbLabelXYWH(verb),
|
|
419
|
+
labelOffset: { distance: t ?? EDGY_RELATION_LABEL_DISTANCE },
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
const single = (el) => ({
|
|
425
|
+
a: el,
|
|
426
|
+
});
|
|
192
427
|
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
193
428
|
function tpl(name, preview, elements) {
|
|
194
|
-
return {
|
|
429
|
+
return {
|
|
430
|
+
name,
|
|
431
|
+
type: 'template',
|
|
432
|
+
preview,
|
|
433
|
+
content: makeTemplateSnapshot(elements, name),
|
|
434
|
+
};
|
|
195
435
|
}
|
|
436
|
+
/** Exported for direct insertion from the EDGY senior toolbar menu. */
|
|
437
|
+
export const edgyDynamicTemplate = tpl('EDGY dynamic', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/><path d="M55 30 H80 M56 31 L66 52 M79 31 L69 52" stroke="#fff" stroke-width="2"/><rect x="51" y="26" width="8" height="8" fill="#fff"/><circle cx="80" cy="30" r="4" fill="#fff"/><path d="M62 49 h6 l3 3 -3 3 h-6 z" fill="#fff"/></svg>`, dynamic());
|
|
196
438
|
export const edgyTemplateCategory = {
|
|
197
439
|
name: 'EDGY',
|
|
198
440
|
templates: [
|
|
@@ -200,8 +442,17 @@ export const edgyTemplateCategory = {
|
|
|
200
442
|
tpl('Customer journey', `<svg ${ATTRS} fill="none"><path d="M20 24 H110 L122 40 L110 56 H20 Z" fill="#f3a3c0"/><rect x="26" y="30" width="22" height="20" fill="none" stroke="#fff"/><rect x="54" y="30" width="22" height="20" fill="none" stroke="#fff"/><rect x="82" y="30" width="22" height="20" fill="none" stroke="#fff"/></svg>`, journey()),
|
|
201
443
|
tpl('Service blueprint', `<svg ${ATTRS} fill="none"><rect x="8" y="14" width="119" height="16" fill="#fbd5e0"/><rect x="8" y="32" width="119" height="34" fill="#d4e9f8"/><rect x="20" y="18" width="22" height="9" fill="#f5246e" opacity="0.5"/><rect x="20" y="40" width="22" height="9" fill="#2f6ff0" opacity="0.4"/><rect x="60" y="40" width="22" height="9" fill="#2f6ff0" opacity="0.4"/></svg>`, blueprint()),
|
|
202
444
|
tpl('Organisation chart', `<svg ${ATTRS} fill="none"><rect x="52" y="12" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="14" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="52" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><rect x="90" y="38" width="32" height="14" rx="2" fill="#4fd0ea"/><path d="M68 26 V32 M30 32 H106 M30 32 V38 M68 32 V38 M106 32 V38" stroke="#262626"/></svg>`, orgChart()),
|
|
203
|
-
tpl('Facets diagram', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/></svg>`, single({
|
|
204
|
-
|
|
445
|
+
tpl('Facets diagram', `<svg ${ATTRS}><circle cx="55" cy="34" r="18" fill="#00ea4e" opacity="0.9"/><circle cx="80" cy="34" r="18" fill="#034cee" opacity="0.9"/><circle cx="67" cy="54" r="18" fill="#ff0056" opacity="0.9"/></svg>`, single({
|
|
446
|
+
type: 'edgy',
|
|
447
|
+
cropToCircles: true,
|
|
448
|
+
role: EDGY_ROLE.facets,
|
|
449
|
+
xywh: `[0,0,${CROP_LABELED.w * 1.5},${CROP_LABELED.h * 1.5}]`,
|
|
450
|
+
})),
|
|
451
|
+
edgyDynamicTemplate,
|
|
452
|
+
tpl('People', `<svg ${ATTRS} fill="#262626"><circle cx="67" cy="32" r="9" fill="none" stroke="#262626" stroke-width="2.4"/><path d="M50 60 a17 17 0 0 1 34 0" fill="none" stroke="#262626" stroke-width="2.4"/></svg>`, {
|
|
453
|
+
n: enode('people', 0, 0, 64, 64),
|
|
454
|
+
l: label(-28, 70, 120, 24, 'People', { fontSize: 16 }),
|
|
455
|
+
}),
|
|
205
456
|
tpl('Outcome', `<svg ${ATTRS} fill="none"><rect x="20" y="24" width="95" height="34" rx="6" stroke="#262626" stroke-width="2"/></svg>`, single(enode('outcome', 0, 0, 130, 80, { text: 'Outcome' }))),
|
|
206
457
|
tpl('Object', `<svg ${ATTRS} fill="none"><rect x="20" y="24" width="95" height="34" stroke="#262626" stroke-width="2"/></svg>`, single(enode('object', 0, 0, 130, 80, { text: 'Object' }))),
|
|
207
458
|
tpl('Activity', `<svg ${ATTRS} fill="none"><path d="M20 24 H98 L116 41 H116 L98 58 H20 Z" stroke="#262626" stroke-width="2" stroke-linejoin="round"/></svg>`, single(enode('activity', 0, 0, 140, 80, { text: 'Activity' }))),
|