@formicoidea/labre-framework-cynefin 0.23.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/package.json +24 -0
- package/src/cynefin/consts.ts +188 -0
- package/src/cynefin/element-renderer.ts +156 -0
- package/src/cynefin/element-view.ts +32 -0
- package/src/cynefin/toolbar/config.ts +60 -0
- package/src/descriptor.ts +8 -0
- package/src/effects.ts +20 -0
- package/src/estuarine/consts.ts +69 -0
- package/src/estuarine/element-renderer.ts +122 -0
- package/src/estuarine/element-view.ts +32 -0
- package/src/estuarine/toolbar/config.ts +65 -0
- package/src/index.ts +1 -0
- package/src/templates/index.ts +130 -0
- package/src/toolbar/icons.ts +30 -0
- package/src/toolbar/menu.ts +156 -0
- package/src/toolbar/senior-button.ts +95 -0
- package/src/toolbar/senior-tool.ts +15 -0
- package/src/utils.ts +11 -0
- package/src/view.ts +44 -0
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formicoidea/labre-framework-cynefin",
|
|
3
|
+
"description": "Labre cynefin-estuarine framework for @formicoidea/labre-core.",
|
|
4
|
+
"version": "0.23.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"author": "lajola",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"toeverything"
|
|
10
|
+
],
|
|
11
|
+
"license": "MPL-2.0",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./view": "./src/view.ts",
|
|
15
|
+
"./descriptor": "./src/descriptor.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@formicoidea/labre-core": "0.23.0",
|
|
22
|
+
"lit": "^3.2.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual constants for the Liminal Cynefin diagram, reproduced from the official
|
|
3
|
+
* SVG (viewBox 0 0 1080 777). All geometry is authored in that fixed reference
|
|
4
|
+
* space and scaled uniformly to the element bounds by the renderer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const REF_W = 1080;
|
|
8
|
+
export const REF_H = 777;
|
|
9
|
+
|
|
10
|
+
export const COLORS = {
|
|
11
|
+
boundary: '#333333',
|
|
12
|
+
teal: '#2a9d99',
|
|
13
|
+
/** Domain headings + subheadings (h1 / h2). */
|
|
14
|
+
heading: '#6d6e71',
|
|
15
|
+
/** Body, small annotations and the big A / C glyphs. */
|
|
16
|
+
body: '#231f20',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dark boundary strokes drawn *behind* the teal "iterate" curve:
|
|
21
|
+
* [svg path, lineWidth, miterJoin].
|
|
22
|
+
*/
|
|
23
|
+
export const DARK_BACK_PATHS: ReadonlyArray<readonly [string, number, boolean]> = [
|
|
24
|
+
// Main arc: top segment (Complex|Complicated) then left segment (Complex|Chaotic)
|
|
25
|
+
['M 550.1 17 A 296 296 0 0 1 338 328.5 A 448.7 448.7 0 0 1 26 331', 15.5, false],
|
|
26
|
+
// Thin "Confusion" arc sweeping down towards the cliff
|
|
27
|
+
['M 649 294 C 644 382, 588 462, 440 506', 5, false],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Teal "iterate" curve, drawn *over* the back arcs and the dashed paving. */
|
|
31
|
+
export const TEAL_PATH =
|
|
32
|
+
'M 475.1 9.1 C 477.5 13.3, 484.4 26.1, 489.3 34.6 C 494.2 43.1, 499.7 51.7, 504.4 60.2 C 509.1 68.7, 513.4 77.2, 517.6 85.7 C 521.8 94.2, 525.7 102.7, 529.7 111.2 C 533.7 119.7, 538.1 128.3, 541.8 136.8 C 545.5 145.3, 549.0 153.8, 551.9 162.3 C 554.8 170.8, 557.1 179.3, 559.0 187.8 C 560.9 196.3, 562.0 204.8, 563.0 213.3 C 564.0 221.8, 564.8 230.4, 565.0 238.9 C 565.2 247.4, 565.0 255.9, 564.0 264.4 C 563.0 272.9, 561.2 281.4, 559.0 289.9 C 556.8 298.4, 554.6 307.0, 550.9 315.5 C 547.2 324.0, 542.3 332.5, 536.8 341.0 C 531.2 349.5, 524.9 358.3, 517.6 366.5 C 510.4 374.7, 501.6 383.0, 493.3 390.0 C 485.0 397.0, 476.4 403.1, 468.0 408.4 C 459.6 413.7, 451.2 418.0, 442.8 421.7 C 434.4 425.4, 425.9 427.4, 417.5 430.8 C 409.1 434.2, 400.3 437.5, 392.2 442.1 C 384.1 446.7, 377.1 455.5, 369.0 458.4 C 360.9 461.3, 352.1 459.4, 343.7 459.4 C 335.3 459.4, 326.9 459.1, 318.5 458.4 C 310.1 457.7, 301.6 456.8, 293.2 455.4 C 284.8 454.0, 276.4 452.2, 268.0 450.3 C 259.6 448.4, 251.1 446.2, 242.7 444.1 C 234.3 442.1, 225.8 440.0, 217.4 438.0 C 209.0 436.0, 200.6 434.4, 192.2 431.9 C 183.8 429.3, 175.3 426.1, 166.9 422.7 C 158.5 419.3, 150.0 415.5, 141.6 411.4 C 133.2 407.3, 124.8 403.3, 116.4 398.2 C 108.0 393.1, 99.5 386.9, 91.1 380.8 C 82.7 374.7, 74.3 368.7, 65.9 361.4 C 57.5 354.1, 45.7 342.8, 40.6 336.9 C 35.5 330.9, 36.4 327.6, 35.5 325.7';
|
|
33
|
+
export const TEAL_WIDTH = 10.5;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Dark boundary strokes drawn *over* the teal curve:
|
|
37
|
+
* [svg path, lineWidth, miterJoin].
|
|
38
|
+
*/
|
|
39
|
+
export const DARK_FRONT_PATHS: ReadonlyArray<readonly [string, number, boolean]> = [
|
|
40
|
+
// Thick descending branch with the bottom elbow (right edge of the cliff)
|
|
41
|
+
['M 340 332 C 390 440, 437 525, 472 632 Q 479 658, 453 700', 15.5, true],
|
|
42
|
+
// Thin left line (left edge of the cliff)
|
|
43
|
+
['M 345 356 C 372 440, 408 540, 413 655 C 414 685, 412 710, 412 738', 4, false],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Cliff hatching: [x1,y1,x2,y2], lineWidth 3. */
|
|
47
|
+
export const HATCHES: ReadonlyArray<readonly [number, number, number, number]> = [
|
|
48
|
+
[375, 420, 371, 431],
|
|
49
|
+
[386, 443, 380, 461],
|
|
50
|
+
[395, 462, 386, 483],
|
|
51
|
+
[402, 477, 392, 506],
|
|
52
|
+
[412, 497, 400, 539],
|
|
53
|
+
[417, 511, 403, 558],
|
|
54
|
+
[427, 533, 409, 591],
|
|
55
|
+
[435, 551, 411, 612],
|
|
56
|
+
[444, 573, 415, 659],
|
|
57
|
+
[455, 603, 415, 700],
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Dashed Complicated↔Clear boundary, as oriented square pavings: [x,y,size,rotateDeg]. */
|
|
61
|
+
export const DASH_RECTS: ReadonlyArray<readonly [number, number, number, number]> = [
|
|
62
|
+
[511, 245, 13.8, 206.8], [530, 255.5, 13.7, 205.2], [549.5, 265.5, 13.6, 203.5],
|
|
63
|
+
[569.5, 274.5, 13.5, 201.9], [590, 282.5, 13.5, 200.2], [610.5, 289.5, 13.4, 198.6],
|
|
64
|
+
[631, 296, 13.3, 196.9], [673.5, 307, 13.2, 193.6], [695, 311.5, 13.1, 192.0],
|
|
65
|
+
[716, 315.5, 13.0, 190.3], [738, 319, 12.9, 188.7], [759.5, 322, 12.8, 187.0],
|
|
66
|
+
[781, 324.5, 12.8, 185.4], [803, 325.5, 12.7, 183.7], [824.5, 326.5, 12.6, 182.1],
|
|
67
|
+
[846, 327.5, 12.5, 180.5], [868, 327.5, 12.4, 178.8], [889.5, 326.5, 12.3, 177.2],
|
|
68
|
+
[912, 325.5, 12.2, 175.5], [933, 323.5, 12.1, 173.9], [954.5, 321.5, 12.1, 172.2],
|
|
69
|
+
[976, 318, 12.0, 170.6], [998, 314, 11.9, 168.9], [1019, 310, 11.8, 167.3],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The four domain blocks. Each has a heading (h1) and, when descriptions are
|
|
74
|
+
* shown, a subheading (h2) and three decision lines whose lead word is bold.
|
|
75
|
+
* All three text levels share the block's left `x`.
|
|
76
|
+
*/
|
|
77
|
+
export interface DomainBlock {
|
|
78
|
+
heading: string;
|
|
79
|
+
/** Left edge shared by heading, subheading and body lines. */
|
|
80
|
+
x: number;
|
|
81
|
+
/** Heading (h1) baseline. */
|
|
82
|
+
hy: number;
|
|
83
|
+
subheading: string;
|
|
84
|
+
/** Subheading (h2) baseline. */
|
|
85
|
+
sy: number;
|
|
86
|
+
/** Decision lines: bold lead word + remainder, with their baseline. */
|
|
87
|
+
lines: ReadonlyArray<{ lead: string; rest: string; y: number }>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const DOMAINS: ReadonlyArray<DomainBlock> = [
|
|
91
|
+
{
|
|
92
|
+
heading: 'Complex',
|
|
93
|
+
x: 37,
|
|
94
|
+
hy: 31,
|
|
95
|
+
subheading: 'Adaptive system',
|
|
96
|
+
sy: 53,
|
|
97
|
+
lines: [
|
|
98
|
+
{ lead: 'Probe', rest: ' the context with parallel experiments', y: 71 },
|
|
99
|
+
{ lead: 'Sense', rest: ' how the context reacts', y: 90 },
|
|
100
|
+
{ lead: 'Respond', rest: ' by amplifying positive experiments', y: 109 },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
heading: 'Complicated',
|
|
105
|
+
x: 779,
|
|
106
|
+
hy: 31,
|
|
107
|
+
subheading: 'Ordered system',
|
|
108
|
+
sy: 53,
|
|
109
|
+
lines: [
|
|
110
|
+
{ lead: 'Sense', rest: ' the context with analytical methods', y: 71 },
|
|
111
|
+
{ lead: 'Analyse', rest: ' observations', y: 90 },
|
|
112
|
+
{ lead: 'Respond', rest: ' by applying one of many good solutions', y: 109 },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
heading: 'Chaotic',
|
|
117
|
+
x: 37,
|
|
118
|
+
hy: 587,
|
|
119
|
+
subheading: 'Un-ordered system',
|
|
120
|
+
sy: 609,
|
|
121
|
+
lines: [
|
|
122
|
+
{ lead: 'Act', rest: ' on the context to stabilize (it or yourself)', y: 627 },
|
|
123
|
+
{ lead: 'Sense', rest: ' how the context reacts', y: 646 },
|
|
124
|
+
{ lead: 'Respond', rest: ' by re-acting', y: 665 },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
heading: 'Clear',
|
|
129
|
+
x: 779,
|
|
130
|
+
hy: 587,
|
|
131
|
+
subheading: 'Ordered system',
|
|
132
|
+
sy: 609,
|
|
133
|
+
lines: [
|
|
134
|
+
{ lead: 'Sense', rest: ' the context with analytical methods', y: 627 },
|
|
135
|
+
{ lead: 'Categorize', rest: ' observations', y: 646 },
|
|
136
|
+
{ lead: 'Respond', rest: ' by applying tried and true practices', y: 665 },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
/** Teal annotation labels (centered): [text, x, y]. */
|
|
142
|
+
export const TEAL_LABELS: ReadonlyArray<readonly [string, number, number]> = [
|
|
143
|
+
['iterate', 510, 16],
|
|
144
|
+
['iterate', 533, 230],
|
|
145
|
+
['strategy by design', 257, 225],
|
|
146
|
+
['radical innovation', 268, 390],
|
|
147
|
+
['by design', 268, 406],
|
|
148
|
+
['extreme repurposing', 217, 496],
|
|
149
|
+
['good practice', 814, 196],
|
|
150
|
+
['best practice', 751, 459],
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/** Small exaptation sub-labels (centered): [text, x, y]. */
|
|
154
|
+
export const SMALL_LABELS: ReadonlyArray<readonly [string, number, number]> = [
|
|
155
|
+
['dispositional exaptation', 257, 239],
|
|
156
|
+
['stimulated exaptation', 268, 419],
|
|
157
|
+
['stress-based exaptation', 217, 510],
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The two central markers — Aporia (A) and Confusion (C) — each a big glyph and
|
|
162
|
+
* a name, with an optional teal note ("prepare to exit"). All centered.
|
|
163
|
+
*/
|
|
164
|
+
export interface Marker {
|
|
165
|
+
letter: string;
|
|
166
|
+
/** Big glyph position. */
|
|
167
|
+
lx: number;
|
|
168
|
+
ly: number;
|
|
169
|
+
name: string;
|
|
170
|
+
/** Name (body) position. */
|
|
171
|
+
nx: number;
|
|
172
|
+
ny: number;
|
|
173
|
+
/** Optional teal note position + text. */
|
|
174
|
+
note?: { text: string; x: number; y: number };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const MARKERS: ReadonlyArray<Marker> = [
|
|
178
|
+
{
|
|
179
|
+
letter: 'A',
|
|
180
|
+
lx: 444,
|
|
181
|
+
ly: 334,
|
|
182
|
+
name: 'Aporia',
|
|
183
|
+
nx: 447,
|
|
184
|
+
ny: 349,
|
|
185
|
+
note: { text: 'prepare to exit', x: 449, y: 366 },
|
|
186
|
+
},
|
|
187
|
+
{ letter: 'C', lx: 531, ly: 419, name: 'Confusion', nx: 529, ny: 436 },
|
|
188
|
+
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRenderer,
|
|
3
|
+
ElementRendererExtension,
|
|
4
|
+
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
+
import type { CynefinElementModel } from '@formicoidea/labre-core/model';
|
|
6
|
+
|
|
7
|
+
import { FONT_FAMILY, refScale } from '../utils';
|
|
8
|
+
import {
|
|
9
|
+
COLORS,
|
|
10
|
+
DARK_BACK_PATHS,
|
|
11
|
+
DARK_FRONT_PATHS,
|
|
12
|
+
DASH_RECTS,
|
|
13
|
+
DOMAINS,
|
|
14
|
+
HATCHES,
|
|
15
|
+
MARKERS,
|
|
16
|
+
REF_H,
|
|
17
|
+
REF_W,
|
|
18
|
+
SMALL_LABELS,
|
|
19
|
+
TEAL_LABELS,
|
|
20
|
+
TEAL_PATH,
|
|
21
|
+
TEAL_WIDTH,
|
|
22
|
+
} from './consts';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Canvas renderer for the Liminal Cynefin diagram — reproduces the official SVG:
|
|
26
|
+
* the dark hand-drawn boundary, the teal "iterate" curve, the dashed
|
|
27
|
+
* Complicated↔Clear paving and the hatched cliff, plus the four domain blocks
|
|
28
|
+
* (heading + Probe/Sense/Respond decisions), the teal annotation labels and the
|
|
29
|
+
* central Aporia (A) / Confusion (C) markers. Drawn in the fixed reference space
|
|
30
|
+
* and scaled uniformly to the element bounds.
|
|
31
|
+
*/
|
|
32
|
+
export const cynefin: ElementRenderer<CynefinElementModel> = (
|
|
33
|
+
model,
|
|
34
|
+
ctx,
|
|
35
|
+
matrix
|
|
36
|
+
) => {
|
|
37
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
38
|
+
const cx = w / 2;
|
|
39
|
+
const cy = h / 2;
|
|
40
|
+
ctx.setTransform(
|
|
41
|
+
matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
|
|
45
|
+
ctx.translate(ox, oy);
|
|
46
|
+
ctx.scale(s, s);
|
|
47
|
+
|
|
48
|
+
ctx.lineCap = 'butt';
|
|
49
|
+
|
|
50
|
+
// ── Dark boundary strokes (behind the teal curve) ───────────────────
|
|
51
|
+
ctx.strokeStyle = COLORS.boundary;
|
|
52
|
+
for (const [d, lw, miter] of DARK_BACK_PATHS) {
|
|
53
|
+
ctx.lineJoin = miter ? 'miter' : 'round';
|
|
54
|
+
ctx.lineWidth = lw;
|
|
55
|
+
ctx.stroke(new Path2D(d));
|
|
56
|
+
}
|
|
57
|
+
ctx.lineJoin = 'round';
|
|
58
|
+
|
|
59
|
+
// ── Dashed boundary (oriented square pavings) ───────────────────────
|
|
60
|
+
ctx.fillStyle = COLORS.boundary;
|
|
61
|
+
for (const [x, y, sz, rot] of DASH_RECTS) {
|
|
62
|
+
ctx.save();
|
|
63
|
+
ctx.translate(x, y);
|
|
64
|
+
ctx.rotate((rot * Math.PI) / 180);
|
|
65
|
+
ctx.fillRect(-sz / 2, -sz / 2, sz, sz);
|
|
66
|
+
ctx.restore();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Teal "iterate" liminal curve ────────────────────────────────────
|
|
70
|
+
if (model.showLiminalLine) {
|
|
71
|
+
ctx.strokeStyle = COLORS.teal;
|
|
72
|
+
ctx.lineWidth = TEAL_WIDTH;
|
|
73
|
+
ctx.stroke(new Path2D(TEAL_PATH));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Dark boundary strokes (over the teal curve) ─────────────────────
|
|
77
|
+
ctx.strokeStyle = COLORS.boundary;
|
|
78
|
+
for (const [d, lw, miter] of DARK_FRONT_PATHS) {
|
|
79
|
+
ctx.lineJoin = miter ? 'miter' : 'round';
|
|
80
|
+
ctx.lineWidth = lw;
|
|
81
|
+
ctx.stroke(new Path2D(d));
|
|
82
|
+
}
|
|
83
|
+
ctx.lineJoin = 'round';
|
|
84
|
+
|
|
85
|
+
// ── Cliff hatching ──────────────────────────────────────────────────
|
|
86
|
+
ctx.lineWidth = 3;
|
|
87
|
+
for (const [x1, y1, x2, y2] of HATCHES) {
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.moveTo(x1, y1);
|
|
90
|
+
ctx.lineTo(x2, y2);
|
|
91
|
+
ctx.stroke();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.textBaseline = 'alphabetic';
|
|
95
|
+
|
|
96
|
+
// ── Titles: domain headings + A / C marker glyphs and names ─────────
|
|
97
|
+
if (model.showTitles) {
|
|
98
|
+
ctx.textAlign = 'left';
|
|
99
|
+
ctx.fillStyle = COLORS.heading;
|
|
100
|
+
ctx.font = `700 30px ${FONT_FAMILY}`;
|
|
101
|
+
for (const d of DOMAINS) ctx.fillText(d.heading, d.x, d.hy);
|
|
102
|
+
|
|
103
|
+
ctx.textAlign = 'center';
|
|
104
|
+
for (const m of MARKERS) {
|
|
105
|
+
ctx.fillStyle = COLORS.body;
|
|
106
|
+
ctx.font = `700 38px ${FONT_FAMILY}`;
|
|
107
|
+
ctx.fillText(m.letter, m.lx, m.ly);
|
|
108
|
+
ctx.font = `13.5px ${FONT_FAMILY}`;
|
|
109
|
+
ctx.fillText(m.name, m.nx, m.ny);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Explanatory text: subheadings, decisions, annotations, notes ────
|
|
114
|
+
if (model.showDescriptions) {
|
|
115
|
+
// Subheadings (h2) + bold-lead decision lines
|
|
116
|
+
ctx.textAlign = 'left';
|
|
117
|
+
for (const d of DOMAINS) {
|
|
118
|
+
ctx.fillStyle = COLORS.heading;
|
|
119
|
+
ctx.font = `700 15px ${FONT_FAMILY}`;
|
|
120
|
+
ctx.fillText(d.subheading, d.x, d.sy);
|
|
121
|
+
|
|
122
|
+
ctx.fillStyle = COLORS.body;
|
|
123
|
+
for (const { lead, rest, y } of d.lines) {
|
|
124
|
+
ctx.font = `700 13.5px ${FONT_FAMILY}`;
|
|
125
|
+
ctx.fillText(lead, d.x, y);
|
|
126
|
+
const leadW = ctx.measureText(lead).width;
|
|
127
|
+
ctx.font = `13.5px ${FONT_FAMILY}`;
|
|
128
|
+
ctx.fillText(rest, d.x + leadW, y);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
ctx.textAlign = 'center';
|
|
133
|
+
|
|
134
|
+
// Teal annotation labels
|
|
135
|
+
ctx.fillStyle = COLORS.teal;
|
|
136
|
+
ctx.font = `700 15px ${FONT_FAMILY}`;
|
|
137
|
+
for (const [t, x, y] of TEAL_LABELS) ctx.fillText(t, x, y);
|
|
138
|
+
|
|
139
|
+
// Small exaptation sub-labels
|
|
140
|
+
ctx.fillStyle = COLORS.body;
|
|
141
|
+
ctx.font = `10.5px ${FONT_FAMILY}`;
|
|
142
|
+
for (const [t, x, y] of SMALL_LABELS) ctx.fillText(t, x, y);
|
|
143
|
+
|
|
144
|
+
// Marker notes ("prepare to exit")
|
|
145
|
+
ctx.fillStyle = COLORS.teal;
|
|
146
|
+
ctx.font = `700 15px ${FONT_FAMILY}`;
|
|
147
|
+
for (const m of MARKERS) {
|
|
148
|
+
if (m.note) ctx.fillText(m.note.text, m.note.x, m.note.y);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const CynefinRendererExtension = ElementRendererExtension(
|
|
154
|
+
'cynefin',
|
|
155
|
+
cynefin
|
|
156
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CynefinElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import {
|
|
3
|
+
GfxElementModelView,
|
|
4
|
+
GfxViewInteractionExtension,
|
|
5
|
+
} from '@formicoidea/labre-core/std/gfx';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* View for the Cynefin background. Registering it ensures `gfx.view.get(model)`
|
|
9
|
+
* returns a view (required so move / select interactions work).
|
|
10
|
+
*/
|
|
11
|
+
export class CynefinView extends GfxElementModelView<CynefinElementModel> {
|
|
12
|
+
static override type: string = 'cynefin';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
|
|
17
|
+
* true (toggled from the toolbar). Moving / selecting stays available.
|
|
18
|
+
*/
|
|
19
|
+
export const CynefinInteraction = GfxViewInteractionExtension<CynefinView>(
|
|
20
|
+
CynefinView.type,
|
|
21
|
+
{
|
|
22
|
+
handleResize({ model }) {
|
|
23
|
+
return {
|
|
24
|
+
beforeResize({ set }) {
|
|
25
|
+
if (!model.resizeEnabled) {
|
|
26
|
+
set({ allowedHandlers: [] });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { CynefinElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
import {
|
|
4
|
+
type ToolbarContext,
|
|
5
|
+
type ToolbarModuleConfig,
|
|
6
|
+
ToolbarModuleExtension,
|
|
7
|
+
} from '@formicoidea/labre-core/shared/services';
|
|
8
|
+
import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
|
|
9
|
+
import { html, type TemplateResult } from 'lit';
|
|
10
|
+
|
|
11
|
+
const ResizeIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H5v4M15 19h4v-4" /><path d="M5 5l6 6M19 19l-6-6" /></svg>`;
|
|
12
|
+
const TitlesIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14M9 7v11" /></svg>`;
|
|
13
|
+
const DescIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8h14M5 12h14M5 16h9" /></svg>`;
|
|
14
|
+
const LiminalIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 17 C9 6 15 6 19 7" /></svg>`;
|
|
15
|
+
|
|
16
|
+
type CynefinToggleProp =
|
|
17
|
+
| 'resizeEnabled'
|
|
18
|
+
| 'showTitles'
|
|
19
|
+
| 'showDescriptions'
|
|
20
|
+
| 'showLiminalLine';
|
|
21
|
+
|
|
22
|
+
function booleanToggle(
|
|
23
|
+
id: string,
|
|
24
|
+
tooltip: string,
|
|
25
|
+
icon: TemplateResult,
|
|
26
|
+
prop: CynefinToggleProp
|
|
27
|
+
) {
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
tooltip,
|
|
31
|
+
icon,
|
|
32
|
+
active(ctx: ToolbarContext) {
|
|
33
|
+
const models = ctx.getSurfaceModelsByType(CynefinElementModel);
|
|
34
|
+
return models.length > 0 && models.every(model => model[prop]);
|
|
35
|
+
},
|
|
36
|
+
run(ctx: ToolbarContext) {
|
|
37
|
+
const models = ctx.getSurfaceModelsByType(CynefinElementModel);
|
|
38
|
+
if (!models.length) return;
|
|
39
|
+
const enable = !models.every(model => model[prop]);
|
|
40
|
+
ctx.std.store.captureSync();
|
|
41
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
42
|
+
for (const model of models) crud.updateElement(model.id, { [prop]: enable });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const cynefinToolbarConfig = {
|
|
48
|
+
actions: [
|
|
49
|
+
booleanToggle('a.toggle-resize', 'Enable / lock resizing', ResizeIcon, 'resizeEnabled'),
|
|
50
|
+
booleanToggle('b.toggle-titles', 'Show / hide titles', TitlesIcon, 'showTitles'),
|
|
51
|
+
booleanToggle('c.toggle-descriptions', 'Show / hide explanatory text', DescIcon, 'showDescriptions'),
|
|
52
|
+
booleanToggle('d.toggle-liminal', 'Show / hide liminal line', LiminalIcon, 'showLiminalLine'),
|
|
53
|
+
],
|
|
54
|
+
when: ctx => ctx.getSurfaceModelsByType(CynefinElementModel).length > 0,
|
|
55
|
+
} as const satisfies ToolbarModuleConfig;
|
|
56
|
+
|
|
57
|
+
export const cynefinToolbarExtension = ToolbarModuleExtension({
|
|
58
|
+
id: BlockFlavourIdentifier('affine:surface:cynefin'),
|
|
59
|
+
config: cynefinToolbarConfig,
|
|
60
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CynefinEstuarineViewExtension } from './view.js';
|
|
2
|
+
|
|
3
|
+
/** Host wiring for the cynefin-estuarine framework. */
|
|
4
|
+
export const cynefinFramework = {
|
|
5
|
+
flag: 'cynefin-estuarine',
|
|
6
|
+
telemetry: 'cynefin',
|
|
7
|
+
viewExtension: CynefinEstuarineViewExtension,
|
|
8
|
+
} as const;
|
package/src/effects.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { EdgelessCynefinEstuarineMenu } from './toolbar/menu';
|
|
2
|
+
import { EdgelessCynefinEstuarineSeniorButton } from './toolbar/senior-button';
|
|
3
|
+
|
|
4
|
+
export function effects() {
|
|
5
|
+
customElements.define(
|
|
6
|
+
'edgeless-cynefin-estuarine-menu',
|
|
7
|
+
EdgelessCynefinEstuarineMenu
|
|
8
|
+
);
|
|
9
|
+
customElements.define(
|
|
10
|
+
'edgeless-cynefin-estuarine-senior-button',
|
|
11
|
+
EdgelessCynefinEstuarineSeniorButton
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
interface HTMLElementTagNameMap {
|
|
17
|
+
'edgeless-cynefin-estuarine-menu': EdgelessCynefinEstuarineMenu;
|
|
18
|
+
'edgeless-cynefin-estuarine-senior-button': EdgelessCynefinEstuarineSeniorButton;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual constants for the Estuarine framework map, reproduced from the official
|
|
3
|
+
* SVG (viewBox 0 0 690 801). All geometry is authored in that fixed reference
|
|
4
|
+
* space and scaled uniformly to the element bounds by the renderer. The e axis
|
|
5
|
+
* is vertical & double-headed (energy), the t axis horizontal & single-headed
|
|
6
|
+
* (time only flows one way).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const REF_W = 690;
|
|
10
|
+
export const REF_H = 801;
|
|
11
|
+
|
|
12
|
+
export const COLORS = {
|
|
13
|
+
axis: '#941253',
|
|
14
|
+
/** Italic e / t axis letters. */
|
|
15
|
+
axisLabel: '#c0392b',
|
|
16
|
+
liminal: '#5ecc44',
|
|
17
|
+
/** LIMINAL legend (darker than the curve). */
|
|
18
|
+
liminalLabel: '#2e7d32',
|
|
19
|
+
volatile: '#e63322',
|
|
20
|
+
counterfactual: '#1a1a1a',
|
|
21
|
+
/** VOLATILE + COUNTER FACTUAL legends. */
|
|
22
|
+
label: '#1a1a1a',
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
/** e axis (vertical, double-headed): x, top y, bottom y. */
|
|
26
|
+
export const E_AXIS = { x: 43.5, y1: 97, y2: 763 } as const;
|
|
27
|
+
/** t axis (horizontal, single-headed → right): y, left x, right x. */
|
|
28
|
+
export const T_AXIS = { y: 649, x1: 28, x2: 616 } as const;
|
|
29
|
+
export const AXIS_WIDTH = 8;
|
|
30
|
+
|
|
31
|
+
/** Filled arrowhead triangles: [[tipX,tipY],[baseAX,baseAY],[baseBX,baseBY]]. */
|
|
32
|
+
export const ARROWHEADS: ReadonlyArray<
|
|
33
|
+
readonly [readonly [number, number], readonly [number, number], readonly [number, number]]
|
|
34
|
+
> = [
|
|
35
|
+
[[43.5, 72], [30, 100], [57, 100]], // e — top
|
|
36
|
+
[[43.5, 785], [30, 758], [57, 758]], // e — bottom
|
|
37
|
+
[[643, 649], [613, 636], [613, 662]], // t — right
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Liminal: green boundary rising gently then dipping at the right end. */
|
|
41
|
+
export const LIMINAL_PATH =
|
|
42
|
+
'M 63 193 C 67 192, 78 189, 85 188 C 92 187, 100 186, 107 185 C 114 184, 122 183, 129 183 C 136 183, 144 183, 151 183 C 158 183, 166 183, 173 183 C 180 183, 188 184, 195 185 C 202 186, 210 188, 217 189 C 224 190, 232 192, 239 194 C 246 196, 254 198, 261 201 C 268 204, 276 207, 283 210 C 290 213, 298 217, 305 220 C 312 223, 320 226, 327 230 C 334 234, 342 238, 349 242 C 356 246, 364 250, 371 255 C 378 260, 386 264, 393 269 C 400 274, 408 278, 415 283 C 422 288, 430 292, 437 297 C 444 302, 451 306, 458 310 C 465 314, 473 319, 480 323 C 487 327, 495 332, 502 335 C 509 338, 517 341, 524 343 C 531 345, 539 348, 546 349 C 553 350, 561 350, 568 350 C 575 350, 583 348, 590 346 C 597 344, 605 340, 612 336 C 619 332, 626 325, 633 319 C 640 313, 648 302, 651 298 C 654 294, 654 295, 654 294';
|
|
43
|
+
export const LIMINAL_WIDTH = 4.5;
|
|
44
|
+
|
|
45
|
+
/** Counter-factual: dark boundary sweeping from the top down to the right. */
|
|
46
|
+
export const COUNTERFACTUAL_PATH =
|
|
47
|
+
'M 422 30 C 420 33, 414 41, 411 47 C 408 53, 405 59, 402 65 C 399 71, 397 77, 395 83 C 393 89, 392 95, 391 101 C 390 107, 389 113, 389 119 C 389 125, 389 131, 390 137 C 391 143, 392 149, 394 155 C 396 161, 399 167, 402 173 C 405 179, 408 185, 412 191 C 416 197, 421 203, 426 209 C 431 215, 436 221, 442 226 C 448 231, 454 237, 460 241 C 466 245, 472 249, 478 252 C 484 255, 490 258, 496 260 C 502 262, 508 264, 514 266 C 520 268, 526 270, 532 271 C 538 272, 544 274, 550 275 C 556 276, 562 277, 568 278 C 574 279, 580 279, 586 279 C 592 279, 598 280, 604 280 C 610 280, 616 281, 622 281 C 628 281, 634 280, 640 280 C 646 280, 655 279, 658 279';
|
|
48
|
+
export const COUNTERFACTUAL_WIDTH = 5.5;
|
|
49
|
+
|
|
50
|
+
/** Volatile: red boundary descending along the left, bulging right. */
|
|
51
|
+
export const VOLATILE_PATH =
|
|
52
|
+
'M 58 446 C 61 447, 70 451, 76 454 C 82 457, 88 462, 94 466 C 100 470, 107 476, 112 481 C 117 486, 122 492, 126 498 C 130 504, 135 509, 139 515 C 143 521, 145 526, 148 532 C 151 538, 153 544, 155 550 C 157 556, 159 562, 160 568 C 161 574, 162 580, 163 586 C 164 592, 164 598, 164 604 C 164 610, 166 616, 166 622 C 166 628, 166 634, 165 640 C 164 646, 164 652, 163 658 C 162 664, 162 670, 161 676 C 160 682, 160 688, 159 694 C 158 700, 155 706, 154 712 C 153 718, 152 724, 151 730 C 150 736, 148 746, 147 749';
|
|
53
|
+
export const VOLATILE_WIDTH = 5;
|
|
54
|
+
|
|
55
|
+
/** Uppercase legends: anchored centre, alphabetic baseline, with letter-spacing. */
|
|
56
|
+
export const LABELS = {
|
|
57
|
+
counterfactual: { text: 'COUNTER FACTUAL', x: 422, y: 25, size: 20, color: COLORS.label },
|
|
58
|
+
liminal: { text: 'LIMINAL', x: 316, y: 192, size: 18, color: COLORS.liminalLabel },
|
|
59
|
+
volatile: { text: 'VOLATILE', x: 219, y: 783, size: 20, color: COLORS.volatile },
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
/** Italic Georgia axis letters (left-anchored, alphabetic baseline). */
|
|
63
|
+
export const AXIS_LABELS = {
|
|
64
|
+
e: { text: 'e', x: 14, y: 138 },
|
|
65
|
+
t: { text: 't', x: 580, y: 685 },
|
|
66
|
+
size: 34,
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
export const LABEL_LETTER_SPACING = 4;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ElementRenderer,
|
|
3
|
+
ElementRendererExtension,
|
|
4
|
+
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
+
import type { EstuarineElementModel } from '@formicoidea/labre-core/model';
|
|
6
|
+
|
|
7
|
+
import { FONT_FAMILY, refScale } from '../utils';
|
|
8
|
+
import {
|
|
9
|
+
ARROWHEADS,
|
|
10
|
+
AXIS_LABELS,
|
|
11
|
+
AXIS_WIDTH,
|
|
12
|
+
COLORS,
|
|
13
|
+
COUNTERFACTUAL_PATH,
|
|
14
|
+
COUNTERFACTUAL_WIDTH,
|
|
15
|
+
E_AXIS,
|
|
16
|
+
LABEL_LETTER_SPACING,
|
|
17
|
+
LABELS,
|
|
18
|
+
LIMINAL_PATH,
|
|
19
|
+
LIMINAL_WIDTH,
|
|
20
|
+
REF_H,
|
|
21
|
+
REF_W,
|
|
22
|
+
T_AXIS,
|
|
23
|
+
VOLATILE_PATH,
|
|
24
|
+
VOLATILE_WIDTH,
|
|
25
|
+
} from './consts';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Canvas renderer for the Estuarine framework map — reproduces the official SVG:
|
|
29
|
+
* the e (vertical, double-headed) / t (horizontal, single-headed) axes and the
|
|
30
|
+
* three reference curves (Liminal / Volatile / Counter-factual), each with its
|
|
31
|
+
* legend and individually hideable. Drawn in the fixed reference space and
|
|
32
|
+
* scaled uniformly to the element bounds.
|
|
33
|
+
*/
|
|
34
|
+
export const estuarine: ElementRenderer<EstuarineElementModel> = (
|
|
35
|
+
model,
|
|
36
|
+
ctx,
|
|
37
|
+
matrix
|
|
38
|
+
) => {
|
|
39
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
40
|
+
const cx = w / 2;
|
|
41
|
+
const cy = h / 2;
|
|
42
|
+
ctx.setTransform(
|
|
43
|
+
matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const { s, ox, oy } = refScale(w, h, REF_W, REF_H);
|
|
47
|
+
ctx.translate(ox, oy);
|
|
48
|
+
ctx.scale(s, s);
|
|
49
|
+
|
|
50
|
+
ctx.lineCap = 'round';
|
|
51
|
+
ctx.lineJoin = 'round';
|
|
52
|
+
|
|
53
|
+
// ── Axes ────────────────────────────────────────────────────────────
|
|
54
|
+
ctx.strokeStyle = COLORS.axis;
|
|
55
|
+
ctx.fillStyle = COLORS.axis;
|
|
56
|
+
ctx.lineWidth = AXIS_WIDTH;
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.moveTo(E_AXIS.x, E_AXIS.y1);
|
|
59
|
+
ctx.lineTo(E_AXIS.x, E_AXIS.y2);
|
|
60
|
+
ctx.moveTo(T_AXIS.x1, T_AXIS.y);
|
|
61
|
+
ctx.lineTo(T_AXIS.x2, T_AXIS.y);
|
|
62
|
+
ctx.stroke();
|
|
63
|
+
for (const [[tx, ty], [ax, ay], [bx, by]] of ARROWHEADS) {
|
|
64
|
+
ctx.beginPath();
|
|
65
|
+
ctx.moveTo(tx, ty);
|
|
66
|
+
ctx.lineTo(ax, ay);
|
|
67
|
+
ctx.lineTo(bx, by);
|
|
68
|
+
ctx.closePath();
|
|
69
|
+
ctx.fill();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Uppercase legend (centre-anchored, alphabetic baseline, letter-spaced).
|
|
73
|
+
const hasSpacing = 'letterSpacing' in ctx;
|
|
74
|
+
const legend = (l: { text: string; x: number; y: number; size: number; color: string }) => {
|
|
75
|
+
ctx.fillStyle = l.color;
|
|
76
|
+
ctx.font = `600 ${l.size}px ${FONT_FAMILY}`;
|
|
77
|
+
ctx.textAlign = 'center';
|
|
78
|
+
ctx.textBaseline = 'alphabetic';
|
|
79
|
+
if (hasSpacing) ctx.letterSpacing = `${LABEL_LETTER_SPACING}px`;
|
|
80
|
+
ctx.fillText(l.text, l.x, l.y);
|
|
81
|
+
if (hasSpacing) ctx.letterSpacing = '0px';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ── Liminal (green) ─────────────────────────────────────────────────
|
|
85
|
+
if (model.showLiminal) {
|
|
86
|
+
ctx.strokeStyle = COLORS.liminal;
|
|
87
|
+
ctx.lineWidth = LIMINAL_WIDTH;
|
|
88
|
+
ctx.stroke(new Path2D(LIMINAL_PATH));
|
|
89
|
+
legend(LABELS.liminal);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Volatile (red) ──────────────────────────────────────────────────
|
|
93
|
+
if (model.showVolatile) {
|
|
94
|
+
ctx.strokeStyle = COLORS.volatile;
|
|
95
|
+
ctx.lineWidth = VOLATILE_WIDTH;
|
|
96
|
+
ctx.stroke(new Path2D(VOLATILE_PATH));
|
|
97
|
+
legend(LABELS.volatile);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Counter-factual (dark) ──────────────────────────────────────────
|
|
101
|
+
if (model.showCounterfactual) {
|
|
102
|
+
ctx.strokeStyle = COLORS.counterfactual;
|
|
103
|
+
ctx.lineWidth = COUNTERFACTUAL_WIDTH;
|
|
104
|
+
ctx.stroke(new Path2D(COUNTERFACTUAL_PATH));
|
|
105
|
+
legend(LABELS.counterfactual);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Italic e / t axis letters ───────────────────────────────────────
|
|
109
|
+
if (model.showAxisLabels) {
|
|
110
|
+
ctx.fillStyle = COLORS.axisLabel;
|
|
111
|
+
ctx.font = `italic 700 ${AXIS_LABELS.size}px Georgia, serif`;
|
|
112
|
+
ctx.textAlign = 'left';
|
|
113
|
+
ctx.textBaseline = 'alphabetic';
|
|
114
|
+
ctx.fillText(AXIS_LABELS.e.text, AXIS_LABELS.e.x, AXIS_LABELS.e.y);
|
|
115
|
+
ctx.fillText(AXIS_LABELS.t.text, AXIS_LABELS.t.x, AXIS_LABELS.t.y);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const EstuarineRendererExtension = ElementRendererExtension(
|
|
120
|
+
'estuarine',
|
|
121
|
+
estuarine
|
|
122
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EstuarineElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import {
|
|
3
|
+
GfxElementModelView,
|
|
4
|
+
GfxViewInteractionExtension,
|
|
5
|
+
} from '@formicoidea/labre-core/std/gfx';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* View for the Estuarine background. Registering it ensures `gfx.view.get(model)`
|
|
9
|
+
* returns a view (required so move / select interactions work).
|
|
10
|
+
*/
|
|
11
|
+
export class EstuarineView extends GfxElementModelView<EstuarineElementModel> {
|
|
12
|
+
static override type: string = 'estuarine';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resize gating: the resize handles are hidden unless `model.resizeEnabled` is
|
|
17
|
+
* true (toggled from the toolbar). Moving / selecting stays available.
|
|
18
|
+
*/
|
|
19
|
+
export const EstuarineInteraction = GfxViewInteractionExtension<EstuarineView>(
|
|
20
|
+
EstuarineView.type,
|
|
21
|
+
{
|
|
22
|
+
handleResize({ model }) {
|
|
23
|
+
return {
|
|
24
|
+
beforeResize({ set }) {
|
|
25
|
+
if (!model.resizeEnabled) {
|
|
26
|
+
set({ allowedHandlers: [] });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { EstuarineElementModel } from '@formicoidea/labre-core/model';
|
|
3
|
+
import {
|
|
4
|
+
type ToolbarContext,
|
|
5
|
+
type ToolbarModuleConfig,
|
|
6
|
+
ToolbarModuleExtension,
|
|
7
|
+
} from '@formicoidea/labre-core/shared/services';
|
|
8
|
+
import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
|
|
9
|
+
import { html, type TemplateResult } from 'lit';
|
|
10
|
+
|
|
11
|
+
const ResizeIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H5v4M15 19h4v-4" /><path d="M5 5l6 6M19 19l-6-6" /></svg>`;
|
|
12
|
+
// All curve icons use currentColor so the toolbar can grey them when inactive;
|
|
13
|
+
// they are distinguished by shape (wave / arc / hooked curve).
|
|
14
|
+
const LiminalIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 13c3-6 6 4 9 0s6-6 9 0" /></svg>`;
|
|
15
|
+
const VolatileIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M7 4a9 9 0 0 1 0 18" /></svg>`;
|
|
16
|
+
const CounterfactualIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7c7 0 11 4 12 14" /></svg>`;
|
|
17
|
+
const AxisIcon = html`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v18M6 12h15" /><path d="M3 6l3-3 3 3M18 9l3 3-3 3" /></svg>`;
|
|
18
|
+
|
|
19
|
+
type EstuarineToggleProp =
|
|
20
|
+
| 'resizeEnabled'
|
|
21
|
+
| 'showLiminal'
|
|
22
|
+
| 'showVolatile'
|
|
23
|
+
| 'showCounterfactual'
|
|
24
|
+
| 'showAxisLabels';
|
|
25
|
+
|
|
26
|
+
function booleanToggle(
|
|
27
|
+
id: string,
|
|
28
|
+
tooltip: string,
|
|
29
|
+
icon: TemplateResult,
|
|
30
|
+
prop: EstuarineToggleProp
|
|
31
|
+
) {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
tooltip,
|
|
35
|
+
icon,
|
|
36
|
+
active(ctx: ToolbarContext) {
|
|
37
|
+
const models = ctx.getSurfaceModelsByType(EstuarineElementModel);
|
|
38
|
+
return models.length > 0 && models.every(model => model[prop]);
|
|
39
|
+
},
|
|
40
|
+
run(ctx: ToolbarContext) {
|
|
41
|
+
const models = ctx.getSurfaceModelsByType(EstuarineElementModel);
|
|
42
|
+
if (!models.length) return;
|
|
43
|
+
const enable = !models.every(model => model[prop]);
|
|
44
|
+
ctx.std.store.captureSync();
|
|
45
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
46
|
+
for (const model of models) crud.updateElement(model.id, { [prop]: enable });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const estuarineToolbarConfig = {
|
|
52
|
+
actions: [
|
|
53
|
+
booleanToggle('a.toggle-resize', 'Enable / lock resizing', ResizeIcon, 'resizeEnabled'),
|
|
54
|
+
booleanToggle('b.toggle-liminal', 'Show / hide the Liminal line', LiminalIcon, 'showLiminal'),
|
|
55
|
+
booleanToggle('c.toggle-volatile', 'Show / hide the Volatile line', VolatileIcon, 'showVolatile'),
|
|
56
|
+
booleanToggle('d.toggle-counterfactual', 'Show / hide the Counter-factual line', CounterfactualIcon, 'showCounterfactual'),
|
|
57
|
+
booleanToggle('e.toggle-axis-labels', 'Show / hide axis labels (e / t)', AxisIcon, 'showAxisLabels'),
|
|
58
|
+
],
|
|
59
|
+
when: ctx => ctx.getSurfaceModelsByType(EstuarineElementModel).length > 0,
|
|
60
|
+
} as const satisfies ToolbarModuleConfig;
|
|
61
|
+
|
|
62
|
+
export const estuarineToolbarExtension = ToolbarModuleExtension({
|
|
63
|
+
id: BlockFlavourIdentifier('affine:surface:estuarine'),
|
|
64
|
+
config: estuarineToolbarConfig,
|
|
65
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeTemplateSnapshot,
|
|
3
|
+
type SurfaceElementsJSON,
|
|
4
|
+
surfaceText,
|
|
5
|
+
type Template,
|
|
6
|
+
type TemplateCategory,
|
|
7
|
+
} from '@formicoidea/labre-core/gfx/template';
|
|
8
|
+
import { FontFamily, ShapeStyle, TextAlign } from '@formicoidea/labre-core/model';
|
|
9
|
+
|
|
10
|
+
import { REF_H as CYN_H, REF_W as CYN_W } from '../cynefin/consts';
|
|
11
|
+
import { REF_H as EST_H, REF_W as EST_W } from '../estuarine/consts';
|
|
12
|
+
|
|
13
|
+
const HEX_SIZE = 60;
|
|
14
|
+
const HEX_FILL = '#34c724';
|
|
15
|
+
const HEX_STROKE = '#1f1f1f';
|
|
16
|
+
const HEX_VERTICES = [
|
|
17
|
+
[1, 0.5],
|
|
18
|
+
[0.75, 0.933],
|
|
19
|
+
[0.25, 0.933],
|
|
20
|
+
[0, 0.5],
|
|
21
|
+
[0.25, 0.067],
|
|
22
|
+
[0.75, 0.067],
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function sticky(x: number, y: number, text: string) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'shape',
|
|
28
|
+
shapeType: 'rect',
|
|
29
|
+
filled: true,
|
|
30
|
+
fillColor: '#fff3b0',
|
|
31
|
+
strokeColor: '#d9b740',
|
|
32
|
+
strokeWidth: 1.5,
|
|
33
|
+
shapeStyle: ShapeStyle.General,
|
|
34
|
+
roughness: 0,
|
|
35
|
+
radius: 8,
|
|
36
|
+
text: surfaceText(text),
|
|
37
|
+
color: '#1a1a1a',
|
|
38
|
+
fontFamily: FontFamily.Inter,
|
|
39
|
+
fontSize: 18,
|
|
40
|
+
textAlign: TextAlign.Center,
|
|
41
|
+
xywh: `[${x},${y},200,70]`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hexagon(x: number, y: number) {
|
|
46
|
+
return {
|
|
47
|
+
type: 'shape',
|
|
48
|
+
shapeType: 'polygon',
|
|
49
|
+
vertices: HEX_VERTICES,
|
|
50
|
+
filled: true,
|
|
51
|
+
fillColor: HEX_FILL,
|
|
52
|
+
strokeColor: HEX_STROKE,
|
|
53
|
+
strokeWidth: 2,
|
|
54
|
+
shapeStyle: ShapeStyle.General,
|
|
55
|
+
roughness: 0,
|
|
56
|
+
xywh: `[${x},${y},${HEX_SIZE},${HEX_SIZE}]`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function caption(x: number, y: number, str: string) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: surfaceText(str),
|
|
64
|
+
color: '#1a1a1a',
|
|
65
|
+
fontFamily: FontFamily.Inter,
|
|
66
|
+
fontSize: 16,
|
|
67
|
+
textAlign: TextAlign.Center,
|
|
68
|
+
xywh: `[${x},${y},120,24]`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tpl(name: string, preview: string, elements: SurfaceElementsJSON): Template {
|
|
73
|
+
return { name, type: 'template', preview, content: makeTemplateSnapshot(elements, name) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ATTRS = 'width="100%" height="100%" viewBox="0 0 135 80" xmlns="http://www.w3.org/2000/svg"';
|
|
77
|
+
|
|
78
|
+
const cynefinBg = (xywh: string) => ({ type: 'cynefin', xywh });
|
|
79
|
+
const estuarineBg = (xywh: string) => ({ type: 'estuarine', xywh });
|
|
80
|
+
|
|
81
|
+
export const cynefinTemplateCategory: TemplateCategory = {
|
|
82
|
+
name: 'Cynefin',
|
|
83
|
+
templates: [
|
|
84
|
+
tpl(
|
|
85
|
+
'Decision sorting',
|
|
86
|
+
`<svg ${ATTRS} fill="none"><rect x="8" y="10" width="119" height="60" rx="4" stroke="#2a9d99" stroke-width="1.5"/><path d="M67 10 V70 M8 40 H127" stroke="#9aa0a6"/><rect x="18" y="18" width="34" height="14" rx="2" fill="#fff3b0"/><rect x="83" y="18" width="34" height="14" rx="2" fill="#fff3b0"/><rect x="18" y="48" width="34" height="14" rx="2" fill="#fff3b0"/><rect x="83" y="48" width="34" height="14" rx="2" fill="#fff3b0"/></svg>`,
|
|
87
|
+
{
|
|
88
|
+
bg: cynefinBg(`[0,0,${CYN_W},${CYN_H}]`),
|
|
89
|
+
s1: sticky(190, 175, 'Probe & learn'),
|
|
90
|
+
s2: sticky(690, 175, 'Expert analysis'),
|
|
91
|
+
s3: sticky(190, 505, 'Act now'),
|
|
92
|
+
s4: sticky(690, 505, 'Known issue'),
|
|
93
|
+
}
|
|
94
|
+
),
|
|
95
|
+
tpl(
|
|
96
|
+
'Cynefin framework',
|
|
97
|
+
`<svg ${ATTRS} fill="none"><rect x="14" y="12" width="107" height="56" rx="4" stroke="#2a9d99" stroke-width="1.6"/><path d="M67 12 V68 M14 40 H121" stroke="#9aa0a6"/></svg>`,
|
|
98
|
+
{ bg: cynefinBg(`[0,0,${CYN_W},${CYN_H}]`) }
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const estuarineTemplateCategory: TemplateCategory = {
|
|
104
|
+
name: 'Estuarine',
|
|
105
|
+
templates: [
|
|
106
|
+
tpl(
|
|
107
|
+
'Constraint map',
|
|
108
|
+
`<svg ${ATTRS} fill="none"><path d="M20 12 V70 M20 70 H120" stroke="#941253" stroke-width="2"/><g fill="#34c724" stroke="#1f1f1f"><path d="M44 28 l6 4 l0 8 l-6 4 l-6 -4 l0 -8 z"/><path d="M74 40 l6 4 l0 8 l-6 4 l-6 -4 l0 -8 z"/><path d="M56 52 l6 4 l0 8 l-6 4 l-6 -4 l0 -8 z"/></g></svg>`,
|
|
109
|
+
{
|
|
110
|
+
bg: estuarineBg(`[0,0,${EST_W},${EST_H}]`),
|
|
111
|
+
h1: hexagon(150, 220),
|
|
112
|
+
c1: caption(120, 284, 'Policy'),
|
|
113
|
+
h2: hexagon(330, 360),
|
|
114
|
+
c2: caption(300, 424, 'Habit'),
|
|
115
|
+
h3: hexagon(230, 520),
|
|
116
|
+
c3: caption(200, 584, 'Budget'),
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
tpl(
|
|
120
|
+
'Estuarine map',
|
|
121
|
+
`<svg ${ATTRS} fill="none"><path d="M24 10 V70 M24 70 H120" stroke="#941253" stroke-width="2.4"/><path d="M30 52 q40 -30 84 -34" stroke="#5ecc44" stroke-width="2" fill="none"/></svg>`,
|
|
122
|
+
{ bg: estuarineBg(`[0,0,${EST_W},${EST_H}]`) }
|
|
123
|
+
),
|
|
124
|
+
tpl(
|
|
125
|
+
'Hexagon constraint',
|
|
126
|
+
`<svg ${ATTRS} fill="none"><path d="M67 24 l18 11 l0 22 l-18 11 l-18 -11 l0 -22 z" fill="#34c724" stroke="#1f1f1f" stroke-width="2"/></svg>`,
|
|
127
|
+
{ hex: hexagon(0, 0) }
|
|
128
|
+
),
|
|
129
|
+
],
|
|
130
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { svg } from 'lit';
|
|
2
|
+
|
|
3
|
+
/** Colored Cynefin glyph for the main toolbar button. */
|
|
4
|
+
export const cynefinToolbarIcon = svg`<svg width="100%" height="100%" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
5
|
+
<g stroke="#3f444a" stroke-width="3" stroke-linecap="round">
|
|
6
|
+
<path d="M30 8 C28 18 33 24 26 30 C20 35 14 33 9 33"/>
|
|
7
|
+
<path d="M26 30 C28 40 28 46 28 50"/>
|
|
8
|
+
<path d="M34 22 C40 23 46 24 50 25" stroke-dasharray="3 3"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g stroke="#3f444a" stroke-width="1.4"><path d="M22 33 l-2 4M24 38 l-2 5M26 43 l-2 5"/></g>
|
|
11
|
+
</svg>`;
|
|
12
|
+
|
|
13
|
+
/** Menu icon — create the Cynefin diagram. */
|
|
14
|
+
export const cynefinMenuIcon = svg`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
15
|
+
<path d="M13 3 C12 8 14 11 11 13 C8 15 5 14 3 14" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
|
16
|
+
<path d="M11 13 C12 18 12 20 12 22" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
|
17
|
+
<path d="M15 10 C18 10.5 20 11 22 11.5" stroke="currentColor" stroke-width="1.6" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
|
|
18
|
+
</svg>`;
|
|
19
|
+
|
|
20
|
+
/** Menu icon — create the Estuarine map. */
|
|
21
|
+
export const estuarineMenuIcon = svg`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
22
|
+
<path d="M5 3 V20 M5 17 H21" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
|
23
|
+
<path d="M3 6 l2-2 2 2 M3 17 l2 2 2 -2 M18 15 l3 2 -3 2" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
24
|
+
<path d="M6 10 C11 6 14 13 21 9" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round"/>
|
|
25
|
+
</svg>`;
|
|
26
|
+
|
|
27
|
+
/** Menu icon — hexagon constraint node. */
|
|
28
|
+
export const hexagonMenuIcon = svg`<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
29
|
+
<polygon points="21,12 16.5,19.8 7.5,19.8 3,12 7.5,4.2 16.5,4.2" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
|
|
30
|
+
</svg>`;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
|
|
3
|
+
import { ShapeStyle } from '@formicoidea/labre-core/model';
|
|
4
|
+
import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
|
|
5
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
6
|
+
import { css, html, LitElement } from 'lit';
|
|
7
|
+
|
|
8
|
+
import { REF_H as CYN_H, REF_W as CYN_W } from '../cynefin/consts';
|
|
9
|
+
import { REF_H as EST_H, REF_W as EST_W } from '../estuarine/consts';
|
|
10
|
+
import {
|
|
11
|
+
cynefinMenuIcon,
|
|
12
|
+
estuarineMenuIcon,
|
|
13
|
+
hexagonMenuIcon,
|
|
14
|
+
} from './icons';
|
|
15
|
+
|
|
16
|
+
/** Estuarine map default size (REF aspect, scaled up so it reads on canvas). */
|
|
17
|
+
const MAP_SCALE = 1.2;
|
|
18
|
+
const HEX_SIZE = 60;
|
|
19
|
+
const HEX_FILL = '#34c724';
|
|
20
|
+
const HEX_STROKE = '#1f1f1f';
|
|
21
|
+
/** Flat-top regular hexagon, normalized vertices. */
|
|
22
|
+
const HEX_VERTICES: number[][] = [
|
|
23
|
+
[1, 0.5],
|
|
24
|
+
[0.75, 0.933],
|
|
25
|
+
[0.25, 0.933],
|
|
26
|
+
[0, 0.5],
|
|
27
|
+
[0.25, 0.067],
|
|
28
|
+
[0.75, 0.067],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The popover above the toolbar hosting both frameworks: create the Cynefin
|
|
33
|
+
* diagram, the Estuarine map, or a hexagon constraint node.
|
|
34
|
+
*/
|
|
35
|
+
export class EdgelessCynefinEstuarineMenu extends EdgelessToolbarToolMixin(
|
|
36
|
+
LitElement
|
|
37
|
+
) {
|
|
38
|
+
static override styles = css`
|
|
39
|
+
:host {
|
|
40
|
+
position: absolute;
|
|
41
|
+
display: flex;
|
|
42
|
+
z-index: -1;
|
|
43
|
+
}
|
|
44
|
+
.menu-content {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
}
|
|
49
|
+
.button-group-container {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: 14px;
|
|
53
|
+
fill: var(--affine-icon-color);
|
|
54
|
+
}
|
|
55
|
+
.button-group-container svg {
|
|
56
|
+
width: 24px;
|
|
57
|
+
height: 24px;
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
override type = EmptyTool;
|
|
62
|
+
|
|
63
|
+
private _finish(id: string) {
|
|
64
|
+
const { gfx } = this;
|
|
65
|
+
gfx.doc.captureSync();
|
|
66
|
+
gfx.tool.setTool(DefaultTool);
|
|
67
|
+
gfx.selection.set({ elements: [id], editing: false });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private _createCynefin() {
|
|
71
|
+
const { gfx } = this;
|
|
72
|
+
if (!gfx.surface) return;
|
|
73
|
+
const { centerX, centerY } = gfx.viewport;
|
|
74
|
+
const id = gfx.surface.addElement({
|
|
75
|
+
type: 'cynefin',
|
|
76
|
+
xywh: new Bound(
|
|
77
|
+
centerX - CYN_W / 2,
|
|
78
|
+
centerY - CYN_H / 2,
|
|
79
|
+
CYN_W,
|
|
80
|
+
CYN_H
|
|
81
|
+
).serialize(),
|
|
82
|
+
});
|
|
83
|
+
this._finish(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private _createMap() {
|
|
87
|
+
const { gfx } = this;
|
|
88
|
+
if (!gfx.surface) return;
|
|
89
|
+
const width = EST_W * MAP_SCALE;
|
|
90
|
+
const height = EST_H * MAP_SCALE;
|
|
91
|
+
const { centerX, centerY } = gfx.viewport;
|
|
92
|
+
const id = gfx.surface.addElement({
|
|
93
|
+
type: 'estuarine',
|
|
94
|
+
xywh: new Bound(
|
|
95
|
+
centerX - width / 2,
|
|
96
|
+
centerY - height / 2,
|
|
97
|
+
width,
|
|
98
|
+
height
|
|
99
|
+
).serialize(),
|
|
100
|
+
});
|
|
101
|
+
this._finish(id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private _createHexagon() {
|
|
105
|
+
const { gfx } = this;
|
|
106
|
+
if (!gfx.surface) return;
|
|
107
|
+
const { centerX: cx, centerY: cy } = gfx.viewport;
|
|
108
|
+
const id = gfx.surface.addElement({
|
|
109
|
+
type: 'shape',
|
|
110
|
+
shapeType: 'polygon',
|
|
111
|
+
vertices: HEX_VERTICES,
|
|
112
|
+
filled: true,
|
|
113
|
+
fillColor: HEX_FILL,
|
|
114
|
+
strokeColor: HEX_STROKE,
|
|
115
|
+
strokeWidth: 2,
|
|
116
|
+
shapeStyle: ShapeStyle.General,
|
|
117
|
+
roughness: 0,
|
|
118
|
+
xywh: new Bound(
|
|
119
|
+
cx - HEX_SIZE / 2,
|
|
120
|
+
cy - HEX_SIZE / 2,
|
|
121
|
+
HEX_SIZE,
|
|
122
|
+
HEX_SIZE
|
|
123
|
+
).serialize(),
|
|
124
|
+
});
|
|
125
|
+
this._finish(id);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override render() {
|
|
129
|
+
return html`
|
|
130
|
+
<edgeless-slide-menu>
|
|
131
|
+
<div class="menu-content">
|
|
132
|
+
<div class="button-group-container">
|
|
133
|
+
<edgeless-tool-icon-button
|
|
134
|
+
.tooltip=${'Cynefin framework'}
|
|
135
|
+
@click=${this._createCynefin}
|
|
136
|
+
>
|
|
137
|
+
${cynefinMenuIcon}
|
|
138
|
+
</edgeless-tool-icon-button>
|
|
139
|
+
<edgeless-tool-icon-button
|
|
140
|
+
.tooltip=${'Estuarine map'}
|
|
141
|
+
@click=${this._createMap}
|
|
142
|
+
>
|
|
143
|
+
${estuarineMenuIcon}
|
|
144
|
+
</edgeless-tool-icon-button>
|
|
145
|
+
<edgeless-tool-icon-button
|
|
146
|
+
.tooltip=${'Hexagon node'}
|
|
147
|
+
@click=${this._createHexagon}
|
|
148
|
+
>
|
|
149
|
+
${hexagonMenuIcon}
|
|
150
|
+
</edgeless-tool-icon-button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</edgeless-slide-menu>
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
|
|
3
|
+
import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
|
|
4
|
+
import { SignalWatcher } from '@formicoidea/labre-core/global/lit';
|
|
5
|
+
import { css, html, LitElement } from 'lit';
|
|
6
|
+
|
|
7
|
+
import { cynefinToolbarIcon } from './icons';
|
|
8
|
+
|
|
9
|
+
/** Main toolbar button that opens the combined Cynefin / Estuarine sub-menu. */
|
|
10
|
+
export class EdgelessCynefinEstuarineSeniorButton extends EdgelessToolbarToolMixin(
|
|
11
|
+
SignalWatcher(LitElement)
|
|
12
|
+
) {
|
|
13
|
+
static override styles = css`
|
|
14
|
+
:host,
|
|
15
|
+
.ce-button {
|
|
16
|
+
display: block;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
:host {
|
|
21
|
+
position: relative;
|
|
22
|
+
}
|
|
23
|
+
.ce-root {
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 64px;
|
|
26
|
+
position: relative;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: flex-end;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
}
|
|
33
|
+
.ce-card {
|
|
34
|
+
--y: -4px;
|
|
35
|
+
--s: 1;
|
|
36
|
+
position: absolute;
|
|
37
|
+
bottom: 0;
|
|
38
|
+
width: 54px;
|
|
39
|
+
height: 54px;
|
|
40
|
+
transform: translateY(var(--y)) scale(var(--s));
|
|
41
|
+
transition: transform 0.3s ease;
|
|
42
|
+
}
|
|
43
|
+
.ce-card svg {
|
|
44
|
+
display: block;
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 100%;
|
|
47
|
+
}
|
|
48
|
+
.ce-root:hover .ce-card {
|
|
49
|
+
--y: -10px;
|
|
50
|
+
--s: 1.07;
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
override enableActiveBackground = true;
|
|
55
|
+
|
|
56
|
+
override type = EmptyTool;
|
|
57
|
+
|
|
58
|
+
private _toggleMenu() {
|
|
59
|
+
if (this.popper) {
|
|
60
|
+
this.popper.dispose();
|
|
61
|
+
this.popper = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.setEdgelessTool(DefaultTool);
|
|
65
|
+
const menu = this.createPopper('edgeless-cynefin-estuarine-menu', this);
|
|
66
|
+
menu.element.edgeless = this.edgeless;
|
|
67
|
+
|
|
68
|
+
const el = menu.element as HTMLElement;
|
|
69
|
+
const wrap = el.parentElement;
|
|
70
|
+
if (wrap) {
|
|
71
|
+
wrap.style.overflow = 'visible';
|
|
72
|
+
wrap.style.justifyContent = 'flex-end';
|
|
73
|
+
}
|
|
74
|
+
Object.assign(el.style, {
|
|
75
|
+
position: 'static',
|
|
76
|
+
width: 'max-content',
|
|
77
|
+
maxWidth: 'calc(100vw - 16px)',
|
|
78
|
+
marginLeft: '0',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override render() {
|
|
83
|
+
return html`<edgeless-toolbar-button
|
|
84
|
+
class="ce-button"
|
|
85
|
+
.tooltip=${this.popper ? '' : 'Cynefin / Estuarine'}
|
|
86
|
+
.tooltipOffset=${4}
|
|
87
|
+
.active=${!!this.popper}
|
|
88
|
+
@click=${this._toggleMenu}
|
|
89
|
+
>
|
|
90
|
+
<div class="ce-root">
|
|
91
|
+
<div class="ce-card">${cynefinToolbarIcon}</div>
|
|
92
|
+
</div>
|
|
93
|
+
</edgeless-toolbar-button>`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SeniorToolExtension } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
/** A single senior tool hosting both the Cynefin and Estuarine frameworks. */
|
|
5
|
+
export const cynefinEstuarineSeniorTool = SeniorToolExtension(
|
|
6
|
+
'cynefin-estuarine',
|
|
7
|
+
({ block }) => {
|
|
8
|
+
return {
|
|
9
|
+
name: 'Cynefin / Estuarine',
|
|
10
|
+
content: html`<edgeless-cynefin-estuarine-senior-button
|
|
11
|
+
.edgeless=${block}
|
|
12
|
+
></edgeless-cynefin-estuarine-senior-button>`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
);
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform fit of a fixed reference design (`refW × refH`) into an element of
|
|
3
|
+
* size `w × h`: the scale factor plus the centering offsets (letterboxed).
|
|
4
|
+
* Keeps the artwork undistorted at any element size.
|
|
5
|
+
*/
|
|
6
|
+
export function refScale(w: number, h: number, refW: number, refH: number) {
|
|
7
|
+
const s = Math.min(w / refW, h / refH);
|
|
8
|
+
return { s, ox: (w - refW * s) / 2, oy: (h - refH * s) / 2 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FONT_FAMILY = 'Inter, sans-serif';
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ViewExtensionContext,
|
|
3
|
+
ViewExtensionProvider,
|
|
4
|
+
} from '@formicoidea/labre-core/ext-loader';
|
|
5
|
+
import { extendTemplateCategory } from '@formicoidea/labre-core/gfx/template';
|
|
6
|
+
|
|
7
|
+
import { CynefinRendererExtension } from './cynefin/element-renderer';
|
|
8
|
+
import { CynefinInteraction, CynefinView } from './cynefin/element-view';
|
|
9
|
+
import { cynefinToolbarExtension } from './cynefin/toolbar/config';
|
|
10
|
+
import { effects } from './effects';
|
|
11
|
+
import { EstuarineRendererExtension } from './estuarine/element-renderer';
|
|
12
|
+
import { EstuarineInteraction, EstuarineView } from './estuarine/element-view';
|
|
13
|
+
import { estuarineToolbarExtension } from './estuarine/toolbar/config';
|
|
14
|
+
import {
|
|
15
|
+
cynefinTemplateCategory,
|
|
16
|
+
estuarineTemplateCategory,
|
|
17
|
+
} from './templates';
|
|
18
|
+
import { cynefinEstuarineSeniorTool } from './toolbar/senior-tool';
|
|
19
|
+
|
|
20
|
+
export class CynefinEstuarineViewExtension extends ViewExtensionProvider {
|
|
21
|
+
override name = 'affine-cynefin-estuarine-gfx';
|
|
22
|
+
|
|
23
|
+
override effect(): void {
|
|
24
|
+
super.effect();
|
|
25
|
+
effects();
|
|
26
|
+
extendTemplateCategory(cynefinTemplateCategory);
|
|
27
|
+
extendTemplateCategory(estuarineTemplateCategory);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override setup(context: ViewExtensionContext) {
|
|
31
|
+
super.setup(context);
|
|
32
|
+
context.register(CynefinView);
|
|
33
|
+
context.register(CynefinRendererExtension);
|
|
34
|
+
context.register(EstuarineView);
|
|
35
|
+
context.register(EstuarineRendererExtension);
|
|
36
|
+
if (this.isEdgeless(context.scope)) {
|
|
37
|
+
context.register(CynefinInteraction);
|
|
38
|
+
context.register(EstuarineInteraction);
|
|
39
|
+
context.register(cynefinEstuarineSeniorTool);
|
|
40
|
+
context.register(cynefinToolbarExtension);
|
|
41
|
+
context.register(estuarineToolbarExtension);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|