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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distance.js - DISTANCE-FROM Constraint
|
|
3
|
+
* Migrated from mirror.js:94-102
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
* Uses ctx.value (multiplier, default 1) + ctx.grid
|
|
6
|
+
*/
|
|
7
|
+
import { registerConstraint } from './registry.js';
|
|
8
|
+
|
|
9
|
+
registerConstraint('DISTANCE-FROM', {
|
|
10
|
+
check(subj, ref, { grid, value }) {
|
|
11
|
+
const gapX = Math.max(
|
|
12
|
+
0,
|
|
13
|
+
Math.max(subj.bbox.x, ref.bbox.x) -
|
|
14
|
+
Math.min(subj.bbox.x + subj.bbox.w, ref.bbox.x + ref.bbox.w),
|
|
15
|
+
);
|
|
16
|
+
const gapY = Math.max(
|
|
17
|
+
0,
|
|
18
|
+
Math.max(subj.bbox.y, ref.bbox.y) -
|
|
19
|
+
Math.min(subj.bbox.y + subj.bbox.h, ref.bbox.y + ref.bbox.h),
|
|
20
|
+
);
|
|
21
|
+
const actualDist = Math.sqrt(gapX * gapX + gapY * gapY);
|
|
22
|
+
const targetValue = value ?? 1;
|
|
23
|
+
// D-003: Ein negativer Ziel-Abstand ist eine INVALIDE Mess-Vorgabe — kein
|
|
24
|
+
// physikalisch sinnvoller Abstand kann < 0 sein. Ohne diese Wache liefe
|
|
25
|
+
// `actualDist >= expectedDist` mit expectedDist < 0 IMMER auf pass=true
|
|
26
|
+
// hinaus (jede nicht-negative reale Distanz erfuellt eine negative Schranke),
|
|
27
|
+
// d.h. das Auge meldete still "Alles korrekt" auf eine unmessbare Vorgabe.
|
|
28
|
+
// Das verletzt Prinzip-3 (keine stille Korrektur/Beschoenigung im Auge):
|
|
29
|
+
// wir CLAMPEN NICHT (das waere eine stille Korrektur der Vorgabe), sondern
|
|
30
|
+
// reichen den Wert unveraendert durch und melden ehrlich Unmessbarkeit.
|
|
31
|
+
// pass:null → arbitrate.js sortiert nach `unchecked`, NIE nach `passing`.
|
|
32
|
+
if (targetValue < 0) {
|
|
33
|
+
return {
|
|
34
|
+
pass: null,
|
|
35
|
+
reasonCode: 'INVALID_MEASUREMENT',
|
|
36
|
+
reasonCategory: 'SPECIFICATION',
|
|
37
|
+
detail: `Negativer Ziel-Abstand (${targetValue}) ist keine messbare Vorgabe — ein Abstand kann nicht < 0 sein.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const expectedDist = targetValue * grid.cellW;
|
|
41
|
+
const pass = actualDist >= expectedDist;
|
|
42
|
+
const shortfall = expectedDist - actualDist;
|
|
43
|
+
if (pass) {
|
|
44
|
+
return { pass: true, detail: null };
|
|
45
|
+
}
|
|
46
|
+
//
|
|
47
|
+
// + F-CODEX-Re-Review-F3 (Patch-2, 2026-05-31):
|
|
48
|
+
// Strukturierte Pixel-Korrektur dx/dy zusaetzlich zur detail-Prosa.
|
|
49
|
+
// REGEL-3 verlangt maschinenlesbare Deltas. INV-3 fordert Korrektheit:
|
|
50
|
+
// nach REALER AABB-Translation MUSS actualDist_after >= expectedDist.
|
|
51
|
+
//
|
|
52
|
+
// Strategie: VECTOR-SCALING (nicht 1D-Dominant-Axis).
|
|
53
|
+
// Begruendung: Euklid-Distanz ist 2D. Die alte 1D-Dominant-Axis-Variante
|
|
54
|
+
// lieferte fuer gapX=gapY=10, expected=20 ein dx=6 → neue Distanz
|
|
55
|
+
// sqrt(16^2+10^2)=18.87 < 20 (Codex-Probe Patch-1). Korrekt ist
|
|
56
|
+
// Skalierung des AABB-Gap-Vektors auf expectedDist via
|
|
57
|
+
// factor = expectedDist/actualDist.
|
|
58
|
+
//
|
|
59
|
+
// Verifikation Codex-Probe (gapX=gapY=10, expected=20):
|
|
60
|
+
// actualDist=14.14, factor=1.414, rawDx=rawDy=4.14
|
|
61
|
+
// nach Apply: gap=(14.14,14.14), dist=19.99 ~ 20 ✓
|
|
62
|
+
//
|
|
63
|
+
// Sonderfaelle (in Reihenfolge):
|
|
64
|
+
// 1) actualDist == 0 (Elemente raeumlich ueberlappend, gapX=gapY=0):
|
|
65
|
+
// Vector-Skalierung undefiniert. Push subj's linke (bzw. rechte) Kante
|
|
66
|
+
// auf ref's gegenueberliegende Kante + expectedDist.
|
|
67
|
+
// F-CODEX-Re-Review-F3-Fix:
|
|
68
|
+
// Patch-1 setzte dx = expectedDist. Bei subj=ref=(0,0,10,10),
|
|
69
|
+
// expected=20 → dx=20, neuer subj.x=20, AABB-gapX=20-10=10 → newDist=10 < 20 ✗
|
|
70
|
+
// Korrekt: targetSubjX = ref.x + ref.w + expectedDist (push rechts)
|
|
71
|
+
// dx = targetSubjX - subj.x = (ref.x + ref.w - subj.x) + expectedDist
|
|
72
|
+
// Verifikation F-3-Probe (subj=ref=(0,0,10,10), expected=20):
|
|
73
|
+
// dx = (0+10-0) + 20 = 30. Neuer subj.x=30. AABB-gapX=30-10=20.
|
|
74
|
+
// newDist=20 >= 20 ✓
|
|
75
|
+
// Sign-Wahl: signX aus centerDx (falls != 0), sonst default +1
|
|
76
|
+
// (push rechts, analog arrange()-Fallback Z.148).
|
|
77
|
+
// 2) centerDx == 0 UND centerDy == 0 (identisch zentriert ABER mit gap>0,
|
|
78
|
+
// z.B. konzentrische Boxen): Sign-Vektor undefiniert. Push in X um
|
|
79
|
+
// shortfall (Konsistenz mit arrange()-Fallback). Hinweis: bei
|
|
80
|
+
// identisch zentrierten Boxen IST actualDist == 0 (gapX=gapY=0
|
|
81
|
+
// geometrisch zwingend), d.h. dieser Pfad ist defensiv und wird
|
|
82
|
+
// vom Sonderfall 1 oben bereits abgedeckt — bleibt nur fuer
|
|
83
|
+
// Floating-Point-Pathologie.
|
|
84
|
+
// 3) Sonst: rawDx/rawDy = (factor-1) * gapX/gapY, Sign aus center-Relativ.
|
|
85
|
+
// Math.ceil fuer Korrektheit. Mindestens 1px nur wenn rawAchse != 0
|
|
86
|
+
// (kein Achsen-Push fuer perfekt-vertikale/horizontale Gaps mit gap=0).
|
|
87
|
+
const sCx = subj.bbox.x + subj.bbox.w / 2;
|
|
88
|
+
const sCy = subj.bbox.y + subj.bbox.h / 2;
|
|
89
|
+
const rCx = ref.bbox.x + ref.bbox.w / 2;
|
|
90
|
+
const rCy = ref.bbox.y + ref.bbox.h / 2;
|
|
91
|
+
const centerDx = sCx - rCx;
|
|
92
|
+
const centerDy = sCy - rCy;
|
|
93
|
+
// G2 (D-006): detail beschreibt NUR das WAS/WIEVIEL-daneben (Ist-Distanz
|
|
94
|
+
// vs. Soll-Distanz, Defizit). Die strukturierte Korrektur lebt
|
|
95
|
+
// ausschliesslich in dx/dy — prose.js/structured.js bauen den
|
|
96
|
+
// Korrektur-Hinweis aus diesen Feldern, nicht mehr aus dem detail-String.
|
|
97
|
+
const detail = `Zu nah dran (${Math.round(actualDist)}px statt ${Math.round(expectedDist)}px, Defizit ~${Math.round(shortfall)}px)`;
|
|
98
|
+
// Sonderfall 1: actualDist == 0 → keine Skalierung moeglich.
|
|
99
|
+
// F-CODEX-Re-Review-F3-Fix: dx MUSS die raeumliche Ueberlappung +
|
|
100
|
+
// expectedDist ueberwinden. Push entlang dominanter Achse (X bei
|
|
101
|
+
// |centerDx|>=|centerDy|, sonst Y). Default +X bei centerDx==centerDy==0.
|
|
102
|
+
if (actualDist === 0) {
|
|
103
|
+
const dominantX = Math.abs(centerDx) >= Math.abs(centerDy);
|
|
104
|
+
if (dominantX) {
|
|
105
|
+
const signX = centerDx >= 0 ? 1 : -1;
|
|
106
|
+
// signX>0: subj nach rechts. targetSubjX = ref.x + ref.w + expectedDist
|
|
107
|
+
// signX<0: subj nach links. targetSubjX = ref.x - subj.w - expectedDist
|
|
108
|
+
const targetSubjX =
|
|
109
|
+
signX > 0
|
|
110
|
+
? ref.bbox.x + ref.bbox.w + expectedDist
|
|
111
|
+
: ref.bbox.x - subj.bbox.w - expectedDist;
|
|
112
|
+
const rawDx = targetSubjX - subj.bbox.x;
|
|
113
|
+
const dxRounded =
|
|
114
|
+
rawDx >= 0 ? Math.ceil(rawDx) : -Math.ceil(Math.abs(rawDx));
|
|
115
|
+
const dxFinal = dxRounded === 0 ? signX : dxRounded;
|
|
116
|
+
return { pass: false, detail, dx: dxFinal };
|
|
117
|
+
} else {
|
|
118
|
+
const signY = centerDy >= 0 ? 1 : -1;
|
|
119
|
+
const targetSubjY =
|
|
120
|
+
signY > 0
|
|
121
|
+
? ref.bbox.y + ref.bbox.h + expectedDist
|
|
122
|
+
: ref.bbox.y - subj.bbox.h - expectedDist;
|
|
123
|
+
const rawDy = targetSubjY - subj.bbox.y;
|
|
124
|
+
const dyRounded =
|
|
125
|
+
rawDy >= 0 ? Math.ceil(rawDy) : -Math.ceil(Math.abs(rawDy));
|
|
126
|
+
const dyFinal = dyRounded === 0 ? signY : dyRounded;
|
|
127
|
+
return { pass: false, detail, dy: dyFinal };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Sonderfall 2: identisch zentriert (centerDx==0 AND centerDy==0).
|
|
131
|
+
// Defensiv: geometrisch wird das bereits von Sonderfall 1 abgefangen
|
|
132
|
+
// (centers identisch ⇒ gapX=gapY=0 ⇒ actualDist=0), bleibt nur fuer
|
|
133
|
+
// Floating-Point-Pathologie.
|
|
134
|
+
if (centerDx === 0 && centerDy === 0) {
|
|
135
|
+
return {
|
|
136
|
+
pass: false,
|
|
137
|
+
detail,
|
|
138
|
+
dx: Math.max(1, Math.round(shortfall)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Vector-Scaling.
|
|
142
|
+
const factor = expectedDist / actualDist; // > 1 weil pass=false
|
|
143
|
+
const rawDx = (factor - 1) * gapX;
|
|
144
|
+
const rawDy = (factor - 1) * gapY;
|
|
145
|
+
// Sign aus center-Relativ (push-weg-vektor). Bei centerDx==0 (perfekt
|
|
146
|
+
// vertikal ausgerichtet): rawDx ist ohnehin 0 (gapX==0 in dem Setup) →
|
|
147
|
+
// sign-Wahl egal, dx=0.
|
|
148
|
+
const signX = centerDx >= 0 ? 1 : -1;
|
|
149
|
+
const signY = centerDy >= 0 ? 1 : -1;
|
|
150
|
+
// Aufrunden (Math.ceil) statt Math.round gegen Rundungs-Defizit:
|
|
151
|
+
// Math.round(4.142)=4 wuerde fuer Codex-Probe newDist=19.8 < 20 liefern
|
|
152
|
+
// und INV-3-Korrektheit brechen. Math.ceil garantiert
|
|
153
|
+
// newGap_axis >= raw_target → newDist >= expectedDist.
|
|
154
|
+
// Mindest-Push 1px erfolgt nur wenn rawAchse != 0 (sonst wuerde ein
|
|
155
|
+
// 1px-Push auf eine Achse ohne Gap kreiert, was semantisch falsch waere).
|
|
156
|
+
let dx = 0;
|
|
157
|
+
let dy = 0;
|
|
158
|
+
if (rawDx !== 0) {
|
|
159
|
+
const r = Math.ceil(Math.abs(rawDx));
|
|
160
|
+
dx = Math.max(1, r) * signX;
|
|
161
|
+
}
|
|
162
|
+
if (rawDy !== 0) {
|
|
163
|
+
const r = Math.ceil(Math.abs(rawDy));
|
|
164
|
+
dy = Math.max(1, r) * signY;
|
|
165
|
+
}
|
|
166
|
+
// Mindestens eine der beiden Achsen MUSS != 0 sein (REGEL-3-Vertrag).
|
|
167
|
+
// Wenn beide rawDx, rawDy == 0 waeren, waere actualDist == 0 — und das
|
|
168
|
+
// ist oben (Sonderfall 1) bereits abgefangen. Defensiv: wenn doch beide
|
|
169
|
+
// 0 (Floating-Point-Pathologie), fallback dx=1.
|
|
170
|
+
if (dx === 0 && dy === 0) {
|
|
171
|
+
dx = 1;
|
|
172
|
+
}
|
|
173
|
+
// Output: nur gesetzte Achsen aufnehmen (kanonische Form analog
|
|
174
|
+
// centered-in.js — undefined statt 0 ist semantisch "keine Korrektion").
|
|
175
|
+
const out = { pass: false, detail };
|
|
176
|
+
if (dx !== 0) out.dx = dx;
|
|
177
|
+
if (dy !== 0) out.dy = dy;
|
|
178
|
+
return out;
|
|
179
|
+
},
|
|
180
|
+
arrange(subj, ref, { canvas, value }) {
|
|
181
|
+
const cellsX = Math.max(4, Math.min(16, Math.round(canvas.width / 50)));
|
|
182
|
+
const cellW = canvas.width / cellsX;
|
|
183
|
+
const minDist = (value ?? 1) * cellW;
|
|
184
|
+
|
|
185
|
+
// AABB gap metric — identical to check() for consistency (Audit Fix J)
|
|
186
|
+
const gapX = Math.max(
|
|
187
|
+
0,
|
|
188
|
+
Math.max(subj.bbox.x, ref.bbox.x) -
|
|
189
|
+
Math.min(subj.bbox.x + subj.bbox.w, ref.bbox.x + ref.bbox.w),
|
|
190
|
+
);
|
|
191
|
+
const gapY = Math.max(
|
|
192
|
+
0,
|
|
193
|
+
Math.max(subj.bbox.y, ref.bbox.y) -
|
|
194
|
+
Math.min(subj.bbox.y + subj.bbox.h, ref.bbox.y + ref.bbox.h),
|
|
195
|
+
);
|
|
196
|
+
const actualDist = Math.sqrt(gapX * gapX + gapY * gapY);
|
|
197
|
+
|
|
198
|
+
// Already far enough apart — no-op
|
|
199
|
+
if (actualDist >= minDist) {
|
|
200
|
+
return { x: subj.bbox.x, y: subj.bbox.y };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Push along dominant axis (center-to-center direction)
|
|
204
|
+
const sCx = subj.bbox.x + subj.bbox.w / 2;
|
|
205
|
+
const sCy = subj.bbox.y + subj.bbox.h / 2;
|
|
206
|
+
const rCx = ref.bbox.x + ref.bbox.w / 2;
|
|
207
|
+
const rCy = ref.bbox.y + ref.bbox.h / 2;
|
|
208
|
+
const dx = sCx - rCx;
|
|
209
|
+
const dy = sCy - rCy;
|
|
210
|
+
|
|
211
|
+
// Deterministic fallback for identical positions: push right (Audit Fix D)
|
|
212
|
+
if (dx === 0 && dy === 0) {
|
|
213
|
+
return { x: ref.bbox.x + ref.bbox.w + minDist, y: subj.bbox.y };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Push along dominant axis so AABB gap = minDist
|
|
217
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
218
|
+
const sign = dx >= 0 ? 1 : -1;
|
|
219
|
+
const targetX =
|
|
220
|
+
sign > 0
|
|
221
|
+
? ref.bbox.x + ref.bbox.w + minDist
|
|
222
|
+
: ref.bbox.x - subj.bbox.w - minDist;
|
|
223
|
+
return { x: targetX, y: subj.bbox.y };
|
|
224
|
+
} else {
|
|
225
|
+
const sign = dy >= 0 ? 1 : -1;
|
|
226
|
+
const targetY =
|
|
227
|
+
sign > 0
|
|
228
|
+
? ref.bbox.y + ref.bbox.h + minDist
|
|
229
|
+
: ref.bbox.y - subj.bbox.h - minDist;
|
|
230
|
+
return { x: subj.bbox.x, y: targetY };
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fill.js - FILL Constraint (arrange-only)
|
|
3
|
+
* Element fills the entire canvas (x:0, y:0, width:W, height:H)
|
|
4
|
+
* Vector Mirror v2.0 Phase 3a
|
|
5
|
+
*/
|
|
6
|
+
import { registerConstraint } from './registry.js';
|
|
7
|
+
|
|
8
|
+
registerConstraint('FILL', {
|
|
9
|
+
// FILL braucht keine Referenz (Element fuellt die Leinwand). Ohne diesen
|
|
10
|
+
// Marker wuerde die pipeline-Wache (fail-closed-Default) FILL faelschlich
|
|
11
|
+
// als referenz-pflichtig behandeln.
|
|
12
|
+
requiresReference: false,
|
|
13
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
14
|
+
check(_subj, _ref, _ctx) {
|
|
15
|
+
// arrange-only constraint — check always returns null
|
|
16
|
+
return { pass: null, detail: 'FILL ist ein arrange-only Constraint' };
|
|
17
|
+
},
|
|
18
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
19
|
+
arrange(_subj, _ref, { canvas }) {
|
|
20
|
+
return { x: 0, y: 0, width: canvas.width, height: canvas.height };
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inside.js - INSIDE Constraint
|
|
3
|
+
* Migrated from mirror.js:53-62
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
import { registerConstraint } from './registry.js';
|
|
7
|
+
|
|
8
|
+
registerConstraint('INSIDE', {
|
|
9
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
10
|
+
check(subj, ref, _ctx) {
|
|
11
|
+
const inside =
|
|
12
|
+
subj.bbox.x >= ref.bbox.x &&
|
|
13
|
+
subj.bbox.y >= ref.bbox.y &&
|
|
14
|
+
subj.bbox.x + subj.bbox.w <= ref.bbox.x + ref.bbox.w &&
|
|
15
|
+
subj.bbox.y + subj.bbox.h <= ref.bbox.y + ref.bbox.h;
|
|
16
|
+
if (inside) return { pass: true, detail: null };
|
|
17
|
+
const dx =
|
|
18
|
+
subj.bbox.x < ref.bbox.x
|
|
19
|
+
? ref.bbox.x - subj.bbox.x
|
|
20
|
+
: subj.bbox.x + subj.bbox.w > ref.bbox.x + ref.bbox.w
|
|
21
|
+
? ref.bbox.x + ref.bbox.w - (subj.bbox.x + subj.bbox.w)
|
|
22
|
+
: 0;
|
|
23
|
+
const dy =
|
|
24
|
+
subj.bbox.y < ref.bbox.y
|
|
25
|
+
? ref.bbox.y - subj.bbox.y
|
|
26
|
+
: subj.bbox.y + subj.bbox.h > ref.bbox.y + ref.bbox.h
|
|
27
|
+
? ref.bbox.y + ref.bbox.h - (subj.bbox.y + subj.bbox.h)
|
|
28
|
+
: 0;
|
|
29
|
+
const cParts = [];
|
|
30
|
+
if (dx !== 0) cParts.push(`dx=${Math.round(dx)}px`);
|
|
31
|
+
if (dy !== 0) cParts.push(`dy=${Math.round(dy)}px`);
|
|
32
|
+
const result = {
|
|
33
|
+
pass: false,
|
|
34
|
+
detail: `Ragt aus #${ref.id} heraus. Korrektur: ${cParts.join(', ')}`,
|
|
35
|
+
};
|
|
36
|
+
if (dx !== 0) result.dx = Math.round(dx);
|
|
37
|
+
if (dy !== 0) result.dy = Math.round(dy);
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
41
|
+
arrange(subj, ref, _ctx) {
|
|
42
|
+
const x = Math.max(
|
|
43
|
+
ref.bbox.x,
|
|
44
|
+
Math.min(subj.bbox.x, ref.bbox.x + ref.bbox.w - subj.bbox.w),
|
|
45
|
+
);
|
|
46
|
+
const y = Math.max(
|
|
47
|
+
ref.bbox.y,
|
|
48
|
+
Math.min(subj.bbox.y, ref.bbox.y + ref.bbox.h - subj.bbox.h),
|
|
49
|
+
);
|
|
50
|
+
return { x, y };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loader.js - Side-Effect Imports for all Constraints + REGEL-3 Validation-Wrapper
|
|
3
|
+
* ADR-020: New constraint = 1 file + 1 import here
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*
|
|
6
|
+
*
|
|
7
|
+
* Zusaetzlich zum side-effect-Registrieren exportiert loader.js jetzt
|
|
8
|
+
* `validatedCheckConstraint(type, subj, ref, ctx)`. Diese Funktion
|
|
9
|
+
* delegiert an registry.checkConstraint und prueft das Resultat gegen
|
|
10
|
+
* constraintCheckSchema (REGEL-3 Output-Boundary-Gate, WELLE-β-004).
|
|
11
|
+
*
|
|
12
|
+
* pipeline.js bleibt UNVERAENDERT — Edit C ist additive API. Die
|
|
13
|
+
* Drift-Verhinderung greift via test_regel3_invariants.js (Edit H),
|
|
14
|
+
* das validatedCheckConstraint generativ ueber alle registrierten
|
|
15
|
+
* Constraints aufruft. Damit faengt jeder neue Constraint, der REGEL-3
|
|
16
|
+
* bricht (kein dx/dy bei fail), schon im Unit-Test (Fail-Fast).
|
|
17
|
+
*
|
|
18
|
+
* Warum nicht registry.js direkt patchen: registry.js liegt ausserhalb
|
|
19
|
+
* der 8 Patch-Files (H-9). Andon-Cord-#3-Vermeidung — andere Constraints
|
|
20
|
+
* (CENTERED-IN, NO-OVERLAP, etc.) bleiben unangetastet im Pipeline-Pfad,
|
|
21
|
+
* keine Pre-Existing-Drift wird stillschweigend "nebenbei" gefixed.
|
|
22
|
+
*/
|
|
23
|
+
import './centered-in.js';
|
|
24
|
+
import './no-overlap.js';
|
|
25
|
+
import './inside.js';
|
|
26
|
+
import './aligned.js';
|
|
27
|
+
import './positional.js';
|
|
28
|
+
import './distance.js';
|
|
29
|
+
import './same-size.js';
|
|
30
|
+
import './color.js';
|
|
31
|
+
import './fill.js';
|
|
32
|
+
|
|
33
|
+
import { constraintCheckSchema } from './_schema.js';
|
|
34
|
+
import { checkConstraint } from './registry.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* REGEL-3 Output-Boundary-Gate.
|
|
38
|
+
*
|
|
39
|
+
* Aufruf delegiert an checkConstraint, validiert das Resultat gegen
|
|
40
|
+
* constraintCheckSchema und wirft bei Verletzung einen Error mit
|
|
41
|
+
* Constraint-Typ + Detail. Bei `pass === null` (Unbekannter Constraint)
|
|
42
|
+
* wird das Resultat ohne Validation zurueckgegeben — `null` ist eine
|
|
43
|
+
* gueltige "kein Verdict moeglich"-Antwort und faellt aus REGEL-3 raus.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} type Constraint-Typ (z.B. 'CENTERED-IN', 'DISTANCE-FROM')
|
|
46
|
+
* @param {object} subj Subject-Element aus gridMap
|
|
47
|
+
* @param {object|null} ref Reference-Element aus gridMap
|
|
48
|
+
* @param {object} ctx Kontext { grid, value? }
|
|
49
|
+
* @returns {{ pass: boolean|null, detail: string|null, dx?: number, dy?: number, dw?: number, dh?: number }}
|
|
50
|
+
* @throws Error wenn Constraint REGEL-3 verletzt (fail ohne dx/dy)
|
|
51
|
+
*/
|
|
52
|
+
export function validatedCheckConstraint(type, subj, ref, ctx) {
|
|
53
|
+
const result = checkConstraint(type, subj, ref, ctx);
|
|
54
|
+
// pass === null → "Unbekannter Constraint" (registry.js Z.29).
|
|
55
|
+
// Das ist KEIN REGEL-3-Bruch, sondern eine Lookup-Failure — durchreichen.
|
|
56
|
+
if (result.pass === null) return result;
|
|
57
|
+
const parsed = constraintCheckSchema.safeParse(result);
|
|
58
|
+
if (!parsed.success) {
|
|
59
|
+
const reason = parsed.error.issues.map((i) => i.message).join('; ');
|
|
60
|
+
throw new Error(
|
|
61
|
+
`[REGEL-3] Constraint '${type}' verletzt Output-Boundary-Vertrag: ${reason}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* no-overlap.js - NO-OVERLAP Constraint
|
|
3
|
+
* Migrated from mirror.js:42-52
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { bboxOverlap } from '../../lib/bbox.js';
|
|
8
|
+
import { registerConstraint } from './registry.js';
|
|
9
|
+
|
|
10
|
+
registerConstraint('NO-OVERLAP', {
|
|
11
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
12
|
+
check(subj, ref, _ctx) {
|
|
13
|
+
const overlaps = bboxOverlap(subj.bbox, ref.bbox);
|
|
14
|
+
if (!overlaps) return { pass: true, detail: null };
|
|
15
|
+
const dxL = ref.bbox.x + ref.bbox.w - subj.bbox.x;
|
|
16
|
+
const dxR = ref.bbox.x - (subj.bbox.x + subj.bbox.w);
|
|
17
|
+
const dyT = ref.bbox.y + ref.bbox.h - subj.bbox.y;
|
|
18
|
+
const dyB = ref.bbox.y - (subj.bbox.y + subj.bbox.h);
|
|
19
|
+
const moves = [
|
|
20
|
+
{ v: dxL, type: 'dx', abs: Math.abs(dxL) },
|
|
21
|
+
{ v: dxR, type: 'dx', abs: Math.abs(dxR) },
|
|
22
|
+
{ v: dyT, type: 'dy', abs: Math.abs(dyT) },
|
|
23
|
+
{ v: dyB, type: 'dy', abs: Math.abs(dyB) },
|
|
24
|
+
];
|
|
25
|
+
const best = moves.reduce((a, b) => (a.abs < b.abs ? a : b));
|
|
26
|
+
const result = {
|
|
27
|
+
pass: false,
|
|
28
|
+
detail: `Überlappt #${ref.id}. Kürzester Fluchtweg: ${best.type}=${Math.round(best.v)}px`,
|
|
29
|
+
};
|
|
30
|
+
result[best.type] = Math.round(best.v);
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
34
|
+
arrange(subj, ref, _ctx) {
|
|
35
|
+
if (!bboxOverlap(subj.bbox, ref.bbox))
|
|
36
|
+
return { x: subj.bbox.x, y: subj.bbox.y };
|
|
37
|
+
const dxL = ref.bbox.x + ref.bbox.w - subj.bbox.x;
|
|
38
|
+
const dxR = ref.bbox.x - (subj.bbox.x + subj.bbox.w);
|
|
39
|
+
const dyT = ref.bbox.y + ref.bbox.h - subj.bbox.y;
|
|
40
|
+
const dyB = ref.bbox.y - (subj.bbox.y + subj.bbox.h);
|
|
41
|
+
const moves = [
|
|
42
|
+
{ dx: dxL, dy: 0, abs: Math.abs(dxL) },
|
|
43
|
+
{ dx: dxR, dy: 0, abs: Math.abs(dxR) },
|
|
44
|
+
{ dx: 0, dy: dyT, abs: Math.abs(dyT) },
|
|
45
|
+
{ dx: 0, dy: dyB, abs: Math.abs(dyB) },
|
|
46
|
+
];
|
|
47
|
+
const best = moves.reduce((a, b) => (a.abs < b.abs ? a : b));
|
|
48
|
+
return { x: subj.bbox.x + best.dx, y: subj.bbox.y + best.dy };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* positional.js - LEFT-OF + ABOVE Constraints
|
|
3
|
+
* Migrated from mirror.js:80-93
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
import { registerConstraint } from './registry.js';
|
|
7
|
+
|
|
8
|
+
registerConstraint('LEFT-OF', {
|
|
9
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
10
|
+
check(subj, ref, _ctx) {
|
|
11
|
+
const expectedMaxX = ref.bbox.x;
|
|
12
|
+
const actualMaxX = subj.bbox.x + subj.bbox.w;
|
|
13
|
+
const pass = actualMaxX <= expectedMaxX;
|
|
14
|
+
const dx = actualMaxX - expectedMaxX;
|
|
15
|
+
if (pass) return { pass, detail: null };
|
|
16
|
+
return {
|
|
17
|
+
pass,
|
|
18
|
+
detail: `Nicht links davon. Korrektur: dx=${Math.round(-dx)}px`,
|
|
19
|
+
dx: Math.round(-dx),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
23
|
+
arrange(subj, ref, _ctx) {
|
|
24
|
+
return { x: ref.bbox.x - subj.bbox.w };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerConstraint('ABOVE', {
|
|
29
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
30
|
+
check(subj, ref, _ctx) {
|
|
31
|
+
const expectedMaxY = ref.bbox.y;
|
|
32
|
+
const actualMaxY = subj.bbox.y + subj.bbox.h;
|
|
33
|
+
const pass = actualMaxY <= expectedMaxY;
|
|
34
|
+
const dy = actualMaxY - expectedMaxY;
|
|
35
|
+
if (pass) return { pass, detail: null };
|
|
36
|
+
return {
|
|
37
|
+
pass,
|
|
38
|
+
detail: `Nicht oberhalb. Korrektur: dy=${Math.round(-dy)}px`,
|
|
39
|
+
dy: Math.round(-dy),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
43
|
+
arrange(subj, ref, _ctx) {
|
|
44
|
+
return { y: ref.bbox.y - subj.bbox.h };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registry.js - Constraint Registry (Plug-In Pattern)
|
|
3
|
+
* ADR-020: Map<type, {check, arrange}>
|
|
4
|
+
* ADR-022: Context-Object Pattern (ctx = { grid, value? })
|
|
5
|
+
* Vector Mirror v2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const handlers = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registers a constraint handler.
|
|
12
|
+
*
|
|
13
|
+
* `requiresReference` (default true) markiert, ob `check()`/`arrange()` ein
|
|
14
|
+
* Referenz-Element (`ref`) DEREFERENZIEREN — z.B. `ref.bbox`. Der Default ist
|
|
15
|
+
* fail-closed (true): ein neu registrierter, unmarkierter Constraint gilt als
|
|
16
|
+
* referenz-pflichtig, damit eine fehlende Referenz NIE still in einen
|
|
17
|
+
* null-Deref-Crash laeuft (REGEL-8). Nur Constraints, die ohne Referenz
|
|
18
|
+
* arbeiten (COLOR, FILL), setzen `requiresReference: false` explizit.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} type - Constraint type (e.g. 'CENTERED-IN')
|
|
21
|
+
* @param {{ check: Function, arrange?: Function, requiresReference?: boolean }} handler
|
|
22
|
+
*/
|
|
23
|
+
export function registerConstraint(
|
|
24
|
+
type,
|
|
25
|
+
{ check, arrange, requiresReference },
|
|
26
|
+
) {
|
|
27
|
+
handlers.set(type, {
|
|
28
|
+
check,
|
|
29
|
+
arrange,
|
|
30
|
+
requiresReference: requiresReference !== false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ob ein Constraint-Typ ueberhaupt registriert (bekannt) ist.
|
|
36
|
+
*
|
|
37
|
+
* Trennt die zwei Diagnosen sauber: ein UNBEKANNTER Typ (Tippfehler/Phantasie)
|
|
38
|
+
* darf NICHT vom ref-Guard maskiert werden — er fliesst weiter zu
|
|
39
|
+
* checkConstraint → CONSTRAINT_TYPE_UNKNOWN (+ Levenshtein-Vorschlag). Nur fuer
|
|
40
|
+
* BEKANNTE, ref-pflichtige Typen ohne ref greift der fail-closed REFERENCE_NOT_FOUND.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} type
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isRegistered(type) {
|
|
46
|
+
return handlers.has(type);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ob ein Constraint-Typ ein Referenz-Element braucht (siehe registerConstraint).
|
|
51
|
+
* Fail-closed: unbekannte Typen gelten als referenz-pflichtig (true), damit ein
|
|
52
|
+
* fehlender ref nie in einen Deref-Crash faellt.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} type
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function requiresReference(type) {
|
|
58
|
+
const h = handlers.get(type);
|
|
59
|
+
return h ? h.requiresReference : true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks a single constraint.
|
|
64
|
+
* @param {string} type
|
|
65
|
+
* @param {object} subj - Subject element from gridMap
|
|
66
|
+
* @param {object|null} ref - Reference element from gridMap
|
|
67
|
+
* @param {object} ctx - { grid, value? }
|
|
68
|
+
* @returns {{ pass: boolean|null, detail?: string, dx?: number, dy?: number }}
|
|
69
|
+
*/
|
|
70
|
+
export function checkConstraint(type, subj, ref, ctx) {
|
|
71
|
+
const h = handlers.get(type);
|
|
72
|
+
if (!h) return { pass: null, detail: `Unbekannter Constraint: ${type}` };
|
|
73
|
+
return h.check(subj, ref, ctx);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Arranges a single constraint (inverse of check).
|
|
78
|
+
* @param {string} type
|
|
79
|
+
* @param {object} subj - Subject element { bbox }
|
|
80
|
+
* @param {object|null} ref - Reference element { bbox }
|
|
81
|
+
* @param {object} ctx - { canvas, value? }
|
|
82
|
+
* @returns {object|null} Attribute patch or null if no arrange handler
|
|
83
|
+
*/
|
|
84
|
+
export function arrangeConstraint(type, subj, ref, ctx) {
|
|
85
|
+
const h = handlers.get(type);
|
|
86
|
+
if (!h?.arrange) return null;
|
|
87
|
+
return h.arrange(subj, ref, ctx);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lists all registered constraint types.
|
|
92
|
+
*/
|
|
93
|
+
export function listConstraints() {
|
|
94
|
+
return [...handlers.entries()].map(([type, h]) => ({
|
|
95
|
+
type,
|
|
96
|
+
hasArrange: !!h.arrange,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* same-size.js - SAME-SIZE Constraint
|
|
3
|
+
* Migrated from mirror.js:73-79
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getTolX, getTolY } from '../tolerance.js';
|
|
8
|
+
import { registerConstraint } from './registry.js';
|
|
9
|
+
|
|
10
|
+
registerConstraint('SAME-SIZE', {
|
|
11
|
+
check(subj, ref, { grid }) {
|
|
12
|
+
if (ref.bbox.w === 0 || ref.bbox.h === 0)
|
|
13
|
+
return { pass: null, detail: 'Referenz hat Grösse 0' };
|
|
14
|
+
const dw = subj.bbox.w - ref.bbox.w;
|
|
15
|
+
const dh = subj.bbox.h - ref.bbox.h;
|
|
16
|
+
const pass =
|
|
17
|
+
Math.abs(dw) <= getTolX(ref.bbox.w, grid) &&
|
|
18
|
+
Math.abs(dh) <= getTolY(ref.bbox.h, grid);
|
|
19
|
+
if (pass) return { pass, detail: null };
|
|
20
|
+
// G2 (D-006): detail beschreibt NUR das WAS/WIEVIEL-daneben. Die
|
|
21
|
+
// strukturierte Korrektur lebt ausschliesslich in dw/dh — prose.js/
|
|
22
|
+
// structured.js bauen den Hinweis aus diesen Feldern, nicht mehr aus
|
|
23
|
+
// dem detail-String (kein String-Leak, gate-bar in S8).
|
|
24
|
+
return {
|
|
25
|
+
pass,
|
|
26
|
+
detail: `Grösse weicht ab (Δw=${Math.round(dw)}px, Δh=${Math.round(dh)}px)`,
|
|
27
|
+
dw: Math.round(-dw),
|
|
28
|
+
dh: Math.round(-dh),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
32
|
+
arrange(_subj, ref, _ctx) {
|
|
33
|
+
return { width: ref.bbox.w, height: ref.bbox.h };
|
|
34
|
+
},
|
|
35
|
+
});
|