feat: honor png trns render smoke

This commit is contained in:
aliyun1121003339 2026-07-06 19:54:19 +08:00
parent bdb6675629
commit 67dd505e1d
10 changed files with 225 additions and 32 deletions

File diff suppressed because one or more lines are too long

View file

@ -126,7 +126,7 @@ node scripts/export-circuitikz.js \
--expected-svg-text v_{out}
```
For `.png` screenshot artifacts, the smoke check decodes non-interlaced 1/2/4/8-bit indexed-color PNG output, 1/2/4/8/16-bit grayscale PNG output, and 8/16-bit RGB, grayscale-alpha, or RGBA PNG output. Indexed-color and sub-byte grayscale images support packed samples; indexed-color images also support PLTE palette entries and optional tRNS alpha. 16-bit direct samples are normalized into the same 8-bit RGBA comparison space used by the smoke checks. It verifies positive dimensions, records the foreground pixel bounding box as `foregroundBounds`, records foreground density inside that box as `foregroundDensity`, and requires at least one pixel that differs from the top-left background color. Blank screenshots fail with `render-png-blank`; foreground content that touches the image boundary fails with `render-png-content-clipped`; foreground pixels that are unusually dense inside a non-trivial bounding box fail with `render-png-foreground-dense`; malformed or unsupported PNGs fail with `render-png-invalid` or `render-png-unsupported`.
For `.png` screenshot artifacts, the smoke check decodes non-interlaced 1/2/4/8-bit indexed-color PNG output, 1/2/4/8/16-bit grayscale PNG output, and 8/16-bit RGB, grayscale-alpha, or RGBA PNG output. Indexed-color and sub-byte grayscale images support packed samples; indexed-color images also support PLTE palette entries and optional tRNS alpha; grayscale/RGB images support tRNS transparent samples. 16-bit direct samples are normalized into the same 8-bit RGBA comparison space used by the smoke checks. It verifies positive dimensions, records the foreground pixel bounding box as `foregroundBounds`, records foreground density inside that box as `foregroundDensity`, and requires at least one pixel that differs from the top-left background color. Blank screenshots fail with `render-png-blank`; foreground content that touches the image boundary fails with `render-png-content-clipped`; foreground pixels that are unusually dense inside a non-trivial bounding box fail with `render-png-foreground-dense`; malformed or unsupported PNGs fail with `render-png-invalid` or `render-png-unsupported`.
The result is recorded as `compileExecution.renderSmoke`. Missing or empty artifacts add `render-artifact-missing` or `render-artifact-empty`; SVG structure failures add diagnostics such as `render-svg-invalid`, `render-svg-dimension-missing`, `render-svg-no-visible-elements`, `render-svg-text-missing`, `render-svg-text-path-only`, `render-svg-out-of-bounds`, `render-svg-text-overlap`, `render-svg-label-overlap`, or `render-svg-path-glyph-overlap`; PNG screenshot failures add diagnostics such as `render-png-blank`, `render-png-content-clipped`, or `render-png-foreground-dense`.

View file

@ -126,7 +126,7 @@ node scripts/export-circuitikz.js \
--expected-svg-text v_{out}
```
对于 `.png` screenshot artifactsmoke check 会解码非交错的 1/2/4/8-bit indexed-color PNG 输出、1/2/4/8/16-bit grayscale PNG 输出,以及 8/16-bit RGB、grayscale-alpha 或 RGBA PNG 输出。indexed-color 与 sub-byte grayscale 图像支持 packed samplesindexed-color 图像还支持 PLTE palette entries 与可选 tRNS alpha。16-bit direct samples 会被归一化到 smoke check 使用的同一个 8-bit RGBA 比较空间。它会检查正的尺寸,把前景像素包围盒记录为 `foregroundBounds`,把包围盒内前景密度记录为 `foregroundDensity`,并要求至少一个不同于左上角背景色的像素。空白截图会以 `render-png-blank` 失败;前景内容贴到图像边界会以 `render-png-content-clipped` 失败;非平凡包围盒内前景像素异常密集会以 `render-png-foreground-dense` 失败;格式损坏或不支持的 PNG 会以 `render-png-invalid``render-png-unsupported` 失败。
对于 `.png` screenshot artifactsmoke check 会解码非交错的 1/2/4/8-bit indexed-color PNG 输出、1/2/4/8/16-bit grayscale PNG 输出,以及 8/16-bit RGB、grayscale-alpha 或 RGBA PNG 输出。indexed-color 与 sub-byte grayscale 图像支持 packed samplesindexed-color 图像还支持 PLTE palette entries 与可选 tRNS alphagrayscale/RGB 图像支持 tRNS transparent samples。16-bit direct samples 会被归一化到 smoke check 使用的同一个 8-bit RGBA 比较空间。它会检查正的尺寸,把前景像素包围盒记录为 `foregroundBounds`,把包围盒内前景密度记录为 `foregroundDensity`,并要求至少一个不同于左上角背景色的像素。空白截图会以 `render-png-blank` 失败;前景内容贴到图像边界会以 `render-png-content-clipped` 失败;非平凡包围盒内前景像素异常密集会以 `render-png-foreground-dense` 失败;格式损坏或不支持的 PNG 会以 `render-png-invalid``render-png-unsupported` 失败。
检查结果会记录为 `compileExecution.renderSmoke`。缺失或空 artifact 会追加 `render-artifact-missing``render-artifact-empty`SVG 结构失败会追加 `render-svg-invalid`、`render-svg-dimension-missing`、`render-svg-no-visible-elements`、`render-svg-text-missing`、`render-svg-text-path-only`、`render-svg-out-of-bounds`、`render-svg-text-overlap`、`render-svg-label-overlap` 或 `render-svg-path-glyph-overlap` 等 diagnosticPNG screenshot 失败会追加 `render-png-blank`、`render-png-content-clipped` 或 `render-png-foreground-dense` 等 diagnostic。

View file

