@1agh/maude 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/cli/commands/design.mjs +5 -0
  2. package/cli/lib/design-link.mjs +13 -6
  3. package/cli/lib/gitignore-block.mjs +1 -0
  4. package/cli/lib/gitignore-block.test.mjs +1 -0
  5. package/package.json +8 -8
  6. package/plugins/design/dev-server/bin/_svg-optimize.mjs +35 -0
  7. package/plugins/design/dev-server/bin/draw-build.sh +48 -0
  8. package/plugins/design/dev-server/bin/draw-proof.sh +129 -0
  9. package/plugins/design/dev-server/bin/svg-optimize.sh +24 -0
  10. package/plugins/design/dev-server/canvas-lib.tsx +110 -0
  11. package/plugins/design/dev-server/config.schema.json +10 -0
  12. package/plugins/design/dev-server/context.ts +9 -0
  13. package/plugins/design/dev-server/dist/client.bundle.js +3 -3
  14. package/plugins/design/dev-server/draw/brush.ts +639 -0
  15. package/plugins/design/dev-server/draw/composition.ts +229 -0
  16. package/plugins/design/dev-server/draw/geometry.ts +578 -0
  17. package/plugins/design/dev-server/draw/index.ts +28 -0
  18. package/plugins/design/dev-server/draw/layout.ts +260 -0
  19. package/plugins/design/dev-server/draw/optimize.ts +65 -0
  20. package/plugins/design/dev-server/draw/palette.ts +417 -0
  21. package/plugins/design/dev-server/draw/primitives.ts +643 -0
  22. package/plugins/design/dev-server/draw/serialize.ts +458 -0
  23. package/plugins/design/dev-server/draw/test/brush.test.ts +213 -0
  24. package/plugins/design/dev-server/draw/test/composition.test.ts +141 -0
  25. package/plugins/design/dev-server/draw/test/geometry.test.ts +199 -0
  26. package/plugins/design/dev-server/draw/test/gradient.test.ts +167 -0
  27. package/plugins/design/dev-server/draw/test/layout.test.ts +120 -0
  28. package/plugins/design/dev-server/draw/test/optimize.test.ts +40 -0
  29. package/plugins/design/dev-server/draw/test/palette.test.ts +123 -0
  30. package/plugins/design/dev-server/draw/test/primitives.test.ts +129 -0
  31. package/plugins/design/dev-server/draw/test/serialize.test.ts +105 -0
  32. package/plugins/design/dev-server/sync/index.ts +73 -17
  33. package/plugins/design/dev-server/test/sync-runtime.test.ts +52 -0
  34. package/plugins/design/dev-server/tsconfig.json +8 -1
  35. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +15 -0
  36. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @file draw/composition.ts — Phase 25.1 compositional scaffolding layer
