vector-mirror 1.0.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/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +65 -0
- package/src/adapters/emitter/prose.js +689 -0
- package/src/adapters/emitter/structured.js +649 -0
- package/src/adapters/renderer/playwright.js +7345 -0
- package/src/core/arbitrate.js +266 -0
- package/src/core/constraints/_schema.js +89 -0
- package/src/core/constraints/aligned.js +42 -0
- package/src/core/constraints/centered-in.js +29 -0
- package/src/core/constraints/color.js +63 -0
- package/src/core/constraints/distance.js +233 -0
- package/src/core/constraints/fill.js +22 -0
- package/src/core/constraints/inside.js +52 -0
- package/src/core/constraints/loader.js +65 -0
- package/src/core/constraints/no-overlap.js +50 -0
- package/src/core/constraints/positional.js +46 -0
- package/src/core/constraints/registry.js +98 -0
- package/src/core/constraints/same-size.js +35 -0
- package/src/core/diff.js +118 -0
- package/src/core/element_vocabulary.js +241 -0
- package/src/core/grid.js +240 -0
- package/src/core/honesty.js +214 -0
- package/src/core/sanitizer/auto_ids.js +104 -0
- package/src/core/tolerance.js +22 -0
- package/src/core/use_graph.js +541 -0
- package/src/interface/claims.js +439 -0
- package/src/interface/schema.js +626 -0
- package/src/interface/server.js +57 -0
- package/src/interface/tools.js +437 -0
- package/src/lib/bbox.js +17 -0
- package/src/lib/breaker.js +240 -0
- package/src/lib/geom.js +144 -0
- package/src/lib/palette.js +236 -0
- package/src/lib/transforms.js +111 -0
- package/src/pipeline.js +1983 -0
package/src/lib/geom.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* geom.js — AABB Geometry Mathematics for Vector Mirror v2.5
|
|
3
|
+
*
|
|
4
|
+
* Source of truth for axis-aligned bounding-box mathematics.
|
|
5
|
+
* All inputs are boxes of shape `{x, y, w, h}` (top-left + extent).
|
|
6
|
+
*
|
|
7
|
+
* Pure math: Node-only, no Playwright / DOM dependency.
|
|
8
|
+
*
|
|
9
|
+
* References:
|
|
10
|
+
* - RB-02 Pattern 4.2 — Pomona / dyn4j AABB Signed-Gap formulation
|
|
11
|
+
* - FIX_PLAN_2026-04-18 §1.2 P1-01
|
|
12
|
+
* - ADR-024 Kernel-Vertrag (bbox is the inviolable geometric authority)
|
|
13
|
+
*
|
|
14
|
+
* Notes:
|
|
15
|
+
* - SVG y-axis grows downward → "north" overflow means a sticks out *above* b.
|
|
16
|
+
* - Touching edges (signedDist = 0) count as separated, not overlapping.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-axis signed gaps between two AABBs.
|
|
21
|
+
* Negative on an axis ⇒ the boxes overlap on that axis.
|
|
22
|
+
* Used internally; exposed indirectly via signedGapComponents.
|
|
23
|
+
*
|
|
24
|
+
* @param {{x:number,y:number,w:number,h:number}} a
|
|
25
|
+
* @param {{x:number,y:number,w:number,h:number}} b
|
|
26
|
+
* @returns {{dx:number, dy:number}}
|
|
27
|
+
*/
|
|
28
|
+
function axisGaps(a, b) {
|
|
29
|
+
const dx = Math.max(a.x - (b.x + b.w), b.x - (a.x + a.w));
|
|
30
|
+
const dy = Math.max(a.y - (b.y + b.h), b.y - (a.y + a.h));
|
|
31
|
+
return { dx, dy };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Signed Euclidean gap between two AABBs (Pomona / dyn4j formulation).
|
|
36
|
+
*
|
|
37
|
+
* > 0 separated, value = shortest empty-space distance between rectangles
|
|
38
|
+
* = 0 touching (edge or corner aligned, no overlap)
|
|
39
|
+
* < 0 overlapping, value = penetration depth along the dominant axis
|
|
40
|
+
*
|
|
41
|
+
* @param {{x:number,y:number,w:number,h:number}} a
|
|
42
|
+
* @param {{x:number,y:number,w:number,h:number}} b
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
export function signedGapBoxToBox(a, b) {
|
|
46
|
+
const { dx, dy } = axisGaps(a, b);
|
|
47
|
+
if (dx < 0 && dy < 0) return Math.max(dx, dy);
|
|
48
|
+
return Math.hypot(Math.max(dx, 0), Math.max(dy, 0));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Signed gap with per-axis components, for vector-based corrections
|
|
53
|
+
* (Spotter "dx=-14px, dy=+3px" output).
|
|
54
|
+
*
|
|
55
|
+
* Components dx / dy are negative when the boxes overlap on that axis,
|
|
56
|
+
* zero when edges align, positive when there is empty space.
|
|
57
|
+
*
|
|
58
|
+
* `signedDist` follows {@link signedGapBoxToBox}.
|
|
59
|
+
*
|
|
60
|
+
* @returns {{dx:number, dy:number, signedDist:number}}
|
|
61
|
+
*/
|
|
62
|
+
export function signedGapComponents(a, b) {
|
|
63
|
+
const { dx, dy } = axisGaps(a, b);
|
|
64
|
+
const signedDist =
|
|
65
|
+
dx < 0 && dy < 0
|
|
66
|
+
? Math.max(dx, dy)
|
|
67
|
+
: Math.hypot(Math.max(dx, 0), Math.max(dy, 0));
|
|
68
|
+
return { dx, dy, signedDist };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Containment ratio: how much of subject `a` lies within reference `b`.
|
|
73
|
+
* 0 ⇒ a is fully outside b (no intersection)
|
|
74
|
+
* 1 ⇒ a is fully inside b
|
|
75
|
+
* 0..1 partial — subject straddles a boundary
|
|
76
|
+
*
|
|
77
|
+
* Subjects with zero area collapse to 0 (they have no area to contain).
|
|
78
|
+
*
|
|
79
|
+
* @param {{x:number,y:number,w:number,h:number}} a subject (typically element bbox)
|
|
80
|
+
* @param {{x:number,y:number,w:number,h:number}} b reference (typically canvas bbox)
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
export function containmentRatio(a, b) {
|
|
84
|
+
const ix = Math.max(a.x, b.x);
|
|
85
|
+
const iy = Math.max(a.y, b.y);
|
|
86
|
+
const ix2 = Math.min(a.x + a.w, b.x + b.w);
|
|
87
|
+
const iy2 = Math.min(a.y + a.h, b.y + b.h);
|
|
88
|
+
const intersection = Math.max(0, ix2 - ix) * Math.max(0, iy2 - iy);
|
|
89
|
+
const area = a.w * a.h;
|
|
90
|
+
return area === 0 ? 0 : intersection / area;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Overflow per cardinal edge — by how many pixels subject `a` exceeds
|
|
95
|
+
* container `b` on each side. Each component is non-negative.
|
|
96
|
+
*
|
|
97
|
+
* Compass mapping (SVG y-axis grows downward):
|
|
98
|
+
* north — a's top edge is above b's top edge
|
|
99
|
+
* south — a's bottom edge is below b's bottom edge
|
|
100
|
+
* east — a's right edge is right of b's right edge
|
|
101
|
+
* west — a's left edge is left of b's left edge
|
|
102
|
+
*
|
|
103
|
+
* @returns {{north:number, east:number, south:number, west:number}}
|
|
104
|
+
*/
|
|
105
|
+
export function overflowPerEdge(a, b) {
|
|
106
|
+
return {
|
|
107
|
+
north: Math.max(0, b.y - a.y),
|
|
108
|
+
east: Math.max(0, a.x + a.w - (b.x + b.w)),
|
|
109
|
+
south: Math.max(0, a.y + a.h - (b.y + b.h)),
|
|
110
|
+
west: Math.max(0, b.x - a.x),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Minimum Translation Vector — the smallest delta to apply to `a` so that
|
|
116
|
+
* `a` and `b` no longer overlap. Returns `{dx:0, dy:0, abs:0}` if the boxes
|
|
117
|
+
* are already separated (or merely touching).
|
|
118
|
+
*
|
|
119
|
+
* Resolution rules when overlapping:
|
|
120
|
+
* - Push along the axis with the smaller penetration depth.
|
|
121
|
+
* - On equal penetration, prefer the x-axis (deterministic tie-break).
|
|
122
|
+
* - Sign points away from b: a moves away from b's center.
|
|
123
|
+
* - When centers coincide, prefer +x (deterministic for fully congruent boxes).
|
|
124
|
+
*
|
|
125
|
+
* @returns {{dx:number, dy:number, abs:number}}
|
|
126
|
+
*/
|
|
127
|
+
export function mtv(a, b) {
|
|
128
|
+
const { dx, dy } = axisGaps(a, b);
|
|
129
|
+
if (dx >= 0 || dy >= 0) return { dx: 0, dy: 0, abs: 0 };
|
|
130
|
+
|
|
131
|
+
const penX = -dx;
|
|
132
|
+
const penY = -dy;
|
|
133
|
+
|
|
134
|
+
if (penX <= penY) {
|
|
135
|
+
const aCx = a.x + a.w / 2;
|
|
136
|
+
const bCx = b.x + b.w / 2;
|
|
137
|
+
const sign = aCx < bCx ? -1 : 1;
|
|
138
|
+
return { dx: sign * penX, dy: 0, abs: penX };
|
|
139
|
+
}
|
|
140
|
+
const aCy = a.y + a.h / 2;
|
|
141
|
+
const bCy = b.y + b.h / 2;
|
|
142
|
+
const sign = aCy < bCy ? -1 : 1;
|
|
143
|
+
return { dx: 0, dy: sign * penY, abs: penY };
|
|
144
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* palette.js - W3C CSS Named Colors & CIELAB Delta-E Logic
|
|
3
|
+
* Migrated from color-names.js (v1.6) + parseColor from grid.js
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PALETTE = [
|
|
8
|
+
{ name: 'aliceblue', r: 240, g: 248, b: 255 },
|
|
9
|
+
{ name: 'antiquewhite', r: 250, g: 235, b: 215 },
|
|
10
|
+
{ name: 'aqua', r: 0, g: 255, b: 255 },
|
|
11
|
+
{ name: 'aquamarine', r: 127, g: 255, b: 212 },
|
|
12
|
+
{ name: 'azure', r: 240, g: 255, b: 255 },
|
|
13
|
+
{ name: 'beige', r: 245, g: 245, b: 220 },
|
|
14
|
+
{ name: 'bisque', r: 255, g: 228, b: 196 },
|
|
15
|
+
{ name: 'black', r: 0, g: 0, b: 0 },
|
|
16
|
+
{ name: 'blanchedalmond', r: 255, g: 235, b: 205 },
|
|
17
|
+
{ name: 'blue', r: 0, g: 0, b: 255 },
|
|
18
|
+
{ name: 'blueviolet', r: 138, g: 43, b: 226 },
|
|
19
|
+
{ name: 'brown', r: 165, g: 42, b: 42 },
|
|
20
|
+
{ name: 'burlywood', r: 222, g: 184, b: 135 },
|
|
21
|
+
{ name: 'cadetblue', r: 95, g: 158, b: 160 },
|
|
22
|
+
{ name: 'chartreuse', r: 127, g: 255, b: 0 },
|
|
23
|
+
{ name: 'chocolate', r: 210, g: 105, b: 30 },
|
|
24
|
+
{ name: 'coral', r: 255, g: 127, b: 80 },
|
|
25
|
+
{ name: 'cornflowerblue', r: 100, g: 149, b: 237 },
|
|
26
|
+
{ name: 'cornsilk', r: 255, g: 248, b: 220 },
|
|
27
|
+
{ name: 'crimson', r: 220, g: 20, b: 60 },
|
|
28
|
+
{ name: 'cyan', r: 0, g: 255, b: 255 },
|
|
29
|
+
{ name: 'darkblue', r: 0, g: 0, b: 139 },
|
|
30
|
+
{ name: 'darkcyan', r: 0, g: 139, b: 139 },
|
|
31
|
+
{ name: 'darkgoldenrod', r: 184, g: 134, b: 11 },
|
|
32
|
+
{ name: 'darkgray', r: 169, g: 169, b: 169 },
|
|
33
|
+
{ name: 'darkgreen', r: 0, g: 100, b: 0 },
|
|
34
|
+
{ name: 'darkkhaki', r: 189, g: 183, b: 107 },
|
|
35
|
+
{ name: 'darkmagenta', r: 139, g: 0, b: 139 },
|
|
36
|
+
{ name: 'darkolivegreen', r: 85, g: 107, b: 47 },
|
|
37
|
+
{ name: 'darkorange', r: 255, g: 140, b: 0 },
|
|
38
|
+
{ name: 'darkorchid', r: 153, g: 50, b: 204 },
|
|
39
|
+
{ name: 'darkred', r: 139, g: 0, b: 0 },
|
|
40
|
+
{ name: 'darksalmon', r: 233, g: 150, b: 122 },
|
|
41
|
+
{ name: 'darkseagreen', r: 143, g: 188, b: 143 },
|
|
42
|
+
{ name: 'darkslateblue', r: 72, g: 61, b: 139 },
|
|
43
|
+
{ name: 'darkslategray', r: 47, g: 79, b: 79 },
|
|
44
|
+
{ name: 'darkturquoise', r: 0, g: 206, b: 209 },
|
|
45
|
+
{ name: 'darkviolet', r: 148, g: 0, b: 211 },
|
|
46
|
+
{ name: 'deeppink', r: 255, g: 20, b: 147 },
|
|
47
|
+
{ name: 'deepskyblue', r: 0, g: 191, b: 255 },
|
|
48
|
+
{ name: 'dimgray', r: 105, g: 105, b: 105 },
|
|
49
|
+
{ name: 'dodgerblue', r: 30, g: 144, b: 255 },
|
|
50
|
+
{ name: 'firebrick', r: 178, g: 34, b: 34 },
|
|
51
|
+
{ name: 'floralwhite', r: 255, g: 250, b: 240 },
|
|
52
|
+
{ name: 'forestgreen', r: 34, g: 139, b: 34 },
|
|
53
|
+
{ name: 'fuchsia', r: 255, g: 0, b: 255 },
|
|
54
|
+
{ name: 'gainsboro', r: 220, g: 220, b: 220 },
|
|
55
|
+
{ name: 'ghostwhite', r: 248, g: 248, b: 255 },
|
|
56
|
+
{ name: 'gold', r: 255, g: 215, b: 0 },
|
|
57
|
+
{ name: 'goldenrod', r: 218, g: 165, b: 32 },
|
|
58
|
+
{ name: 'gray', r: 128, g: 128, b: 128 },
|
|
59
|
+
{ name: 'green', r: 0, g: 128, b: 0 },
|
|
60
|
+
{ name: 'greenyellow', r: 173, g: 255, b: 47 },
|
|
61
|
+
{ name: 'honeydew', r: 240, g: 255, b: 240 },
|
|
62
|
+
{ name: 'hotpink', r: 255, g: 105, b: 180 },
|
|
63
|
+
{ name: 'indianred', r: 205, g: 92, b: 92 },
|
|
64
|
+
{ name: 'indigo', r: 75, g: 0, b: 130 },
|
|
65
|
+
{ name: 'ivory', r: 255, g: 255, b: 240 },
|
|
66
|
+
{ name: 'khaki', r: 240, g: 230, b: 140 },
|
|
67
|
+
{ name: 'lavender', r: 230, g: 230, b: 250 },
|
|
68
|
+
{ name: 'lavenderblush', r: 255, g: 240, b: 245 },
|
|
69
|
+
{ name: 'lawngreen', r: 124, g: 252, b: 0 },
|
|
70
|
+
{ name: 'lemonchiffon', r: 255, g: 250, b: 205 },
|
|
71
|
+
{ name: 'lightblue', r: 173, g: 216, b: 230 },
|
|
72
|
+
{ name: 'lightcoral', r: 240, g: 128, b: 128 },
|
|
73
|
+
{ name: 'lightcyan', r: 224, g: 255, b: 255 },
|
|
74
|
+
{ name: 'lightgoldenrodyellow', r: 250, g: 250, b: 210 },
|
|
75
|
+
{ name: 'lightgray', r: 211, g: 211, b: 211 },
|
|
76
|
+
{ name: 'lightgreen', r: 144, g: 238, b: 144 },
|
|
77
|
+
{ name: 'lightpink', r: 255, g: 182, b: 193 },
|
|
78
|
+
{ name: 'lightsalmon', r: 255, g: 160, b: 122 },
|
|
79
|
+
{ name: 'lightseagreen', r: 32, g: 178, b: 170 },
|
|
80
|
+
{ name: 'lightskyblue', r: 135, g: 206, b: 250 },
|
|
81
|
+
{ name: 'lightslategray', r: 119, g: 136, b: 153 },
|
|
82
|
+
{ name: 'lightsteelblue', r: 176, g: 196, b: 222 },
|
|
83
|
+
{ name: 'lightyellow', r: 255, g: 255, b: 224 },
|
|
84
|
+
{ name: 'lime', r: 0, g: 255, b: 0 },
|
|
85
|
+
{ name: 'limegreen', r: 50, g: 205, b: 50 },
|
|
86
|
+
{ name: 'linen', r: 250, g: 240, b: 230 },
|
|
87
|
+
{ name: 'magenta', r: 255, g: 0, b: 255 },
|
|
88
|
+
{ name: 'maroon', r: 128, g: 0, b: 0 },
|
|
89
|
+
{ name: 'mediumaquamarine', r: 102, g: 205, b: 170 },
|
|
90
|
+
{ name: 'mediumblue', r: 0, g: 0, b: 205 },
|
|
91
|
+
{ name: 'mediumorchid', r: 186, g: 85, b: 211 },
|
|
92
|
+
{ name: 'mediumpurple', r: 147, g: 112, b: 219 },
|
|
93
|
+
{ name: 'mediumseagreen', r: 60, g: 179, b: 113 },
|
|
94
|
+
{ name: 'mediumslateblue', r: 123, g: 104, b: 238 },
|
|
95
|
+
{ name: 'mediumspringgreen', r: 0, g: 250, b: 154 },
|
|
96
|
+
{ name: 'mediumturquoise', r: 72, g: 209, b: 204 },
|
|
97
|
+
{ name: 'mediumvioletred', r: 199, g: 21, b: 133 },
|
|
98
|
+
{ name: 'midnightblue', r: 25, g: 25, b: 112 },
|
|
99
|
+
{ name: 'mintcream', r: 245, g: 255, b: 250 },
|
|
100
|
+
{ name: 'mistyrose', r: 255, g: 228, b: 225 },
|
|
101
|
+
{ name: 'moccasin', r: 255, g: 228, b: 181 },
|
|
102
|
+
{ name: 'navajowhite', r: 255, g: 222, b: 173 },
|
|
103
|
+
{ name: 'navy', r: 0, g: 0, b: 128 },
|
|
104
|
+
{ name: 'oldlace', r: 253, g: 245, b: 230 },
|
|
105
|
+
{ name: 'olive', r: 128, g: 128, b: 0 },
|
|
106
|
+
{ name: 'olivedrab', r: 107, g: 142, b: 35 },
|
|
107
|
+
{ name: 'orange', r: 255, g: 165, b: 0 },
|
|
108
|
+
{ name: 'orangered', r: 255, g: 69, b: 0 },
|
|
109
|
+
{ name: 'orchid', r: 218, g: 112, b: 214 },
|
|
110
|
+
{ name: 'palegoldenrod', r: 238, g: 232, b: 170 },
|
|
111
|
+
{ name: 'palegreen', r: 152, g: 251, b: 152 },
|
|
112
|
+
{ name: 'paleturquoise', r: 175, g: 238, b: 238 },
|
|
113
|
+
{ name: 'palevioletred', r: 219, g: 112, b: 147 },
|
|
114
|
+
{ name: 'papayawhip', r: 255, g: 239, b: 213 },
|
|
115
|
+
{ name: 'peachpuff', r: 255, g: 218, b: 185 },
|
|
116
|
+
{ name: 'peru', r: 205, g: 133, b: 63 },
|
|
117
|
+
{ name: 'pink', r: 255, g: 192, b: 203 },
|
|
118
|
+
{ name: 'plum', r: 221, g: 160, b: 221 },
|
|
119
|
+
{ name: 'powderblue', r: 176, g: 224, b: 230 },
|
|
120
|
+
{ name: 'purple', r: 128, g: 0, b: 128 },
|
|
121
|
+
{ name: 'rebeccapurple', r: 102, g: 51, b: 153 },
|
|
122
|
+
{ name: 'red', r: 255, g: 0, b: 0 },
|
|
123
|
+
{ name: 'rosybrown', r: 188, g: 143, b: 143 },
|
|
124
|
+
{ name: 'royalblue', r: 65, g: 105, b: 225 },
|
|
125
|
+
{ name: 'saddlebrown', r: 139, g: 69, b: 19 },
|
|
126
|
+
{ name: 'salmon', r: 250, g: 128, b: 114 },
|
|
127
|
+
{ name: 'sandybrown', r: 244, g: 164, b: 96 },
|
|
128
|
+
{ name: 'seagreen', r: 46, g: 139, b: 87 },
|
|
129
|
+
{ name: 'seashell', r: 255, g: 245, b: 238 },
|
|
130
|
+
{ name: 'sienna', r: 160, g: 82, b: 45 },
|
|
131
|
+
{ name: 'silver', r: 192, g: 192, b: 192 },
|
|
132
|
+
{ name: 'skyblue', r: 135, g: 206, b: 235 },
|
|
133
|
+
{ name: 'slateblue', r: 106, g: 90, b: 205 },
|
|
134
|
+
{ name: 'slategray', r: 112, g: 128, b: 144 },
|
|
135
|
+
{ name: 'snow', r: 255, g: 250, b: 250 },
|
|
136
|
+
{ name: 'springgreen', r: 0, g: 255, b: 127 },
|
|
137
|
+
{ name: 'steelblue', r: 70, g: 130, b: 180 },
|
|
138
|
+
{ name: 'tan', r: 210, g: 180, b: 140 },
|
|
139
|
+
{ name: 'teal', r: 0, g: 128, b: 128 },
|
|
140
|
+
{ name: 'thistle', r: 216, g: 191, b: 216 },
|
|
141
|
+
{ name: 'tomato', r: 255, g: 99, b: 71 },
|
|
142
|
+
{ name: 'turquoise', r: 64, g: 224, b: 208 },
|
|
143
|
+
{ name: 'violet', r: 238, g: 130, b: 238 },
|
|
144
|
+
{ name: 'wheat', r: 245, g: 222, b: 179 },
|
|
145
|
+
{ name: 'white', r: 255, g: 255, b: 255 },
|
|
146
|
+
{ name: 'whitesmoke', r: 245, g: 245, b: 245 },
|
|
147
|
+
{ name: 'yellow', r: 255, g: 255, b: 0 },
|
|
148
|
+
{ name: 'yellowgreen', r: 154, g: 205, b: 50 },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* sRGB (0-255) -> CIELAB (D65)
|
|
153
|
+
*/
|
|
154
|
+
function srgbToLab(r, g, b) {
|
|
155
|
+
let lr = r / 255,
|
|
156
|
+
lg = g / 255,
|
|
157
|
+
lb = b / 255;
|
|
158
|
+
lr = lr > 0.04045 ? ((lr + 0.055) / 1.055) ** 2.4 : lr / 12.92;
|
|
159
|
+
lg = lg > 0.04045 ? ((lg + 0.055) / 1.055) ** 2.4 : lg / 12.92;
|
|
160
|
+
lb = lb > 0.04045 ? ((lb + 0.055) / 1.055) ** 2.4 : lb / 12.92;
|
|
161
|
+
|
|
162
|
+
const x = (lr * 0.4124564 + lg * 0.3575761 + lb * 0.1804375) / 0.95047;
|
|
163
|
+
const y = lr * 0.2126729 + lg * 0.7151522 + lb * 0.072175;
|
|
164
|
+
const z = (lr * 0.0193339 + lg * 0.119192 + lb * 0.9503041) / 1.08883;
|
|
165
|
+
|
|
166
|
+
const f = (t) => (t > 0.008856 ? t ** (1 / 3) : 7.787 * t + 16 / 116);
|
|
167
|
+
return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(z))];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const PALETTE_LAB = PALETTE.map((c) => ({
|
|
171
|
+
...c,
|
|
172
|
+
lab: srgbToLab(c.r, c.g, c.b),
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns the closest W3C color name for given RGB values.
|
|
177
|
+
*/
|
|
178
|
+
export function rgbToColorName(r, g, b) {
|
|
179
|
+
const [L1, a1, b1] = srgbToLab(r, g, b);
|
|
180
|
+
let minDe = Infinity,
|
|
181
|
+
closest = 'black';
|
|
182
|
+
|
|
183
|
+
for (const c of PALETTE_LAB) {
|
|
184
|
+
const [L2, a2, b2] = c.lab;
|
|
185
|
+
const de = Math.sqrt((L1 - L2) ** 2 + (a1 - a2) ** 2 + (b1 - b2) ** 2);
|
|
186
|
+
if (de < minDe) {
|
|
187
|
+
minDe = de;
|
|
188
|
+
closest = c.name;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return closest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parses CSS color string to W3C named color.
|
|
196
|
+
* Migrated from grid.js parseColor().
|
|
197
|
+
*
|
|
198
|
+
* §H9 P3 MESS-GRANULARITÄT: parseColor IST der Mess-Farbraum des Systems —
|
|
199
|
+
* jede numerische Farbe (hex/rgb) wird auf den NÄCHSTEN der 140 W3C-Namen
|
|
200
|
+
* quantisiert (CIELAB-Distanz, rgbToColorName). Konsequenz für Vergleiche
|
|
201
|
+
* (z.B. COLOR-Constraint): Hexes derselben Palette-Zelle sind GLEICH
|
|
202
|
+
* (#ff0000 ≡ #ff0001 ≡ 'red'), Hexes verschiedener Zellen verschieden
|
|
203
|
+
* (#ff0000 'red' ≢ #ff6347 'tomato'). Bewusste Mess-Granularität — eine
|
|
204
|
+
* hex-exakte Unterscheidung wäre ein eigener Kanal (Design-Runde), keine
|
|
205
|
+
* stille Änderung dieses Quantisierers.
|
|
206
|
+
*/
|
|
207
|
+
export function parseColor(css) {
|
|
208
|
+
if (!css || css === 'none' || css === 'rgba(0, 0, 0, 0)')
|
|
209
|
+
return 'transparent';
|
|
210
|
+
if (css === 'indeterminate') return 'indeterminate'; // §HEAL3b: ehrlicher Nicht-Mess-Token, erstklassig
|
|
211
|
+
if (css.startsWith('url(')) return 'gradient';
|
|
212
|
+
// §H9 K-08a: Hex-Formen (#rgb/#rrggbb) sind verlustfrei nach rgb auflösbar
|
|
213
|
+
// und werden in DENSELBEN Namensraum quantisiert wie die Mess-Seite
|
|
214
|
+
// (rgbToColorName/CIELAB) — kein zweites Farbsystem, keine Toleranz-Schwelle.
|
|
215
|
+
// Andere Token fallen wie bisher unverändert durch (Fallback unten).
|
|
216
|
+
const hex = css.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
217
|
+
if (hex) {
|
|
218
|
+
const h =
|
|
219
|
+
hex[1].length === 3
|
|
220
|
+
? hex[1].replace(/./g, (c) => c + c)
|
|
221
|
+
: hex[1];
|
|
222
|
+
return rgbToColorName(
|
|
223
|
+
parseInt(h.slice(0, 2), 16),
|
|
224
|
+
parseInt(h.slice(2, 4), 16),
|
|
225
|
+
parseInt(h.slice(4, 6), 16),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const match = css.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
229
|
+
if (match)
|
|
230
|
+
return rgbToColorName(
|
|
231
|
+
parseInt(match[1], 10),
|
|
232
|
+
parseInt(match[2], 10),
|
|
233
|
+
parseInt(match[3], 10),
|
|
234
|
+
);
|
|
235
|
+
return css;
|
|
236
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transforms.js — SVG transform-Token-Algebra (Hexagonal-rein, lib/-Schicht)
|
|
3
|
+
* Vector Mirror v2.6 · Sprint §1.5 Transform-Fallback
|
|
4
|
+
*
|
|
5
|
+
* lib-Modul (REGEL-4 Hexagonal): pure Funktionen, KEINE Importe aus adapters/
|
|
6
|
+
* oder interface/. Konsumenten: pipeline.js (arrange-Pfad) + structured.js
|
|
7
|
+
* (Emitter §1.5 Transform-Fallback).
|
|
8
|
+
*
|
|
9
|
+
* Extraktion aus pipeline.js (§1.5 Block B): buildTransform + hasTranslateTransform
|
|
10
|
+
* lebten im Orchestrator; der Emitter braucht sie für den Transform-Fallback.
|
|
11
|
+
* Verschiebung in lib/ macht sie für beide Schichten importierbar, ohne dass der
|
|
12
|
+
* Emitter den Orchestrator zieht (Hexagonal-Layering).
|
|
13
|
+
*
|
|
14
|
+
* ── Zwei Translate-Merge-Semantiken (R-A-Auflösung §1.5, bewusst getrennt) ──
|
|
15
|
+
* buildTransform (REPLACE-IN-PLACE): translate ersetzt den bestehenden
|
|
16
|
+
* translate-Token an dessen Original-Position (bzw. Listenanfang, wenn keiner
|
|
17
|
+
* existiert). Korrekt für arrange(): dort sind dx/dy Deltas im LOKALEN,
|
|
18
|
+
* un-transformierten Element-Koordinatensystem (state.bbox ist die deklarierte
|
|
19
|
+
* Local-BBox, NICHT die CTM-projizierte). Der Autor-transform (scale/rotate)
|
|
20
|
+
* bleibt als Kontext erhalten — Erhalt der Autor-Intention.
|
|
21
|
+
* Empirie-Anker: test_arrange.js:428 'rotate(45) translate(5 6) scale(2)'
|
|
22
|
+
* → 'rotate(45) translate(140 280) scale(2)'.
|
|
23
|
+
*
|
|
24
|
+
* prependTranslate (FRONT-PREPEND): translate steht IMMER am Listenanfang
|
|
25
|
+
* (zuletzt angewendet = wirkt in finaler Welt-Koordinate). Korrekt für den
|
|
26
|
+
* §1.5-Position-Fix: dort kommen dx/dy vom Spotter, berechnet auf der
|
|
27
|
+
* CTM-PROJIZIERTEN Welt-BBox. Die Gegenbewegung muss außen wrappen, damit sie
|
|
28
|
+
* unabhängig von einem Autor-scale()/rotate() in Welt-px wirkt (SOTA Präzision 1).
|
|
29
|
+
*
|
|
30
|
+
* Beide Funktionen entfernen vorhandene translate-Tokens VOR dem Insert →
|
|
31
|
+
* Idempotenz (G-14): wiederholte Anwendung mit gleichem dx/dy ist Fixpunkt.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* True, wenn der transform-String einen translate/translateX/translateY/
|
|
36
|
+
* translateZ-Token trägt. Case-insensitiv; tolerant gegen null/undefined.
|
|
37
|
+
*/
|
|
38
|
+
export function hasTranslateTransform(transform) {
|
|
39
|
+
return /\btranslate[XYZ]?\s*\(/i.test(transform ?? '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Zerlegt einen transform-String in Funktions-Tokens (z.B. ['rotate(45)',
|
|
44
|
+
* 'scale(2)']). Liefert null, wenn kein Token erkennbar ist.
|
|
45
|
+
*/
|
|
46
|
+
function tokenize(transform) {
|
|
47
|
+
const trimmed = transform?.trim();
|
|
48
|
+
if (!trimmed) return [];
|
|
49
|
+
return trimmed.match(/[a-zA-Z]+\s*\([^)]*\)/g);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* buildTransform (REPLACE-IN-PLACE) — siehe Modul-Doku.
|
|
54
|
+
* Setzt translate(dx dy) an die Position des ersten bestehenden translate-Tokens
|
|
55
|
+
* (oder an den Listenanfang, wenn keiner existiert) und entfernt alle übrigen
|
|
56
|
+
* translate-Tokens. Erhält die relative Ordnung der Nicht-translate-Tokens.
|
|
57
|
+
*
|
|
58
|
+
* @param {string|undefined} existingTransform - bestehender transform-Attribut-Wert.
|
|
59
|
+
* @param {number} dx - X-Verschiebung.
|
|
60
|
+
* @param {number} dy - Y-Verschiebung.
|
|
61
|
+
* @returns {string} neuer transform-String.
|
|
62
|
+
*/
|
|
63
|
+
export function buildTransform(existingTransform, dx, dy) {
|
|
64
|
+
const translate = `translate(${dx} ${dy})`;
|
|
65
|
+
const trimmed = existingTransform?.trim();
|
|
66
|
+
if (!trimmed) return translate;
|
|
67
|
+
|
|
68
|
+
const tokens = tokenize(trimmed);
|
|
69
|
+
if (!tokens) return `${translate} ${trimmed}`;
|
|
70
|
+
|
|
71
|
+
const nextTokens = [];
|
|
72
|
+
let translateIndex = null;
|
|
73
|
+
|
|
74
|
+
for (const token of tokens) {
|
|
75
|
+
if (/^translate[XYZ]?\s*\(/i.test(token)) {
|
|
76
|
+
if (translateIndex === null) translateIndex = nextTokens.length;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
nextTokens.push(token.trim());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const insertAt = translateIndex ?? 0;
|
|
83
|
+
nextTokens.splice(insertAt, 0, translate);
|
|
84
|
+
return nextTokens.join(' ').trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* prependTranslate (FRONT-PREPEND) — siehe Modul-Doku.
|
|
89
|
+
* Stellt translate(dx dy) IMMER an den Listenanfang und entfernt alle
|
|
90
|
+
* bestehenden translate-Tokens (Idempotenz). Erhält die relative Ordnung der
|
|
91
|
+
* Nicht-translate-Tokens hinter dem neuen translate.
|
|
92
|
+
*
|
|
93
|
+
* Für den §1.5-Position-Fix: dx/dy sind Welt-px-Deltas, die außen wrappen
|
|
94
|
+
* müssen, damit sie unabhängig von Autor-scale()/rotate() wirken.
|
|
95
|
+
*
|
|
96
|
+
* @param {string|undefined} existingTransform - bestehender transform-Attribut-Wert.
|
|
97
|
+
* @param {number} dx - X-Verschiebung in Welt-px.
|
|
98
|
+
* @param {number} dy - Y-Verschiebung in Welt-px.
|
|
99
|
+
* @returns {string} neuer transform-String, beginnt mit 'translate('.
|
|
100
|
+
*/
|
|
101
|
+
export function prependTranslate(existingTransform, dx, dy) {
|
|
102
|
+
const translate = `translate(${dx} ${dy})`;
|
|
103
|
+
const tokens = tokenize(existingTransform);
|
|
104
|
+
if (!tokens || tokens.length === 0) return translate;
|
|
105
|
+
|
|
106
|
+
const kept = tokens
|
|
107
|
+
.filter((token) => !/^translate[XYZ]?\s*\(/i.test(token))
|
|
108
|
+
.map((token) => token.trim());
|
|
109
|
+
|
|
110
|
+
return [translate, ...kept].join(' ').trim();
|
|
111
|
+
}
|