@ -146,12 +146,12 @@ Phase A is documented. Phase B/C now have a constrained repository prototype:
- `src/diagram/adapters/circuitikz/circuitikzRepairBrief.ts`, `--repair-brief-output`, and `--repair-brief` produce and consume schema `notemd.circuitikz.repair-brief.v1`, a topology-preserving repair handoff that carries the source `CircuitSpec`, topology signature, compile/render diagnostics, allowed changes, prohibited topology changes, and next verification steps. Candidate specs can be checked against the brief signature before output is written.
- `src/diagram/adapters/circuitikz/circuitikzDiagnostics.ts` parses existing LaTeX/TikZJax compile logs into actionable diagnostics without spawning a compiler or depending on shell command resolution.
- `src/diagram/adapters/circuitikz/circuitikzCompileRunner.ts` can run an explicitly configured local renderer with `shell: false`, placeholder-expanded arguments, structured executable diagnostics for empty, missing, or invalid renderer commands, generated-log diagnostics, and optional `--expected-artifact` render-smoke checks.
- `src/diagram/adapters/circuitikz/circuitikzRenderSmoke.ts` inspects expected render artifacts. PDF and other opaque artifacts are checked for existence/non-empty output; SVG artifacts are additionally checked for an `<svg>` root, positive dimensions or `viewBox`, visible drawing elements, optional repeated `--expected-svg-text` tokens, `render-svg-text-path-only` / `pathOnlyGlyphUseCount` classification for path-only labels, path-only glyph placement through resolved `<use href="#...">` geometry, definition-local transforms on reusable glyph paths, path-only glyph overlap diagnostics through `render-svg-path-glyph-overlap`, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry, positioned `tspan` label geometry, bounded-canvas/text-overlap diagnostics, label-vs-drawing overlap diagnostics through `render-svg-label-overlap`, and transform-aware geometry for common group and element transforms; PNG screenshot artifacts are decoded for positive dimensions, non-background pixels, 1/2/4/8-bit indexed-color packed samples with PLTE/tRNS palette data, 1/2/4/8/16-bit grayscale samples, 8/16-bit grayscale-alpha/RGB/RGBA direct samples, foreground bounds, `foregroundDensity`, edge-touching clipped content through `render-png-content-clipped`, and unusually dense foreground blocks through `render-png-foreground-dense`.
- `src/diagram/adapters/circuitikz/circuitikzRenderSmoke.ts` inspects expected render artifacts. PDF and other opaque artifacts are checked for existence/non-empty output; SVG artifacts are additionally checked for an `<svg>` root, positive dimensions or `viewBox`, visible drawing elements, optional repeated `--expected-svg-text` tokens, `render-svg-text-path-only` / `pathOnlyGlyphUseCount` classification for path-only labels, path-only glyph placement through resolved `<use href="#...">` geometry, definition-local transforms on reusable glyph paths, path-only glyph overlap diagnostics through `render-svg-path-glyph-overlap`, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry, positioned `tspan` label geometry, bounded-canvas/text-overlap diagnostics, label-vs-drawing overlap diagnostics through `render-svg-label-overlap`, and transform-aware geometry for common group and element transforms; PNG screenshot artifacts are decoded for positive dimensions, non-background pixels, 1/2/4/8-bit indexed-color packed samples with PLTE/tRNS palette data, grayscale/RGB tRNS transparent samples, 1/2/4/8/16-bit grayscale samples, 8/16-bit grayscale-alpha/RGB/RGBA direct samples, foreground bounds, `foregroundDensity`, edge-touching clipped content through `render-png-content-clipped`, and unusually dense foreground blocks through `render-png-foreground-dense`.
- `src/rendering/diagnostics.ts` summarizes `RenderArtifact.diagnostics` into error/warning/info counts, and `src/ui/DiagramPreviewModal.ts` uses that summary in both the artifact diagnostics panel and preview history entries. Any render target can attach diagnostics to `RenderArtifact.diagnostics`; the preview UI shows the summary, severity, kind, message, and advice without requiring TikZJax or LaTeX in the plugin runtime.
- `src/tests/circuitikzExporter.test.ts`, `src/tests/circuitikzRepairBrief.test.ts`, `src/tests/circuitikzCompileDiagnostics.test.ts`, `src/tests/circuitikzRenderSmoke.test.ts`, `src/tests/circuitikzCompileRunner.test.ts`, and `src/tests/circuitikzExportCli.test.ts` verify deterministic output, topology rejection, topology-preserving repair brief generation and candidate validation, package-script exposure, UTF-8 BOM handling, diagnostic parsing, shell-free compile execution, structured `compile-executable-invalid` / `compile-executable-not-found` diagnostics, diagnostics JSON output, render artifact existence/non-empty smoke checks, SVG structure checks, path-only SVG label classification, path-only glyph placement checks, definition-local glyph transform checks, path-only glyph overlap diagnostics, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry checks, positioned `tspan` label geometry checks, transform-aware SVG geometry checks, SVG label-vs-drawing overlap checks, PNG blank screenshot checks, indexed-color and grayscale PNG packed sample decoding, indexed-color PNG palette decoding, 16-bit direct PNG sample normalization, PNG foreground-bound and foreground-density reporting, PNG clipped-content diagnostics, dense-foreground diagnostics, and nonzero CLI exit for logs or smoke reports with errors.
- `src/tests/circuitikzExporter.test.ts`, `src/tests/circuitikzRepairBrief.test.ts`, `src/tests/circuitikzCompileDiagnostics.test.ts`, `src/tests/circuitikzRenderSmoke.test.ts`, `src/tests/circuitikzCompileRunner.test.ts`, and `src/tests/circuitikzExportCli.test.ts` verify deterministic output, topology rejection, topology-preserving repair brief generation and candidate validation, package-script exposure, UTF-8 BOM handling, diagnostic parsing, shell-free compile execution, structured `compile-executable-invalid` / `compile-executable-not-found` diagnostics, diagnostics JSON output, render artifact existence/non-empty smoke checks, SVG structure checks, path-only SVG label classification, path-only glyph placement checks, definition-local glyph transform checks, path-only glyph overlap diagnostics, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry checks, positioned `tspan` label geometry checks, transform-aware SVG geometry checks, SVG label-vs-drawing overlap checks, PNG blank screenshot checks, indexed-color and grayscale PNG packed sample decoding, indexed-color PNG palette decoding, grayscale/RGB PNG tRNS transparent sample handling, 16-bit direct PNG sample normalization, PNG foreground-bound and foreground-density reporting, PNG clipped-content diagnostics, dense-foreground diagnostics, and nonzero CLI exit for logs or smoke reports with errors.
- `src/tests/diagramPreviewModal.test.ts` verifies that artifact diagnostics are visible in the diagram preview modal and that preview history does not collapse entries with different diagnostics.
Phase D now has log parsing, opt-in local renderer execution, artifact-level smoke checks, SVG structure smoke checks with transform-aware geometry, path-only label classification, path-only glyph placement checks, path-only glyph overlap diagnostics, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` geometry coverage, positioned `tspan` label geometry, text/text and label/drawing overlap diagnostics, first PNG screenshot nonblank, indexed-color and grayscale packed-sample decoding, 16-bit direct sample normalization, edge-clipping, and dense-foreground checks, a topology-preserving repair guard, a topology-preserving repair brief handoff with candidate validation, and a front-end diagnostics surface, but the implementation still deliberately stops before required renderer availability, OCR recognition for path-only glyph text, precise pixel-level overlap detection, full SVG path coverage, and automated Phase E repair execution. It does not bundle LaTeX, make TikZJax a plugin runtime dependency, or run a visual repair loop.
Phase D now has log parsing, opt-in local renderer execution, artifact-level smoke checks, SVG structure smoke checks with transform-aware geometry, path-only label classification, path-only glyph placement checks, path-only glyph overlap diagnostics, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` geometry coverage, positioned `tspan` label geometry, text/text and label/drawing overlap diagnostics, first PNG screenshot nonblank, indexed-color and grayscale packed-sample decoding, grayscale/RGB tRNS transparent sample handling, 16-bit direct sample normalization, edge-clipping, and dense-foreground checks, a topology-preserving repair guard, a topology-preserving repair brief handoff with candidate validation, and a front-end diagnostics surface, but the implementation still deliberately stops before required renderer availability, OCR recognition for path-only glyph text, precise pixel-level overlap detection, full SVG path coverage, and automated Phase E repair execution. It does not bundle LaTeX, make TikZJax a plugin runtime dependency, or run a visual repair loop.
The SVG geometry smoke also covers SVG number grammar for leading-dot decimals and explicit plus signs. This is a small but important renderer-compatibility gate because dvisvgm can emit compact decimals that should remain fractional during bounds checks.