3
+ * @scope plugins/design/dev-server/draw/composition.ts
4
+ * @purpose The missing answer to "blob soup": deterministic COMPOSITION.
5
+ * Random placement is empirically catastrophic — only ~3–12% of
6
+ * randomly-placed multi-element layouts even avoid overlap (Shiripour
7
+ * et al., ACM EICS 2021). The fix is to place elements ON a
8
+ * constructed armature, not scatter-then-hope.
9
+ *
10
+ * Grounded in the Phase-25.1 deep research (recorded in DDR-070 +
11
+ * the rubric's "Phase-25.1 corrections" section):
12
+ * • rule-of-thirds power points + rabatment lines + golden points
13
+ * + dynamic-symmetry "eyes" — REAL, exact, encodable focal
14
+ * scaffolds (place focal elements on the construction points).
15
+ * • VME visual balance as moment-equilibrium (nine-grid weight +
16
+ * Manhattan distance; net moment → 0 = balanced).
17
+ * • φ / root-N are OPTIONAL aspect/scaffold choices, NEVER a
18
+ * mandatory constraint — the golden-ratio "secret formula" is a
19
+ * debunked myth (Naini 2024; Markowsky 1992).
20
+ *
21
+ * CRITICAL: armatures are a GENERATION tool only. "Does it align to
22
+ * a grid?" is mathematically non-discriminating (a dense enough
23
+ * armature fits ANY image — Blake 1921), so it is USELESS as a critic
24
+ * gate. The critic uses discriminating metrics (balance moment,
25
+ * value range, hue-harmony distance) — see palette.ts + the rubric.
26
+ *
27
+ * React-free (DDR-067); pure + deterministic (no Math.random).
28
+ */
29
+
30
+ import type { Rect } from './geometry.ts';
31
+ import type { Point } from './primitives.ts';
32
+
33
+ const PHI = 1.618033988749895;
34
+
35
+ export type ArmatureKind = 'thirds' | 'golden' | 'rabatment' | 'dynamic-symmetry' | 'quad';
36
+
37
+ export interface Armature {
38
+ kind: ArmatureKind;
39
+ box: Rect;
40
+ /** Construction lines (for an optional debug overlay). */
41
+ lines: Array<{ x1: number; y1: number; x2: number; y2: number }>;
42
+ /** The focal "power points" / "eyes" — snap dominant elements here. */
43
+ focals: Point[];
44
+ /** The composition's visual center (canvas center; slightly-high reads better but center is the neutral default). */
45
+ center: Point;
46
+ }
47
+
48
+ function corners(b: Rect) {
49
+ return {
50
+ tl: { x: b.x, y: b.y },
51
+ tr: { x: b.x + b.width, y: b.y },
52
+ bl: { x: b.x, y: b.y + b.height },
53
+ br: { x: b.x + b.width, y: b.y + b.height },
54
+ };
55
+ }
56
+
57
+ /** Foot of the perpendicular from point `p` onto the line through `a`→`b`. */
58
+ function perpFoot(p: Point, a: Point, b: Point): Point {
59
+ const dx = b.x - a.x;
60
+ const dy = b.y - a.y;
61
+ const len2 = dx * dx + dy * dy || 1;
62
+ const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
63
+ return { x: a.x + t * dx, y: a.y + t * dy };
64
+ }
65
+
66
+ /**
67
+ * Build a compositional armature for a canvas. The returned `focals` are the
68
+ * snap targets for the dominant element(s); `lines` are the construction lines.
69
+ * `golden` is offered for completeness but the research shows it has no special
70
+ * status — prefer `thirds` / `rabatment` / `dynamic-symmetry`.
71
+ */
72
+ export function armature(box: Rect, kind: ArmatureKind = 'thirds'): Armature {
73
+ const { x, y, width: W, height: H } = box;
74
+ const center = { x: x + W / 2, y: y + H / 2 };
75
+ const lines: Armature['lines'] = [];
76
+ let focals: Point[] = [];
77
+
78
+ if (kind === 'thirds' || kind === 'golden') {
79
+ const fx = kind === 'golden' ? [W / PHI, W - W / PHI] : [W / 3, (2 * W) / 3];
80
+ const fy = kind === 'golden' ? [H / PHI, H - H / PHI] : [H / 3, (2 * H) / 3];
81
+ for (const vx of fx) lines.push({ x1: x + vx, y1: y, x2: x + vx, y2: y + H });
82
+ for (const hy of fy) lines.push({ x1: x, y1: y + hy, x2: x + W, y2: y + hy });
83
+ for (const hy of fy) for (const vx of fx) focals.push({ x: x + vx, y: y + hy });
84
+ } else if (kind === 'rabatment') {
85
+ // Implied squares folded from each short side. Landscape → vertical lines at
86
+ // x = H and x = W−H; portrait → horizontal lines at y = W and y = H−W. Focal
87
+ // y (landscape) taken at the thirds heights (the verified rabatment geometry
88
+ // governs the dominant axis; thirds fills the minor axis).
89
+ if (W >= H) {
90
+ const vx = [H, W - H];
91
+ const hy = [H / 3, (2 * H) / 3];
92
+ for (const v of vx) lines.push({ x1: x + v, y1: y, x2: x + v, y2: y + H });
93
+ for (const h of hy) for (const v of vx) focals.push({ x: x + v, y: y + h });
94
+ } else {
95
+ const hy = [W, H - W];
96
+ const vx = [W / 3, (2 * W) / 3];
97
+ for (const h of hy) lines.push({ x1: x, y1: y + h, x2: x + W, y2: y + h });
98
+ for (const h of hy) for (const v of vx) focals.push({ x: x + v, y: y + h });
99
+ }
100
+ } else if (kind === 'dynamic-symmetry') {
101
+ const c = corners(box);
102
+ // Main diagonals.
103
+ lines.push({ x1: c.tl.x, y1: c.tl.y, x2: c.br.x, y2: c.br.y });
104
+ lines.push({ x1: c.tr.x, y1: c.tr.y, x2: c.bl.x, y2: c.bl.y });
105
+ // "Eyes" = feet of perpendiculars from the off-diagonal corners onto each main.
106
+ const e1 = perpFoot(c.tr, c.tl, c.br);
107
+ const e2 = perpFoot(c.bl, c.tl, c.br);
108
+ const e3 = perpFoot(c.tl, c.tr, c.bl);
109
+ const e4 = perpFoot(c.br, c.tr, c.bl);
110
+ focals = [e1, e2, e3, e4];
111
+ // Reciprocal segments (corner → its eye) for the overlay.
112
+ lines.push({ x1: c.tr.x, y1: c.tr.y, x2: e1.x, y2: e1.y });
113
+ lines.push({ x1: c.bl.x, y1: c.bl.y, x2: e2.x, y2: e2.y });
114
+ lines.push({ x1: c.tl.x, y1: c.tl.y, x2: e3.x, y2: e3.y });
115
+ lines.push({ x1: c.br.x, y1: c.br.y, x2: e4.x, y2: e4.y });
116
+ } else {
117
+ // quad — the four quarter-centers (a simple, balanced fallback grid).
118
+ for (const qy of [0.25, 0.75])
119
+ for (const qx of [0.25, 0.75]) focals.push({ x: x + W * qx, y: y + H * qy });
120
+ }
121
+
122
+ return { kind, box, lines, focals, center };
123
+ }
124
+
125
+ /** Nearest focal point on an armature to `p` (snap target). */
126
+ export function snapToFocal(p: Point, arm: Armature): Point {
127
+ let best = arm.focals[0];
128
+ let bestD = Number.POSITIVE_INFINITY;
129
+ for (const f of arm.focals) {
130
+ const d = Math.hypot(f.x - p.x, f.y - p.y);
131
+ if (d < bestD) {
132
+ bestD = d;
133
+ best = f;
134
+ }
135
+ }
136
+ return best;
137
+ }
138
+
139
+ /**
140
+ * Assign `n` elements to distinct composition slots. The first (dominant) gets a
141
+ * strong focal point; the rest fill the remaining focals, then a jittered grid —
142
+ * but NEVER pure random scatter. Deterministic given the armature.
143
+ */
144
+ export function assignSlots(n: number, arm: Armature): Point[] {
145
+ const out: Point[] = [];
146
+ const focals = [...arm.focals];
147
+ for (let i = 0; i < n; i++) {
148
+ if (i < focals.length) {
149
+ out.push(focals[i]);
150
+ } else {
151
+ // Beyond the focals, lay a calm grid inside the box (rows of ~√extra).
152
+ const extra = i - focals.length;
153
+ const cols = Math.max(2, Math.ceil(Math.sqrt(n - focals.length)));
154
+ const r = Math.floor(extra / cols);
155
+ const c = extra % cols;
156
+ const rows = Math.ceil((n - focals.length) / cols);
157
+ out.push({
158
+ x: arm.box.x + arm.box.width * ((c + 1) / (cols + 1)),
159
+ y: arm.box.y + arm.box.height * ((r + 1) / (rows + 1)),
160
+ });
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ // VME visual balance — moment equilibrium (nine-grid weight + Manhattan)
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+
170
+ export interface WeightedBox {
171
+ bbox: Rect;
172
+ /** Optional explicit visual weight; defaults to bbox area. */
173
+ weight?: number;
174
+ }
175
+
176
+ export interface BalanceResult {
177
+ /** Net moment vector about the visual center, normalized to [-1,1] per axis. */
178
+ moment: Point;
179
+ /** 0 (wildly off-balance) … 1 (perfectly balanced). */
180
+ score: number;
181
+ }
182
+
183
+ function boxCentroid(b: Rect): Point {
184
+ return { x: b.x + b.width / 2, y: b.y + b.height / 2 };
185
+ }
186
+
187
+ /**
188
+ * Visual balance as moment equilibrium (VME). Each element exerts a moment about
189
+ * the canvas visual center proportional to its weight × Manhattan offset; a
190
+ * balanced composition has a net moment near zero. Returns the normalized net
191
+ * moment + a [0,1] balance score (the discriminating critic metric for balance).
192
+ */
193
+ export function balanceMoment(elements: WeightedBox[], box: Rect): BalanceResult {
194
+ const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
195
+ const halfW = box.width / 2 || 1;
196
+ const halfH = box.height / 2 || 1;
197
+ let mx = 0;
198
+ let my = 0;
199
+ let totalW = 0;
200
+ for (const el of elements) {
201
+ const w = el.weight ?? Math.max(0, el.bbox.width * el.bbox.height);
202
+ const c = boxCentroid(el.bbox);
203
+ mx += w * (c.x - center.x);
204
+ my += w * (c.y - center.y);
205
+ totalW += w;
206
+ }
207
+ if (totalW === 0) return { moment: { x: 0, y: 0 }, score: 1 };
208
+ const nx = mx / (totalW * halfW);
209
+ const ny = my / (totalW * halfH);
210
+ // Manhattan-style aggregate imbalance (matches VME's non-Euclidean weighting).
211
+ const imbalance = Math.min(1, (Math.abs(nx) + Math.abs(ny)) / 2);
212
+ return { moment: { x: nx, y: ny }, score: Math.max(0, 1 - imbalance) };
213
+ }
214
+
215
+ /**
216
+ * Dominance check: does the composition have ONE clear focal element? Returns the
217
+ * size ratio of the largest element's visual weight to the next-largest. A ratio
218
+ * well above 1 means a single element commands attention; ~1 means competing
219
+ * foci (the "everything is equally loud" failure). Threshold tuning is the
220
+ * critic's job — this just reports the measurable ratio.
221
+ */
222
+ export function dominanceRatio(elements: WeightedBox[]): number {
223
+ const weights = elements
224
+ .map((e) => e.weight ?? Math.max(0, e.bbox.width * e.bbox.height))
225
+ .sort((a, b) => b - a);
226
+ if (weights.length === 0) return 0;
227
+ if (weights.length === 1) return Number.POSITIVE_INFINITY;
228
+ return weights[1] > 0 ? weights[0] / weights[1] : Number.POSITIVE_INFINITY;
229
+ }