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/core/diff.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff.js - Diff Computation between GridMaps
|
|
3
|
+
* Migrated from mirror.js:113-147 + findClosest (164-172)
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*
|
|
6
|
+
* Core module: NO imports from adapters/ or interface/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Computes changes between two GridMaps.
|
|
11
|
+
* Phase 1: Exact ID matching. Phase 2: Proximity matching.
|
|
12
|
+
*/
|
|
13
|
+
export function computeDiff(oldMap, newMap) {
|
|
14
|
+
const changes = [];
|
|
15
|
+
const matchedOldIds = new Set();
|
|
16
|
+
const matchedNewIds = new Set();
|
|
17
|
+
|
|
18
|
+
// Phase 1: Exact Match (IDs)
|
|
19
|
+
for (const newEl of newMap.elements) {
|
|
20
|
+
const oldEl = oldMap.elements.find((e) => e.id === newEl.id);
|
|
21
|
+
if (oldEl) {
|
|
22
|
+
matchedOldIds.add(oldEl.id);
|
|
23
|
+
matchedNewIds.add(newEl.id);
|
|
24
|
+
// Sniper-Identität: eine Form-Mutation (circle→ellipse) bei gleicher
|
|
25
|
+
// Zelle/Farbe ist eine echte Szenen-Änderung — ohne diesen Vergleich
|
|
26
|
+
// bliebe sie still (leerer Diff = Mess-Lüge). Exakt im Muster von
|
|
27
|
+
// VERSCHOBEN/FARBÄNDERUNG (oldEl = matched Vorgänger).
|
|
28
|
+
if (oldEl.tag !== newEl.tag)
|
|
29
|
+
changes.push({
|
|
30
|
+
type: 'FORMÄNDERUNG',
|
|
31
|
+
id: newEl.id,
|
|
32
|
+
from: oldEl.tag,
|
|
33
|
+
to: newEl.tag,
|
|
34
|
+
});
|
|
35
|
+
if (oldEl.cell !== newEl.cell)
|
|
36
|
+
changes.push({
|
|
37
|
+
type: 'VERSCHOBEN',
|
|
38
|
+
id: newEl.id,
|
|
39
|
+
from: oldEl.cell,
|
|
40
|
+
to: newEl.cell,
|
|
41
|
+
});
|
|
42
|
+
if (oldEl.color !== newEl.color)
|
|
43
|
+
changes.push({
|
|
44
|
+
type: 'FARBÄNDERUNG',
|
|
45
|
+
id: newEl.id,
|
|
46
|
+
from: oldEl.color,
|
|
47
|
+
to: newEl.color,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Phase 2: Proximity Matching for unmatched
|
|
53
|
+
const unmatchedOld = oldMap.elements.filter((e) => !matchedOldIds.has(e.id));
|
|
54
|
+
const unmatchedNew = newMap.elements.filter((e) => !matchedNewIds.has(e.id));
|
|
55
|
+
|
|
56
|
+
for (const newEl of unmatchedNew) {
|
|
57
|
+
const candidate = findClosest(newEl, unmatchedOld, oldMap.grid);
|
|
58
|
+
if (candidate) {
|
|
59
|
+
matchedOldIds.add(candidate.id);
|
|
60
|
+
unmatchedOld.splice(unmatchedOld.indexOf(candidate), 1);
|
|
61
|
+
// Symmetrisch zu Phase 1: Form-Mutation ist eine echte Änderung, nicht
|
|
62
|
+
// still. (Heute matcht findClosest nur tag-gleich, der Zweig ist also
|
|
63
|
+
// eine Sicherung gegen künftiges tag-agnostisches Matching — kein toter
|
|
64
|
+
// Pfad an der API-Grenze, sondern dieselbe Wahrheit in beiden Phasen.)
|
|
65
|
+
if (candidate.tag !== newEl.tag)
|
|
66
|
+
changes.push({
|
|
67
|
+
type: 'FORMÄNDERUNG',
|
|
68
|
+
id: newEl.id,
|
|
69
|
+
from: candidate.tag,
|
|
70
|
+
to: newEl.tag,
|
|
71
|
+
});
|
|
72
|
+
if (newEl.cell !== candidate.cell)
|
|
73
|
+
changes.push({
|
|
74
|
+
type: 'VERSCHOBEN',
|
|
75
|
+
id: newEl.id,
|
|
76
|
+
from: candidate.cell,
|
|
77
|
+
to: newEl.cell,
|
|
78
|
+
});
|
|
79
|
+
if (newEl.color !== candidate.color)
|
|
80
|
+
changes.push({
|
|
81
|
+
type: 'FARBÄNDERUNG',
|
|
82
|
+
id: newEl.id,
|
|
83
|
+
from: candidate.color,
|
|
84
|
+
to: newEl.color,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
changes.push({
|
|
88
|
+
type: 'NEU',
|
|
89
|
+
id: newEl.id,
|
|
90
|
+
cell: newEl.cell,
|
|
91
|
+
color: newEl.color,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
unmatchedOld.forEach((oldEl) => {
|
|
97
|
+
changes.push({ type: 'ENTFERNT', id: oldEl.id, cell: oldEl.cell });
|
|
98
|
+
});
|
|
99
|
+
return changes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Finds the closest element by tag + distance within 2 cell threshold.
|
|
104
|
+
* Private to diff module (DDD Shared Kernel, ADR C-1).
|
|
105
|
+
*/
|
|
106
|
+
function findClosest(target, candidates, grid) {
|
|
107
|
+
let best = null,
|
|
108
|
+
minD = Math.max(grid.cellW, grid.cellH) * 2;
|
|
109
|
+
candidates.forEach((c) => {
|
|
110
|
+
if (c.tag !== target.tag) return;
|
|
111
|
+
const d = Math.sqrt((target.cx - c.cx) ** 2 + (target.cy - c.cy) ** 2);
|
|
112
|
+
if (d < minD) {
|
|
113
|
+
minD = d;
|
|
114
|
+
best = c;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return best;
|
|
118
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* element_vocabulary.js — SSOT für Element-Vokabular (Sprint-β2 §1.3 LAYER-TRENNUNG)
|
|
3
|
+
* Vector Mirror v2.6
|
|
4
|
+
*
|
|
5
|
+
* Core module (REGEL-4 Hexagonal): lebt in core/, darf aus core/ + node:* importieren,
|
|
6
|
+
* wird von adapters/* und tests/* konsumiert; importiert NICHT aus adapters/ oder interface/.
|
|
7
|
+
*
|
|
8
|
+
* Layer-Trennung (Patch2, Mini-Review-Konvergenz Opus+Codex):
|
|
9
|
+
* - DOMAIN-Layer: isSpotterTag(tag) Predicate + AUTO_ID_FORMAT_REGEX (hier).
|
|
10
|
+
* - FORMAT-Layer: formatAutoId/extractAutoIds/computeSvgHashPrefix in auto_ids.js
|
|
11
|
+
* (pure Format-Helper, keine Domain-Listen).
|
|
12
|
+
*
|
|
13
|
+
* Begründung Predicate-API (F-PATCH-CODEX-001 + F-OPUS-MINI-002 konvergent):
|
|
14
|
+
* `Object.freeze(new Set(...))` ist auf V8 SCHEIN-Defensive — interne Set-Slots
|
|
15
|
+
* sind nicht via Property-Descriptor geschützt, `set.add(...)` läuft trotzdem
|
|
16
|
+
* durch. Konsequenz: das Set in Modul-Scope PRIVAT halten, nur als Predicate
|
|
17
|
+
* nach aussen geben → keine externe Mutation möglich (V8-mechanisch).
|
|
18
|
+
*
|
|
19
|
+
* Tag-Wahl SPOTTER-Set (15 Tags, MDN-Renderable minus pure Container):
|
|
20
|
+
* - Wurzel-Quelle: MDN <https://developer.mozilla.org/en-US/docs/Web/SVG/Element>
|
|
21
|
+
* "Renderable elements" — Elemente, die grafische Ausgabe erzeugen können.
|
|
22
|
+
* - Pure Container ausgenommen: <g>, <svg>, <symbol> (haben keine eigene Geometrie,
|
|
23
|
+
* ihre Geometrie ist die ihrer Kinder; in SKIP_TAGS).
|
|
24
|
+
* - Inkludiert: a, circle, ellipse, foreignObject, image, line, path, polygon,
|
|
25
|
+
* polyline, rect, switch, text, textPath, tspan, use.
|
|
26
|
+
* - Adressiert F-OPUS-MINI-001 (Verhaltens-Drift): vor Patch1 lieferte
|
|
27
|
+
* getStablePath() für ALLE non-SKIP-Tags eine ID, der Spotter sah <a>,
|
|
28
|
+
* <switch>, <textPath>, <tspan>. Patch1 mit 10er-Liste droppte diese stumm.
|
|
29
|
+
* - <foreignObject> Defensive: DOMPurify USE_PROFILES.svg strippt aktuell,
|
|
30
|
+
* aber Profile-Erweiterung ist zukünftige Möglichkeit — Predicate ist
|
|
31
|
+
* dann ohne Code-Änderung korrekt.
|
|
32
|
+
*
|
|
33
|
+
* Wurzel-Fix für F-CODEX-002 (Tag-Allowlist nicht erzwungen) + SC-4-Drift
|
|
34
|
+
* (Format-Regex nicht ausführbar grep-bar): 4 Reviewer (Validator + Opus
|
|
35
|
+
* + Gemini + Codex) konvergent fanden 4 widersprüchliche Tag-Listen verstreut
|
|
36
|
+
* im Code. Die Synthese identifizierte als Wurzel: "Kein Single-Source-of-
|
|
37
|
+
* Truth für Element-Vokabular".
|
|
38
|
+
*
|
|
39
|
+
* Pattern-Vorlage: fabric.js
|
|
40
|
+
* `parser/constants.ts` (zentrale Constants, alle Konsumenten importieren).
|
|
41
|
+
*
|
|
42
|
+
* Quellen für die übrigen Sets:
|
|
43
|
+
* - SKIP_TAGS: Spotter-Negativ-Liste — semantisch nicht-geometrisch oder
|
|
44
|
+
* Container/Definitions/Style/Script. Disjunkt-Konstellation: SKIP_TAGS
|
|
45
|
+
* und das (private) Spotter-Set überlappen nicht.
|
|
46
|
+
* - DELTA_ATTRIBUTABLE_TAGS: Untermenge des Spotter-Sets, für die der
|
|
47
|
+
* Emitter (structured.js) eine delta-zu-Attribut-Mapping kennt
|
|
48
|
+
* (Subset-Drift-Test via isSpotterTag-Predicate).
|
|
49
|
+
*
|
|
50
|
+
* Format-Vertrag (AUTO_ID_FORMAT_REGEX):
|
|
51
|
+
* Aus _SPOTTER_SET dynamisch abgeleitet — Drift ist mechanisch ausgeschlossen,
|
|
52
|
+
* weil die Regex bei Vokabular-Änderung automatisch folgt.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Privates Spotter-Set (15 Tags). NICHT exportiert — nur über isSpotterTag()
|
|
57
|
+
* und AUTO_ID_FORMAT_REGEX zugänglich. Damit ist externe Mutation
|
|
58
|
+
* V8-mechanisch ausgeschlossen (Predicate hat keine .add/.delete-Surface).
|
|
59
|
+
*
|
|
60
|
+
* Case-Konvention (Patch3, F-PATCH2-OPUS-001 + F-PATCH2-CODEX-001 konvergent
|
|
61
|
+
* HIGH): ALLE Tokens lowercase. Begründung — Browser-DOM-API liefert über
|
|
62
|
+
* `el.tagName.toLowerCase()` einheitlich lowercase (HTML-Konvention; im
|
|
63
|
+
* SVG-Namespace ist tagName zwar case-preserving, aber Renderer-Adapter
|
|
64
|
+
* normalisiert defensiv). Vorherige Patch2-Schreibweise (`foreignObject`,
|
|
65
|
+
* `textPath` camelCase) erzeugte Asymmetrie zum Browser-Pfad, weshalb diese
|
|
66
|
+
* beiden Tags im Renderer stumm gedropt wurden. Lowercase-everywhere ist
|
|
67
|
+
* die kanonische Single-Boundary-Normalisierung (kein Lookup-Table,
|
|
68
|
+
* SKIP_TAGS war ohnehin schon lowercase — Symmetrie hergestellt).
|
|
69
|
+
*
|
|
70
|
+
* Reihenfolge: alphabetisch (Determinismus für die abgeleitete Regex-Alternation).
|
|
71
|
+
*/
|
|
72
|
+
const _SPOTTER_SET = new Set([
|
|
73
|
+
'a',
|
|
74
|
+
'circle',
|
|
75
|
+
'ellipse',
|
|
76
|
+
'foreignobject',
|
|
77
|
+
'image',
|
|
78
|
+
'line',
|
|
79
|
+
'path',
|
|
80
|
+
'polygon',
|
|
81
|
+
'polyline',
|
|
82
|
+
'rect',
|
|
83
|
+
'switch',
|
|
84
|
+
'text',
|
|
85
|
+
'textpath',
|
|
86
|
+
'tspan',
|
|
87
|
+
'use',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Predicate: ist `tag` ein Spotter-Tag (geometrisch messbar via getBBox,
|
|
92
|
+
* kein purer Container)? O(1) Set-Lookup auf privates Set.
|
|
93
|
+
*
|
|
94
|
+
* Konsumenten (playwright.js Browser-Scope, Tests): nutzen diesen Predicate
|
|
95
|
+
* statt eines Set-Exports — das verhindert die V8-Set-Mutation-Lücke
|
|
96
|
+
* (F-PATCH-CODEX-001 + F-OPUS-MINI-002).
|
|
97
|
+
*
|
|
98
|
+
* Hinweis Browser-Bridge (playwright.js): Predicate-Funktionen können nicht
|
|
99
|
+
* über page.evaluate() serialisiert werden. Adapter berechnet stattdessen
|
|
100
|
+
* EINMAL `[..._SPOTTER_SET]` indirekt via `isSpotterTag`-Probing über eine
|
|
101
|
+
* bekannte Tag-Liste ODER importiert eine separate Liste vom Renderer-
|
|
102
|
+
* Adapter-Lifecycle. ABER: hier in core/ exportieren wir bewusst nur den
|
|
103
|
+
* Predicate. Renderer-Bridge-Strategie ist Adapter-Sache (siehe playwright.js).
|
|
104
|
+
*
|
|
105
|
+
* Case-Defensive (Patch3): Input wird via `String(tag).toLowerCase()`
|
|
106
|
+
* normalisiert. Damit liefert der Predicate für `textPath`, `textpath`
|
|
107
|
+
* und `TEXTPATH` einheitlich `true`. Non-String Inputs (null, undefined,
|
|
108
|
+
* Zahlen) werden via `String(...)` zu ihrer String-Repräsentation
|
|
109
|
+
* koerciert (`"null"`, `"undefined"`, `"123"`) und matchen damit das Set
|
|
110
|
+
* NICHT — funktional false ohne Exception. Bewusste Wahl: defensiv
|
|
111
|
+
* statt strikt, weil der Browser-Pfad in einer Promise-Chain läuft und
|
|
112
|
+
* unerwartete TypeError-Throws schwer zu lokalisieren wären.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} tag - SVG-Element-Tag in beliebiger Schreibweise;
|
|
115
|
+
* Predicate normalisiert intern via toLowerCase().
|
|
116
|
+
* @returns {boolean} true wenn Spotter sehen soll, false sonst.
|
|
117
|
+
*/
|
|
118
|
+
export function isSpotterTag(tag) {
|
|
119
|
+
return _SPOTTER_SET.has(String(tag).toLowerCase());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Frozen Snapshot-Array des Spotter-Sets, AUSSCHLIESSLICH für die Renderer-
|
|
124
|
+
* Adapter-Bridge (playwright.js → page.evaluate). Sets sind nicht JSON-
|
|
125
|
+
* serialisierbar, Predicate-Funktionen nicht über page.evaluate übertragbar
|
|
126
|
+
* (Browser-Sandbox-Boundary, H-10/PH-10). Daher diese Array-API: Adapter
|
|
127
|
+
* serialisiert sie als Arg-Pass, der Browser-Scope rekonstruiert ein eigenes
|
|
128
|
+
* Set über `new Set(arr)` (mutation-isoliert vom Node-Land).
|
|
129
|
+
*
|
|
130
|
+
* Object.freeze auf einem Array IST effektiv: push/pop/splice werfen im
|
|
131
|
+
* strict-mode bzw. werden silent verworfen. Im Gegensatz zu Set ist das
|
|
132
|
+
* V8-Mutations-Lock hier real (Property-Descriptor sperrt length + Indizes).
|
|
133
|
+
*
|
|
134
|
+
* Reihenfolge: dieselbe wie _SPOTTER_SET (alphabetisch). Tests dürfen sich
|
|
135
|
+
* NICHT auf eine bestimmte Reihenfolge verlassen — die Liste ist semantisch
|
|
136
|
+
* ein Set, der Array-Wrapper ist Serialisierungs-Trick.
|
|
137
|
+
*
|
|
138
|
+
* Case-Garantie (Patch3): Liste ist via `[..._SPOTTER_SET]` direkt aus dem
|
|
139
|
+
* lowercase-Set abgeleitet — jedes Element ist lowercase. Drift-Guard in
|
|
140
|
+
* playwright.js verifiziert das zusätzlich Build-/Runtime (Case-Invariante).
|
|
141
|
+
*
|
|
142
|
+
* Konsumenten-Hinweis: Code, der über die Tag-Mitgliedschaft entscheiden
|
|
143
|
+
* muss, soll `isSpotterTag(tag)` benutzen, NICHT diese Liste iterieren.
|
|
144
|
+
* Die Liste ist nur das Bridge-Snapshot zur Browser-Sandbox.
|
|
145
|
+
*/
|
|
146
|
+
export const SPOTTER_TAGS_LIST = Object.freeze([..._SPOTTER_SET]);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Skip-Tags: Renderer-seitige Negativ-Liste (playwright.js iteriert
|
|
150
|
+
* darüber hinweg). Container (`g`, `svg`, `symbol`), Definitions (`defs`,
|
|
151
|
+
* `marker`), Style/Script/Metadata sowie Mask/Clip/Filter — keiner davon
|
|
152
|
+
* trägt eigene Geometrie, die der Spotter messen soll.
|
|
153
|
+
*
|
|
154
|
+
* Bleibt als Set-Export, weil die Browser-Bridge die Liste iteriert
|
|
155
|
+
* (`new Set(skipTagsArr)` im Browser-Scope, Mutation in core ist nicht
|
|
156
|
+
* Trust-Boundary). pattern/svg/marker/symbol sind hier nicht alle drin —
|
|
157
|
+
* sie werden durch `!isSpotterTag(tag)` ohnehin dynamisch geskippt; SKIP_TAGS
|
|
158
|
+
* ist die EXPLIZITE Liste der Tags, die playwright.js per Fast-Path früh
|
|
159
|
+
* verwirft (vor jeder Style/BBox-Inspektion).
|
|
160
|
+
*
|
|
161
|
+
* §HEAL3 (ST-A) Doku-Schuld (HEAL §7-4): `pattern` fehlt hier BEWUSST und
|
|
162
|
+
* BLEIBT es — SKIP_TAGS unterdrückt nur die Container SELBST, nicht deren
|
|
163
|
+
* KINDER (das <rect> IN einem <defs>/<symbol>/<clipPath>/<pattern>), die als
|
|
164
|
+
* Phantome durchrutschen würden. Der kategorische closest()-Schnitt in
|
|
165
|
+
* playwright.js (`el.closest('defs,symbol,clipPath,mask,pattern,marker')`)
|
|
166
|
+
* deckt BEIDE Lecks zugleich: die fehlende Container-Mitgliedschaft (pattern)
|
|
167
|
+
* UND die Container-Kinder. Daher KEINE Erweiterung von SKIP_TAGS nötig —
|
|
168
|
+
* das wäre eine zweite, falsche Vergleichs-Ebene (SKIP_TAGS testet gegen
|
|
169
|
+
* tagName.toLowerCase() → lowercase `clippath`; closest() matcht den
|
|
170
|
+
* qualifizierten SVG-Namen → camelCase `clipPath`). NICHT vermischen.
|
|
171
|
+
*/
|
|
172
|
+
export const SKIP_TAGS = Object.freeze(
|
|
173
|
+
new Set([
|
|
174
|
+
'defs',
|
|
175
|
+
'title',
|
|
176
|
+
'desc',
|
|
177
|
+
'metadata',
|
|
178
|
+
'style',
|
|
179
|
+
'script',
|
|
180
|
+
'g',
|
|
181
|
+
'filter',
|
|
182
|
+
'clippath',
|
|
183
|
+
'mask',
|
|
184
|
+
'symbol',
|
|
185
|
+
'marker',
|
|
186
|
+
'stop',
|
|
187
|
+
]),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delta-attributable Tags: Untermenge des Spotter-Sets, für die der
|
|
192
|
+
* Emitter (structured.js `deltaToAttribute`) eine dx/dy/dw/dh→Attribut-
|
|
193
|
+
* Mapping vorhält. Subset-Invariante zum Spotter-Set (Drift-Test in
|
|
194
|
+
* test_element_vocabulary.js prüft `every tag => isSpotterTag(tag)`).
|
|
195
|
+
*
|
|
196
|
+
* Bleibt als Set-Export, weil der Drift-Test iteriert.
|
|
197
|
+
*/
|
|
198
|
+
export const DELTA_ATTRIBUTABLE_TAGS = Object.freeze(
|
|
199
|
+
new Set(['circle', 'rect', 'text', 'ellipse', 'line', 'image']),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Predicate: trägt `tag` eine native dx/dy/dw/dh→Attribut-Mapping (= ist er in
|
|
204
|
+
* DELTA_ATTRIBUTABLE_TAGS)? Single-Point-of-Truth für die §1.5-Verzweigung in
|
|
205
|
+
* structured.js buildFix: JA → bestehender Attribut-Pfad (x/y/cx/cy/r/...),
|
|
206
|
+
* NEIN → Transform-Fallback (translate) bzw. SIZE_FIX_UNSUPPORTED_FOR_TAG.
|
|
207
|
+
*
|
|
208
|
+
* Symmetrisch zu isSpotterTag: normalisiert Input via String(tag).toLowerCase()
|
|
209
|
+
* (Browser-Pfad liefert lowercase; defensive Koerzierung für null/undefined/
|
|
210
|
+
* Zahlen → false ohne Throw).
|
|
211
|
+
*
|
|
212
|
+
* @param {string} tag - SVG-Element-Tag in beliebiger Schreibweise.
|
|
213
|
+
* @returns {boolean} true wenn der Tag eine native Delta-Attribut-Mapping hat.
|
|
214
|
+
*/
|
|
215
|
+
export function isTagDeltaAttributable(tag) {
|
|
216
|
+
return DELTA_ATTRIBUTABLE_TAGS.has(String(tag).toLowerCase());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Auto-ID-Format-Regex — aus _SPOTTER_SET DYNAMISCH abgeleitet.
|
|
221
|
+
*
|
|
222
|
+
* Format-Vertrag (D-004, ADR-025 §3): `_<8hex>_<tag><n>` mit
|
|
223
|
+
* <tag> ∈ Spotter-Set, <8hex> = lowercase hex (sha256-Prefix),
|
|
224
|
+
* <n> = monoton steigender, tag-lokaler Counter (1-basiert).
|
|
225
|
+
*
|
|
226
|
+
* Drift-Garantie: wenn _SPOTTER_SET sich ändert, folgt die Regex
|
|
227
|
+
* AUTOMATISCH — keine zweite, hardcoded Liste pflegen.
|
|
228
|
+
*
|
|
229
|
+
* Case-Konvention (Patch3, F-PATCH2-OPUS-001 + F-PATCH2-CODEX-001): kanonische
|
|
230
|
+
* Auto-ID-Schreibweise ist lowercase. Begründung — _SPOTTER_SET ist
|
|
231
|
+
* lowercase, Renderer (playwright.js) baut Auto-IDs aus tag-lowercase +
|
|
232
|
+
* hash-prefix-lowercase + zähler. Damit ist die Auto-ID kanonisch
|
|
233
|
+
* lowercase, und die Format-Regex ist deterministisch case-sensitive
|
|
234
|
+
* (kein `i`-Flag mehr). Das `i`-Flag aus Patch2 war kompensatorisch zur
|
|
235
|
+
* camelCase-Set-Asymmetrie; mit lowercase-everywhere ist die Asymmetrie
|
|
236
|
+
* eliminiert. Konsequenz: `_deadbeef_foreignObject1` matcht nicht mehr —
|
|
237
|
+
* solche IDs sind per Vertrag nicht produzierbar (Renderer normalisiert).
|
|
238
|
+
*/
|
|
239
|
+
export const AUTO_ID_FORMAT_REGEX = new RegExp(
|
|
240
|
+
`^_[0-9a-f]{8}_(${[..._SPOTTER_SET].join('|')})\\d+$`,
|
|
241
|
+
);
|
package/src/core/grid.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grid.js - Grid Mapping & Offset Correction
|
|
3
|
+
* Migrated from grid.js (v1.6), parseColor moved to lib/palette.js
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*
|
|
6
|
+
* Core module: NO imports from adapters/ or interface/
|
|
7
|
+
*/
|
|
8
|
+
import { parseColor } from '../lib/palette.js';
|
|
9
|
+
|
|
10
|
+
export function mapToGridMap(resolved, lastGridMap = null) {
|
|
11
|
+
const { canvas, elements } = resolved;
|
|
12
|
+
const grid = createGrid(canvas);
|
|
13
|
+
|
|
14
|
+
const mappedElements = elements.map((el) => {
|
|
15
|
+
const cx = el.bbox.x + el.bbox.w / 2;
|
|
16
|
+
const cy = el.bbox.y + el.bbox.h / 2;
|
|
17
|
+
|
|
18
|
+
const col = clamp(
|
|
19
|
+
Math.floor((cx - canvas.vbX) / grid.cellW),
|
|
20
|
+
0,
|
|
21
|
+
grid.cellsX - 1,
|
|
22
|
+
);
|
|
23
|
+
const row = clamp(
|
|
24
|
+
Math.floor((cy - canvas.vbY) / grid.cellH),
|
|
25
|
+
0,
|
|
26
|
+
grid.cellsY - 1,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
let cell = String.fromCharCode(65 + col) + (row + 1);
|
|
30
|
+
|
|
31
|
+
// Hysteresis (10% Overlap Rule)
|
|
32
|
+
if (lastGridMap) {
|
|
33
|
+
const lastEl = lastGridMap.elements.find((e) => e.id === el.id);
|
|
34
|
+
if (lastEl && lastEl.cell !== cell) {
|
|
35
|
+
const overlap = cellOverlapRatio(
|
|
36
|
+
el.bbox,
|
|
37
|
+
col,
|
|
38
|
+
row,
|
|
39
|
+
grid,
|
|
40
|
+
canvas.vbX,
|
|
41
|
+
canvas.vbY,
|
|
42
|
+
);
|
|
43
|
+
if (overlap < 0.1) cell = lastEl.cell;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const xInCell = (cx - canvas.vbX - col * grid.cellW) / grid.cellW;
|
|
48
|
+
const yInCell = (cy - canvas.vbY - row * grid.cellH) / grid.cellH;
|
|
49
|
+
const direction = getDirection(xInCell, yInCell);
|
|
50
|
+
const span = computeSpan(el.bbox, grid, canvas.vbX, canvas.vbY);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: el.id,
|
|
54
|
+
tag: el.tag,
|
|
55
|
+
cell,
|
|
56
|
+
direction,
|
|
57
|
+
span,
|
|
58
|
+
// §F-AT-7-02 STILLE STROKE-FARB-LÜGE (Heilung, Opt-A-Kern). Die EINE `color`
|
|
59
|
+
// war fill-only — ein nur-stroke-sichtbares Element meldete `transparent`
|
|
60
|
+
// (Lese-Lüge). Wenn der Renderer visible_color_source='stroke' gemessen hat
|
|
61
|
+
// (fill trägt keine sichtbare Farbe bei, stroke malt sichtbar), wird die EINE
|
|
62
|
+
// sichtbare Farbe aus el.stroke projiziert — kein Layer-Crossing (grid.js
|
|
63
|
+
// importiert nur palette.js), reine Projektion eines bereits gemessenen
|
|
64
|
+
// Signals. Bei 'multiple' (beide sichtbar) bleibt color=fill (Status quo); die
|
|
65
|
+
// MULTIPLE_PAINT_SOURCES-Warning trägt dort die Wahrheit (Visions-Gate: keine
|
|
66
|
+
// willkürliche Einzel-Farbe küren). SK4: der echte parsed fill wird INTERN in
|
|
67
|
+
// fill_color separat bewahrt — die Palette (structured.js) liest fill_color,
|
|
68
|
+
// NICHT color, sonst entstünde eine neue Lüge (stroke-only → fill:rot).
|
|
69
|
+
color:
|
|
70
|
+
el.visible_color_source === 'stroke'
|
|
71
|
+
? parseColor(el.stroke)
|
|
72
|
+
: parseColor(el.fill),
|
|
73
|
+
fill_color: parseColor(el.fill),
|
|
74
|
+
stroke: parseColor(el.stroke),
|
|
75
|
+
opacity: el.opacity,
|
|
76
|
+
bbox: el.bbox,
|
|
77
|
+
cx: Math.round(cx),
|
|
78
|
+
cy: Math.round(cy),
|
|
79
|
+
textContent: el.textContent,
|
|
80
|
+
// §1.2b L-002a: bbox_reliability + warnings vom Renderer-Element durchreichen
|
|
81
|
+
// (REGEL-3 Spotter-Anti-Luege auf MCP-Caller-Ebene). Reine Daten-Durchreichung,
|
|
82
|
+
// kein Layer-Crossing (H-16 REGEL-4 Hexagonal: core/ importiert weiter nichts
|
|
83
|
+
// aus adapters/ oder interface/).
|
|
84
|
+
bbox_reliability: el.bbox_reliability,
|
|
85
|
+
// §1.5 Block G: parent_id/parent_tag vom Renderer-Element durchreichen
|
|
86
|
+
// (tspan/textPath-Kontext, R-C). EXAKT analog zum warnings-Muster oben:
|
|
87
|
+
// conditional-spread, nur wenn vorhanden (Top-Level-Elemente tragen sie
|
|
88
|
+
// nicht). Reine Daten-Durchreichung, kein Layer-Crossing.
|
|
89
|
+
...(el.parent_id != null ? { parent_id: el.parent_id } : {}),
|
|
90
|
+
...(el.parent_tag != null ? { parent_tag: el.parent_tag } : {}),
|
|
91
|
+
// §1.5 Block H / Patch P1+P2: Autor-transform + tspan native_dx durchreichen
|
|
92
|
+
// (gleiches conditional-spread-Muster). transform → buildTransformFix.current
|
|
93
|
+
// (Autor-scale/rotate-Erhalt); native_dx → buildTspanShiftFix-Relativbasis.
|
|
94
|
+
...(el.transform != null ? { transform: el.transform } : {}),
|
|
95
|
+
...(el.native_dx != null ? { native_dx: el.native_dx } : {}),
|
|
96
|
+
...(el.warnings ? { warnings: el.warnings } : {}),
|
|
97
|
+
// §E4 (F-AT-004, DoD-3): Paint-Extent-Ehrlichkeit vom Renderer-Element
|
|
98
|
+
// durchreichen — EXAKT dasselbe conditional-spread-Muster wie bbox_reliability/
|
|
99
|
+
// parent_id/transform/warnings oben. REINE Daten-Durchreichung: die geom-bbox-
|
|
100
|
+
// basierte cell/span/cx/cy/direction-Abbildung bleibt UNVERÄNDERT (VISION P3:
|
|
101
|
+
// der Konsument/das Gehirn entscheidet, was es mit dem Flag tut). Kein
|
|
102
|
+
// Layer-Crossing (REGEL-4: grid.js importiert weiter nichts aus adapters/).
|
|
103
|
+
...(el.has_paint_overflow != null
|
|
104
|
+
? { has_paint_overflow: el.has_paint_overflow }
|
|
105
|
+
: {}),
|
|
106
|
+
...(el.visual_bbox != null ? { visual_bbox: el.visual_bbox } : {}),
|
|
107
|
+
// §HEAL-R6 / T1 (F-AT-6-01, DoD-2): Paint-Presence-Felder durchreichen —
|
|
108
|
+
// EXAKT dasselbe conditional-spread-Muster (REINE Daten-Durchreichung, kein
|
|
109
|
+
// Layer-Crossing; core/ importiert weiter nichts aus adapters/interface).
|
|
110
|
+
// fill/stroke_paint_factor sind immer vorhanden (Diagnostik); paint_visible
|
|
111
|
+
// nur bei painted===false (Negativ-Kontrolle: sichtbare Elemente tragen es nicht).
|
|
112
|
+
...(el.fill_paint_factor != null
|
|
113
|
+
? { fill_paint_factor: el.fill_paint_factor }
|
|
114
|
+
: {}),
|
|
115
|
+
...(el.stroke_paint_factor != null
|
|
116
|
+
? { stroke_paint_factor: el.stroke_paint_factor }
|
|
117
|
+
: {}),
|
|
118
|
+
...(el.paint_visible != null ? { paint_visible: el.paint_visible } : {}),
|
|
119
|
+
// §D5 / R6-STATE (state_dependent): reine Daten-Durchreichung (REGEL-4, kein
|
|
120
|
+
// Layer-Crossing). grid baut das Element-Objekt NEU — ohne diese Zeile ist das
|
|
121
|
+
// Feld downstream undefined. Die Warning fließt schon via warnings@81.
|
|
122
|
+
...(el.state_dependent != null
|
|
123
|
+
? { state_dependent: el.state_dependent }
|
|
124
|
+
: {}),
|
|
125
|
+
// §F-AT-6-09 / R6-MEDIA (media_dependent): reine Daten-Durchreichung (REGEL-4,
|
|
126
|
+
// kein Layer-Crossing). grid baut das Element-Objekt NEU — ohne diese Zeile ist
|
|
127
|
+
// das Feld downstream undefined. Die Warning fließt schon via warnings@81.
|
|
128
|
+
...(el.media_dependent != null
|
|
129
|
+
? { media_dependent: el.media_dependent }
|
|
130
|
+
: {}),
|
|
131
|
+
// §HEAL-5 / Zeit-Achse (motion_dependent): reine Daten-Durchreichung
|
|
132
|
+
// (REGEL-4, kein Layer-Crossing) — exakt nach media_dependent-Vorlage.
|
|
133
|
+
// grid baut das Element-Objekt NEU — ohne diese Zeile ist das Feld
|
|
134
|
+
// downstream undefined (und die Verdikt-Wache in pipeline.js bliebe
|
|
135
|
+
// blind). Die Warning fließt schon via warnings@oben.
|
|
136
|
+
...(el.motion_dependent != null
|
|
137
|
+
? { motion_dependent: el.motion_dependent }
|
|
138
|
+
: {}),
|
|
139
|
+
// §H10 R11-06 / Paint-Zeit-Achse (paint_time_variant): reine Daten-
|
|
140
|
+
// Durchreichung (REGEL-4, kein Layer-Crossing) — exakt nach
|
|
141
|
+
// motion_dependent-Vorlage.
|
|
142
|
+
...(el.paint_time_variant != null
|
|
143
|
+
? { paint_time_variant: el.paint_time_variant }
|
|
144
|
+
: {}),
|
|
145
|
+
// §F-AT-7-02 (SK3): internes Quellen-Feld der sichtbaren Farbe durchreichen —
|
|
146
|
+
// reine Daten-Durchreichung (REGEL-4, kein Layer-Crossing). Die Prosa nutzt
|
|
147
|
+
// es für Note/Suffix-Unterdrückung; die Warning (COLOR_FROM_STROKE /
|
|
148
|
+
// MULTIPLE_PAINT_SOURCES) fließt schon via warnings@oben. KEIN Schema-Feld.
|
|
149
|
+
...(el.visible_color_source != null
|
|
150
|
+
? { visible_color_source: el.visible_color_source }
|
|
151
|
+
: {}),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// §H10 R11-01: Existenz-Register (css-unsichtbar geskippte Elemente) vom
|
|
156
|
+
// Renderer durchreichen — reine Daten-Durchreichung (REGEL-4, kein
|
|
157
|
+
// Layer-Crossing), optional-by-default (leer/fehlend ⇒ Feld fehlt).
|
|
158
|
+
return {
|
|
159
|
+
canvas,
|
|
160
|
+
grid,
|
|
161
|
+
elements: mappedElements,
|
|
162
|
+
...(Array.isArray(resolved.hidden) && resolved.hidden.length > 0
|
|
163
|
+
? { hidden: resolved.hidden }
|
|
164
|
+
: {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createGrid(canvas) {
|
|
169
|
+
const cellsX = clamp(Math.round(canvas.width / 50), 4, 16);
|
|
170
|
+
const cellsY = clamp(Math.round(canvas.height / 50), 4, 16);
|
|
171
|
+
return {
|
|
172
|
+
cellsX,
|
|
173
|
+
cellsY,
|
|
174
|
+
cellW: canvas.width / cellsX,
|
|
175
|
+
cellH: canvas.height / cellsY,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cellOverlapRatio(bbox, col, row, grid, vbX, vbY) {
|
|
180
|
+
const cellX = vbX + col * grid.cellW;
|
|
181
|
+
const cellY = vbY + row * grid.cellH;
|
|
182
|
+
const overlapX = Math.max(
|
|
183
|
+
0,
|
|
184
|
+
Math.min(bbox.x + bbox.w, cellX + grid.cellW) - Math.max(bbox.x, cellX),
|
|
185
|
+
);
|
|
186
|
+
const overlapY = Math.max(
|
|
187
|
+
0,
|
|
188
|
+
Math.min(bbox.y + bbox.h, cellY + grid.cellH) - Math.max(bbox.y, cellY),
|
|
189
|
+
);
|
|
190
|
+
const area = overlapX * overlapY;
|
|
191
|
+
const bboxArea = bbox.w * bbox.h;
|
|
192
|
+
return bboxArea > 0 ? area / bboxArea : 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getDirection(x, y) {
|
|
196
|
+
const dx = x < 0.25 ? 'LINKS' : x > 0.75 ? 'RECHTS' : null;
|
|
197
|
+
const dy = y < 0.25 ? 'OBEN' : y > 0.75 ? 'UNTEN' : null;
|
|
198
|
+
if (!dx && !dy) return 'MITTE';
|
|
199
|
+
if (dx && dy)
|
|
200
|
+
return `ECKE-${dy === 'OBEN' ? 'O' : 'U'}${dx === 'LINKS' ? 'L' : 'R'}`;
|
|
201
|
+
return dx ? `${dx}ER RAND` : `${dy}ER RAND`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// §H10 R11-04 (O1): die bbox ist das HALBOFFENE Intervall [x, x+w). Der End-
|
|
205
|
+
// Index ist end-exklusiv (ceil(end/cellW) - 1) statt der früheren "-1px"-
|
|
206
|
+
// Pixel-Heuristik, die w>=1 voraussetzte und für Sub-Pixel-Elemente auf/nahe
|
|
207
|
+
// Grid-Linien invertierte Ranges erzeugte ("C3-B2", Ende vor Anfang — Boden-
|
|
208
|
+
// Wahrheit probe_R11-04). Das Math.max(sCol, …) ist Teil der exakten
|
|
209
|
+
// Mathematik (degeneriertes Intervall w=0 → Zelle des Punktes), kein Guard:
|
|
210
|
+
// START<=END ist damit Theorem, nicht Hoffnung. Exakt-auf-Grenze-Verhalten
|
|
211
|
+
// bleibt erhalten (x=0, w=25, cellW=25 → nur Zelle A); <1px-Grenzkreuzer
|
|
212
|
+
// (x=24.9, w=0.3) melden neu ehrlich beide Zellen.
|
|
213
|
+
function computeSpan(bbox, grid, vbX, vbY) {
|
|
214
|
+
const sCol = clamp(
|
|
215
|
+
Math.floor((bbox.x - vbX) / grid.cellW),
|
|
216
|
+
0,
|
|
217
|
+
grid.cellsX - 1,
|
|
218
|
+
);
|
|
219
|
+
const sRow = clamp(
|
|
220
|
+
Math.floor((bbox.y - vbY) / grid.cellH),
|
|
221
|
+
0,
|
|
222
|
+
grid.cellsY - 1,
|
|
223
|
+
);
|
|
224
|
+
const eCol = clamp(
|
|
225
|
+
Math.max(sCol, Math.ceil((bbox.x + bbox.w - vbX) / grid.cellW) - 1),
|
|
226
|
+
0,
|
|
227
|
+
grid.cellsX - 1,
|
|
228
|
+
);
|
|
229
|
+
const eRow = clamp(
|
|
230
|
+
Math.max(sRow, Math.ceil((bbox.y + bbox.h - vbY) / grid.cellH) - 1),
|
|
231
|
+
0,
|
|
232
|
+
grid.cellsY - 1,
|
|
233
|
+
);
|
|
234
|
+
if (sCol === eCol && sRow === eRow) return null;
|
|
235
|
+
return `${String.fromCharCode(65 + sCol)}${sRow + 1}-${String.fromCharCode(65 + eCol)}${eRow + 1}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function clamp(v, min, max) {
|
|
239
|
+
return Math.max(min, Math.min(max, v));
|
|
240
|
+
}
|