View file

@ -146,12 +146,12 @@ Phase A 已经文档化。Phase B/C 现在有了受约束的仓库内原型:
- `src/diagram/adapters/circuitikz/circuitikzRepairBrief.ts`、`--repair-brief-output` 与 `--repair-brief` 会生成并消费 schema `notemd.circuitikz.repair-brief.v1`,作为 topology-preserving repair handoff携带 source `CircuitSpec`、topology signature、compile/render diagnostics、允许修改项、禁止拓扑修改项和下一步验证步骤。candidate spec 可以在写出 output 前按 brief signature 执行校验。
- `src/diagram/adapters/circuitikz/circuitikzDiagnostics.ts` 会把已有 LaTeX/TikZJax compile logs 解析为 actionable diagnostics不 spawn 编译器,也不依赖 shell command resolution。
- `src/diagram/adapters/circuitikz/circuitikzCompileRunner.ts` 可以用 `shell: false`、placeholder-expanded arguments、针对空、缺失或无效 renderer command 的结构化 executable diagnostics、生成 log diagnostics 和可选 `--expected-artifact` render-smoke checks 运行显式配置的本地 renderer。
- `src/diagram/adapters/circuitikz/circuitikzRenderSmoke.ts` 会检查预期 render artifact。PDF 和其他 opaque artifacts 检查存在与非空SVG artifacts 还会检查 `<svg>` root、正的尺寸或 `viewBox`、可见绘图元素、可选重复传入的 `--expected-svg-text` tokens、用于 path-only labels 的 `render-svg-text-path-only` / `pathOnlyGlyphUseCount` 分类、通过解析 `<use href="#...">` 几何执行的 path-only glyph placement checks、可复用 glyph path 上的 definition-local transforms、通过 `render-svg-path-glyph-overlap` 执行的 path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry、positioned `tspan` label geometry、bounded-canvas/text-overlap diagnostics、通过 `render-svg-label-overlap` 报告的 label-vs-drawing overlap diagnostics以及常见 group 与 element transforms 的 transform-aware geometryPNG screenshot artifacts 会被解码并检查正尺寸、非背景像素、1/2/4/8-bit indexed-color packed samples 与 PLTE/tRNS palette data、1/2/4/8/16-bit grayscale samples、8/16-bit grayscale-alpha/RGB/RGBA direct samples、前景包围盒、`foregroundDensity`、通过 `render-png-content-clipped` 报告的贴边裁剪内容,以及通过 `render-png-foreground-dense` 报告的异常密集前景块。
- `src/diagram/adapters/circuitikz/circuitikzRenderSmoke.ts` 会检查预期 render artifact。PDF 和其他 opaque artifacts 检查存在与非空SVG artifacts 还会检查 `<svg>` root、正的尺寸或 `viewBox`、可见绘图元素、可选重复传入的 `--expected-svg-text` tokens、用于 path-only labels 的 `render-svg-text-path-only` / `pathOnlyGlyphUseCount` 分类、通过解析 `<use href="#...">` 几何执行的 path-only glyph placement checks、可复用 glyph path 上的 definition-local transforms、通过 `render-svg-path-glyph-overlap` 执行的 path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry、positioned `tspan` label geometry、bounded-canvas/text-overlap diagnostics、通过 `render-svg-label-overlap` 报告的 label-vs-drawing overlap diagnostics以及常见 group 与 element transforms 的 transform-aware geometryPNG screenshot artifacts 会被解码并检查正尺寸、非背景像素、1/2/4/8-bit indexed-color packed samples 与 PLTE/tRNS palette data、grayscale/RGB tRNS transparent samples、1/2/4/8/16-bit grayscale samples、8/16-bit grayscale-alpha/RGB/RGBA direct samples、前景包围盒、`foregroundDensity`、通过 `render-png-content-clipped` 报告的贴边裁剪内容,以及通过 `render-png-foreground-dense` 报告的异常密集前景块。
- `src/rendering/diagnostics.ts` 会把 `RenderArtifact.diagnostics` 汇总为 error/warning/info 计数,`src/ui/DiagramPreviewModal.ts` 会在 artifact diagnostics panel 和 preview history entries 中使用同一份 summary。任意 render target 都可以把 diagnostics 挂到 `RenderArtifact.diagnostics`preview UI 会显示 summary、severity、kind、message 和 advice而不要求插件 runtime 内置 TikZJax 或 LaTeX。
- `src/tests/circuitikzExporter.test.ts`、`src/tests/circuitikzRepairBrief.test.ts`、`src/tests/circuitikzCompileDiagnostics.test.ts`、`src/tests/circuitikzRenderSmoke.test.ts`、`src/tests/circuitikzCompileRunner.test.ts` 与 `src/tests/circuitikzExportCli.test.ts` 验证确定性输出、拓扑拒绝、topology-preserving repair brief generation 与 candidate validation、package-script 暴露、UTF-8 BOM 处理、diagnostic parsing、shell-free compile execution、结构化 `compile-executable-invalid` / `compile-executable-not-found` diagnostics、diagnostics JSON 输出、render artifact 存在/非空 smoke checks、SVG 结构检查、path-only SVG label classification、path-only glyph placement checks、definition-local glyph transform checks、path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry checks、positioned `tspan` label geometry checks、transform-aware SVG geometry checks、SVG label-vs-drawing overlap checks、PNG 空白截图检查、indexed-color 与 grayscale PNG packed sample decoding、indexed-color PNG palette decoding、16-bit direct PNG sample normalization、PNG 前景包围盒与前景密度报告、PNG 裁剪内容诊断、异常密集前景诊断,以及 compile log 或 smoke report 含错误时 CLI 非零退出。
- `src/tests/circuitikzExporter.test.ts`、`src/tests/circuitikzRepairBrief.test.ts`、`src/tests/circuitikzCompileDiagnostics.test.ts`、`src/tests/circuitikzRenderSmoke.test.ts`、`src/tests/circuitikzCompileRunner.test.ts` 与 `src/tests/circuitikzExportCli.test.ts` 验证确定性输出、拓扑拒绝、topology-preserving repair brief generation 与 candidate validation、package-script 暴露、UTF-8 BOM 处理、diagnostic parsing、shell-free compile execution、结构化 `compile-executable-invalid` / `compile-executable-not-found` diagnostics、diagnostics JSON 输出、render artifact 存在/非空 smoke checks、SVG 结构检查、path-only SVG label classification、path-only glyph placement checks、definition-local glyph transform checks、path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry checks、positioned `tspan` label geometry checks、transform-aware SVG geometry checks、SVG label-vs-drawing overlap checks、PNG 空白截图检查、indexed-color 与 grayscale PNG packed sample decoding、indexed-color PNG palette decoding、grayscale/RGB PNG tRNS transparent sample handling、16-bit direct PNG sample normalization、PNG 前景包围盒与前景密度报告、PNG 裁剪内容诊断、异常密集前景诊断,以及 compile log 或 smoke report 含错误时 CLI 非零退出。
- `src/tests/diagramPreviewModal.test.ts` 验证 artifact diagnostics 会显示在 diagram preview modal 中,并且 preview history 不会把 diagnostics 不同的条目错误去重。
Phase D 现在已经具备 log parsing、opt-in local renderer execution、artifact-level smoke checks、带 transform-aware geometry、path-only label classification、path-only glyph placement checks、path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` geometry coverage、positioned `tspan` label geometry、text/text 与 label/drawing overlap diagnostics 的 SVG structure smoke checks、第一层 PNG screenshot nonblank、indexed-color 与 grayscale packed-sample decoding、16-bit direct sample normalization、edge-clipping 与 dense-foreground checks、topology-preserving repair guard、带 candidate validation 的 topology-preserving repair brief handoff以及前端 diagnostics surface但实现仍有意停在 required renderer availability、path-only glyph text 的 OCR recognition、精确 pixel-level overlap detection、完整 SVG path coverage 和自动化 Phase E repair execution 之前。它不会捆绑 LaTeX、不会把 TikZJax 变成插件 runtime 依赖,也不会运行视觉修复闭环。
Phase D 现在已经具备 log parsing、opt-in local renderer execution、artifact-level smoke checks、带 transform-aware geometry、path-only label classification、path-only glyph placement checks、path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` geometry coverage、positioned `tspan` label geometry、text/text 与 label/drawing overlap diagnostics 的 SVG structure smoke checks、第一层 PNG screenshot nonblank、indexed-color 与 grayscale packed-sample decoding、grayscale/RGB tRNS transparent sample handling、16-bit direct sample normalization、edge-clipping 与 dense-foreground checks、topology-preserving repair guard、带 candidate validation 的 topology-preserving repair brief handoff以及前端 diagnostics surface但实现仍有意停在 required renderer availability、path-only glyph text 的 OCR recognition、精确 pixel-level overlap detection、完整 SVG path coverage 和自动化 Phase E repair execution 之前。它不会捆绑 LaTeX、不会把 TikZJax 变成插件 runtime 依赖,也不会运行视觉修复闭环。
SVG geometry smoke 也覆盖 leading-dot decimals 与 explicit plus signs 的 SVG number grammar。这是一个很小但关键的 renderer-compatibility gate因为 dvisvgm 可能输出紧凑小数bounds checks 中必须保留其小数语义。

