claudecode-omc 5.6.6 → 5.6.8
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/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
- package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
- package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
- package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
- package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
- package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
- package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
- package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
- package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
- package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
- package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
- package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
- package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
- package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
- package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
- package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
- package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/.local/skills/swiftui-pro/SKILL.md +108 -0
- package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
- package/.local/skills/swiftui-pro/references/api.md +39 -0
- package/.local/skills/swiftui-pro/references/data.md +43 -0
- package/.local/skills/swiftui-pro/references/design.md +32 -0
- package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
- package/.local/skills/swiftui-pro/references/navigation.md +14 -0
- package/.local/skills/swiftui-pro/references/performance.md +46 -0
- package/.local/skills/swiftui-pro/references/swift.md +56 -0
- package/.local/skills/swiftui-pro/references/views.md +36 -0
- package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
- package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/bundled/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pixel-diff.mjs — Stage 5: visual diff cascade for h5-to-swiftui
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node pixel-diff.mjs <imgA> <imgB> [--bbox-map manifest.json] [--calibration calibration.json]
|
|
7
|
+
* [--component <name>] [--out-mask <path.png>]
|
|
8
|
+
* [--gen-bbox-map gen-bboxes.json]
|
|
9
|
+
*
|
|
10
|
+
* Diff cascade (in order):
|
|
11
|
+
* 1. pHash Hamming (DCT 8×8) — raw distance + phash_fast_candidate flag
|
|
12
|
+
* (necessary-not-sufficient; this script NEVER decides "converged" — the
|
|
13
|
+
* verdict is evaluate-convergence.mjs's job, not pixel-diff's)
|
|
14
|
+
* 2. Region split using bbox-map (if provided):
|
|
15
|
+
* - text regions: scored by layout-box IoU + fg/bg token-color CIEDE2000 ΔE
|
|
16
|
+
* (NOT glyph SSIM — cross-renderer glyphs are not comparable)
|
|
17
|
+
* IoU is REAL only when --gen-bbox-map supplies the generated bbox for
|
|
18
|
+
* the region; otherwise iou is `null` (never a fabricated 1.0).
|
|
19
|
+
* - non-text regions: SSIM (8-pixel window) + CIEDE2000 ΔE
|
|
20
|
+
* 3. AA-tolerant diff mask PNG (YIQ threshold + AA skip)
|
|
21
|
+
* 4. Inter-component spacing delta — `null` unless generated positions are
|
|
22
|
+
* supplied via --gen-bbox-map (never a fabricated {top:0,leading:0})
|
|
23
|
+
*
|
|
24
|
+
* Inputs are assumed CO-REGISTERED (same dimensions). pixel-diff.mjs does NOT
|
|
25
|
+
* normalize: callers MUST pre-normalize both sides via Stage 2.5
|
|
26
|
+
* `calibration.transform` (crop/resample/P3→sRGB) BEFORE calling this.
|
|
27
|
+
* Mismatched dimensions are a HARD error (exit 1), never a silent partial diff.
|
|
28
|
+
*
|
|
29
|
+
* --gen-bbox-map <json> schema (component-relative pixel coords):
|
|
30
|
+
* { "ComponentName": [ { "label": "price", "x": .., "y": .., "w": .., "h": .. }, ... ] }
|
|
31
|
+
* or a flat array [ { "label": .., "x": .., "y": .., "w": .., "h": .. } ].
|
|
32
|
+
* A region's IoU is computed via bboxIoU(refBox, genBox) when a generated
|
|
33
|
+
* box with the same label (or same mark order) is present.
|
|
34
|
+
*
|
|
35
|
+
* Outputs:
|
|
36
|
+
* JSON to stdout — schema: h5-to-swiftui/diff@1
|
|
37
|
+
* Optional diff mask PNG via --out-mask
|
|
38
|
+
*
|
|
39
|
+
* Exit codes:
|
|
40
|
+
* 0 — diff computed, JSON on stdout
|
|
41
|
+
* 1 — fatal error (bad args, file not found, image-size mismatch)
|
|
42
|
+
* 2 — image dependency (pngjs) not installed (actionable hint printed)
|
|
43
|
+
*
|
|
44
|
+
* Examples:
|
|
45
|
+
* node pixel-diff.mjs reference.png generated.png
|
|
46
|
+
* node pixel-diff.mjs reference.png generated.png --bbox-map manifest.json --out-mask diff.png
|
|
47
|
+
* node pixel-diff.mjs reference.png generated.png --calibration calibration.json --component ProductCard
|
|
48
|
+
* node pixel-diff.mjs ref.png gen.png --bbox-map manifest.json --gen-bbox-map gen-bboxes.json
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
52
|
+
import { resolve, dirname } from 'node:path';
|
|
53
|
+
|
|
54
|
+
import {
|
|
55
|
+
requirePng,
|
|
56
|
+
loadPng,
|
|
57
|
+
savePng,
|
|
58
|
+
pHash,
|
|
59
|
+
hammingDistance,
|
|
60
|
+
ssimWindow,
|
|
61
|
+
ciede2000Region,
|
|
62
|
+
buildDiffMask,
|
|
63
|
+
bboxIoU,
|
|
64
|
+
quantile,
|
|
65
|
+
} from './_imglib.mjs';
|
|
66
|
+
|
|
67
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
|
|
71
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
72
|
+
console.log(`pixel-diff.mjs — h5-to-swiftui visual diff cascade
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
node pixel-diff.mjs <imgA> <imgB> [--bbox-map manifest.json]
|
|
76
|
+
[--calibration calibration.json]
|
|
77
|
+
[--component <name>] [--out-mask <path.png>]
|
|
78
|
+
[--gen-bbox-map <gen-bboxes.json>]
|
|
79
|
+
|
|
80
|
+
Arguments:
|
|
81
|
+
<imgA> Reference image (PNG, sRGB)
|
|
82
|
+
<imgB> Generated image (PNG, sRGB — P3 must be pre-converted)
|
|
83
|
+
--bbox-map <path> reference/manifest.json with bbox data for region split
|
|
84
|
+
--calibration <path> calibration.json from calibrate-render.mjs
|
|
85
|
+
--component <name> Component name for the output schema (default: "unknown")
|
|
86
|
+
--out-mask <path> Write AA-tolerant diff mask PNG to this path
|
|
87
|
+
--gen-bbox-map <path> Generated-component bboxes (component-relative px).
|
|
88
|
+
When present, text-region IoU is the REAL bboxIoU
|
|
89
|
+
against the matching generated box. When absent,
|
|
90
|
+
iou is null (never a fabricated 1.0).
|
|
91
|
+
|
|
92
|
+
Output (stdout):
|
|
93
|
+
JSON matching schema h5-to-swiftui/diff@1
|
|
94
|
+
|
|
95
|
+
Exit codes:
|
|
96
|
+
0 Success — JSON on stdout
|
|
97
|
+
1 Fatal error (bad args, file not found, image-size mismatch)
|
|
98
|
+
2 pngjs not installed
|
|
99
|
+
|
|
100
|
+
Co-registration:
|
|
101
|
+
pixel-diff.mjs does NOT normalize. Inputs MUST already be the same
|
|
102
|
+
dimensions; pre-normalize via Stage 2.5 calibration.transform first.
|
|
103
|
+
Mismatched dimensions are a HARD error (exit 1), not a silent partial diff.
|
|
104
|
+
|
|
105
|
+
Diff cascade:
|
|
106
|
+
pHash Hamming → raw distance + phash_fast_candidate (necessary, NOT
|
|
107
|
+
sufficient — the verdict is evaluate-convergence.mjs's)
|
|
108
|
+
text regions → IoU (real via --gen-bbox-map, else null) + fg/bg ΔE
|
|
109
|
+
non-text regions → SSIM (8px window) + CIEDE2000 ΔE
|
|
110
|
+
full image → AA-tolerant mask + global SSIM
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
node pixel-diff.mjs ref.png gen.png
|
|
114
|
+
node pixel-diff.mjs ref.png gen.png --bbox-map .h5-to-swiftui/reference/manifest.json \\
|
|
115
|
+
--component ProductCard --out-mask .h5-to-swiftui/diff/ProductCard.iter1.mask.png
|
|
116
|
+
node pixel-diff.mjs ref.png gen.png --bbox-map manifest.json \\
|
|
117
|
+
--gen-bbox-map .h5-to-swiftui/gen/ProductCard.bboxes.json --component ProductCard
|
|
118
|
+
`);
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Collect positional args
|
|
123
|
+
const positionals = args.filter(a => !a.startsWith('--'));
|
|
124
|
+
if (positionals.length < 2) {
|
|
125
|
+
console.error('Error: <imgA> and <imgB> are required.\nRun with --help for usage.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const imgAPath = resolve(positionals[0]);
|
|
130
|
+
const imgBPath = resolve(positionals[1]);
|
|
131
|
+
|
|
132
|
+
function getFlag(flag, fallback = null) {
|
|
133
|
+
const idx = args.indexOf(flag);
|
|
134
|
+
if (idx === -1) return fallback;
|
|
135
|
+
const val = args[idx + 1];
|
|
136
|
+
if (!val || val.startsWith('--')) {
|
|
137
|
+
console.error(`Error: ${flag} requires an argument.`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
return val;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const bboxMapPath = getFlag('--bbox-map');
|
|
144
|
+
const calibPath = getFlag('--calibration');
|
|
145
|
+
const componentName = getFlag('--component', 'unknown');
|
|
146
|
+
const outMaskPath = getFlag('--out-mask');
|
|
147
|
+
const genBboxMapPath = getFlag('--gen-bbox-map');
|
|
148
|
+
|
|
149
|
+
// ── Validate inputs ───────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
for (const [label, p] of [['imgA', imgAPath], ['imgB', imgBPath]]) {
|
|
152
|
+
if (!existsSync(p)) {
|
|
153
|
+
console.error(`Error: ${label} not found: ${p}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (bboxMapPath && !existsSync(bboxMapPath)) {
|
|
159
|
+
console.error(`Error: --bbox-map not found: ${bboxMapPath}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (calibPath && !existsSync(calibPath)) {
|
|
164
|
+
console.error(`Error: --calibration not found: ${calibPath}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (genBboxMapPath && !existsSync(genBboxMapPath)) {
|
|
169
|
+
console.error(`Error: --gen-bbox-map not found: ${genBboxMapPath}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Load PNG dependency early for clear error ─────────────────────────────────
|
|
174
|
+
await requirePng();
|
|
175
|
+
|
|
176
|
+
// ── Load images ───────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
let imgA, imgB;
|
|
179
|
+
try {
|
|
180
|
+
[imgA, imgB] = await Promise.all([loadPng(imgAPath), loadPng(imgBPath)]);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error(`Error: failed to load image: ${e.message}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// HARD error on size mismatch — a silent partial diff fabricates a metric.
|
|
187
|
+
// Inputs MUST be co-registered first via Stage 2.5 calibration.transform
|
|
188
|
+
// (crop → resample → P3→sRGB). pixel-diff.mjs deliberately does NOT normalize.
|
|
189
|
+
if (imgA.width !== imgB.width || imgA.height !== imgB.height) {
|
|
190
|
+
console.error(
|
|
191
|
+
`Error: image sizes differ — A=${imgA.width}×${imgA.height}, B=${imgB.width}×${imgB.height}\n` +
|
|
192
|
+
'pixel-diff.mjs requires CO-REGISTERED inputs (identical dimensions).\n' +
|
|
193
|
+
'Pre-normalize BOTH sides through Stage 2.5 calibration.transform\n' +
|
|
194
|
+
'(crop simulator chrome → resample to common logical raster → P3→sRGB)\n' +
|
|
195
|
+
'BEFORE calling pixel-diff.mjs. It refuses a partial overlap diff because\n' +
|
|
196
|
+
'that would emit a fabricated metric over a mis-registered region.\n' +
|
|
197
|
+
(calibPath
|
|
198
|
+
? '--calibration was supplied, but pixel-diff.mjs does not itself apply\n' +
|
|
199
|
+
'the transform: the caller must pre-normalize the rasters.\n'
|
|
200
|
+
: '')
|
|
201
|
+
);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const overlapW = imgA.width;
|
|
206
|
+
const overlapH = imgA.height;
|
|
207
|
+
|
|
208
|
+
// ── Load optional JSON inputs ─────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
let bboxMap = null;
|
|
211
|
+
let calibData = null;
|
|
212
|
+
let genBboxMap = null;
|
|
213
|
+
|
|
214
|
+
if (bboxMapPath) {
|
|
215
|
+
try {
|
|
216
|
+
const { readFileSync } = await import('node:fs');
|
|
217
|
+
bboxMap = JSON.parse(readFileSync(bboxMapPath, 'utf8'));
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.error(`Error: cannot parse --bbox-map: ${e.message}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (genBboxMapPath) {
|
|
225
|
+
try {
|
|
226
|
+
const { readFileSync } = await import('node:fs');
|
|
227
|
+
genBboxMap = JSON.parse(readFileSync(genBboxMapPath, 'utf8'));
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error(`Error: cannot parse --gen-bbox-map: ${e.message}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (calibPath) {
|
|
235
|
+
try {
|
|
236
|
+
const { readFileSync } = await import('node:fs');
|
|
237
|
+
calibData = JSON.parse(readFileSync(calibPath, 'utf8'));
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.error(`Error: cannot parse --calibration: ${e.message}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Step 1: pHash Hamming ─────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
const hashA = pHash(imgA);
|
|
247
|
+
const hashB = pHash(imgB);
|
|
248
|
+
const phashHamming = hammingDistance(hashA, hashB);
|
|
249
|
+
|
|
250
|
+
// ── Step 2: Region-split diff ─────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extract bounding boxes for a component from the manifest.
|
|
254
|
+
* Returns { textBoxes: [{x,y,w,h,mark,label}], nontextBoxes: [{x,y,w,h,mark,label}] }
|
|
255
|
+
* Falls back to treating the whole image as non-text if no manifest.
|
|
256
|
+
*/
|
|
257
|
+
function extractRegions(manifest, compName) {
|
|
258
|
+
const textBoxes = [];
|
|
259
|
+
const nontextBoxes = [];
|
|
260
|
+
|
|
261
|
+
if (!manifest) {
|
|
262
|
+
return { textBoxes, nontextBoxes: [{ x: 0, y: 0, w: overlapW, h: overlapH, mark: 0, label: 'full' }] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Walk screens → components; match by component name
|
|
266
|
+
let markIndex = 1;
|
|
267
|
+
const screens = manifest.screens ?? [];
|
|
268
|
+
for (const screen of screens) {
|
|
269
|
+
for (const comp of (screen.components ?? [])) {
|
|
270
|
+
if (comp.name !== compName && compName !== 'unknown') continue;
|
|
271
|
+
const bbox = comp.bbox_css_px;
|
|
272
|
+
if (!bbox) continue;
|
|
273
|
+
const region = {
|
|
274
|
+
x: Math.round(bbox.x ?? 0),
|
|
275
|
+
y: Math.round(bbox.y ?? 0),
|
|
276
|
+
w: Math.round(bbox.width ?? bbox.w ?? 0),
|
|
277
|
+
h: Math.round(bbox.height ?? bbox.h ?? 0),
|
|
278
|
+
mark: markIndex++,
|
|
279
|
+
label: comp.name,
|
|
280
|
+
};
|
|
281
|
+
// Heuristic: classify as text if the component name contains a text keyword
|
|
282
|
+
const isText = /text|label|title|heading|paragraph|caption|link|badge|tag/i.test(comp.name) ||
|
|
283
|
+
(comp.is_text === true);
|
|
284
|
+
if (isText) textBoxes.push(region);
|
|
285
|
+
else nontextBoxes.push(region);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Also check for explicit bboxes array on the manifest (bbox_map format)
|
|
290
|
+
const bboxes = manifest.bboxes ?? [];
|
|
291
|
+
for (const bb of bboxes) {
|
|
292
|
+
const region = {
|
|
293
|
+
x: Math.round(bb.x ?? 0),
|
|
294
|
+
y: Math.round(bb.y ?? 0),
|
|
295
|
+
w: Math.round(bb.w ?? bb.width ?? 0),
|
|
296
|
+
h: Math.round(bb.h ?? bb.height ?? 0),
|
|
297
|
+
mark: markIndex++,
|
|
298
|
+
label: bb.label ?? bb.name ?? 'region',
|
|
299
|
+
fg_color: bb.fg_color,
|
|
300
|
+
bg_color: bb.bg_color,
|
|
301
|
+
};
|
|
302
|
+
if (bb.is_text) textBoxes.push(region);
|
|
303
|
+
else nontextBoxes.push(region);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fallback: no matching regions found → whole image as non-text
|
|
307
|
+
if (textBoxes.length === 0 && nontextBoxes.length === 0) {
|
|
308
|
+
nontextBoxes.push({ x: 0, y: 0, w: overlapW, h: overlapH, mark: 0, label: 'full' });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { textBoxes, nontextBoxes };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const { textBoxes, nontextBoxes } = extractRegions(bboxMap, componentName);
|
|
315
|
+
|
|
316
|
+
// Score non-text regions: SSIM + CIEDE2000
|
|
317
|
+
const nontextResults = [];
|
|
318
|
+
for (const box of nontextBoxes) {
|
|
319
|
+
const roi = { x: box.x, y: box.y, w: box.w, h: box.h };
|
|
320
|
+
const ssim = ssimWindow(imgA, imgB, roi, 8);
|
|
321
|
+
const { p95: deltaE_p95 } = ciede2000Region(imgA, imgB, roi);
|
|
322
|
+
nontextResults.push({
|
|
323
|
+
mark: box.mark,
|
|
324
|
+
label: box.label,
|
|
325
|
+
ssim: Math.round(ssim * 1000) / 1000,
|
|
326
|
+
deltaE_p95: Math.round(deltaE_p95 * 100) / 100,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Resolve a generated bbox for a reference text region.
|
|
331
|
+
// genBboxMap accepted forms:
|
|
332
|
+
// { "<component>": [ {label,x,y,w,h}, ... ] } (component-keyed)
|
|
333
|
+
// [ {label,x,y,w,h}, ... ] (flat array)
|
|
334
|
+
// Match priority: same `label`, else positional (Nth text box ↔ Nth gen box).
|
|
335
|
+
function genBoxListFor(map, compName) {
|
|
336
|
+
if (!map) return null;
|
|
337
|
+
if (Array.isArray(map)) return map;
|
|
338
|
+
if (compName && Array.isArray(map[compName])) return map[compName];
|
|
339
|
+
// single-component map with one array value
|
|
340
|
+
const vals = Object.values(map).filter(Array.isArray);
|
|
341
|
+
return vals.length === 1 ? vals[0] : null;
|
|
342
|
+
}
|
|
343
|
+
const genBoxList = genBoxListFor(genBboxMap, componentName);
|
|
344
|
+
|
|
345
|
+
function findGenBox(refBox, idx) {
|
|
346
|
+
if (!genBoxList) return null;
|
|
347
|
+
if (refBox.label) {
|
|
348
|
+
const byLabel = genBoxList.find(
|
|
349
|
+
g => (g.label ?? g.name) === refBox.label
|
|
350
|
+
);
|
|
351
|
+
if (byLabel) return byLabel;
|
|
352
|
+
}
|
|
353
|
+
return genBoxList[idx] ?? null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Score text regions: IoU + token-color ΔE (fg + bg)
|
|
357
|
+
// - iou: REAL layout-box IoU via bboxIoU(refBox, genBox) when --gen-bbox-map
|
|
358
|
+
// supplies the matching generated box; otherwise `null` (never a
|
|
359
|
+
// fabricated 1.0 — the absence of a generated bbox is reported honestly).
|
|
360
|
+
// - fg_deltaE: median pixel color in the top-center strip (glyph-dominated).
|
|
361
|
+
// - bg_deltaE: corner samples (background-dominated).
|
|
362
|
+
const textResults = [];
|
|
363
|
+
let textIdx = 0;
|
|
364
|
+
for (const box of textBoxes) {
|
|
365
|
+
const refBox = { x: box.x, y: box.y, w: box.w, h: box.h, label: box.label };
|
|
366
|
+
|
|
367
|
+
// REAL IoU only when a generated bbox is supplied for this region.
|
|
368
|
+
const genBox = findGenBox(refBox, textIdx);
|
|
369
|
+
let iou = null;
|
|
370
|
+
let iouNote;
|
|
371
|
+
if (genBox) {
|
|
372
|
+
iou = Math.round(
|
|
373
|
+
bboxIoU(
|
|
374
|
+
{ x: refBox.x, y: refBox.y, w: refBox.w, h: refBox.h },
|
|
375
|
+
{
|
|
376
|
+
x: genBox.x ?? 0,
|
|
377
|
+
y: genBox.y ?? 0,
|
|
378
|
+
w: genBox.w ?? genBox.width ?? 0,
|
|
379
|
+
h: genBox.h ?? genBox.height ?? 0,
|
|
380
|
+
}
|
|
381
|
+
) * 1000
|
|
382
|
+
) / 1000;
|
|
383
|
+
} else {
|
|
384
|
+
iouNote =
|
|
385
|
+
'no generated bbox supplied (--gen-bbox-map absent or no match) — ' +
|
|
386
|
+
'IoU is null, NOT assumed 1.0';
|
|
387
|
+
}
|
|
388
|
+
textIdx++;
|
|
389
|
+
|
|
390
|
+
// fg: top-center strip (glyph color region)
|
|
391
|
+
const fgRoi = {
|
|
392
|
+
x: refBox.x + Math.floor(refBox.w * 0.25),
|
|
393
|
+
y: refBox.y,
|
|
394
|
+
w: Math.max(1, Math.floor(refBox.w * 0.5)),
|
|
395
|
+
h: Math.max(1, Math.floor(refBox.h * 0.3)),
|
|
396
|
+
};
|
|
397
|
+
const { p95: fg_deltaE } = ciede2000Region(imgA, imgB, clipRoi(fgRoi, overlapW, overlapH));
|
|
398
|
+
|
|
399
|
+
// bg: corner samples
|
|
400
|
+
const bgRoi = {
|
|
401
|
+
x: refBox.x,
|
|
402
|
+
y: refBox.y + Math.floor(refBox.h * 0.7),
|
|
403
|
+
w: Math.max(1, Math.floor(refBox.w * 0.2)),
|
|
404
|
+
h: Math.max(1, Math.floor(refBox.h * 0.3)),
|
|
405
|
+
};
|
|
406
|
+
const { p95: bg_deltaE } = ciede2000Region(imgA, imgB, clipRoi(bgRoi, overlapW, overlapH));
|
|
407
|
+
|
|
408
|
+
textResults.push({
|
|
409
|
+
mark: box.mark,
|
|
410
|
+
label: box.label,
|
|
411
|
+
iou, // REAL bboxIoU, or null when no gen bbox
|
|
412
|
+
...(iouNote ? { iou_note: iouNote } : {}),
|
|
413
|
+
fg_deltaE: Math.round(fg_deltaE * 100) / 100,
|
|
414
|
+
bg_deltaE: Math.round(bg_deltaE * 100) / 100,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function clipRoi(roi, maxW, maxH) {
|
|
419
|
+
return {
|
|
420
|
+
x: Math.max(0, Math.min(roi.x, maxW - 1)),
|
|
421
|
+
y: Math.max(0, Math.min(roi.y, maxH - 1)),
|
|
422
|
+
w: Math.max(1, Math.min(roi.w, maxW - roi.x)),
|
|
423
|
+
h: Math.max(1, Math.min(roi.h, maxH - roi.y)),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Step 3: AA-tolerant diff mask ─────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
const { mask, diffPixelCount, totalPixels } = buildDiffMask(imgA, imgB);
|
|
430
|
+
|
|
431
|
+
if (outMaskPath) {
|
|
432
|
+
const maskDir = dirname(outMaskPath);
|
|
433
|
+
mkdirSync(maskDir, { recursive: true });
|
|
434
|
+
try {
|
|
435
|
+
await savePng(outMaskPath, mask);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
process.stderr.write(`Warning: could not write diff mask: ${e.message}\n`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Step 4: Global SSIM ───────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
const globalSsim = ssimWindow(imgA, imgB, { x: 0, y: 0, w: overlapW, h: overlapH }, 8);
|
|
444
|
+
|
|
445
|
+
// ── Step 5: Inter-component spacing delta ────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
// Spacing delta is only real when generated component positions are known
|
|
448
|
+
// (supplied via --gen-bbox-map). Without them we emit `null` + a note — NOT
|
|
449
|
+
// fabricated zeros, which would be a false "no drift" signal.
|
|
450
|
+
let interComponentSpacingDelta = null;
|
|
451
|
+
let spacingNote;
|
|
452
|
+
|
|
453
|
+
if (bboxMap && bboxMap.screens) {
|
|
454
|
+
const screens = bboxMap.screens ?? [];
|
|
455
|
+
const allComps = screens.flatMap(s => s.components ?? []);
|
|
456
|
+
if (allComps.length >= 2) {
|
|
457
|
+
const sorted = allComps
|
|
458
|
+
.filter(c => c.bbox_css_px)
|
|
459
|
+
.sort((a, b) => (a.bbox_css_px.y ?? 0) - (b.bbox_css_px.y ?? 0));
|
|
460
|
+
if (sorted.length >= 2) {
|
|
461
|
+
const first = sorted[0].bbox_css_px;
|
|
462
|
+
const second = sorted[1].bbox_css_px;
|
|
463
|
+
const expectedGap = (second.y ?? 0) - ((first.y ?? 0) + (first.height ?? 0));
|
|
464
|
+
|
|
465
|
+
if (genBoxList && genBoxList.length >= 2) {
|
|
466
|
+
// Real delta: compare reference gap vs generated gap.
|
|
467
|
+
const g = [...genBoxList]
|
|
468
|
+
.filter(b => (b.y ?? 0) !== undefined)
|
|
469
|
+
.sort((a, b) => (a.y ?? 0) - (b.y ?? 0));
|
|
470
|
+
const genGap =
|
|
471
|
+
(g[1].y ?? 0) - ((g[0].y ?? 0) + (g[0].h ?? g[0].height ?? 0));
|
|
472
|
+
const topDelta = Math.round(genGap - expectedGap);
|
|
473
|
+
const leadingDelta = Math.round((g[0].x ?? 0) - (first.x ?? 0));
|
|
474
|
+
interComponentSpacingDelta = {
|
|
475
|
+
top: topDelta,
|
|
476
|
+
leading: leadingDelta,
|
|
477
|
+
expected_gap_px: Math.round(expectedGap),
|
|
478
|
+
measured_gap_px: Math.round(genGap),
|
|
479
|
+
};
|
|
480
|
+
} else {
|
|
481
|
+
// No generated positions — report null, not fabricated zeros.
|
|
482
|
+
spacingNote =
|
|
483
|
+
'null: generated component positions unknown ' +
|
|
484
|
+
'(--gen-bbox-map absent); reporting null, NOT zeros';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Assemble output (schema: h5-to-swiftui/diff@1) ───────────────────────────
|
|
491
|
+
|
|
492
|
+
const output = {
|
|
493
|
+
schema: 'h5-to-swiftui/diff@1',
|
|
494
|
+
component: componentName,
|
|
495
|
+
phash_hamming: phashHamming,
|
|
496
|
+
// necessary-not-sufficient: a low phash distance is a FAST CANDIDATE only.
|
|
497
|
+
// It is NOT a terminal "converged" signal — the region gate must still pass.
|
|
498
|
+
// The verdict is evaluate-convergence.mjs's job, not pixel-diff's.
|
|
499
|
+
phash_fast_candidate: phashHamming <= 5,
|
|
500
|
+
regions: {
|
|
501
|
+
text: textResults.map(r => ({
|
|
502
|
+
mark: r.mark,
|
|
503
|
+
...(r.label && r.label !== 'region' ? { label: r.label } : {}),
|
|
504
|
+
iou: r.iou, // number | null
|
|
505
|
+
...(r.iou_note ? { iou_note: r.iou_note } : {}),
|
|
506
|
+
fg_deltaE: r.fg_deltaE,
|
|
507
|
+
bg_deltaE: r.bg_deltaE,
|
|
508
|
+
})),
|
|
509
|
+
nontext: nontextResults.map(r => ({
|
|
510
|
+
mark: r.mark,
|
|
511
|
+
...(r.label && r.label !== 'full' ? { label: r.label } : {}),
|
|
512
|
+
ssim: r.ssim,
|
|
513
|
+
deltaE_p95: r.deltaE_p95,
|
|
514
|
+
})),
|
|
515
|
+
},
|
|
516
|
+
// null (not {top:0,leading:0}) when generated positions are unknown.
|
|
517
|
+
inter_component_spacing_delta_pt: interComponentSpacingDelta,
|
|
518
|
+
...(spacingNote ? { inter_component_spacing_delta_note: spacingNote } : {}),
|
|
519
|
+
diff_mask_png: outMaskPath ?? null,
|
|
520
|
+
global_ssim: Math.round(globalSsim * 1000) / 1000,
|
|
521
|
+
diff_pixel_fraction: Math.round((diffPixelCount / totalPixels) * 10000) / 10000,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Attach calibration floor reference if provided
|
|
525
|
+
if (calibData?.floor) {
|
|
526
|
+
output.calibration_floor_ref = calibData.floor;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
530
|
+
process.exit(0);
|