View file

@ -72,6 +72,11 @@ interface PngPalette {
alpha: number[];
}
interface PngTransparency {
grayscale?: number;
rgb?: [number, number, number];
}
type PngPixelLayout = {
kind: 'direct';
colorType: number;
@ -1491,6 +1496,37 @@ function parsePngPalette(chunks: PngChunk[], colorType: number): PngPalette | un
};
}
function parsePngTransparency(chunks: PngChunk[], colorType: number): PngTransparency | undefined {
const transparency = chunks.find(chunk => chunk.type === 'tRNS');
if (!transparency || colorType === 3) {
return undefined;
}
if (colorType === 0) {
if (transparency.data.length !== 2) {
throw new Error('PNG grayscale transparency chunk is malformed.');
}
return {
grayscale: transparency.data.readUInt16BE(0)
};
}
if (colorType === 2) {
if (transparency.data.length !== 6) {
throw new Error('PNG RGB transparency chunk is malformed.');
}
return {
rgb: [
transparency.data.readUInt16BE(0),
transparency.data.readUInt16BE(2),
transparency.data.readUInt16BE(4)
]
};
}
return undefined;
}
function readPackedSample(decoded: Buffer, pixelIndex: number, width: number, bitDepth: number, scanlineByteLength: number): number {
if (bitDepth === 8) {
return decoded[pixelIndex];
@ -1515,24 +1551,29 @@ function normalizePngSample(sample: number, bitDepth: number): number {
return Math.round(sample * 255 / ((1 << bitDepth) - 1));
}
function readDirectSample(decoded: Buffer, offset: number, bitDepth: number): number {
function readRawDirectSample(decoded: Buffer, offset: number, bitDepth: number): number {
if (bitDepth === 16) {
return normalizePngSample(decoded.readUInt16BE(offset), bitDepth);
return decoded.readUInt16BE(offset);
}
return decoded[offset];
}
function readDirectSample(decoded: Buffer, offset: number, bitDepth: number): number {
return normalizePngSample(readRawDirectSample(decoded, offset, bitDepth), bitDepth);
}
function readPixel(
decoded: Buffer,
pixelIndex: number,
width: number,
layout: PngPixelLayout,
palette?: PngPalette
palette?: PngPalette,
transparency?: PngTransparency
): [number, number, number, number] {
if (layout.kind === 'grayscale') {
const sample = readPackedSample(decoded, pixelIndex, width, layout.bitDepth, layout.scanlineByteLength);
const gray = normalizePngSample(sample, layout.bitDepth);
return [gray, gray, gray, 255];
return [gray, gray, gray, transparency?.grayscale === sample ? 0 : 255];
}
if (layout.kind === 'indexed') {
@ -1547,16 +1588,25 @@ function readPixel(
const offset = pixelIndex * layout.bytesPerPixel;
switch (layout.colorType) {
case 0: {
const gray = readDirectSample(decoded, offset, layout.bitDepth);
return [gray, gray, gray, 255];
const sample = readRawDirectSample(decoded, offset, layout.bitDepth);
const gray = normalizePngSample(sample, layout.bitDepth);
return [gray, gray, gray, transparency?.grayscale === sample ? 0 : 255];
}
case 2:
case 2: {
const red = readRawDirectSample(decoded, offset, layout.bitDepth);
const green = readRawDirectSample(decoded, offset + layout.bytesPerSample, layout.bitDepth);
const blue = readRawDirectSample(decoded, offset + layout.bytesPerSample * 2, layout.bitDepth);
const transparent = transparency?.rgb
&& transparency.rgb[0] === red
&& transparency.rgb[1] === green
&& transparency.rgb[2] === blue;
return [
readDirectSample(decoded, offset, layout.bitDepth),
readDirectSample(decoded, offset + layout.bytesPerSample, layout.bitDepth),
readDirectSample(decoded, offset + layout.bytesPerSample * 2, layout.bitDepth),
255
normalizePngSample(red, layout.bitDepth),
normalizePngSample(green, layout.bitDepth),
normalizePngSample(blue, layout.bitDepth),
transparent ? 0 : 255
];
}
case 4: {
const gray = readDirectSample(decoded, offset, layout.bitDepth);
return [gray, gray, gray, readDirectSample(decoded, offset + layout.bytesPerSample, layout.bitDepth)];
@ -1591,6 +1641,7 @@ function extractPngSmoke(pngBytes: Buffer): CircuitikzPngSmokeReport {
throw new Error('PNG dimensions must be positive.');
}
const palette = parsePngPalette(chunks, header.colorType);
const transparency = parsePngTransparency(chunks, header.colorType);
const idatChunks = chunks.filter(chunk => chunk.type === 'IDAT');
if (idatChunks.length === 0) {
@ -1600,7 +1651,7 @@ function extractPngSmoke(pngBytes: Buffer): CircuitikzPngSmokeReport {
const inflated = zlib.inflateSync(Buffer.concat(idatChunks.map(chunk => chunk.data)));
const decoded = unfilterPngScanlines(inflated, header.height, pixelLayout.scanlineByteLength, pixelLayout.filterByteStride);
const decodedPixelCount = header.width * header.height;
const background = readPixel(decoded, 0, header.width, pixelLayout, palette);
const background = readPixel(decoded, 0, header.width, pixelLayout, palette, transparency);
let nonBackgroundPixelCount = 0;
let minX = header.width;
let minY = header.height;
@ -1608,7 +1659,7 @@ function extractPngSmoke(pngBytes: Buffer): CircuitikzPngSmokeReport {
let maxY = -1;
for (let pixelIndex = 0; pixelIndex < decodedPixelCount; pixelIndex += 1) {
const pixel = readPixel(decoded, pixelIndex, header.width, pixelLayout, palette);
const pixel = readPixel(decoded, pixelIndex, header.width, pixelLayout, palette, transparency);
if (pixel[3] > 0 && colorDistance(pixel, background) > 8) {
nonBackgroundPixelCount += 1;
const x = pixelIndex % header.width;

View file

@ -116,7 +116,8 @@ function createGrayscalePng(
width: number,
height: number,
bitDepth: number,
samples: number[]
samples: number[],
transparentSample?: number
): Buffer {
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
@ -134,12 +135,62 @@ function createGrayscalePng(
scanlines.push(...packPngSamples(samples.slice(row * width, row * width + width), bitDepth));
}
return Buffer.concat([
const chunks = [
signature,
pngChunk('IHDR', ihdr),
pngChunk('IHDR', ihdr)
];
if (transparentSample !== undefined) {
const transparency = Buffer.alloc(2);
transparency.writeUInt16BE(transparentSample, 0);
chunks.push(pngChunk('tRNS', transparency));
}
chunks.push(
pngChunk('IDAT', zlib.deflateSync(Buffer.from(scanlines))),
pngChunk('IEND', Buffer.alloc(0))
]);
);
return Buffer.concat(chunks);
}
function createRgbPng(
width: number,
height: number,
pixels: Array<[number, number, number]>,
transparentColor?: [number, number, number]
): Buffer {
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 2;
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
const scanlines: number[] = [];
for (let row = 0; row < height; row += 1) {
scanlines.push(0);
for (let column = 0; column < width; column += 1) {
scanlines.push(...pixels[row * width + column]);
}
}
const chunks = [
signature,
pngChunk('IHDR', ihdr)
];
if (transparentColor) {
const transparency = Buffer.alloc(6);
transparentColor.forEach((sample, index) => transparency.writeUInt16BE(sample, index * 2));
chunks.push(pngChunk('tRNS', transparency));
}
chunks.push(
pngChunk('IDAT', zlib.deflateSync(Buffer.from(scanlines))),
pngChunk('IEND', Buffer.alloc(0))
);
return Buffer.concat(chunks);
}
function createIndexedPng(
@ -1265,6 +1316,95 @@ describe('circuitikz render smoke inspection', () => {
}
});
test('ignores grayscale PNG tRNS transparent samples when checking foreground', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-png-gray-trns-smoke-'));
const pngPath = path.join(tempRoot, 'gray-trns-render.png');
fs.writeFileSync(
pngPath,
createGrayscalePng(
3,
3,
8,
[
255, 255, 255,
255, 0, 255,
255, 255, 255
],
0
)
);
try {
const report = inspectCircuitikzRenderArtifact({
expectedArtifactPath: pngPath
});
expect(report.artifactKind).toBe('png');
expect(report.png).toEqual(expect.objectContaining({
bitDepth: 8,
colorType: 0,
decodedPixelCount: 9,
nonBackgroundPixelCount: 0,
foregroundBounds: undefined
}));
expect(report.diagnostics).toEqual([
expect.objectContaining({
kind: 'render-png-blank',
message: expect.stringContaining('visually blank')
})
]);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test('ignores RGB PNG tRNS transparent colors when checking foreground', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-png-rgb-trns-smoke-'));
const pngPath = path.join(tempRoot, 'rgb-trns-render.png');
fs.writeFileSync(
pngPath,
createRgbPng(
3,
3,
[
[255, 255, 255],
[255, 255, 255],
[255, 255, 255],
[255, 255, 255],
[255, 0, 0],
[255, 255, 255],
[255, 255, 255],
[255, 255, 255],
[255, 255, 255]
],
[255, 0, 0]
)
);
try {
const report = inspectCircuitikzRenderArtifact({
expectedArtifactPath: pngPath
});
expect(report.artifactKind).toBe('png');
expect(report.png).toEqual(expect.objectContaining({
bitDepth: 8,
colorType: 2,
decodedPixelCount: 9,
nonBackgroundPixelCount: 0,
foregroundBounds: undefined
}));
expect(report.diagnostics).toEqual([
expect.objectContaining({
kind: 'render-png-blank',
message: expect.stringContaining('visually blank')
})
]);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
test.each([
{
label: 'grayscale',

View file

@ -256,7 +256,7 @@ SVG smoke verifies the `<svg>` root, positive dimensions or `viewBox`, at least
The same geometry pass follows SVG number grammar for leading-dot decimals and explicit plus signs, so compact dvisvgm coordinates such as `.5`, `-.5`, or `+.5` stay fractional during bounds checks instead of becoming false out-of-bounds geometry or being skipped.
If the renderer emits `.png`, the same expected-artifact path becomes a first screenshot smoke: Notemd decodes non-interlaced 1/2/4/8-bit indexed-color PNG files, 1/2/4/8/16-bit grayscale PNG files, and 8/16-bit grayscale-alpha/RGB/RGBA PNG files. Indexed-color and sub-byte grayscale images support packed samples; indexed-color images also support PLTE and optional tRNS data. 16-bit direct samples are normalized into the same 8-bit RGBA comparison space used by the smoke checks. The smoke check verifies positive dimensions, records foreground bounds as `foregroundBounds`, records foreground density inside that box as `foregroundDensity`, fails with `render-png-blank` when every visible pixel matches the top-left background color, fails with `render-png-content-clipped` when foreground content touches the image boundary, and fails with `render-png-foreground-dense` when foreground pixels are unusually dense inside a non-trivial bounding box. This catches blank screenshots, obvious canvas clipping, and first pixel-level crowding failures without adding a platform-specific shell dependency. It is not yet OCR-level label recognition, precise text-overlap detection, or topology-preserving image repair.
If the renderer emits `.png`, the same expected-artifact path becomes a first screenshot smoke: Notemd decodes non-interlaced 1/2/4/8-bit indexed-color PNG files, 1/2/4/8/16-bit grayscale PNG files, and 8/16-bit grayscale-alpha/RGB/RGBA PNG files. Indexed-color and sub-byte grayscale images support packed samples; indexed-color images also support PLTE and optional tRNS data; grayscale/RGB images support tRNS transparent samples. 16-bit direct samples are normalized into the same 8-bit RGBA comparison space used by the smoke checks. The smoke check verifies positive dimensions, records foreground bounds as `foregroundBounds`, records foreground density inside that box as `foregroundDensity`, fails with `render-png-blank` when every visible pixel matches the top-left background color, fails with `render-png-content-clipped` when foreground content touches the image boundary, and fails with `render-png-foreground-dense` when foreground pixels are unusually dense inside a non-trivial bounding box. This catches blank screenshots, obvious canvas clipping, and first pixel-level crowding failures without adding a platform-specific shell dependency. It is not yet OCR-level label recognition, precise text-overlap detection, or topology-preserving image repair.
When diagnostics show a failed compile or render-smoke run, the CLI can also write a topology-preserving repair brief:
@ -321,7 +321,7 @@ For a CMOS inverter, the prompt should request an explicit topology plus layout
| General diagrams | Spec-first pipeline implemented for Mermaid, JSON Canvas, Vega-Lite, HTML | Keep expanding semantic verification coverage |
| Editable figures | `editable-html-svg`, Draw.io XML, and Drawnix JSON artifact boundaries implemented | Add richer primitives only after tests prove editability |
| CLI support | `npm run diagram:export-artifact` exports editable HTML/SVG, Draw.io, and Drawnix from one `DiagramSpec` | Add target-specific smoke fixtures when new targets ship |
| circuitikz | `CircuitSpec -> circuitikz` prototype exports common-source and CMOS inverter golden templates, rejects repair topology drift through `--topology-reference`, emits topology-preserving repair briefs through `--repair-brief-output` and schema `notemd.circuitikz.repair-brief.v1`, validates repair candidates through `--repair-brief`, parses compile logs, can run explicit local renderers plus `--expected-artifact`, SVG `--expected-svg-text`, `render-svg-text-path-only` / `pathOnlyGlyphUseCount` classification for path-only labels, path-only glyph placement checks for `<use href="#...">`, path-only glyph overlap diagnostics through `render-svg-path-glyph-overlap`, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry checks, positioned `tspan` label geometry, transform-aware geometry for SVG bounded-canvas/text-overlap and label-vs-drawing smoke through `render-svg-label-overlap`, and PNG nonblank / clipped / dense-foreground screenshot smoke checks through `foregroundBounds`, `foregroundDensity`, `render-png-content-clipped`, and `render-png-foreground-dense` without shell parsing, includes aggregate maintainer smoke fixtures through `npm run diagram:smoke-circuitikz`, and has generic preview diagnostics, diagnostic summary counts, diagnostics-aware history entries, and source-only fallback through `RenderArtifact.diagnostics` and the preview modal | Add OCR-level label recognition, precise pixel-level overlap checks, broader SVG path coverage where needed, and automated topology-preserving repair execution |
| circuitikz | `CircuitSpec -> circuitikz` prototype exports common-source and CMOS inverter golden templates, rejects repair topology drift through `--topology-reference`, emits topology-preserving repair briefs through `--repair-brief-output` and schema `notemd.circuitikz.repair-brief.v1`, validates repair candidates through `--repair-brief`, parses compile logs, can run explicit local renderers plus `--expected-artifact`, SVG `--expected-svg-text`, `render-svg-text-path-only` / `pathOnlyGlyphUseCount` classification for path-only labels, path-only glyph placement checks for `<use href="#...">`, path-only glyph overlap diagnostics through `render-svg-path-glyph-overlap`, exact arc bounds for A/a arc extrema, exact Bezier curve bounds for C/S/Q/T curve extrema, stroke-width-aware SVG bounds and label overlap checks, `polyline` / `polygon` drawing geometry checks, positioned `tspan` label geometry, transform-aware geometry for SVG bounded-canvas/text-overlap and label-vs-drawing smoke through `render-svg-label-overlap`, and PNG nonblank / clipped / dense-foreground screenshot smoke checks, including indexed-color palette alpha and grayscale/RGB tRNS transparent samples, through `foregroundBounds`, `foregroundDensity`, `render-png-content-clipped`, and `render-png-foreground-dense` without shell parsing, includes aggregate maintainer smoke fixtures through `npm run diagram:smoke-circuitikz`, and has generic preview diagnostics, diagnostic summary counts, diagnostics-aware history entries, and source-only fallback through `RenderArtifact.diagnostics` and the preview modal | Add OCR-level label recognition, precise pixel-level overlap checks, broader SVG path coverage where needed, and automated topology-preserving repair execution |
| TikZJax integration | Candidate render host for Obsidian-side display | Keep it optional; do not make TikZJax a hard plugin runtime dependency |
## Configuration

View file

@ -256,7 +256,7 @@ SVG smoke 会检查 `<svg>` root、正的尺寸或 `viewBox`、至少一个可
同一层 geometry pass 也按 SVG number grammar 处理 leading-dot decimals 与 explicit plus signs因此 dvisvgm 输出的 `.5`、`-.5` 或 `+.5` 这类紧凑坐标会在 bounds checks 中保留小数语义,而不会变成 false out-of-bounds geometry 或被静默跳过。
如果 renderer 输出 `.png`,同一个 expected-artifact 路径会变成第一层 screenshot smokeNotemd 会解码非交错的 1/2/4/8-bit indexed-color PNG 文件、1/2/4/8/16-bit grayscale PNG 文件,以及 8/16-bit grayscale-alpha/RGB/RGBA PNG 文件。indexed-color 与 sub-byte grayscale 图像支持 packed samplesindexed-color 图像还支持 PLTE 与可选 tRNS 数据。16-bit direct samples 会被归一化到 smoke check 使用的同一个 8-bit RGBA 比较空间。smoke check 会检查正尺寸,把前景包围盒记录为 `foregroundBounds`,把包围盒内前景密度记录为 `foregroundDensity`,在所有可见像素都与左上角背景色一致时以 `render-png-blank` 失败,在前景内容贴到图像边界时以 `render-png-content-clipped` 失败,并在非平凡包围盒内前景像素异常密集时以 `render-png-foreground-dense` 失败。这能在不引入平台特定 shell 依赖的情况下捕获空白截图、明显画布裁剪和第一层 pixel-level crowding failure但它还不是 OCR-level label recognition、精确 text-overlap detection 或 topology-preserving image repair。
如果 renderer 输出 `.png`,同一个 expected-artifact 路径会变成第一层 screenshot smokeNotemd 会解码非交错的 1/2/4/8-bit indexed-color PNG 文件、1/2/4/8/16-bit grayscale PNG 文件,以及 8/16-bit grayscale-alpha/RGB/RGBA PNG 文件。indexed-color 与 sub-byte grayscale 图像支持 packed samplesindexed-color 图像还支持 PLTE 与可选 tRNS 数据grayscale/RGB 图像支持 tRNS transparent samples。16-bit direct samples 会被归一化到 smoke check 使用的同一个 8-bit RGBA 比较空间。smoke check 会检查正尺寸,把前景包围盒记录为 `foregroundBounds`,把包围盒内前景密度记录为 `foregroundDensity`,在所有可见像素都与左上角背景色一致时以 `render-png-blank` 失败,在前景内容贴到图像边界时以 `render-png-content-clipped` 失败,并在非平凡包围盒内前景像素异常密集时以 `render-png-foreground-dense` 失败。这能在不引入平台特定 shell 依赖的情况下捕获空白截图、明显画布裁剪和第一层 pixel-level crowding failure但它还不是 OCR-level label recognition、精确 text-overlap detection 或 topology-preserving image repair。
当 diagnostics 显示 compile 或 render-smoke 失败时CLI 也可以写出 topology-preserving repair brief
@ -321,7 +321,7 @@ runner 使用 `docs/maintainer/fixtures/circuitikz/common-source-nmos-v1.json`
| General diagrams | Mermaid、JSON Canvas、Vega-Lite、HTML 的 spec-first pipeline 已实现 | 继续扩展 semantic verification coverage |
| Editable figures | `editable-html-svg`、Draw.io XML、Drawnix JSON artifact boundaries 已实现 | 只有在测试证明 editability 后再加入更丰富 primitives |
| CLI support | `npm run diagram:export-artifact` 可从同一个 `DiagramSpec` 导出 editable HTML/SVG、Draw.io、Drawnix | 新 target 发布时增加 target-specific smoke fixtures |
| circuitikz | `CircuitSpec -> circuitikz` 原型已经可通过 `npm run diagram:export-circuitikz` 导出 common-source 与 CMOS inverter golden templates、通过 `--topology-reference` 拒绝 repair topology drift、通过 `--repair-brief-output` 和 schema `notemd.circuitikz.repair-brief.v1` 写出 topology-preserving repair briefs、通过 `--repair-brief` 校验 repair candidates、解析 compile logs并在不经过 shell parsing 的情况下运行显式本地 renderer、`--expected-artifact`、SVG `--expected-svg-text`、用于 path-only labels 的 `render-svg-text-path-only` / `pathOnlyGlyphUseCount` 分类、针对 `<use href="#...">` 的 path-only glyph placement checks、通过 `render-svg-path-glyph-overlap` 执行的 path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry checks、positioned `tspan` label geometry、带 transform-aware geometry 的 SVG bounded-canvas / text-overlap 以及通过 `render-svg-label-overlap` 报告的 label-vs-drawing smoke以及通过 `foregroundBounds`、`foregroundDensity`、`render-png-content-clipped` 和 `render-png-foreground-dense` 执行的 PNG nonblank / clipped / dense-foreground screenshot smoke checks现在还通过 `npm run diagram:smoke-circuitikz` 提供聚合 maintainer smoke fixtures并通过 `RenderArtifact.diagnostics` 和 preview modal 具备通用 preview diagnostics、diagnostic summary counts、diagnostics-aware history entries 与 source-only fallback | 增加 OCR-level label recognition、精确 pixel-level overlap checks、按需补齐更完整 SVG path coverage并推进自动化 topology-preserving repair execution |
| circuitikz | `CircuitSpec -> circuitikz` 原型已经可通过 `npm run diagram:export-circuitikz` 导出 common-source 与 CMOS inverter golden templates、通过 `--topology-reference` 拒绝 repair topology drift、通过 `--repair-brief-output` 和 schema `notemd.circuitikz.repair-brief.v1` 写出 topology-preserving repair briefs、通过 `--repair-brief` 校验 repair candidates、解析 compile logs并在不经过 shell parsing 的情况下运行显式本地 renderer、`--expected-artifact`、SVG `--expected-svg-text`、用于 path-only labels 的 `render-svg-text-path-only` / `pathOnlyGlyphUseCount` 分类、针对 `<use href="#...">` 的 path-only glyph placement checks、通过 `render-svg-path-glyph-overlap` 执行的 path-only glyph overlap diagnostics、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、stroke-width-aware SVG bounds 与 label overlap checks、`polyline` / `polygon` drawing geometry checks、positioned `tspan` label geometry、带 transform-aware geometry 的 SVG bounded-canvas / text-overlap 以及通过 `render-svg-label-overlap` 报告的 label-vs-drawing smoke以及通过 `foregroundBounds`、`foregroundDensity`、`render-png-content-clipped` 和 `render-png-foreground-dense` 执行的 PNG nonblank / clipped / dense-foreground screenshot smoke checks,包含 indexed-color palette alpha 与 grayscale/RGB tRNS transparent samples;现在还通过 `npm run diagram:smoke-circuitikz` 提供聚合 maintainer smoke fixtures并通过 `RenderArtifact.diagnostics` 和 preview modal 具备通用 preview diagnostics、diagnostic summary counts、diagnostics-aware history entries 与 source-only fallback | 增加 OCR-level label recognition、精确 pixel-level overlap checks、按需补齐更完整 SVG path coverage并推进自动化 topology-preserving repair execution |
| TikZJax integration | Obsidian-side display 的候选 render host | 保持可选,不把 TikZJax 变成硬 runtime 依赖 |
## Configuration