From 434d4e00dbc20dc4acd869b2a8592d4309462f5f Mon Sep 17 00:00:00 2001 From: aliyun1121003339 Date: Thu, 9 Jul 2026 19:14:23 +0800 Subject: [PATCH] feat(diagrams): expose constrained circuitikz exports --- README.md | 23 +- README_zh.md | 18 + change.md | 22 ++ ...05-02-progress-audit-and-next-direction.md | 5 + ...progress-audit-and-next-direction.zh-CN.md | 5 + ...-integration-and-figure-generation-plan.md | 37 +- ...ration-and-figure-generation-plan.zh-CN.md | 37 +- .../maintainer/circuitikz-export-prototype.md | 18 +- .../circuitikz-export-prototype.zh-CN.md | 20 +- .../maintainer/diagram-artifact-export-cli.md | 41 +- .../diagram-artifact-export-cli.zh-CN.md | 41 +- scripts/export-diagram-artifact.js | 73 +++- src/diagram/diagramGenerationService.ts | 49 ++- src/diagram/diagramSpecResponseParser.ts | 1 + src/diagram/intent.ts | 26 ++ src/diagram/planner.ts | 6 + src/diagram/prompts/diagramSpecPrompt.ts | 14 +- src/diagram/spec.ts | 26 +- src/diagram/types.ts | 6 +- src/i18n/locales/en.ts | 2 + .../locales/experimentalDiagramPipeline.ts | 20 +- src/i18n/locales/zh_cn.ts | 2 + src/i18n/locales/zh_tw.ts | 2 + src/rendering/renderers/circuitikzRenderer.ts | 363 ++++++++++++++++++ src/tests/circuitikzRenderer.test.ts | 76 ++++ src/tests/diagramArtifactExportCli.test.ts | 116 +++++- src/tests/diagramDocsContract.test.ts | 5 +- src/tests/diagramGenerationService.test.ts | 69 ++++ src/tests/diagramIntent.test.ts | 12 + src/tests/diagramOperationInput.test.ts | 4 +- src/tests/diagramPreviewModal.test.ts | 29 ++ src/tests/diagramSpecPrompt.test.ts | 14 + src/tests/diagramSpecResponseParser.test.ts | 52 +++ src/tests/diagramSpecValidation.test.ts | 64 +++ src/ui/NotemdSettingTab.ts | 2 + src/ui/NotemdSidebarView.ts | 2 + website/docs/features/diagrams.mdx | 43 ++- .../current/features/diagrams.mdx | 43 ++- 38 files changed, 1263 insertions(+), 125 deletions(-) create mode 100644 src/rendering/renderers/circuitikzRenderer.ts create mode 100644 src/tests/circuitikzRenderer.test.ts diff --git a/README.md b/README.md index bed748ba..6d5eaf8a 100644 --- a/README.md +++ b/README.md @@ -172,24 +172,29 @@ That's it! Explore the settings to unlock more features like web research, trans SUMM - **Experimental Diagram Pipeline**: - - A spec-first diagram path can route note content into Mermaid, Obsidian JSON Canvas, or Vega-Lite instead of forcing every case through Mermaid text generation. + - A spec-first diagram path can route note content into Mermaid, Obsidian JSON Canvas, Vega-Lite, HTML, editable HTML/SVG, Draw.io, Drawnix, or constrained circuitikz instead of forcing every case through Mermaid text generation. - Current Mermaid adapter coverage in the spec-first path includes `mindmap`, `flowchart`, `sequenceDiagram`, `classDiagram`, `erDiagram`, and `stateDiagram-v2`. - Current Vega-Lite adapter coverage in the spec-first path includes cartesian `bar`, `line`, `area`, and `point` charts, plus controlled `scatter`, `pie`, and `table` layout hints that map onto safe built-in Vega-Lite templates. - For `dataChart` plans, the planner now seeds preferred Vega-Lite chart templates (`line`, `pie`, `scatter`, `table`, or fallback `bar`) so omitted `layoutHints.chartType` values do not silently collapse to the wrong chart shape. - Generated Mermaid artifacts are now validated with `mermaid.parse` before the renderer returns them, so malformed diagrams fail early instead of quietly leaking into preview/export steps. - Generated `.canvas` and `.json` artifacts are saved through the same output-path policy as Mermaid summaries, and preview surfaces now cover Mermaid, JSON Canvas, and Vega-Lite results. - HTML fallback artifacts are now generated as dedicated `.html` summaries when a richer renderer is not available, and the preview modal can open them through the iframe fallback path instead of only showing escaped source text. - - Preview modals can now export rendered Mermaid/Canvas/Vega-Lite output as `.svg` and `.png` files beside the source note or beside the generated artifact, giving you stable image handoff paths without flattening everything into screenshots first. - - Preview-only runs can also persist the raw generated artifact beside the current note using target-aware extensions and suffixes (`_summ.md`, `_diagram.canvas`, `_diagram.json`), so validation and handoff do not require rerunning the LLM step. + - Preview modals can now export rendered Mermaid/Canvas/Vega-Lite and SVG-companion output as `.svg`, `.png`, and `.pdf` files beside the source note or beside the generated artifact, with PNG/PDF export defaulting to 300 PPI and clamped at 600 PPI. + - Preview-only runs can also persist the raw generated artifact beside the current note using target-aware extensions and suffixes (`_summ.md`, `_diagram.canvas`, `_diagram.json`, `_diagram.drawio`, `_diagram.drawnix`, `_diagram.tex`), so validation and handoff do not require rerunning the LLM step. + - Circuit diagrams are exposed as `intent: "circuit"` plus `circuitikz` render target. They require a validated `CircuitSpec`, write deterministic `.tex`, and attach an SVG preview companion for Obsidian viewing and SVG/PNG/PDF export; the companion is not a LaTeX/TikZJax compile result. - The existing Mermaid auto-fix path remains intact for Mermaid outputs only; non-Mermaid artifacts bypass the fixer instead of being pushed through incompatible post-processing. - Preview UI strings continue to follow the plugin UI locale (`uiLocale: auto` follows Obsidian), and preview/export theme defaults to the active Obsidian light/dark theme so Mermaid, JSON Canvas, Vega-Lite, and HTML fallback previews do not stay locked to the wrong palette after a theme switch. -| Target | Generated artifact | Inline preview | Export SVG | Export PNG | Save raw source | Notes | -| --- | --- | --- | --- | --- | --- | --- | -| Mermaid | `_summ.md` | Yes | Yes | Yes | Yes | Mermaid auto-fix remains available for Mermaid-only flows. | -| JSON Canvas | `_diagram.canvas` | Yes | Yes | Yes | Yes | Preview/export uses a theme-aware Canvas palette. | -| Vega-Lite | `_diagram.json` | Yes | Yes | Yes | Yes | Preview/export uses a theme-aware Vega-Lite config patch. | -| HTML | `_diagram.html` | Yes (iframe fallback) | No | No | Yes | Current pipeline does not promise raster/vector export for HTML artifacts yet. | +| Target | Generated artifact | Inline preview | Export SVG | Export PNG | Export PDF | Save raw source | Notes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Mermaid | `_summ.md` | Yes | Yes | Yes | Yes | Yes | Mermaid auto-fix remains available for Mermaid-only flows. | +| JSON Canvas | `_diagram.canvas` | Yes | Yes | Yes | Yes | Yes | Preview/export uses a theme-aware Canvas palette. | +| Vega-Lite | `_diagram.json` | Yes | Yes | Yes | Yes | Yes | Preview/export uses a theme-aware Vega-Lite config patch. | +| Editable HTML/SVG | `_diagram.html` | Yes | Yes | Yes | Yes | Yes | SVG is generated from the semantic figure model. | +| Draw.io | `_diagram.drawio` + companion SVG/MD | SVG companion | Yes | Yes | Yes | Yes | No diagrams.net runtime is bundled. | +| Drawnix | `_diagram.drawnix` + companion SVG/MD | SVG companion | Yes | Yes | Yes | Yes | No Drawnix or Plait runtime is bundled. | +| Circuitikz | `_diagram.tex` + companion SVG/MD | SVG companion | Yes | Yes | Yes | Yes | Requires `intent: "circuit"` and validated `CircuitSpec`; no runtime LaTeX compile. | +| HTML | `_diagram.html` | Yes (iframe fallback) | No | No | No | Yes | Current pipeline does not promise raster/vector export for generic HTML fallback artifacts yet. | - **Simple Formula Format Correction**: - Quickly fixes single-line math formulas delimited by single `$` to standard double `$$` blocks. diff --git a/README_zh.md b/README_zh.md index 2c9c895c..1da9aebf 100644 --- a/README_zh.md +++ b/README_zh.md @@ -170,6 +170,24 @@ Notemd 是一个**开源 Obsidian 插件**,集成 30+ 种大型语言模型 (L SUMM +- **实验性图表流水线**: + - 规范优先的图表路径可以把笔记内容路由到 Mermaid、Obsidian JSON Canvas、Vega-Lite、HTML、可编辑 HTML/SVG、Draw.io、Drawnix 或受约束 circuitikz,而不是把所有场景都压回 Mermaid 文本生成。 + - `mindmap`、`flowchart`、`sequenceDiagram`、`classDiagram`、`erDiagram` 与 `stateDiagram-v2` 仍由 Mermaid adapter 覆盖;`dataChart` 使用受控 Vega-Lite 模板;`circuit` 使用 `circuitikz` render target。 + - 预览弹窗可把可渲染输出或 SVG companion 导出为 `.svg`、`.png` 与 `.pdf`;PNG/PDF 默认 300 PPI,超过 600 PPI 的配置会被夹到 600。 + - Draw.io、Drawnix 与 Circuitikz 都保持 artifact 边界:插件不捆绑 diagrams.net、Drawnix、Plait、LaTeX 或 TikZJax runtime,而是写出原生源文件和 Obsidian 可查看的 SVG companion。 + - 电路图必须使用 `intent: "circuit"` 与已验证 `CircuitSpec`。`CircuitikzRenderer` 会写出确定性 `.tex`,并附加用于 Obsidian 预览和 SVG/PNG/PDF 导出的 SVG companion;该 companion 不是 LaTeX/TikZJax 编译结果。 + +| 目标 | 生成 artifact | 内联预览 | 导出 SVG | 导出 PNG | 导出 PDF | 保存源文件 | 备注 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Mermaid | `_summ.md` | 是 | 是 | 是 | 是 | 是 | Mermaid-only 流程仍可使用 Mermaid auto-fix。 | +| JSON Canvas | `_diagram.canvas` | 是 | 是 | 是 | 是 | 是 | 预览/导出使用主题感知 Canvas 配色。 | +| Vega-Lite | `_diagram.json` | 是 | 是 | 是 | 是 | 是 | 预览/导出使用主题感知 Vega-Lite 配置补丁。 | +| 可编辑 HTML/SVG | `_diagram.html` | 是 | 是 | 是 | 是 | 是 | SVG 从语义 figure model 生成。 | +| Draw.io | `_diagram.drawio` + SVG/MD companion | SVG companion | 是 | 是 | 是 | 是 | 不捆绑 diagrams.net runtime。 | +| Drawnix | `_diagram.drawnix` + SVG/MD companion | SVG companion | 是 | 是 | 是 | 是 | 不捆绑 Drawnix 或 Plait runtime。 | +| Circuitikz | `_diagram.tex` + SVG/MD companion | SVG companion | 是 | 是 | 是 | 是 | 需要 `intent: "circuit"` 与已验证 `CircuitSpec`;不在 runtime 编译 LaTeX。 | +| HTML | `_diagram.html` | 是(iframe fallback) | 否 | 否 | 否 | 是 | 通用 HTML fallback 暂不承诺栅格/矢量导出。 | + - **简单公式格式修正**: - 快速将单行 `$` 分隔的数学公式转换为标准的 `$$` 块。 - **单文件**: 通过侧边栏按钮或命令面板处理当前文件。 diff --git a/change.md b/change.md index 90db393d..5212d51f 100644 --- a/change.md +++ b/change.md @@ -4,6 +4,28 @@ This document summarizes the major functional and architectural changes implemen --- +## Unreleased + +### English + +- Added first-class experimental diagram UI support for `circuit` intent and the `circuitikz` render target. +- Added `CircuitikzRenderer` for constrained `DiagramSpec.circuitSpec` payloads. It writes deterministic circuitikz `.tex` source and attaches a white-background SVG preview companion for Obsidian viewing. +- Extended the offline diagram artifact CLI so `--target circuitikz` can write TeX plus SVG/PNG/PDF preview companions. Direct `svg`, `png`, and `pdf` targets can also render validated circuit specs through the same companion SVG path. +- Hardened the artifact CLI for npm 11 argument rewriting on Windows by accepting the ordered positional fallback shape, and made the circuitikz SVG canvas background inline white so standalone viewers do not show transparent SVGs as black. +- Kept the circuit path topology-first: the prompt asks for structured `CircuitSpec`, validation rejects free-form/non-circuit payloads, and the preview companion is documented as review evidence rather than a LaTeX/TikZJax compile result. +- Updated the GitHub Pages diagram documentation and maintainer runbooks. The MDX synchronization policy is explicit: do not blindly commit every generated locale file for small feature increments; commit localized MDX only when published route/frontmatter/heading or visible behavior parity requires it. + +### 中文 + +- 为实验性图表 UI 增加一等 `circuit` intent 与 `circuitikz` render target 支持。 +- 新增 `CircuitikzRenderer`,用于受约束的 `DiagramSpec.circuitSpec` payload。它会写出确定性 circuitikz `.tex` 源文件,并附加白底 SVG preview companion 供 Obsidian 查看。 +- 扩展离线 diagram artifact CLI,使 `--target circuitikz` 可以写出 TeX 以及 SVG/PNG/PDF 预览 companion。直接 `svg`、`png` 与 `pdf` target 也可以通过同一条 companion SVG 路径渲染已验证电路 spec。 +- 加固 artifact CLI,使其兼容 Windows 上 npm 11 参数重写后的有序位置参数形态;同时将 circuitikz SVG 画布背景改为 inline 白色,避免 standalone viewer 把透明 SVG 显示成黑底。 +- 电路路径继续保持 topology-first:prompt 要求结构化 `CircuitSpec`,校验拒绝自由形式/非电路 payload,并且文档明确 preview companion 是审查证据,不是 LaTeX/TikZJax 编译结果。 +- 更新 GitHub Pages 图表文档与维护者 runbook。MDX 同步策略已明确:小功能增量不要盲目提交所有 generated locale 文件;只有公开 route/frontmatter/heading 或可见行为需要对齐时,才提交对应 localized MDX。 + +--- + ## 1.9.3 ### Highlights diff --git a/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.md b/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.md index 27ea55b3..dfb1d811 100644 --- a/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.md +++ b/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.md @@ -34,6 +34,8 @@ This audit is not a redesign pass. It is a repo-truth alignment pass. The bigges 5. **Runtime support for 8 intents is not the same thing as UI exposure.** `SUPPORTED_DIAGRAM_INTENTS` still includes `mindmap / flowchart / sequence / classDiagram / erDiagram / stateDiagram / canvasMap / dataChart`, but the settings/sidebar selector currently exposes only `auto + flowchart + sequence + classDiagram + erDiagram + stateDiagram + dataChart`. `mindmap` and `canvasMap` remain runtime capabilities, not current first-class UI choices. + 2026-07-09 update: current main now supports 9 intents by adding `circuit`, and the UI exposes both the circuit intent and the `circuitikz` render target. The historical 2026-05 audit remains useful as evidence that runtime capability and UI exposure must be checked separately. + 6. **Command orchestration is partially unified, not fully unified.** Legacy Mermaid save and experimental save still route through shared diagram orchestration, but `previewExperimentalDiagramCommand` now reads a local `vega-lite` fenced block and previews it directly. That matches the current saved artifact shape for `dataChart`, but it is not the final command-surface end state. @@ -108,9 +110,11 @@ This means the roadmap should no longer be interpreted as "build the platform". **Diagram platform** - The runtime still supports 8 intents. +- 2026-07-09 update: the runtime now supports 9 intents after adding constrained `circuit` / `circuitikz` support. - The main extension seam is now `DiagramSpec -> adapter -> renderer`, not direct Mermaid text generation. - `dataChart` is no longer just "save JSON"; it now saves a Markdown fenced `vega-lite` artifact and previews locally. - `canvasMap` is supported but intentionally not exposed as a current preferred selector option, which is a healthy separation between runtime capability and product surface. +- Circuitikz now has UI-visible intent/render-target options, a constrained `CircuitSpec` renderer, inline-white SVG preview companions, SVG/PNG/PDF artifact export, and npm 11 positional fallback handling at the offline CLI boundary. **Infrastructure** - Progress persistence, architecture docs, release workflow, and README alignment tests are all on mainline. @@ -171,6 +175,7 @@ For remote truth: These are useful directional signals, but they should no longer be documented as hard automated repo gates: - A local DeepSeek verification run covered all 8 intents on 2026-05-02 +- A later local/CI-verifiable path covers the constrained ninth circuit intent through renderer and CLI tests rather than live LLM calls. - The harness was removed from mainline because it depended on a local vault path, live secrets, and nondeterministic network calls ## Drawnix Reference Conclusion diff --git a/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.zh-CN.md b/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.zh-CN.md index 4953f1fa..e2e35575 100644 --- a/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.zh-CN.md +++ b/docs/brainstorms/2026-05-02-progress-audit-and-next-direction.zh-CN.md @@ -34,6 +34,8 @@ topic: progress-audit-next-direction 5. **运行时支持 8 种意图,不等于 UI 首选项全部暴露。** `SUPPORTED_DIAGRAM_INTENTS` 仍覆盖 `mindmap / flowchart / sequence / classDiagram / erDiagram / stateDiagram / canvasMap / dataChart`,但设置页与侧边栏当前只暴露 `auto + flowchart + sequence + classDiagram + erDiagram + stateDiagram + dataChart`。`mindmap` 与 `canvasMap` 仍属运行时能力,不是当前 UI 首选图表选择器的一部分。 + 2026-07-09 更新:当前 main 已通过新增 `circuit` 支持 9 种意图,UI 也已经暴露 circuit intent 与 `circuitikz` render target。2026-05 的历史审计仍然有价值,因为它提醒我们必须分别检查运行时能力与 UI 暴露面。 + 6. **命令编排“部分统一”,不是“完全统一”。** `generateExperimentalDiagramCommand` 与 legacy Mermaid 保存命令仍经过共享 `generateDiagramCommand` 编排,但 `previewExperimentalDiagramCommand` 现在直接读取当前 Markdown 中的 `vega-lite` 围栏并本地预览,不再走共享 LLM 生成路径。这是为了匹配当前 `dataChart` 产物以 Markdown fenced block 保存的现实,而不是最终命令收口形态。 @@ -108,9 +110,11 @@ topic: progress-audit-next-direction **图表平台:** - 运行时仍支持 8 种图表意图。 +- 2026-07-09 更新:新增受约束 `circuit` / `circuitikz` 后,运行时现在支持 9 种图表意图。 - `DiagramSpec -> adapter -> renderer` 的主链已经成立,核心扩展点不再绑死在 Mermaid 文本。 - `dataChart` 已经不再只是“保存 JSON”,而是保存为 Markdown fenced `vega-lite` 并支持本地预览。 - `canvasMap` 是已支持但未在当前 UI 中首选暴露的目标,说明“运行时能力”和“产品默认表面”已开始分层。 +- Circuitikz 现在已有 UI 可见的 intent/render-target 选项、受约束 `CircuitSpec` renderer、inline 白底 SVG preview companion、SVG/PNG/PDF artifact export,并且离线 CLI 边界已兼容 npm 11 的位置参数 fallback。 **基础设施:** - 进度状态持久化、架构文档、release workflow、README 对齐测试都在主线。 @@ -171,6 +175,7 @@ topic: progress-audit-next-direction 以下结论可以作为方向判断的参考,但不能再被文档表述成“仓库当前的硬性自动门槛”: - 2026-05-02 曾对全部 8 种图表意图做过一次实时 DeepSeek API 验证 +- 后续新增的第九种 circuit intent 走受约束 renderer 与 CLI tests,而不是 live LLM gate。 - 相关 harness 已从主线删除,原因是它依赖本地 vault 路径、真实密钥和非确定性网络调用 ## Drawnix 外部参考结论 diff --git a/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.md b/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.md index 66ee1901..2844de56 100644 --- a/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.md +++ b/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.md @@ -291,15 +291,44 @@ Phase F implementation status on 2026-07-05: `src/diagram/adapters/circuitikz/ci 2026-07-06 SVG text-anchor geometry increment: positioned SVG `text` and `tspan` boxes now honor `text-anchor` values `start`, `middle`, and `end` from attributes or inline style. This makes centered and right-aligned labels participate in the same bounded-canvas, text-overlap, and `render-svg-label-overlap` checks as start-anchored labels, while still avoiding a browser-grade text-layout claim. +## 2026-07-09 circuitikz UI And Artifact Export Increment + +The previous architecture deliberately kept circuitikz outside the generic planner until the topology contract was strong enough. That condition is now met for the constrained path, so the implementation advances without changing the core rule: Notemd still does not accept arbitrary TikZ from the LLM. + +Implemented changes: + +- `DiagramIntent` now includes `circuit`; `RenderTarget` now includes `circuitikz`. +- UI settings and the sidebar selector expose **Circuit (Circuitikz)** and **Circuitikz + SVG preview**. +- `DiagramSpec` can carry `circuitSpec` only when `intent` is `circuit`; validation rejects missing circuit payloads and rejects circuit payloads attached to non-circuit intents. +- `buildDiagramSpecPrompt()` asks for structured `CircuitSpec` output and known golden references when the user selects circuit/circuitikz, instead of asking for raw TikZ. +- `CircuitikzRenderer` emits deterministic `.tex` through the existing circuitikz exporter and supplies a white-background SVG preview companion from the same `CircuitSpec`. The canvas background is now an inline `fill="#ffffff"` contract, not only a CSS class, so standalone viewers that strip style blocks do not show a transparent canvas as black. +- `scripts/export-diagram-artifact.js` accepts `--target circuitikz` and can write SVG/PNG/PDF companions from the circuit preview SVG. Direct `svg`, `png`, and `pdf` targets also support `intent: "circuit"` by using the same preview companion. +- The artifact CLI accepts both explicit long-option arguments and the npm 11 positional fallback shape observed on Windows (`input target output previewSvg previewPng previewPdf ppi`), while the maintainer runbook recommends the direct `node` entrypoint for warning-free smoke scripts. +- Preview/export keeps the distinction between source and visual evidence: the SVG/PNG/PDF companion is Obsidian-viewable validation output, not a LaTeX/TikZJax compile result. + +Architecture comparison against prior requirements: + +| Requirement | Current state | Assessment | +|---|---|---| +| Do not generate free-form TikZ | Prompt and validation require `CircuitSpec`; renderer rejects non-circuit specs | Satisfied | +| Make circuitikz discoverable in UI | Settings and sidebar render target/intent options are wired | Satisfied | +| Support Obsidian-viewable outputs for non-renderable source formats | circuitikz now follows Draw.io/Drawnix with `previewSvg`, PNG, and PDF companions | Satisfied for preview evidence | +| Preserve cross-platform execution discipline | The new path does not add shell execution; existing compile runner remains `shell: false` for optional real renderers; npm 11 positional argument rewriting is handled at the CLI boundary | Satisfied | +| Prove real LaTeX rendering quality | Still optional maintainer smoke evidence, not plugin runtime behavior | Open by design | + +MDX synchronization policy: + +Do not blindly commit every generated localized MDX file for each feature increment. The Docusaurus i18n tree is a published website surface, but all-locale generated churn makes code review noisy and hides product changes. Commit localized MDX when route/frontmatter/heading contracts or visible published behavior require parity. For this increment, update the English source page and the explicit zh-CN page that the user called out; leave the rest of the generated locale tree untouched unless website build or localization contract tests require regeneration. + ## Current Architecture Progress Audit | Prior requirement | Current code evidence | Status | Next direction | |---|---|---|---| -| Keep model output semantic instead of free-form renderer text | `DiagramSpec`, `SemanticFigureModel`, and the separate `CircuitSpec` boundary keep renderer syntax behind adapters | Implemented for current targets | Do not widen `DiagramSpec` for circuit-only terminal/layout fields until another target needs them | +| Keep model output semantic instead of free-form renderer text | `DiagramSpec`, `SemanticFigureModel`, and constrained `DiagramSpec.circuitSpec` for `intent: "circuit"` keep renderer syntax behind adapters | Implemented for current targets | Keep rejecting free-form TikZ; promote circuit fields only when another non-circuit target needs them | | Keep editor integrations at artifact boundaries | Draw.io XML, Drawnix JSON, editable HTML/SVG, circuitikz, and SVG companions all export through CLI or render artifacts without embedding third-party editors | Implemented | Add richer primitives only when structural editability tests exist | -| Make renderer execution cross-platform | `circuitikzCompileRunner.ts` uses `shell: false` with placeholder-expanded argument arrays and structured executable diagnostics; `run-circuitikz-smoke-fixtures.js` records missing renderer configuration as JSON evidence instead of probing shell resolution | Implemented | Keep Windows/POSIX behavior in argument arrays, not shell command strings; keep renderer discovery optional unless it can preserve that contract | +| Make renderer execution cross-platform | `circuitikzCompileRunner.ts` uses `shell: false` with placeholder-expanded argument arrays and structured executable diagnostics; `run-circuitikz-smoke-fixtures.js` records missing renderer configuration as JSON evidence instead of probing shell resolution; `export-diagram-artifact.js` accepts npm 11 stripped-flag positional arguments | Implemented | Keep Windows/POSIX behavior in argument arrays, not shell command strings; keep renderer discovery optional unless it can preserve that contract | | Verify circuit output before visual repair | Compile-log diagnostics, topology-preserving repair guard, structured repair prompt handoff, repair acceptance evidence, deterministic layout-hint projection, SVG structural smoke, accessibility metadata label checks through `aria-label`, ``, and `<desc>`, 1/2/4/8-bit indexed-color, grayscale/RGB `tRNS` transparent samples, format-specific Adam7/interlaced and indexed bit-depth rejection guidance, 1/2/4/8/16-bit grayscale, and 8/16-bit grayscale-alpha/RGB/RGBA PNG foreground smoke are in place | Partially implemented | Add OCR-level recognition for path-only visual text, precise pixel overlap gates, and automated topology-preserving repair execution | -| Expose diagnostics to users without pretending source is rendered | `RenderArtifact.diagnostics`, localized diagnostic summary counts, preview history entries, source-only fallback, and explicit `previewSvg` companions are implemented | Implemented | Connect external renderer artifacts only when evidence distinguishes raw source from rendered output | +| Expose diagnostics to users without pretending source is rendered | `RenderArtifact.diagnostics`, localized diagnostic summary counts, preview history entries, source-only fallback, and explicit `previewSvg` companions are implemented; circuitikz preview companions are labeled as semantic preview evidence and carry an inline white canvas background | Implemented | Connect external renderer artifacts only when evidence distinguishes raw source from rendered output | ## Tradeoffs @@ -343,6 +372,6 @@ The HTML/SVG, Draw.io, Drawnix, and first circuitikz export boundaries now exist 2. add screenshot/OCR-level checks beyond structural SVG coordinates, including OCR recognition for path-only glyph text and more precise pixel-level overlap than the current foreground-density and label-vs-drawing box heuristics; 3. keep topology locked during any repair prompt so visual repair cannot change the circuit; 4. connect source-only preview sessions to external artifact outputs only after the renderer evidence is available, keeping raw source and real visual render states distinct; -5. only then consider more circuit families or a plugin-side circuit preview target. +5. expand circuit families only when the golden-reference topology and companion-preview contracts can be validated together; keep real LaTeX/TikZJax visual acceptance behind explicit renderer evidence. Drawnix remains a good export format, not a reason to embed a whiteboard product in the plugin. circuitikz remains a constrained circuit target, not a reason to accept arbitrary TikZ from the LLM. diff --git a/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md b/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md index 9bb60b1e..db6fc4c2 100644 --- a/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md +++ b/docs/brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md @@ -291,15 +291,44 @@ Phase F 于 2026-07-05 的实现状态:`src/diagram/adapters/circuitikz/circui 2026-07-06 SVG text-anchor geometry increment:positioned SVG `text` 与 `tspan` boxes 现在会读取 attribute 或 inline style 中的 `text-anchor` 值 `start`、`middle` 与 `end`。这让居中和右对齐标签与 start-anchored 标签一样进入 bounded-canvas、text-overlap 和 `render-svg-label-overlap` 检查,同时仍不声称具备 browser-grade text layout。 +## 2026-07-09 circuitikz UI 与 artifact export 增量 + +先前架构刻意把 circuitikz 留在通用 planner 之外,直到拓扑契约足够强。现在受约束路径已经满足这个条件,因此实现可以向前推进,但核心规则没有改变:Notemd 仍然不接受 LLM 输出任意 TikZ。 + +已落地变化: + +- `DiagramIntent` 新增 `circuit`;`RenderTarget` 新增 `circuitikz`。 +- 设置 UI 与侧边栏选择器暴露 **电路图 (Circuitikz)** 与 **Circuitikz + SVG 预览**。 +- `DiagramSpec` 只有在 `intent` 为 `circuit` 时才能携带 `circuitSpec`;校验会拒绝缺少电路 payload 的 circuit intent,也会拒绝挂在非电路 intent 上的 circuit payload。 +- 当用户选择 circuit/circuitikz 时,`buildDiagramSpecPrompt()` 要求输出结构化 `CircuitSpec` 与已知 golden references,而不是 raw TikZ。 +- `CircuitikzRenderer` 通过现有 circuitikz exporter 写出确定性 `.tex`,并从同一份 `CircuitSpec` 派生白底 SVG preview companion。画布背景现在是 inline `fill="#ffffff"` 契约,而不是只依赖 CSS class,因此即使某些 standalone viewer 剥离 `<style>`,也不会把透明画布显示成黑底。 +- `scripts/export-diagram-artifact.js` 接受 `--target circuitikz`,并可从电路 preview SVG 写出 SVG/PNG/PDF companion。直接 `svg`、`png` 与 `pdf` target 也支持 `intent: "circuit"`,内部使用同一个 preview companion。 +- artifact CLI 同时接受显式长参数与 Windows 上实测到的 npm 11 位置参数 fallback 形态(`input target output previewSvg previewPng previewPdf ppi`);维护者 runbook 则推荐直接 `node` 入口,避免 smoke 脚本输出 npm warning。 +- 预览/导出继续区分 source 与 visual evidence:SVG/PNG/PDF companion 是 Obsidian 可查看的验证输出,不是 LaTeX/TikZJax 编译结果。 + +与先前要求的架构对比: + +| 要求 | 当前状态 | 判断 | +|---|---|---| +| 不生成自由形式 TikZ | prompt 与 validation 要求 `CircuitSpec`;renderer 拒绝非电路 spec | 已满足 | +| circuitikz 必须能在 UI 中被发现 | settings 与 sidebar 的 intent/render target 选项已接线 | 已满足 | +| 对 Obsidian 无法直接渲染的源格式提供可查看产物 | circuitikz 现在与 Draw.io/Drawnix 一样提供 `previewSvg`、PNG 与 PDF companion | 已满足预览证据边界 | +| 保持跨平台执行纪律 | 新路径没有新增 shell 执行;可选真实 renderer 仍复用 `shell: false` compile runner;npm 11 的位置参数重写在 CLI 边界被吸收 | 已满足 | +| 证明真实 LaTeX 渲染质量 | 仍是可选 maintainer smoke evidence,不是插件 runtime 行为 | 按设计保持开放 | + +MDX 同步策略: + +不要在每个功能增量里盲目提交所有 generated localized MDX。Docusaurus i18n 树是发布网站表面,但全 locale 机械同步会制造巨大的翻译噪声,并掩盖真正的产品变更。只有当 route、frontmatter、heading contract 或公开页面行为要求一致时,才提交对应 localized MDX。本次增量只更新英文源页面和用户明确点名的 zh-CN 页面;除非 website build 或 localization contract test 要求重新生成,否则其他 locale 的 generated MDX 保持不动。 + ## 当前架构推进审计 | 先前方案要求 | 当前代码证据 | 状态 | 后续方向 | |---|---|---|---| -| 保持模型输出为语义层,不回退到自由 renderer text | `DiagramSpec`、`SemanticFigureModel` 与独立 `CircuitSpec` 边界把 renderer 语法留在 adapter 内 | 当前 targets 已实现 | 在其他 target 需要同类字段前,不把 circuit-only terminal/layout 字段扩进 `DiagramSpec` | +| 保持模型输出为语义层,不回退到自由 renderer text | `DiagramSpec`、`SemanticFigureModel`,以及仅在 `intent: "circuit"` 下有效的受约束 `DiagramSpec.circuitSpec`,把 renderer 语法留在 adapter 内 | 当前 targets 已实现 | 继续拒绝自由形式 TikZ;只有当非电路 target 也需要同类字段时,才提升电路字段 | | 把编辑器集成留在 artifact boundary | Draw.io XML、Drawnix JSON、editable HTML/SVG、circuitikz 与 SVG companions 都通过 CLI 或 render artifact 输出,不嵌入第三方编辑器 | 已实现 | 只有 structural editability tests 存在后再扩展更丰富 primitives | -| renderer 执行必须跨平台 | `circuitikzCompileRunner.ts` 使用 `shell: false`、placeholder-expanded argument arrays 和结构化 executable diagnostics;`run-circuitikz-smoke-fixtures.js` 会把缺失 renderer 配置记录为 JSON evidence,而不是探测 shell resolution | 已实现 | Windows/POSIX 行为继续放在参数数组里处理,不拼 shell command string;除非能保持这条契约,否则 renderer discovery 仍应保持可选 | +| renderer 执行必须跨平台 | `circuitikzCompileRunner.ts` 使用 `shell: false`、placeholder-expanded argument arrays 和结构化 executable diagnostics;`run-circuitikz-smoke-fixtures.js` 会把缺失 renderer 配置记录为 JSON evidence,而不是探测 shell resolution;`export-diagram-artifact.js` 接受 npm 11 stripped-flag 后的位置参数形态 | 已实现 | Windows/POSIX 行为继续放在参数数组里处理,不拼 shell command string;除非能保持这条契约,否则 renderer discovery 仍应保持可选 | | 视觉修复前必须先验证电路输出 | compile-log diagnostics、topology-preserving repair guard、structured repair prompt handoff、repair acceptance evidence、deterministic layout-hint projection、SVG structural smoke、通过 `aria-label`、`<title>` 与 `<desc>` 执行的 accessibility metadata label checks、1/2/4/8-bit indexed-color、grayscale/RGB `tRNS` transparent samples、format-specific Adam7/interlaced 与 indexed bit-depth rejection guidance、1/2/4/8/16-bit grayscale 与 8/16-bit grayscale-alpha/RGB/RGBA PNG foreground smoke 已存在 | 部分实现 | 增加 path-only visual text 的 OCR-level recognition、精确 pixel overlap gates 与自动化 topology-preserving repair execution | -| 向用户暴露 diagnostics,但不把源码伪装成渲染结果 | `RenderArtifact.diagnostics`、本地化 diagnostic summary counts、preview history entries、source-only fallback 与显式 `previewSvg` companion 已实现 | 已实现 | 只有 renderer evidence 能区分 raw source 与 rendered output 后,再接外部 renderer artifacts | +| 向用户暴露 diagnostics,但不把源码伪装成渲染结果 | `RenderArtifact.diagnostics`、本地化 diagnostic summary counts、preview history entries、source-only fallback 与显式 `previewSvg` companion 已实现;circuitikz preview companion 被定位为语义预览证据,并携带 inline 白色画布背景 | 已实现 | 只有 renderer evidence 能区分 raw source 与 rendered output 后,再接外部 renderer artifacts | ## 权衡 @@ -343,6 +372,6 @@ HTML/SVG、Draw.io、Drawnix 和第一个 circuitikz export 边界现在都已 2. 增加超过结构化 SVG 坐标的 screenshot/OCR-level 检查,包括 path-only glyph text 的 OCR recognition,以及比当前 foreground-density 与 label-vs-drawing box heuristics 更精确的 pixel-level overlap; 3. repair prompt 中锁定拓扑,避免视觉修复改变电路; 4. 只有在 renderer evidence 可用之后,才把 source-only preview session 接到外部 artifact outputs,继续区分 raw source 与真实视觉渲染状态; -5. 只有到这一步之后,再考虑更多 circuit families 或插件侧 circuit preview target。 +5. 只有当 golden-reference topology 与 companion-preview 契约能一起被验证时,再扩展更多 circuit families;真实 LaTeX/TikZJax visual acceptance 继续放在显式 renderer evidence 后面。 Drawnix 仍然是好的导出格式,不是把白板产品嵌入插件的理由。circuitikz 仍然是受约束 circuit target,不是接受 LLM 任意 TikZ 输出的理由。 diff --git a/docs/maintainer/circuitikz-export-prototype.md b/docs/maintainer/circuitikz-export-prototype.md index 9ccfb5b2..70d6cf96 100644 --- a/docs/maintainer/circuitikz-export-prototype.md +++ b/docs/maintainer/circuitikz-export-prototype.md @@ -212,7 +212,9 @@ npm run diagram:smoke-circuitikz -- \ This is the first real-environment smoke boundary. PNG screenshot smoke checks cover positive dimensions, non-background pixels, foreground bounds, foreground density, too-small foreground footprints through `render-png-foreground-too-small`, edge-touching clipped content, and unusually dense foreground blocks. It still does not make LaTeX or TikZJax mandatory for normal CI or plugin startup; it gives maintainers a repeatable command for local release evidence when a renderer is installed. The command stays cross-platform because the fixture runner delegates to the existing shell-free compile runner instead of resolving a platform shell. -Obsidian's Preview diagram command can also reopen saved circuitikz source artifacts as a circuitikz source-only preview when the file extension is `.tex` or `.tikz` and the source contains `\usepackage{circuitikz}` or `\begin{circuitikz}`. This front-end path uses the generic source-only preview and artifact diagnostics surface; it does not compile LaTeX, invoke TikZJax, or promote circuitikz into the generic `DiagramSpec` render-target planner. +Obsidian's Preview diagram command can also reopen saved circuitikz source artifacts as a circuitikz source-only preview when the file extension is `.tex` or `.tikz` and the source contains `\usepackage{circuitikz}` or `\begin{circuitikz}`. This front-end path uses the generic source-only preview and artifact diagnostics surface; it does not compile LaTeX or invoke TikZJax. + +2026-07-09 UI/render-target increment: circuit diagrams are now exposed in the experimental diagram UI as `intent: "circuit"` plus render target `circuitikz`. The generic `DiagramSpec` is still not widened with free-form TikZ fields; it may embed a validated `DiagramSpec.circuitSpec` only when the intent is `circuit`. `CircuitikzRenderer` emits the deterministic `.tex` artifact through the existing circuitikz exporter and supplies a white-background SVG preview companion derived from the same `CircuitSpec`. That companion enables Obsidian-viewable SVG plus PNG/PDF export at the preview/export boundary, while real LaTeX/TikZJax compile evidence remains an optional smoke-runner concern. ## Supported Circuit Families @@ -242,20 +244,21 @@ The CMOS NAND template adds a stronger digital-logic invariant: `MPA` and `MPB` The CMOS NOR template mirrors the digital-logic constraint in the opposite networks: `MPA` and `MPB` must be PMOS devices in a series pull-up stack from `VDD` to `vout`; `MNA` and `MNB` must be NMOS devices in the parallel pull-down network from `vout` to `GND`; `va` must drive `MPA.G` and `MNA.G`; and `vb` must drive `MPB.G` and `MNB.G`. The same layout-hint projection rule applies: port placement can move, but the topology signature cannot. -## Why `CircuitSpec` Is Separate +## Why `CircuitSpec` Remains Constrained -`DiagramSpec` is intentionally not widened for circuit diagrams yet. Circuit diagrams need topology, terminal references, layout lanes, and package conventions that generic flowcharts and data charts do not use. +`CircuitSpec` remains a constrained circuit payload rather than a free-form renderer syntax field. Circuit diagrams need topology, terminal references, layout lanes, and package conventions that generic flowcharts and data charts do not use. The current compromise is deliberate: `DiagramSpec` can carry `circuitSpec` only when `intent` is `circuit`, and validation rejects circuit payloads attached to non-circuit intents. -The boundary is: +The boundary is now: ```text -CircuitSpec - -> topology/template validation +DiagramSpec(intent: "circuit", circuitSpec) + -> CircuitSpec topology/template validation -> deterministic golden-reference circuitikz adapter -> .tex artifact + -> optional SVG preview companion for Obsidian / PNG / PDF review ``` -This keeps the model-facing contract narrow and makes topology drift testable. The exporter rejects invalid topology before writing an output file. +This keeps the model-facing contract narrow and makes topology drift testable. The exporter rejects invalid topology before writing an output file, and the companion SVG is a preview artifact from the same validated topology rather than a claim that LaTeX was compiled in the plugin runtime. For the current golden templates, `layoutHints.inputSide` and `layoutHints.outputSide` are projected into deterministic input/output port placement. A topology-preserving repair can move input or output ports to the other side through layout hints while keeping the same `topologySignature`; the exporter rewrites only the presentation route and node anchor. This is not a general autorouter. It is a constrained layout projection for the supported golden families. @@ -265,6 +268,7 @@ Canonical regression commands: ```bash npm test -- --runInBand src/tests/circuitikzExporter.test.ts src/tests/circuitikzCompileDiagnostics.test.ts src/tests/circuitikzRenderSmoke.test.ts src/tests/circuitikzCompileRunner.test.ts src/tests/circuitikzExportCli.test.ts --runTestsByPath +npm test -- --runInBand src/tests/circuitikzRenderer.test.ts src/tests/diagramGenerationService.test.ts src/tests/diagramArtifactExportCli.test.ts src/tests/diagramPreviewModal.test.ts --runTestsByPath ``` The tests verify: diff --git a/docs/maintainer/circuitikz-export-prototype.zh-CN.md b/docs/maintainer/circuitikz-export-prototype.zh-CN.md index d835d53f..d2b9baf0 100644 --- a/docs/maintainer/circuitikz-export-prototype.zh-CN.md +++ b/docs/maintainer/circuitikz-export-prototype.zh-CN.md @@ -212,7 +212,9 @@ npm run diagram:smoke-circuitikz -- \ 这是第一条真实环境 smoke 边界。PNG screenshot smoke checks 覆盖正尺寸、非背景像素、前景包围盒、前景密度、通过 `render-png-foreground-too-small` 报告的过小前景 footprint、贴边裁剪内容和异常密集前景块。它仍然不会让 LaTeX 或 TikZJax 成为普通 CI 或插件启动的硬依赖;它给维护者提供一个在本地安装 renderer 后可重复执行的 release evidence 命令。命令保持跨平台,因为 fixture runner 继续委托给现有 shell-free compile runner,而不是解析平台 shell。 -Obsidian 的 Preview diagram 命令也可以把已保存的 circuitikz source artifact 重新打开为 circuitikz source-only preview:文件扩展名必须是 `.tex` 或 `.tikz`,且源码包含 `\usepackage{circuitikz}` 或 `\begin{circuitikz}`。这个前端路径复用通用 source-only preview 与 artifact diagnostics surface;它不会编译 LaTeX、不会调用 TikZJax,也不会把 circuitikz 提升为通用 `DiagramSpec` render-target planner 的目标。 +Obsidian 的 Preview diagram 命令也可以把已保存的 circuitikz source artifact 重新打开为 circuitikz source-only preview:文件扩展名必须是 `.tex` 或 `.tikz`,且源码包含 `\usepackage{circuitikz}` 或 `\begin{circuitikz}`。这个前端路径复用通用 source-only preview 与 artifact diagnostics surface;它不会编译 LaTeX,也不会调用 TikZJax。 + +2026-07-09 UI / render target 增量:电路图现在已经在实验性图表 UI 中暴露为 `intent: "circuit"` 加 `circuitikz` render target。通用 `DiagramSpec` 仍然不会接收自由形式 TikZ 字段;只有当 intent 为 `circuit` 时,才允许嵌入已验证的 `DiagramSpec.circuitSpec`。`CircuitikzRenderer` 会通过现有 circuitikz exporter 写出确定性的 `.tex` artifact,并从同一份 `CircuitSpec` 派生白底 SVG preview companion。该 companion 让 Obsidian 可以查看 SVG,并在 preview/export 边界导出 PNG/PDF;真实 LaTeX/TikZJax 编译证据仍然属于可选 smoke runner 的职责。 ## 已支持的电路族 @@ -242,20 +244,21 @@ CMOS NAND template 进一步固定数字逻辑 invariant:`MPA` 与 `MPB` 必 CMOS NOR template 则锁定互补的数字逻辑 invariant:`MPA` 与 `MPB` 必须是从 `VDD` 到 `vout` 的串联 PMOS pull-up stack;`MNA` 与 `MNB` 必须是从 `vout` 到 `GND` 的并联 NMOS pull-down network;`va` 必须同时驱动 `MPA.G` 与 `MNA.G`;`vb` 必须同时驱动 `MPB.G` 与 `MNB.G`。同样只允许通过 layout hints 移动端口展示位置,不能改变 topology signature。 -## 为什么 `CircuitSpec` 独立存在 +## 为什么 `CircuitSpec` 仍然保持受约束 -当前不会为了电路图过早扩展 `DiagramSpec`。电路图需要拓扑、terminal references、layout lanes 和 package conventions;这些字段并不属于普通 flowchart 或 data chart。 +`CircuitSpec` 仍然是受约束的电路 payload,而不是自由形式 renderer 语法字段。电路图需要拓扑、terminal references、layout lanes 和 package conventions;这些字段并不属于普通 flowchart 或 data chart。当前折中是有意设计的:只有当 `DiagramSpec.intent` 为 `circuit` 时,`DiagramSpec` 才可以携带 `circuitSpec`;如果非电路 intent 携带电路 payload,校验会直接拒绝。 -当前边界是: +当前边界已经变为: ```text -CircuitSpec - -> topology/template validation +DiagramSpec(intent: "circuit", circuitSpec) + -> CircuitSpec topology/template validation -> deterministic golden-reference circuitikz adapter -> .tex artifact + -> optional SVG preview companion for Obsidian / PNG / PDF review ``` -这能让 model-facing contract 保持窄边界,并让拓扑漂移可测试。exporter 会在写出文件前拒绝无效拓扑。 +这能让 model-facing contract 保持窄边界,并让拓扑漂移可测试。exporter 会在写出文件前拒绝无效拓扑;SVG companion 是从同一份已验证拓扑生成的预览 artifact,而不是声称插件 runtime 已经完成 LaTeX 编译。 对于当前 golden templates,`layoutHints.inputSide` 与 `layoutHints.outputSide` 现在会被投影为确定性的 input/output port placement。topology-preserving repair 可以通过 layout hints 把输入或输出端口移到另一侧,同时保持同一个 `topologySignature`;exporter 只会改 presentation route 和 node anchor。这不是通用 autorouter,而是面向已支持 golden families 的受约束 layout projection。 @@ -265,6 +268,7 @@ CircuitSpec ```bash npm test -- --runInBand src/tests/circuitikzExporter.test.ts src/tests/circuitikzCompileDiagnostics.test.ts src/tests/circuitikzRenderSmoke.test.ts src/tests/circuitikzCompileRunner.test.ts src/tests/circuitikzExportCli.test.ts --runTestsByPath +npm test -- --runInBand src/tests/circuitikzRenderer.test.ts src/tests/diagramGenerationService.test.ts src/tests/diagramArtifactExportCli.test.ts src/tests/diagramPreviewModal.test.ts --runTestsByPath ``` 测试覆盖: @@ -299,4 +303,4 @@ npm test -- --runInBand src/tests/circuitikzExporter.test.ts src/tests/circuitik ## 非目标 -这个原型不捆绑 LaTeX、不把 TikZJax 作为 Obsidian runtime 依赖调用、不 OCR 识别 path-only glyph text、不做精确 pixel-level overlap detection、不覆盖尚未支持的 SVG path geometry,也不使用渲染图像反馈进行自动修复。这些是后续 gate。它也不接受任意自然语言电路请求。当前重要声明更窄:经过验证的 `CircuitSpec` 输入可以为五个 golden families 生成稳定、可读的 circuitikz,已有 compile logs 可以转换为 actionable diagnostics,并且显式配置的本地 renderer 可以在不走 shell-specific command parsing 的情况下执行,同时可选证明具体输出 artifact 已经创建;如果输出是 SVG 或 PNG,还能证明它具备进入后续视觉检查的基本结构。SVG 输出现在包含常见 SVG transforms 的 transform-aware geometry、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、positioned `tspan` label geometry、path-only label classification、path-only glyph overlap detection 与保守 label-vs-drawing overlap detection;PNG 输出则会暴露前景包围盒、前景密度与 format-specific unsupported-export guidance,用于在 topology-preserving repair loop 之前拒绝明显画布裁剪、第一层 pixel crowding、Adam7 interlaced PNG 和 unsupported indexed-color bit depths。 +这个原型不捆绑 LaTeX、不把 TikZJax 作为 Obsidian runtime 依赖调用、不 OCR 识别 path-only glyph text、不做精确 pixel-level overlap detection、不覆盖尚未支持的 SVG path geometry,也不使用渲染图像反馈进行自动修复。这些是后续 gate。它也不接受任意自然语言电路请求。当前重要声明更窄:经过验证的 `CircuitSpec` 输入可以为六个 golden families 生成稳定、可读的 circuitikz,已有 compile logs 可以转换为 actionable diagnostics,并且显式配置的本地 renderer 可以在不走 shell-specific command parsing 的情况下执行,同时可选证明具体输出 artifact 已经创建;如果输出是 SVG 或 PNG,还能证明它具备进入后续视觉检查的基本结构。SVG 输出现在包含常见 SVG transforms 的 transform-aware geometry、针对 A/a arc extrema 的 exact arc bounds、针对 C/S/Q/T curve extrema 的 exact Bezier curve bounds、positioned `tspan` label geometry、path-only label classification、path-only glyph overlap detection 与保守 label-vs-drawing overlap detection;PNG 输出则会暴露前景包围盒、前景密度与 format-specific unsupported-export guidance,用于在 topology-preserving repair loop 之前拒绝明显画布裁剪、第一层 pixel crowding、Adam7 interlaced PNG 和 unsupported indexed-color bit depths。 diff --git a/docs/maintainer/diagram-artifact-export-cli.md b/docs/maintainer/diagram-artifact-export-cli.md index 72f7c594..84404454 100644 --- a/docs/maintainer/diagram-artifact-export-cli.md +++ b/docs/maintainer/diagram-artifact-export-cli.md @@ -11,21 +11,26 @@ topic: diagram-artifact-export-cli It accepts a checked `DiagramSpec` JSON file and writes one artifact without requiring Obsidian, `obsidian-cli`, diagrams.net Desktop, Drawnix, or Plait. SVG and source targets are pure TypeScript exports. PNG and PDF targets use Playwright Chromium to render the same standalone SVG into fixed-size visual evidence. The input may be UTF-8 with or without a BOM, which keeps files produced by Windows PowerShell usable without a separate normalization step. -```bash -npm run diagram:export-artifact -- --input spec.json --target editable-html-svg --output figure.html -npm run diagram:export-artifact -- --input spec.json --target drawio --output figure.drawio --preview-svg-output figure.drawio.svg -npm run diagram:export-artifact -- --input spec.json --target drawnix --output figure.drawnix --preview-svg-output figure.drawnix.svg --preview-png-output figure.drawnix.png --preview-pdf-output figure.drawnix.pdf --ppi 300 -npm run diagram:export-artifact -- --input spec.json --target svg --output figure.svg -npm run diagram:export-artifact -- --input spec.json --target png --output figure.png --ppi 300 -npm run diagram:export-artifact -- --input spec.json --target pdf --output figure.pdf --ppi 300 -``` - -Direct entrypoint: +Recommended direct entrypoint: ```bash +node scripts/export-diagram-artifact.js --input spec.json --target editable-html-svg --output figure.html node scripts/export-diagram-artifact.js --input spec.json --target drawio --output figure.drawio --preview-svg-output figure.drawio.svg --preview-png-output figure.drawio.png --preview-pdf-output figure.drawio.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target drawnix --output figure.drawnix --preview-svg-output figure.drawnix.svg --preview-png-output figure.drawnix.png --preview-pdf-output figure.drawnix.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input circuit-spec.json --target circuitikz --output circuit.tex --preview-svg-output circuit.svg --preview-png-output circuit.png --preview-pdf-output circuit.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target svg --output figure.svg +node scripts/export-diagram-artifact.js --input spec.json --target png --output figure.png --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target pdf --output figure.pdf --ppi 300 ``` +The package script stays available for existing automation: + +```bash +npm run diagram:export-artifact -- --input spec.json --target drawio --output figure.drawio +``` + +On npm 11, especially on Windows, npm may rewrite long options after `npm run ... --` into ordered positional arguments and print warning lines. The CLI therefore accepts both the explicit flag form and this positional fallback: `input target output previewSvg previewPng previewPdf ppi`. Prefer the direct `node` entrypoint in maintainer smoke scripts when warning-free output matters. + `--ppi` controls raster density for PNG/PDF output. The default is `300`; values above `600` are clamped to `600`. SVG stays vector-sized and ignores this value. PNG output also writes or replaces the `pHYs` physical pixel density chunk, so the selected PPI is visible to image viewers and layout tools instead of only being reflected in pixel dimensions. @@ -37,9 +42,10 @@ PNG output also writes or replaces the `pHYs` physical pixel density chunk, so t | `editable-html-svg` | self-contained `.html` with inline SVG | `DiagramSpec -> SemanticFigureModel -> EditableHtmlSvgRenderer` | annotation gaps from `collectEditableSvgAnnotationGaps()` must be empty | | `drawio` | uncompressed diagrams.net `mxfile` XML, optionally with `--preview-svg-output`, `--preview-png-output`, and `--preview-pdf-output` companions | `DiagramSpec -> SemanticFigureModel -> exportSemanticFigureModelToDrawioXml()` plus `renderSemanticFigureSvg()` | visible label mismatches must be empty | | `drawnix` | minimal `.drawnix` JSON subset, optionally with `--preview-svg-output`, `--preview-png-output`, and `--preview-pdf-output` companions | `DiagramSpec -> SemanticFigureModel -> exportSemanticFigureModelToDrawnixData()` plus `renderSemanticFigureSvg()` | subset validation errors must be empty | -| `svg` | Obsidian-viewable `.svg` generated from the same semantic model | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg()` | semantic node/edge annotations must be present | -| `png` | `.png` visual evidence rendered from the same standalone SVG | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright screenshot` | output dimensions follow SVG CSS size at the selected PPI, with `pHYs` metadata aligned to the selected density | -| `pdf` | single-page `.pdf` visual evidence rendered from the same standalone SVG | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright PDF` | page size follows SVG CSS size; `--ppi` controls raster/screenshot companions | +| `circuitikz` | constrained `.tex` circuitikz source, optionally with SVG/PNG/PDF preview companions | `DiagramSpec(intent: "circuit") -> CircuitSpec -> CircuitikzRenderer -> exportCircuitSpecToCircuitikz()` plus `renderCircuitSpecPreviewSvg()` | `CircuitSpec` validation must pass before TeX or companion output is written | +| `svg` | Obsidian-viewable `.svg` generated from the same semantic model, or from a circuit preview companion when `intent` is `circuit` | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg()` or `CircuitSpec -> renderCircuitSpecPreviewSvg()` | semantic node/edge annotations or validated circuit preview metadata must be present | +| `png` | `.png` visual evidence rendered from the same standalone SVG or circuit preview SVG | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright screenshot`, or `CircuitSpec -> renderCircuitSpecPreviewSvg() -> Playwright screenshot` | output dimensions follow SVG CSS size at the selected PPI, with `pHYs` metadata aligned to the selected density | +| `pdf` | single-page `.pdf` visual evidence rendered from the same standalone SVG or circuit preview SVG | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright PDF`, or `CircuitSpec -> renderCircuitSpecPreviewSvg() -> Playwright PDF` | page size follows SVG CSS size; `--ppi` controls raster/screenshot companions | ## Obsidian Preview Companion Contract @@ -53,6 +59,8 @@ Topic_diagram.drawio.md The Markdown wrapper embeds the SVG with `![[Topic_diagram.drawio.svg]]` and links back to the source artifact. The Preview diagram command also searches these generated wrapper/source/SVG paths when the active source note has no inline diagram fence, so maintainers can verify previously generated local artifacts without regenerating them. +For circuitikz, the SVG companion is intentionally a semantic preview derived from the same validated `CircuitSpec`, not a LaTeX/TikZJax compile result. It exists so Obsidian can display and export reviewable SVG/PNG/PDF evidence even though raw `.tex` is not rendered by Obsidian by default. Real LaTeX/TikZJax compile evidence still belongs to `scripts/export-circuitikz.js` and the circuitikz smoke runner. + ## Why This Boundary This CLI is deliberately artifact-first: @@ -78,9 +86,10 @@ The test writes a single `DiagramSpec` and verifies: - normalized node IDs stay unique after whitespace normalization. - `drawio` XML preserves visible node and edge labels. - `drawnix` JSON contains supported `geometry` and `arrow-line` elements. -- `drawio` and `drawnix` can produce SVG companion files for Obsidian preview validation. -- `svg` emits the same annotated semantic figure sheet directly. -- `png` and `pdf` are part of the public CLI target list, use `--ppi`, and clamp oversized PPI values to `600`. +- `drawio`, `drawnix`, and `circuitikz` can produce SVG companion files for Obsidian preview validation. +- `circuitikz` emits constrained TeX only after `DiagramSpec.circuitSpec` validates, and can export SVG/PNG/PDF preview companions from the same circuit payload. +- `svg` emits the same annotated semantic figure sheet directly, or a validated circuit preview companion for `intent: "circuit"`. +- `png` and `pdf` are part of the public CLI target list, use `--ppi`, default to `300`, and clamp oversized PPI values to `600`. - unsupported targets fail before writing an output file. ## Non-Goals diff --git a/docs/maintainer/diagram-artifact-export-cli.zh-CN.md b/docs/maintainer/diagram-artifact-export-cli.zh-CN.md index 912b62ad..4c8419be 100644 --- a/docs/maintainer/diagram-artifact-export-cli.zh-CN.md +++ b/docs/maintainer/diagram-artifact-export-cli.zh-CN.md @@ -11,21 +11,26 @@ topic: diagram-artifact-export-cli 它读取已验证结构的 `DiagramSpec` JSON 文件,并写出一个 artifact;不需要 Obsidian、`obsidian-cli`、diagrams.net Desktop、Drawnix 或 Plait。SVG 与 source targets 是纯 TypeScript 导出;PNG 与 PDF targets 使用 Playwright Chromium 把同一个 standalone SVG 渲染成固定尺寸的视觉证据。输入文件可以是带 BOM 或不带 BOM 的 UTF-8,这样 Windows PowerShell 生成的 JSON 不需要额外归一化也能直接使用。 -```bash -npm run diagram:export-artifact -- --input spec.json --target editable-html-svg --output figure.html -npm run diagram:export-artifact -- --input spec.json --target drawio --output figure.drawio --preview-svg-output figure.drawio.svg -npm run diagram:export-artifact -- --input spec.json --target drawnix --output figure.drawnix --preview-svg-output figure.drawnix.svg --preview-png-output figure.drawnix.png --preview-pdf-output figure.drawnix.pdf --ppi 300 -npm run diagram:export-artifact -- --input spec.json --target svg --output figure.svg -npm run diagram:export-artifact -- --input spec.json --target png --output figure.png --ppi 300 -npm run diagram:export-artifact -- --input spec.json --target pdf --output figure.pdf --ppi 300 -``` - -直接入口: +推荐直接入口: ```bash +node scripts/export-diagram-artifact.js --input spec.json --target editable-html-svg --output figure.html node scripts/export-diagram-artifact.js --input spec.json --target drawio --output figure.drawio --preview-svg-output figure.drawio.svg --preview-png-output figure.drawio.png --preview-pdf-output figure.drawio.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target drawnix --output figure.drawnix --preview-svg-output figure.drawnix.svg --preview-png-output figure.drawnix.png --preview-pdf-output figure.drawnix.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input circuit-spec.json --target circuitikz --output circuit.tex --preview-svg-output circuit.svg --preview-png-output circuit.png --preview-pdf-output circuit.pdf --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target svg --output figure.svg +node scripts/export-diagram-artifact.js --input spec.json --target png --output figure.png --ppi 300 +node scripts/export-diagram-artifact.js --input spec.json --target pdf --output figure.pdf --ppi 300 ``` +为了兼容既有自动化,package script 仍然可用: + +```bash +npm run diagram:export-artifact -- --input spec.json --target drawio --output figure.drawio +``` + +在 npm 11,尤其是 Windows 上,npm 可能会把 `npm run ... --` 后面的长选项重写为按顺序排列的位置参数,并打印 warning。该 CLI 因此同时接受显式 flag 形态以及这个位置参数 fallback:`input target output previewSvg previewPng previewPdf ppi`。维护者 smoke 脚本如果需要无 warning 输出,应优先使用直接 `node` 入口。 + `--ppi` 控制 PNG/PDF 输出的栅格密度。默认值是 `300`;超过 `600` 的值会被夹到 `600`。SVG 保持矢量尺寸,不受该值影响。 PNG 输出还会写入或替换 `pHYs` 物理像素密度 chunk,因此所选 PPI 不只体现在像素尺寸上,也能被图片查看器和排版工具读取。 @@ -37,9 +42,10 @@ PNG 输出还会写入或替换 `pHYs` 物理像素密度 chunk,因此所选 P | `editable-html-svg` | 自包含 `.html`,包含 inline SVG | `DiagramSpec -> SemanticFigureModel -> EditableHtmlSvgRenderer` | `collectEditableSvgAnnotationGaps()` 必须为空 | | `drawio` | 未压缩 diagrams.net `mxfile` XML,可通过 `--preview-svg-output`、`--preview-png-output` 与 `--preview-pdf-output` 同步写出 companion | `DiagramSpec -> SemanticFigureModel -> exportSemanticFigureModelToDrawioXml()` 加 `renderSemanticFigureSvg()` | visible label mismatch 必须为空 | | `drawnix` | 最小 `.drawnix` JSON subset,可通过 `--preview-svg-output`、`--preview-png-output` 与 `--preview-pdf-output` 同步写出 companion | `DiagramSpec -> SemanticFigureModel -> exportSemanticFigureModelToDrawnixData()` 加 `renderSemanticFigureSvg()` | subset validation error 必须为空 | -| `svg` | Obsidian 可直接查看的 `.svg` | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg()` | 必须保留 semantic node/edge annotations | -| `png` | 从同一个 standalone SVG 渲染出的 `.png` 视觉证据 | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright screenshot` | 输出尺寸按 SVG CSS 尺寸与所选 PPI 对齐,并写入匹配所选密度的 `pHYs` 元数据 | -| `pdf` | 从同一个 standalone SVG 渲染出的单页 `.pdf` 视觉证据 | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright PDF` | 页面尺寸按 SVG CSS 尺寸对齐;`--ppi` 控制栅格 companion | +| `circuitikz` | 受约束 `.tex` circuitikz 源文件,可同步写出 SVG/PNG/PDF 预览 companion | `DiagramSpec(intent: "circuit") -> CircuitSpec -> CircuitikzRenderer -> exportCircuitSpecToCircuitikz()` 加 `renderCircuitSpecPreviewSvg()` | 写出 TeX 或 companion 前必须通过 `CircuitSpec` 校验 | +| `svg` | Obsidian 可直接查看的 `.svg`;当 `intent` 为 `circuit` 时来自电路预览 companion | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg()` 或 `CircuitSpec -> renderCircuitSpecPreviewSvg()` | 必须保留 semantic node/edge annotations,或保留已验证电路预览元数据 | +| `png` | 从同一个 standalone SVG 或电路预览 SVG 渲染出的 `.png` 视觉证据 | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright screenshot`,或 `CircuitSpec -> renderCircuitSpecPreviewSvg() -> Playwright screenshot` | 输出尺寸按 SVG CSS 尺寸与所选 PPI 对齐,并写入匹配所选密度的 `pHYs` 元数据 | +| `pdf` | 从同一个 standalone SVG 或电路预览 SVG 渲染出的单页 `.pdf` 视觉证据 | `DiagramSpec -> SemanticFigureModel -> renderSemanticFigureSvg() -> Playwright PDF`,或 `CircuitSpec -> renderCircuitSpecPreviewSvg() -> Playwright PDF` | 页面尺寸按 SVG CSS 尺寸对齐;`--ppi` 控制栅格 companion | ## Obsidian 预览 companion 契约 @@ -53,6 +59,8 @@ Topic_diagram.drawio.md Markdown wrapper 使用 `![[Topic_diagram.drawio.svg]]` 嵌入 SVG,并链接回 source artifact。Preview diagram 命令在当前 source note 没有 inline diagram fence 时,也会查找这些已生成的 wrapper/source/SVG 路径,因此维护者可以直接验证本地已生成 artifact,而不必重新生成。 +对于 circuitikz,SVG companion 是从同一份已验证 `CircuitSpec` 派生出的语义预览,不是 LaTeX/TikZJax 编译结果。它的作用是让 Obsidian 即使无法默认渲染 raw `.tex`,也能查看并导出可审查的 SVG/PNG/PDF 证据。真实 LaTeX/TikZJax 编译证据仍属于 `scripts/export-circuitikz.js` 与 circuitikz smoke runner 的职责边界。 + ## 为什么放在这个边界 这个 CLI 刻意采用 artifact-first: @@ -78,9 +86,10 @@ npm test -- --runInBand src/tests/diagramArtifactExportCli.test.ts --runTestsByP - 节点 id 在空白归一化后仍保持唯一。 - `drawio` XML 保留可见节点与边 label。 - `drawnix` JSON 包含支持的 `geometry` 与 `arrow-line` element。 -- `drawio` 与 `drawnix` 可以写出用于 Obsidian 预览验证的 SVG companion 文件。 -- `svg` 可以直接输出同一个 annotated semantic figure sheet。 -- `png` 与 `pdf` 属于公开 CLI target,支持 `--ppi`,并会把过大的 PPI 值夹到 `600`。 +- `drawio`、`drawnix` 与 `circuitikz` 可以写出用于 Obsidian 预览验证的 SVG companion 文件。 +- `circuitikz` 只有在 `DiagramSpec.circuitSpec` 通过校验后才会写出受约束 TeX,并可从同一份电路 payload 导出 SVG/PNG/PDF 预览 companion。 +- `svg` 可以直接输出同一个 annotated semantic figure sheet;当 `intent: "circuit"` 时,则输出已验证的电路预览 companion。 +- `png` 与 `pdf` 属于公开 CLI target,支持 `--ppi`,默认值为 `300`,并会把过大的 PPI 值夹到 `600`。 - 不支持的 target 会在写输出前失败。 ## 非目标 diff --git a/scripts/export-diagram-artifact.js b/scripts/export-diagram-artifact.js index 64b52d6b..8b923de2 100644 --- a/scripts/export-diagram-artifact.js +++ b/scripts/export-diagram-artifact.js @@ -5,7 +5,7 @@ const os = require('os'); const path = require('path'); const { buildSync } = require('esbuild'); -const SUPPORTED_TARGETS = ['editable-html-svg', 'drawio', 'drawnix', 'svg', 'png', 'pdf']; +const SUPPORTED_TARGETS = ['editable-html-svg', 'drawio', 'drawnix', 'circuitikz', 'svg', 'png', 'pdf']; const DEFAULT_EXPORT_PPI = 300; const MIN_EXPORT_PPI = 72; const MAX_EXPORT_PPI = 600; @@ -17,11 +17,13 @@ function printUsage() { Usage: node scripts/export-diagram-artifact.js --input <diagram-spec.json> --target <target> --output <artifact-path> [--preview-svg-output <svg-path>] [--preview-png-output <png-path>] [--preview-pdf-output <pdf-path>] [--ppi <72-600>] + node scripts/export-diagram-artifact.js <diagram-spec.json> <target> <artifact-path> [preview-svg-path] [preview-png-path] [preview-pdf-path] [ppi] Targets: editable-html-svg Self-contained HTML with inline editable SVG annotations drawio Uncompressed diagrams.net mxfile XML drawnix Minimal .drawnix JSON subset + circuitikz Circuitikz TeX source from a constrained DiagramSpec.circuitSpec payload svg Obsidian-viewable SVG generated from the same SemanticFigureModel png PNG generated from the same SemanticFigureModel pdf PDF generated from the same SemanticFigureModel @@ -29,6 +31,7 @@ Targets: Example: node scripts/export-diagram-artifact.js --input spec.json --target drawio --output figure.drawio node scripts/export-diagram-artifact.js --input spec.json --target drawio --output figure.drawio --preview-svg-output figure.drawio.svg + node scripts/export-diagram-artifact.js --input circuit-spec.json --target circuitikz --output circuit.tex --preview-svg-output circuit.svg --preview-png-output circuit.png --preview-pdf-output circuit.pdf node scripts/export-diagram-artifact.js --input spec.json --target png --output figure.png --ppi 300 node scripts/export-diagram-artifact.js --input spec.json --target pdf --output figure.pdf --ppi 300 `); @@ -36,6 +39,7 @@ Example: function parseArgs(argv) { const args = {}; + const positional = []; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; @@ -66,13 +70,48 @@ function parseArgs(argv) { args.help = true; break; default: + if (!token.startsWith('-')) { + positional.push(token); + break; + } throw new Error(`Unknown argument: ${token}`); } } + applyPositionalArgs(args, positional); return args; } +function applyPositionalArgs(args, positional) { + if (positional.length === 0) { + return; + } + + const positionalFields = [ + 'input', + 'target', + 'output', + 'previewSvgOutput', + 'previewPngOutput', + 'previewPdfOutput', + 'ppi' + ]; + if (positional.length > positionalFields.length) { + throw new Error(`Unknown positional argument: ${positional[positionalFields.length]}`); + } + + positional.forEach((value, index) => { + const field = positionalFields[index]; + if (args[field] === undefined) { + args[field] = value; + return; + } + if (args[field] !== value) { + throw new Error(`Positional argument "${value}" conflicts with --${field}.`); + } + }); +} + function normalizePpi(value) { if (value === undefined || value === null || value === '') { return DEFAULT_EXPORT_PPI; @@ -335,8 +374,40 @@ function buildExporterBundle(repoRoot) { import { exportSemanticFigureModelToDrawioXml, collectDrawioVisibleLabelMismatches } from './src/diagram/adapters/drawio/drawioExporter'; import { exportSemanticFigureModelToDrawnixData, stringifyDrawnixExportedData, validateDrawnixExportedDataSubset } from './src/diagram/adapters/drawnix/drawnixExporter'; import { EditableHtmlSvgRenderer, collectEditableSvgAnnotationGaps, renderSemanticFigureSvg } from './src/rendering/renderers/editableHtmlSvgRenderer'; + import { CircuitikzRenderer, renderCircuitSpecPreviewSvg } from './src/rendering/renderers/circuitikzRenderer'; export async function exportDiagramArtifact(spec, target) { + if (target === 'circuitikz') { + const renderer = new CircuitikzRenderer(); + const artifact = await renderer.render(spec); + return { + content: artifact.content, + previewSvgContent: artifact.previewSvg?.content, + summary: { + mimeType: artifact.mimeType, + circuitKind: spec.circuitSpec?.circuitKind, + goldenReferenceId: spec.circuitSpec?.goldenReferenceId, + componentCount: Array.isArray(spec.circuitSpec?.components) ? spec.circuitSpec.components.length : 0, + connectionCount: Array.isArray(spec.circuitSpec?.connections) ? spec.circuitSpec.connections.length : 0 + } + }; + } + + if ((target === 'svg' || target === 'png' || target === 'pdf') && spec?.intent === 'circuit' && spec.circuitSpec) { + const svg = renderCircuitSpecPreviewSvg(spec.circuitSpec); + return { + content: svg, + previewSvgContent: svg, + summary: { + mimeType: target === 'svg' ? 'image/svg+xml' : target === 'png' ? 'image/png' : 'application/pdf', + circuitKind: spec.circuitSpec.circuitKind, + goldenReferenceId: spec.circuitSpec.goldenReferenceId, + componentCount: Array.isArray(spec.circuitSpec.components) ? spec.circuitSpec.components.length : 0, + connectionCount: Array.isArray(spec.circuitSpec.connections) ? spec.circuitSpec.connections.length : 0 + } + }; + } + const model = buildSemanticFigureModel(spec); if (target === 'editable-html-svg') { diff --git a/src/diagram/diagramGenerationService.ts b/src/diagram/diagramGenerationService.ts index 5b34322a..27a3145c 100644 --- a/src/diagram/diagramGenerationService.ts +++ b/src/diagram/diagramGenerationService.ts @@ -5,6 +5,7 @@ import { HtmlRenderer } from '../rendering/renderers/htmlRenderer'; import { EditableHtmlSvgRenderer } from '../rendering/renderers/editableHtmlSvgRenderer'; import { DrawioRenderer } from '../rendering/renderers/drawioRenderer'; import { DrawnixRenderer } from '../rendering/renderers/drawnixRenderer'; +import { CircuitikzRenderer } from '../rendering/renderers/circuitikzRenderer'; import { RendererRegistry } from '../rendering/rendererRegistry'; import { RenderArtifact } from '../rendering/types'; import { RendererService } from '../rendering/rendererService'; @@ -131,6 +132,7 @@ function createDefaultRendererService(): RendererService { new EditableHtmlSvgRenderer(), new DrawioRenderer(), new DrawnixRenderer(), + new CircuitikzRenderer(), new HtmlRenderer() ])); } @@ -141,11 +143,34 @@ function resolveRenderTargetForIntent(intent: DiagramIntent): DiagramPlan['rende return 'json-canvas'; case 'dataChart': return 'vega-lite'; + case 'circuit': + return 'circuitikz'; default: return 'mermaid'; } } +function resolvePromptPreferredIntent(plan: DiagramPlan, requestedIntent?: DiagramIntent): DiagramIntent { + if (requestedIntent) { + return requestedIntent; + } + + return plan.renderTarget === 'circuitikz' ? 'circuit' : plan.intent; +} + +function buildGenerationPrompt( + plan: DiagramPlan, + options: Pick<DiagramGenerationOptions, 'requestedIntent' | 'targetLanguage'> +): string { + return buildDiagramSpecPrompt({ + preferredIntent: resolvePromptPreferredIntent(plan, options.requestedIntent), + requiredIntent: options.requestedIntent, + preferredChartType: plan.preferredChartType, + preferredRenderTarget: plan.renderTarget, + targetLanguage: options.targetLanguage + }); +} + function resolveLegacyCompatibleIntent(spec: DiagramSpec, plan: DiagramPlan): DiagramIntent { const requestedIntent = spec.intent || plan.intent; @@ -185,6 +210,7 @@ function mergeSpecDefaults(spec: DiagramSpec, plan: DiagramPlan): DiagramSpec { sections: spec.sections ?? [], callouts: spec.callouts ?? [], dataSeries: spec.dataSeries ?? [], + circuitSpec: resolvedIntent === 'circuit' ? spec.circuitSpec : undefined, layoutHints: Object.keys(normalizedLayoutHints).length > 0 ? normalizedLayoutHints : undefined, evidenceRefs: spec.evidenceRefs ?? [] }; @@ -221,12 +247,7 @@ export async function generateDiagramArtifact( requestedRenderTarget: options.requestedRenderTarget }); - const prompt = buildDiagramSpecPrompt({ - preferredIntent: plan.intent, - requiredIntent: options.requestedIntent, - preferredChartType: plan.preferredChartType, - targetLanguage: options.targetLanguage - }); + const prompt = buildGenerationPrompt(plan, options); let rawResponse = await options.llmInvoker(prompt, markdown); let parsedSpec = parseDiagramSpecResponse(rawResponse); @@ -235,12 +256,8 @@ export async function generateDiagramArtifact( // If user requested a specific intent and LLM returned a different one, retry with stronger prompt if (options.requestedIntent && spec.intent !== options.requestedIntent) { - const retryPrompt = buildDiagramSpecPrompt({ - preferredIntent: plan.intent, - requiredIntent: options.requestedIntent, - preferredChartType: plan.preferredChartType, - targetLanguage: options.targetLanguage - }) + `\n\nCRITICAL: Your previous response used intent "${spec.intent}" but the required intent is "${options.requestedIntent}". This is incorrect. You MUST use "${options.requestedIntent}" as the diagram intent. Do not choose any other intent. Regenerate the DiagramSpec with the correct intent.`; + const retryPrompt = buildGenerationPrompt(plan, options) + + `\n\nCRITICAL: Your previous response used intent "${spec.intent}" but the required intent is "${options.requestedIntent}". This is incorrect. You MUST use "${options.requestedIntent}" as the diagram intent. Do not choose any other intent. Regenerate the DiagramSpec with the correct intent.`; rawResponse = await options.llmInvoker(retryPrompt, markdown); parsedSpec = parseDiagramSpecResponse(rawResponse); @@ -265,11 +282,9 @@ export async function generateDiagramArtifact( const errorMsg = renderError instanceof Error ? renderError.message : String(renderError); // If Mermaid parse failed, retry once with the LLM asking for valid Mermaid syntax if (errorMsg.includes('Mermaid diagram failed validation') || errorMsg.includes('Parse error')) { - const retryPrompt = buildDiagramSpecPrompt({ - preferredIntent: spec.intent, - requiredIntent: options.requestedIntent, - preferredChartType: plan.preferredChartType, - targetLanguage: options.targetLanguage + const retryPrompt = buildGenerationPrompt(plan, { + ...options, + requestedIntent: options.requestedIntent ?? spec.intent }) + `\n\nCRITICAL: Your previous diagram spec rendered invalid Mermaid syntax. The error was: ${errorMsg}. Please regenerate the DiagramSpec with valid, well-formed content. Ensure entity names have no trailing spaces, all braces are properly closed, and the syntax follows standard Mermaid conventions.`; const retryResponse = await options.llmInvoker(retryPrompt, markdown); diff --git a/src/diagram/diagramSpecResponseParser.ts b/src/diagram/diagramSpecResponseParser.ts index d76d9dbe..06aa8816 100644 --- a/src/diagram/diagramSpecResponseParser.ts +++ b/src/diagram/diagramSpecResponseParser.ts @@ -217,6 +217,7 @@ function normalizeSpec(candidate: any): DiagramSpec { sections: Array.isArray(payload.sections) ? payload.sections : [], callouts: Array.isArray(payload.callouts) ? payload.callouts : [], dataSeries: normalizeDataSeries(payload.dataSeries, title), + circuitSpec: payload.circuitSpec && typeof payload.circuitSpec === 'object' ? payload.circuitSpec : undefined, layoutHints: payload.layoutHints && typeof payload.layoutHints === 'object' ? payload.layoutHints : undefined, sourceLanguage: typeof payload.sourceLanguage === 'string' ? payload.sourceLanguage : undefined, outputLanguage: typeof payload.outputLanguage === 'string' ? payload.outputLanguage : undefined, diff --git a/src/diagram/intent.ts b/src/diagram/intent.ts index 7a4cc243..4ac2cf31 100644 --- a/src/diagram/intent.ts +++ b/src/diagram/intent.ts @@ -20,6 +20,32 @@ export function inferDiagramIntent(markdown: string): DiagramIntentResult { return scoreResult('mindmap', 0.2, ['empty content defaults to hierarchical summary']); } + const circuitReasons: string[] = []; + const circuitKeywords = countMatches(normalized, [ + /\bcircuitikz\b/g, + /\btikzjax\b/g, + /\bcircuit\b/g, + /\bschematic\b/g, + /\bcmos\b/g, + /\bnmos\b/g, + /\bpmos\b/g, + /\bmosfet\b/g, + /\btransistor\b/g, + /\binverter\b/g, + /\bcommon-source\b/g, + /\btransmission gate\b/g, + /\bnand\b/g, + /\bnor\b/g, + /\bvdd\b/g, + /\bgnd\b/g, + /\bvin\b/g, + /\bvout\b/g + ]); + if (circuitKeywords >= 2) { + circuitReasons.push('circuit/cmos/mos vocabulary detected'); + return scoreResult('circuit', 0.86, circuitReasons); + } + const chartReasons: string[] = []; const hasMarkdownTable = /\|.+\|/.test(normalized) && /\|\s*-+\s*\|/.test(normalized); const numericCells = (normalized.match(/\|\s*\d+(?:\.\d+)?\s*\|/g) ?? []).length; diff --git a/src/diagram/planner.ts b/src/diagram/planner.ts index e637602b..c6055cc1 100644 --- a/src/diagram/planner.ts +++ b/src/diagram/planner.ts @@ -13,6 +13,8 @@ function resolvePreferredRenderTarget(intent: DiagramIntent): RenderTarget { switch (intent) { case 'canvasMap': return 'json-canvas'; + case 'circuit': + return 'circuitikz'; case 'dataChart': return 'vega-lite'; default: @@ -49,6 +51,10 @@ function resolveFallbackTargets( return []; } + if (primaryTarget === 'circuitikz') { + return []; + } + const fallbackTargets: RenderTarget[] = []; if (primaryTarget !== defaultTarget) { diff --git a/src/diagram/prompts/diagramSpecPrompt.ts b/src/diagram/prompts/diagramSpecPrompt.ts index 5e67e6d1..589dd427 100644 --- a/src/diagram/prompts/diagramSpecPrompt.ts +++ b/src/diagram/prompts/diagramSpecPrompt.ts @@ -1,10 +1,11 @@ import { SUPPORTED_VEGA_LITE_CHART_TYPES, SupportedVegaLiteChartType } from '../adapters/vega/schema'; -import { DiagramIntent } from '../types'; +import { DiagramIntent, RenderTarget } from '../types'; export interface DiagramSpecPromptOptions { preferredIntent?: DiagramIntent; requiredIntent?: DiagramIntent; preferredChartType?: SupportedVegaLiteChartType; + preferredRenderTarget?: RenderTarget; targetLanguage?: string; } @@ -18,6 +19,15 @@ export function buildDiagramSpecPrompt(options: DiagramSpecPromptOptions = {}): const preferredChartTypeLine = options.preferredIntent === 'dataChart' && options.preferredChartType ? `Preferred chart template: ${options.preferredChartType}. Use it when the extracted data supports it.` : ''; + const circuitikzTargetLine = options.preferredIntent === 'circuit' || options.preferredRenderTarget === 'circuitikz' + ? `Circuitikz target rules: +- Set intent: circuit. +- Include a circuitSpec object. Do not encode circuit topology in generic nodes/edges only. +- circuitSpec.style.package must be "circuitikz". +- Use only supported goldenReferenceId values: common-source-nmos-v1, cmos-inverter-v1, cmos-buffer-v1, cmos-transmission-gate-v1, cmos-nand2-v1, cmos-nor2-v1. +- Use layoutHints.inputSide, layoutHints.outputSide, and layoutHints.routingStyle: "orthogonal" when placement is known. +- Do not output raw TikZ or circuitikz source. Return structured JSON only.` + : ''; const targetLanguageLine = options.targetLanguage ? `Write all human-readable labels in ${options.targetLanguage}.` @@ -40,10 +50,12 @@ Supported intents: - erDiagram - stateDiagram - canvasMap +- circuit - dataChart ${preferredIntentLine} ${preferredChartTypeLine} +${circuitikzTargetLine} ${targetLanguageLine} Required DiagramSpec fields: diff --git a/src/diagram/spec.ts b/src/diagram/spec.ts index 07f23408..fd28763d 100644 --- a/src/diagram/spec.ts +++ b/src/diagram/spec.ts @@ -1,4 +1,5 @@ import { ValidationError } from '../types'; +import { validateCircuitSpec } from './adapters/circuitikz/circuitikzExporter'; import { isSupportedVegaLiteChartType, SUPPORTED_VEGA_LITE_CHART_TYPES } from './adapters/vega/schema'; import { DiagramDataSeries, DiagramNode, DiagramSpec, isSupportedDiagramIntent, SUPPORTED_DIAGRAM_INTENTS } from './types'; @@ -117,6 +118,27 @@ function validateNonChartLayoutHints(spec: DiagramSpec, errors: string[]): void } } +function validateCircuitPayload(spec: DiagramSpec, errors: string[]): void { + if (spec.circuitSpec && spec.intent !== 'circuit') { + errors.push('DiagramSpec.circuitSpec is only valid when intent is "circuit".'); + return; + } + + if (spec.intent !== 'circuit') { + return; + } + + if (!spec.circuitSpec) { + errors.push('Diagram intent "circuit" requires a CircuitSpec payload.'); + return; + } + + const circuitValidation = validateCircuitSpec(spec.circuitSpec); + for (const error of circuitValidation.errors) { + errors.push(`CircuitSpec: ${error}`); + } +} + export function validateDiagramSpec(spec: DiagramSpec): DiagramSpecValidationResult { const errors: string[] = []; @@ -129,7 +151,7 @@ export function validateDiagramSpec(spec: DiagramSpec): DiagramSpecValidationRes const nodeIds = new Set<string>(); collectNodeIds(spec.nodes ?? [], nodeIds, errors); - if (spec.intent !== 'dataChart' && nodeIds.size === 0) { + if (spec.intent !== 'dataChart' && spec.intent !== 'circuit' && nodeIds.size === 0) { errors.push(`Diagram intent "${spec.intent}" requires at least one node.`); } @@ -149,6 +171,8 @@ export function validateDiagramSpec(spec: DiagramSpec): DiagramSpecValidationRes validateNonChartLayoutHints(spec, errors); } + validateCircuitPayload(spec, errors); + return { valid: errors.length === 0, errors diff --git a/src/diagram/types.ts b/src/diagram/types.ts index 4ae229ff..f513d512 100644 --- a/src/diagram/types.ts +++ b/src/diagram/types.ts @@ -1,4 +1,5 @@ import type { SupportedVegaLiteChartType } from './adapters/vega/schema'; +import type { CircuitSpec } from './adapters/circuitikz/circuitSpec'; export const SUPPORTED_DIAGRAM_INTENTS = [ 'mindmap', @@ -8,6 +9,7 @@ export const SUPPORTED_DIAGRAM_INTENTS = [ 'erDiagram', 'stateDiagram', 'canvasMap', + 'circuit', 'dataChart' ] as const; @@ -25,7 +27,8 @@ export const SUPPORTED_RENDER_TARGETS = [ 'html', 'editable-html-svg', 'drawio', - 'drawnix' + 'drawnix', + 'circuitikz' ] as const; export type RenderTarget = typeof SUPPORTED_RENDER_TARGETS[number]; @@ -89,6 +92,7 @@ export interface DiagramSpec { sections?: DiagramSection[]; callouts?: DiagramCallout[]; dataSeries?: DiagramDataSeries[]; + circuitSpec?: CircuitSpec; layoutHints?: Record<string, string | number | boolean>; sourceLanguage?: string; outputLanguage?: string; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 10448e15..8635fd1d 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -141,6 +141,7 @@ export const STRINGS_EN = { intentErDiagram: 'ER diagram', intentStateDiagram: 'State diagram', intentCanvasMap: 'Canvas / concept map', + intentCircuit: 'Circuit (Circuitikz)', intentDataChart: 'Data chart (Vega-Lite)', renderTargetName: 'Preferred render target', renderTargetDesc: 'Select the artifact renderer for Generate diagram and preview commands. Auto follows the selected diagram type. The standard Mermaid command still exports Mermaid.', @@ -152,6 +153,7 @@ export const STRINGS_EN = { renderTargetEditableHtmlSvg: 'Editable HTML/SVG', renderTargetDrawio: 'Draw.io + SVG preview', renderTargetDrawnix: 'Drawnix + SVG preview', + renderTargetCircuitikz: 'Circuitikz + SVG preview', exportPpiName: 'Diagram image export PPI', exportPpiDesc: 'Resolution for diagram PNG/PDF preview exports. Default 300 ppi; maximum 600 ppi. SVG keeps vector size and ignores this value.', }, diff --git a/src/i18n/locales/experimentalDiagramPipeline.ts b/src/i18n/locales/experimentalDiagramPipeline.ts index 37bb1cda..a946fdc8 100644 --- a/src/i18n/locales/experimentalDiagramPipeline.ts +++ b/src/i18n/locales/experimentalDiagramPipeline.ts @@ -32,7 +32,9 @@ export const EXPERIMENTAL_DIAGRAM_PIPELINE_LOCALE_EXTENSIONS: Record<string, Dee compatibilityDesc: 'Wählen Sie, wie offensiv experimentelle Diagrammerzeugung und Vorschau Ziele auswählen. "Mermaid-kompatibel" hält Mermaid-kompatible Ausgabe bei. "Beste Passung" versucht zuerst reichere Intents und fällt bei fehlender Unterstützung zurück. Der Standardbefehl "Als Mermaid-Diagramm zusammenfassen" pinnt den spec-first-Pfad immer auf Mermaid-kompatible Ausgabe.', compatibilityLegacy: 'Mermaid-kompatibel', - compatibilityBestFit: 'Beste Passung' + compatibilityBestFit: 'Beste Passung', + intentCircuit: 'Schaltung (Circuitikz)', + renderTargetCircuitikz: 'Circuitikz + SVG-Vorschau' } } } @@ -49,7 +51,9 @@ export const EXPERIMENTAL_DIAGRAM_PIPELINE_LOCALE_EXTENSIONS: Record<string, Dee compatibilityDesc: 'Elige con qué agresividad la generación y vista previa experimentales seleccionan el destino del diagrama. "Compatible con Mermaid" mantiene una salida compatible con Mermaid. "Mejor ajuste" puede probar primero intenciones más ricas y retroceder cuando no haya soporte. El comando estándar "Resumir como diagrama Mermaid" siempre fija la ruta spec-first a una salida compatible con Mermaid.', compatibilityLegacy: 'Compatible con Mermaid', - compatibilityBestFit: 'Mejor ajuste' + compatibilityBestFit: 'Mejor ajuste', + intentCircuit: 'Circuito (Circuitikz)', + renderTargetCircuitikz: 'Circuitikz + vista previa SVG' } } } @@ -83,7 +87,9 @@ export const EXPERIMENTAL_DIAGRAM_PIPELINE_LOCALE_EXTENSIONS: Record<string, Dee compatibilityDesc: 'Choisissez à quel point la génération et l’aperçu expérimentaux sélectionnent agressivement la cible du diagramme. « Compatible Mermaid » conserve une sortie compatible Mermaid. « Meilleur ajustement » peut essayer d’abord des intentions plus riches puis revenir en arrière si elles ne sont pas prises en charge. La commande standard « Résumer en diagramme Mermaid » force toujours le chemin spec-first à rester compatible Mermaid.', compatibilityLegacy: 'Compatible Mermaid', - compatibilityBestFit: 'Meilleur ajustement' + compatibilityBestFit: 'Meilleur ajustement', + intentCircuit: 'Circuit (Circuitikz)', + renderTargetCircuitikz: 'Circuitikz + aperçu SVG' } } } @@ -134,7 +140,9 @@ export const EXPERIMENTAL_DIAGRAM_PIPELINE_LOCALE_EXTENSIONS: Record<string, Dee compatibilityDesc: '実験的な図生成とプレビューがどれだけ積極的に図ターゲットを選ぶかを指定します。「Mermaid 互換」は Mermaid 互換出力を維持します。「最適一致」はより豊かな intent を先に試し、未対応ならフォールバックします。標準の「Mermaid 図として要約」コマンドは、spec-first 経路を常に Mermaid 互換出力へ固定します。', compatibilityLegacy: 'Mermaid 互換', - compatibilityBestFit: '最適一致' + compatibilityBestFit: '最適一致', + intentCircuit: '回路 (Circuitikz)', + renderTargetCircuitikz: 'Circuitikz + SVG プレビュー' } } } @@ -151,7 +159,9 @@ export const EXPERIMENTAL_DIAGRAM_PIPELINE_LOCALE_EXTENSIONS: Record<string, Dee compatibilityDesc: '실험적 다이어그램 생성과 미리보기가 얼마나 적극적으로 다이어그램 대상을 선택할지 지정합니다. "Mermaid 호환"은 Mermaid 호환 출력을 유지합니다. "최적 적합"은 더 풍부한 intent를 먼저 시도하고 지원되지 않으면 되돌아갑니다. 기본 "Mermaid 다이어그램으로 요약" 명령은 spec-first 경로를 항상 Mermaid 호환 출력으로 고정합니다.', compatibilityLegacy: 'Mermaid 호환', - compatibilityBestFit: '최적 적합' + compatibilityBestFit: '최적 적합', + intentCircuit: '회로 (Circuitikz)', + renderTargetCircuitikz: 'Circuitikz + SVG 미리보기' } } } diff --git a/src/i18n/locales/zh_cn.ts b/src/i18n/locales/zh_cn.ts index 8d2aaf03..c8eac796 100644 --- a/src/i18n/locales/zh_cn.ts +++ b/src/i18n/locales/zh_cn.ts @@ -144,6 +144,7 @@ export const STRINGS_ZH_CN: DeepPartial<NotemdEnglishStrings> = { intentErDiagram: 'ER 图', intentStateDiagram: '状态图', intentCanvasMap: '画布 / 概念图', + intentCircuit: '电路图 (Circuitikz)', intentDataChart: '数据图表 (Vega-Lite)', renderTargetName: '首选渲染目标', renderTargetDesc: '选择 Generate diagram 与预览命令使用的 artifact renderer。自动模式会跟随所选图表类型;标准 Mermaid 命令仍导出 Mermaid。', @@ -155,6 +156,7 @@ export const STRINGS_ZH_CN: DeepPartial<NotemdEnglishStrings> = { renderTargetEditableHtmlSvg: '可编辑 HTML/SVG', renderTargetDrawio: 'Draw.io + SVG 预览', renderTargetDrawnix: 'Drawnix + SVG 预览', + renderTargetCircuitikz: 'Circuitikz + SVG 预览', exportPpiName: '图形图片导出 PPI', exportPpiDesc: '图形 PNG/PDF 预览导出的分辨率。默认 300 ppi,最高 600 ppi。SVG 保持矢量尺寸,不受此设置影响。', }, diff --git a/src/i18n/locales/zh_tw.ts b/src/i18n/locales/zh_tw.ts index fa617dae..aedfa3f7 100644 --- a/src/i18n/locales/zh_tw.ts +++ b/src/i18n/locales/zh_tw.ts @@ -144,6 +144,7 @@ export const STRINGS_ZH_TW: DeepPartial<NotemdEnglishStrings> = { intentErDiagram: 'ER 圖', intentStateDiagram: '狀態圖', intentCanvasMap: '畫布 / 概念圖', + intentCircuit: '電路圖 (Circuitikz)', intentDataChart: '資料圖表 (Vega-Lite)', renderTargetName: '首選渲染目標', renderTargetDesc: '選擇 Generate diagram 與預覽命令使用的 artifact renderer。自動模式會跟隨所選圖表類型;標準 Mermaid 命令仍匯出 Mermaid。', @@ -155,6 +156,7 @@ export const STRINGS_ZH_TW: DeepPartial<NotemdEnglishStrings> = { renderTargetEditableHtmlSvg: '可編輯 HTML/SVG', renderTargetDrawio: 'Draw.io + SVG 預覽', renderTargetDrawnix: 'Drawnix + SVG 預覽', + renderTargetCircuitikz: 'Circuitikz + SVG 預覽', exportPpiName: '圖形圖片匯出 PPI', exportPpiDesc: '圖形 PNG/PDF 預覽匯出的解析度。預設 300 ppi,最高 600 ppi。SVG 保持向量尺寸,不受此設定影響。', }, diff --git a/src/rendering/renderers/circuitikzRenderer.ts b/src/rendering/renderers/circuitikzRenderer.ts new file mode 100644 index 00000000..0a5e90a3 --- /dev/null +++ b/src/rendering/renderers/circuitikzRenderer.ts @@ -0,0 +1,363 @@ +import { assertValidDiagramSpec } from '../../diagram/spec'; +import { DiagramSpec } from '../../diagram/types'; +import { CircuitSpec } from '../../diagram/adapters/circuitikz/circuitSpec'; +import { + exportCircuitSpecToCircuitikz, + validateCircuitSpec +} from '../../diagram/adapters/circuitikz/circuitikzExporter'; +import { ValidationError } from '../../types'; +import { DiagramRenderer, RenderArtifact } from '../types'; + +export const NOTEMD_CIRCUITIKZ_PREVIEW_SVG_RENDERER_VERSION = 'notemd-circuitikz-preview-svg@0.1.0'; + +type Point = { + x: number; + y: number; +}; + +type SvgFragment = string; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function normalizeMathLabel(value: string): string { + return value + .replace(/^\$|\$$/g, '') + .replace(/\\bar\{([^}]+)\}/g, '$1_bar') + .replace(/\\/g, ''); +} + +function componentLabel(spec: CircuitSpec, id: string, fallback: string): string { + return normalizeMathLabel(spec.components.find(component => component.id === id)?.label ?? fallback); +} + +function line(start: Point, end: Point, extra = ''): SvgFragment { + return `<line x1="${start.x}" y1="${start.y}" x2="${end.x}" y2="${end.y}"${extra} />`; +} + +function wire(points: Point[]): SvgFragment { + return `<polyline points="${points.map(point => `${point.x},${point.y}`).join(' ')}" />`; +} + +function text(value: string, at: Point, extra = ''): SvgFragment { + return `<text x="${at.x}" y="${at.y}"${extra}>${escapeHtml(value)}</text>`; +} + +function port(label: string, at: Point, side: 'left' | 'right' | 'top' | 'bottom' = 'right'): SvgFragment { + const textOffset = { + left: { x: -12, y: 4, anchor: 'end' }, + right: { x: 12, y: 4, anchor: 'start' }, + top: { x: 0, y: -10, anchor: 'middle' }, + bottom: { x: 0, y: 18, anchor: 'middle' } + }[side]; + + return `<circle cx="${at.x}" cy="${at.y}" r="4" class="notemd-circuit-port" /> + ${text(label, { x: at.x + textOffset.x, y: at.y + textOffset.y }, ` text-anchor="${textOffset.anchor}" class="notemd-circuit-label"`)} + `; +} + +function junction(at: Point): SvgFragment { + return `<circle cx="${at.x}" cy="${at.y}" r="4" class="notemd-circuit-junction" />`; +} + +function ground(at: Point): SvgFragment { + return `<g class="notemd-circuit-ground"> + ${line({ x: at.x, y: at.y - 18 }, at)} + ${line({ x: at.x - 18, y: at.y }, { x: at.x + 18, y: at.y })} + ${line({ x: at.x - 12, y: at.y + 8 }, { x: at.x + 12, y: at.y + 8 })} + ${line({ x: at.x - 6, y: at.y + 16 }, { x: at.x + 6, y: at.y + 16 })} + </g>`; +} + +function vdd(at: Point): SvgFragment { + return `<g class="notemd-circuit-vdd"> + ${line(at, { x: at.x, y: at.y + 18 })} + ${text('VDD', { x: at.x, y: at.y - 8 }, ' text-anchor="middle" class="notemd-circuit-label"')} + </g>`; +} + +function resistor(id: string, at: Point, label: string): SvgFragment { + const top = { x: at.x, y: at.y - 46 }; + const bottom = { x: at.x, y: at.y + 46 }; + const zigzag = [ + `${at.x},${at.y - 30}`, + `${at.x - 14},${at.y - 20}`, + `${at.x + 14},${at.y}`, + `${at.x - 14},${at.y + 20}`, + `${at.x},${at.y + 30}` + ].join(' '); + + return `<g id="${escapeHtml(id)}" class="notemd-circuit-component notemd-circuit-resistor" data-component-id="${escapeHtml(id)}"> + ${line(top, { x: at.x, y: at.y - 30 })} + <polyline points="${zigzag}" /> + ${line({ x: at.x, y: at.y + 30 }, bottom)} + ${text(label, { x: at.x + 28, y: at.y + 4 }, ' class="notemd-circuit-label"')} + </g>`; +} + +function mos(id: string, type: 'pmos' | 'nmos', at: Point, label: string): SvgFragment { + const bubble = type === 'pmos' + ? `<circle cx="${at.x - 26}" cy="${at.y}" r="5" class="notemd-circuit-mos-bubble" />` + : ''; + + return `<g id="${escapeHtml(id)}" class="notemd-circuit-component notemd-circuit-mos notemd-circuit-${type}" data-component-id="${escapeHtml(id)}"> + <rect x="${at.x - 22}" y="${at.y - 40}" width="44" height="80" rx="6" /> + ${line({ x: at.x, y: at.y - 62 }, { x: at.x, y: at.y - 40 })} + ${line({ x: at.x, y: at.y + 40 }, { x: at.x, y: at.y + 62 })} + ${line({ x: at.x - 48, y: at.y }, { x: at.x - 22, y: at.y })} + ${bubble} + ${text(label, { x: at.x + 34, y: at.y + 5 }, ' class="notemd-circuit-label"')} + </g>`; +} + +function connectionLabel(value: string, at: Point): SvgFragment { + return text(value, at, ' text-anchor="middle" class="notemd-circuit-net-label"'); +} + +function renderCommonSource(spec: CircuitSpec): SvgFragment { + const m1Label = componentLabel(spec, 'M1', 'M1'); + const rdLabel = componentLabel(spec, 'RD', 'RD'); + + return ` + ${vdd({ x: 360, y: 78 })} + ${resistor('RD', { x: 360, y: 170 }, rdLabel)} + ${line({ x: 360, y: 216 }, { x: 360, y: 258 })} + ${mos('M1', 'nmos', { x: 360, y: 320 }, m1Label)} + ${ground({ x: 360, y: 416 })} + ${wire([{ x: 104, y: 320 }, { x: 312, y: 320 }])} + ${port('v_{in}', { x: 104, y: 320 }, 'left')} + ${wire([{ x: 360, y: 236 }, { x: 604, y: 236 }])} + ${junction({ x: 360, y: 236 })} + ${port('v_{out}', { x: 604, y: 236 }, 'right')} + ${connectionLabel('common-source-amplifier', { x: 360, y: 488 })} + `; +} + +function renderCmosInverter(spec: CircuitSpec): SvgFragment { + const mpLabel = componentLabel(spec, 'MP', 'MP'); + const mnLabel = componentLabel(spec, 'MN', 'MN'); + + return ` + ${vdd({ x: 360, y: 72 })} + ${mos('MP', 'pmos', { x: 360, y: 178 }, mpLabel)} + ${line({ x: 360, y: 90 }, { x: 360, y: 116 })} + ${line({ x: 360, y: 240 }, { x: 360, y: 270 })} + ${mos('MN', 'nmos', { x: 360, y: 332 }, mnLabel)} + ${ground({ x: 360, y: 428 })} + ${wire([{ x: 106, y: 255 }, { x: 206, y: 255 }, { x: 206, y: 178 }, { x: 312, y: 178 }])} + ${wire([{ x: 206, y: 255 }, { x: 206, y: 332 }, { x: 312, y: 332 }])} + ${port('v_{in}', { x: 106, y: 255 }, 'left')} + ${wire([{ x: 360, y: 255 }, { x: 604, y: 255 }])} + ${junction({ x: 360, y: 255 })} + ${port('v_{out}', { x: 604, y: 255 }, 'right')} + `; +} + +function renderCmosBuffer(spec: CircuitSpec): SvgFragment { + const mp1Label = componentLabel(spec, 'MP1', 'MP1'); + const mn1Label = componentLabel(spec, 'MN1', 'MN1'); + const mp2Label = componentLabel(spec, 'MP2', 'MP2'); + const mn2Label = componentLabel(spec, 'MN2', 'MN2'); + + return ` + ${vdd({ x: 260, y: 72 })} + ${vdd({ x: 500, y: 72 })} + ${mos('MP1', 'pmos', { x: 260, y: 178 }, mp1Label)} + ${mos('MN1', 'nmos', { x: 260, y: 332 }, mn1Label)} + ${mos('MP2', 'pmos', { x: 500, y: 178 }, mp2Label)} + ${mos('MN2', 'nmos', { x: 500, y: 332 }, mn2Label)} + ${line({ x: 260, y: 240 }, { x: 260, y: 270 })} + ${line({ x: 500, y: 240 }, { x: 500, y: 270 })} + ${ground({ x: 260, y: 428 })} + ${ground({ x: 500, y: 428 })} + ${wire([{ x: 84, y: 255 }, { x: 132, y: 255 }, { x: 132, y: 178 }, { x: 212, y: 178 }])} + ${wire([{ x: 132, y: 255 }, { x: 132, y: 332 }, { x: 212, y: 332 }])} + ${port('v_{in}', { x: 84, y: 255 }, 'left')} + ${wire([{ x: 260, y: 255 }, { x: 382, y: 255 }, { x: 382, y: 178 }, { x: 452, y: 178 }])} + ${wire([{ x: 382, y: 255 }, { x: 382, y: 332 }, { x: 452, y: 332 }])} + ${junction({ x: 260, y: 255 })} + ${connectionLabel('v_{mid}', { x: 382, y: 244 })} + ${wire([{ x: 500, y: 255 }, { x: 652, y: 255 }])} + ${junction({ x: 500, y: 255 })} + ${port('v_{out}', { x: 652, y: 255 }, 'right')} + `; +} + +function renderTransmissionGate(spec: CircuitSpec): SvgFragment { + const mpLabel = componentLabel(spec, 'MP', 'MP'); + const mnLabel = componentLabel(spec, 'MN', 'MN'); + + return ` + ${port('v_{in}', { x: 118, y: 260 }, 'left')} + ${wire([{ x: 118, y: 260 }, { x: 240, y: 260 }, { x: 240, y: 196 }])} + ${wire([{ x: 240, y: 260 }, { x: 240, y: 324 }])} + ${mos('MP', 'pmos', { x: 330, y: 196 }, mpLabel)} + ${mos('MN', 'nmos', { x: 330, y: 324 }, mnLabel)} + ${wire([{ x: 330, y: 258 }, { x: 500, y: 258 }, { x: 500, y: 196 }])} + ${wire([{ x: 500, y: 258 }, { x: 500, y: 324 }])} + ${port('v_{out}', { x: 610, y: 258 }, 'right')} + ${wire([{ x: 500, y: 258 }, { x: 610, y: 258 }])} + ${wire([{ x: 282, y: 196 }, { x: 330, y: 128 }, { x: 378, y: 196 }])} + ${wire([{ x: 282, y: 324 }, { x: 330, y: 392 }, { x: 378, y: 324 }])} + ${port('phi_bar', { x: 330, y: 112 }, 'top')} + ${port('phi', { x: 330, y: 410 }, 'bottom')} + `; +} + +function renderCmosNand2(spec: CircuitSpec): SvgFragment { + const mpaLabel = componentLabel(spec, 'MPA', 'MPA'); + const mpbLabel = componentLabel(spec, 'MPB', 'MPB'); + const mnaLabel = componentLabel(spec, 'MNA', 'MNA'); + const mnbLabel = componentLabel(spec, 'MNB', 'MNB'); + + return ` + ${vdd({ x: 360, y: 64 })} + ${wire([{ x: 360, y: 82 }, { x: 360, y: 112 }, { x: 268, y: 112 }, { x: 268, y: 140 }])} + ${wire([{ x: 360, y: 112 }, { x: 452, y: 112 }, { x: 452, y: 140 }])} + ${mos('MPA', 'pmos', { x: 268, y: 202 }, mpaLabel)} + ${mos('MPB', 'pmos', { x: 452, y: 202 }, mpbLabel)} + ${wire([{ x: 268, y: 264 }, { x: 452, y: 264 }, { x: 604, y: 264 }])} + ${port('v_{out}', { x: 604, y: 264 }, 'right')} + ${mos('MNA', 'nmos', { x: 360, y: 326 }, mnaLabel)} + ${mos('MNB', 'nmos', { x: 360, y: 432 }, mnbLabel)} + ${wire([{ x: 360, y: 264 }, { x: 360, y: 264 }])} + ${ground({ x: 360, y: 528 })} + ${wire([{ x: 94, y: 250 }, { x: 178, y: 250 }, { x: 178, y: 202 }, { x: 220, y: 202 }])} + ${wire([{ x: 178, y: 250 }, { x: 178, y: 326 }, { x: 312, y: 326 }])} + ${port('v_A', { x: 94, y: 250 }, 'left')} + ${wire([{ x: 94, y: 354 }, { x: 204, y: 354 }, { x: 204, y: 202 }, { x: 404, y: 202 }])} + ${wire([{ x: 204, y: 354 }, { x: 204, y: 432 }, { x: 312, y: 432 }])} + ${port('v_B', { x: 94, y: 354 }, 'left')} + `; +} + +function renderCmosNor2(spec: CircuitSpec): SvgFragment { + const mpaLabel = componentLabel(spec, 'MPA', 'MPA'); + const mpbLabel = componentLabel(spec, 'MPB', 'MPB'); + const mnaLabel = componentLabel(spec, 'MNA', 'MNA'); + const mnbLabel = componentLabel(spec, 'MNB', 'MNB'); + + return ` + ${vdd({ x: 360, y: 64 })} + ${mos('MPA', 'pmos', { x: 360, y: 156 }, mpaLabel)} + ${mos('MPB', 'pmos', { x: 360, y: 262 }, mpbLabel)} + ${wire([{ x: 360, y: 218 }, { x: 360, y: 200 }])} + ${wire([{ x: 360, y: 324 }, { x: 360, y: 342 }, { x: 604, y: 342 }])} + ${port('v_{out}', { x: 604, y: 342 }, 'right')} + ${mos('MNA', 'nmos', { x: 268, y: 420 }, mnaLabel)} + ${mos('MNB', 'nmos', { x: 452, y: 420 }, mnbLabel)} + ${wire([{ x: 360, y: 342 }, { x: 268, y: 358 }, { x: 268, y: 358 }])} + ${wire([{ x: 360, y: 342 }, { x: 452, y: 358 }, { x: 452, y: 358 }])} + ${wire([{ x: 268, y: 482 }, { x: 452, y: 482 }, { x: 360, y: 482 }])} + ${ground({ x: 360, y: 528 })} + ${wire([{ x: 92, y: 232 }, { x: 190, y: 232 }, { x: 190, y: 156 }, { x: 312, y: 156 }])} + ${wire([{ x: 190, y: 232 }, { x: 190, y: 420 }, { x: 220, y: 420 }])} + ${port('v_A', { x: 92, y: 232 }, 'left')} + ${wire([{ x: 92, y: 322 }, { x: 214, y: 322 }, { x: 214, y: 262 }, { x: 312, y: 262 }])} + ${wire([{ x: 214, y: 322 }, { x: 214, y: 420 }, { x: 404, y: 420 }])} + ${port('v_B', { x: 92, y: 322 }, 'left')} + `; +} + +function renderCircuitBody(spec: CircuitSpec): SvgFragment { + switch (spec.circuitKind) { + case 'common-source-amplifier': + return renderCommonSource(spec); + case 'cmos-inverter': + return renderCmosInverter(spec); + case 'cmos-buffer': + return renderCmosBuffer(spec); + case 'cmos-transmission-gate': + return renderTransmissionGate(spec); + case 'cmos-nand2': + return renderCmosNand2(spec); + case 'cmos-nor2': + return renderCmosNor2(spec); + default: + return ''; + } +} + +function renderCircuitMetadata(spec: CircuitSpec): SvgFragment { + const componentSummary = spec.components + .map(component => `${component.id}:${component.type}`) + .join(' '); + return `<g class="notemd-circuit-metadata"> + ${text(spec.title, { x: 56, y: 48 }, ' class="notemd-circuit-title"')} + ${text(`${spec.circuitKind} / ${spec.goldenReferenceId}`, { x: 56, y: 76 }, ' class="notemd-circuit-subtitle"')} + ${text(componentSummary, { x: 56, y: 548 }, ' class="notemd-circuit-footnote"')} + </g>`; +} + +export function renderCircuitSpecPreviewSvg(spec: CircuitSpec): string { + const validation = validateCircuitSpec(spec); + if (!validation.valid) { + throw new ValidationError(`CircuitSpec preview validation failed: ${validation.errors.join(' ')}`, 'INVALID_INPUT'); + } + + return `<svg xmlns="http://www.w3.org/2000/svg" width="720" height="580" viewBox="0 0 720 580" role="img" aria-labelledby="notemd-circuit-title notemd-circuit-desc" data-notemd-renderer="${NOTEMD_CIRCUITIKZ_PREVIEW_SVG_RENDERER_VERSION}"> + <title id="notemd-circuit-title">${escapeHtml(spec.title)} + ${escapeHtml(`${spec.circuitKind} circuitikz preview companion for ${spec.goldenReferenceId}`)} + + + ${renderCircuitMetadata(spec)} + + ${renderCircuitBody(spec)} + + `; +} + +function resolveCircuitSpec(spec: DiagramSpec): CircuitSpec { + assertValidDiagramSpec(spec); + if (spec.intent !== 'circuit' || !spec.circuitSpec) { + throw new ValidationError('Circuitikz rendering requires DiagramSpec intent "circuit" with a CircuitSpec payload.', 'INVALID_INPUT'); + } + return spec.circuitSpec; +} + +export class CircuitikzRenderer implements DiagramRenderer { + readonly id = 'circuitikz'; + readonly target = 'circuitikz' as const; + + supports(spec: DiagramSpec): boolean { + return spec.intent === 'circuit' + && Boolean(spec.circuitSpec) + && validateCircuitSpec(spec.circuitSpec as CircuitSpec).valid; + } + + async render(spec: DiagramSpec): Promise { + const circuitSpec = resolveCircuitSpec(spec); + const content = exportCircuitSpecToCircuitikz(circuitSpec); + + return { + target: this.target, + content, + mimeType: 'text/x-tex', + sourceIntent: spec.intent, + previewSvg: { + content: renderCircuitSpecPreviewSvg(circuitSpec), + mimeType: 'image/svg+xml' + } + }; + } +} diff --git a/src/tests/circuitikzRenderer.test.ts b/src/tests/circuitikzRenderer.test.ts new file mode 100644 index 00000000..bfd9930e --- /dev/null +++ b/src/tests/circuitikzRenderer.test.ts @@ -0,0 +1,76 @@ +import { CircuitikzRenderer } from '../rendering/renderers/circuitikzRenderer'; +import { DiagramSpec } from '../diagram/types'; + +function createCircuitDiagramSpec(): DiagramSpec { + return { + intent: 'circuit', + title: 'CMOS Inverter', + nodes: [], + circuitSpec: { + circuitKind: 'cmos-inverter', + title: 'CMOS Inverter', + goldenReferenceId: 'cmos-inverter-v1', + style: { + package: 'circuitikz', + voltageConvention: 'american voltages' + }, + nets: ['VDD', 'GND', 'vin', 'vout', 'shared_gate', 'shared_drain'], + components: [ + { + id: 'MP', + type: 'pmos', + label: '$M_P$', + terminals: { S: 'VDD', G: 'shared_gate', D: 'shared_drain' } + }, + { + id: 'MN', + type: 'nmos', + label: '$M_N$', + terminals: { D: 'shared_drain', G: 'shared_gate', S: 'GND' } + } + ], + connections: [ + { from: 'VDD', to: 'MP.S' }, + { from: 'MP.D', to: 'MN.D' }, + { from: 'MN.S', to: 'GND' }, + { from: 'vin', to: 'MP.G' }, + { from: 'vin', to: 'MN.G' }, + { from: 'MP.D', to: 'vout' }, + { from: 'MN.D', to: 'vout' } + ], + layoutHints: { + inputSide: 'left', + outputSide: 'right', + routingStyle: 'orthogonal' + } + } + }; +} + +describe('circuitikz renderer', () => { + test('exports circuitikz TeX source with an Obsidian-viewable SVG companion', async () => { + const renderer = new CircuitikzRenderer(); + const artifact = await renderer.render(createCircuitDiagramSpec()); + + expect(artifact.target).toBe('circuitikz'); + expect(artifact.mimeType).toBe('text/x-tex'); + expect(artifact.content).toContain('\\usepackage{circuitikz}'); + expect(artifact.content).toContain('\\begin{circuitikz}'); + expect(artifact.previewSvg?.mimeType).toBe('image/svg+xml'); + expect(artifact.previewSvg?.content).toContain(' { + const renderer = new CircuitikzRenderer(); + + await expect(renderer.render({ + intent: 'flowchart', + title: 'Wrong Shape', + nodes: [{ id: 'a', label: 'A' }] + })).rejects.toThrow(/CircuitSpec/i); + }); +}); diff --git a/src/tests/diagramArtifactExportCli.test.ts b/src/tests/diagramArtifactExportCli.test.ts index 4770b056..9745625c 100644 --- a/src/tests/diagramArtifactExportCli.test.ts +++ b/src/tests/diagramArtifactExportCli.test.ts @@ -24,6 +24,52 @@ function createSpec(): DiagramSpec { }; } +function createCircuitSpec(): DiagramSpec { + return { + intent: 'circuit', + title: 'CLI Circuit Export', + nodes: [], + circuitSpec: { + circuitKind: 'cmos-inverter', + title: 'CLI Circuit Export', + goldenReferenceId: 'cmos-inverter-v1', + style: { + package: 'circuitikz', + voltageConvention: 'american voltages' + }, + nets: ['VDD', 'GND', 'vin', 'vout', 'shared_gate', 'shared_drain'], + components: [ + { + id: 'MP', + type: 'pmos', + label: '$M_P$', + terminals: { S: 'VDD', G: 'shared_gate', D: 'shared_drain' } + }, + { + id: 'MN', + type: 'nmos', + label: '$M_N$', + terminals: { D: 'shared_drain', G: 'shared_gate', S: 'GND' } + } + ], + connections: [ + { from: 'VDD', to: 'MP.S' }, + { from: 'MP.D', to: 'MN.D' }, + { from: 'MN.S', to: 'GND' }, + { from: 'vin', to: 'MP.G' }, + { from: 'vin', to: 'MN.G' }, + { from: 'MP.D', to: 'vout' }, + { from: 'MN.D', to: 'vout' } + ], + layoutHints: { + inputSide: 'left', + outputSide: 'right', + routingStyle: 'orthogonal' + } + } + }; +} + describe('diagram artifact export CLI', () => { const repoRoot = path.join(__dirname, '..', '..'); const packageJsonPath = path.join(repoRoot, 'package.json'); @@ -38,11 +84,28 @@ describe('diagram artifact export CLI', () => { const cli = require(scriptPath); expect(packageJson.scripts['diagram:export-artifact']).toBe('node scripts/export-diagram-artifact.js'); - expect(cli.SUPPORTED_TARGETS).toEqual(expect.arrayContaining(['svg', 'png', 'pdf'])); + expect(cli.SUPPORTED_TARGETS).toEqual(expect.arrayContaining(['circuitikz', 'svg', 'png', 'pdf'])); expect(cli.normalizePpi(undefined)).toBe(300); expect(cli.normalizePpi('450')).toBe(450); expect(cli.normalizePpi('1200')).toBe(600); expect(cli.pngPixelsPerMeterFromPpi(300)).toBe(11811); + expect(cli.parseArgs([ + 'spec.json', + 'circuitikz', + 'circuit.tex', + 'circuit.svg', + 'circuit.png', + 'circuit.pdf', + '300' + ])).toEqual({ + input: 'spec.json', + target: 'circuitikz', + output: 'circuit.tex', + previewSvgOutput: 'circuit.svg', + previewPngOutput: 'circuit.png', + previewPdfOutput: 'circuit.pdf', + ppi: '300' + }); for (const runbook of [englishRunbook, chineseRunbook]) { expect(runbook).toContain('npm run diagram:export-artifact'); @@ -56,6 +119,7 @@ describe('diagram artifact export CLI', () => { expect(runbook).toContain('editable-html-svg'); expect(runbook).toContain('drawio'); expect(runbook).toContain('drawnix'); + expect(runbook).toContain('circuitikz'); expect(runbook).toContain('svg'); expect(runbook).toContain('png'); expect(runbook).toContain('pdf'); @@ -67,6 +131,54 @@ describe('diagram artifact export CLI', () => { } }); + test('exports circuitikz TeX and SVG/PNG/PDF preview companions from one DiagramSpec file', () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-diagram-artifact-cli-circuitikz-')); + const specPath = path.join(tempRoot, 'circuit.json'); + const texPath = path.join(tempRoot, 'circuit.tex'); + const svgPath = path.join(tempRoot, 'circuit.svg'); + const pngPath = path.join(tempRoot, 'circuit.png'); + const pdfPath = path.join(tempRoot, 'circuit.pdf'); + fs.writeFileSync(specPath, JSON.stringify(createCircuitSpec(), null, 2), 'utf8'); + + try { + const stdout = execFileSync( + process.execPath, + [ + scriptPath, + '--input', specPath, + '--target', 'circuitikz', + '--output', texPath, + '--preview-svg-output', svgPath, + '--preview-png-output', pngPath, + '--preview-pdf-output', pdfPath, + '--ppi', '300' + ], + { + cwd: repoRoot, + encoding: 'utf8' + } + ); + + const result = JSON.parse(stdout); + expect(result).toEqual(expect.objectContaining({ + target: 'circuitikz', + outputPath: texPath, + previewSvgOutputPath: svgPath, + previewPngOutputPath: pngPath, + previewPdfOutputPath: pdfPath, + ppi: 300, + circuitKind: 'cmos-inverter', + goldenReferenceId: 'cmos-inverter-v1' + })); + expect(fs.readFileSync(texPath, 'utf8')).toContain('\\usepackage{circuitikz}'); + expect(fs.readFileSync(svgPath, 'utf8')).toContain('notemd-circuitikz-preview-svg@0.1.0'); + expect(fs.statSync(pngPath).size).toBeGreaterThan(0); + expect(fs.readFileSync(pdfPath).subarray(0, 5).toString('ascii')).toBe('%PDF-'); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }, 30000); + test('exports editable HTML/SVG, Draw.io XML, and Drawnix JSON from one DiagramSpec file', () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-diagram-artifact-cli-')); const specPath = path.join(tempRoot, 'spec.json'); @@ -206,7 +318,7 @@ describe('diagram artifact export CLI', () => { expect(result.status).toBe(1); expect(result.stdout).toBe(''); expect(result.stderr).toContain('Unsupported export target "bad-target"'); - expect(result.stderr).toContain('editable-html-svg, drawio, drawnix, svg, png, pdf'); + expect(result.stderr).toContain('editable-html-svg, drawio, drawnix, circuitikz, svg, png, pdf'); expect(fs.existsSync(outputPath)).toBe(false); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); diff --git a/src/tests/diagramDocsContract.test.ts b/src/tests/diagramDocsContract.test.ts index dca2f971..c6662d97 100644 --- a/src/tests/diagramDocsContract.test.ts +++ b/src/tests/diagramDocsContract.test.ts @@ -10,11 +10,14 @@ describe('diagram documentation contract', () => { test('README documents HTML fallback preview, Mermaid validation, and chart template inference', () => { const readme = fs.readFileSync(readmePath, 'utf8'); - expect(readme).toContain('| HTML | `_diagram.html` | Yes (iframe fallback) | No | No | Yes |'); + expect(readme).toContain('| HTML | `_diagram.html` | Yes (iframe fallback) | No | No | No | Yes |'); expect(readme).toContain('Generated Mermaid artifacts are now validated with `mermaid.parse` before the renderer returns them'); expect(readme).toContain('Invalid Mermaid artifacts now fail early with explicit validation errors before preview/export.'); expect(readme).toContain('HTML fallback previews'); expect(readme).toContain('planner now seeds preferred Vega-Lite chart templates'); + expect(readme).toContain('| Circuitikz | `_diagram.tex` + companion SVG/MD | SVG companion | Yes | Yes | Yes | Yes |'); + expect(readme).toContain('PNG/PDF export defaulting to 300 PPI and clamped at 600 PPI'); + expect(readme).toContain('the companion is not a LaTeX/TikZJax compile result'); }); test('1.8.2 release notes mention HTML fallback preview, Mermaid validation hardening, and chart template inference', () => { diff --git a/src/tests/diagramGenerationService.test.ts b/src/tests/diagramGenerationService.test.ts index b714e75d..ed12be13 100644 --- a/src/tests/diagramGenerationService.test.ts +++ b/src/tests/diagramGenerationService.test.ts @@ -3,6 +3,53 @@ import { RendererRegistry } from '../rendering/rendererRegistry'; import { RendererService } from '../rendering/rendererService'; import { HtmlRenderer } from '../rendering/renderers/htmlRenderer'; import { DiagramRenderer } from '../rendering/types'; +import { DiagramSpec } from '../diagram/types'; + +function createCircuitDiagramSpec(): DiagramSpec { + return { + intent: 'circuit', + title: 'CMOS Inverter', + nodes: [], + circuitSpec: { + circuitKind: 'cmos-inverter', + title: 'CMOS Inverter', + goldenReferenceId: 'cmos-inverter-v1', + style: { + package: 'circuitikz', + voltageConvention: 'american voltages' + }, + nets: ['VDD', 'GND', 'vin', 'vout', 'shared_gate', 'shared_drain'], + components: [ + { + id: 'MP', + type: 'pmos', + label: '$M_P$', + terminals: { S: 'VDD', G: 'shared_gate', D: 'shared_drain' } + }, + { + id: 'MN', + type: 'nmos', + label: '$M_N$', + terminals: { D: 'shared_drain', G: 'shared_gate', S: 'GND' } + } + ], + connections: [ + { from: 'VDD', to: 'MP.S' }, + { from: 'MP.D', to: 'MN.D' }, + { from: 'MN.S', to: 'GND' }, + { from: 'vin', to: 'MP.G' }, + { from: 'vin', to: 'MN.G' }, + { from: 'MP.D', to: 'vout' }, + { from: 'MN.D', to: 'vout' } + ], + layoutHints: { + inputSide: 'left', + outputSide: 'right', + routingStyle: 'orthogonal' + } + } + }; +} describe('diagram generation service', () => { test('generates mermaid content through the experimental pipeline when spec output is valid', async () => { @@ -208,6 +255,28 @@ Client sends work to a queue-backed worker. } }); + test('honors circuitikz render target overrides with TeX source and svg companion', async () => { + const llmInvoker = jest.fn().mockResolvedValue(JSON.stringify(createCircuitDiagramSpec())); + + const result = await generateDiagramArtifact(`# CMOS Inverter + +Draw a circuitikz schematic with PMOS pull-up, NMOS pull-down, VDD, GND, vin, and vout. +`, { + compatibilityMode: 'best-fit', + requestedRenderTarget: 'circuitikz', + targetLanguage: 'en', + llmInvoker + }); + + expect(result.plan.renderTarget).toBe('circuitikz'); + expect(result.plan.fallbackTargets).toEqual([]); + expect(result.spec.intent).toBe('circuit'); + expect(result.artifact.target).toBe('circuitikz'); + expect(result.artifact.content).toContain('\\begin{circuitikz}'); + expect(result.artifact.previewSvg?.content).toContain(' { const result = await generateDiagramArtifact(`# Release Checklist diff --git a/src/tests/diagramIntent.test.ts b/src/tests/diagramIntent.test.ts index c902b2be..fd4556e7 100644 --- a/src/tests/diagramIntent.test.ts +++ b/src/tests/diagramIntent.test.ts @@ -72,6 +72,18 @@ Run B: latency 180 ms, throughput 70 req/s expect(result.reasons.join(' ')).toMatch(/ranked|numeric/i); }); + test('infers circuit intent for CMOS circuit notes', () => { + const markdown = `# CMOS Inverter + +Draw a circuitikz schematic with a PMOS pull-up, NMOS pull-down, VDD, GND, vin, and vout. +`; + + const result = inferDiagramIntent(markdown); + + expect(result.intent).toBe('circuit'); + expect(result.reasons.join(' ')).toMatch(/circuit|cmos|mos/i); + }); + test('falls back to mindmap for general hierarchical notes', () => { const markdown = `# Distributed Systems diff --git a/src/tests/diagramOperationInput.test.ts b/src/tests/diagramOperationInput.test.ts index 5c6fdd5d..9c6568a8 100644 --- a/src/tests/diagramOperationInput.test.ts +++ b/src/tests/diagramOperationInput.test.ts @@ -70,8 +70,8 @@ describe('diagram operation input helpers', () => { expect(input.requestedRenderTarget).toBeUndefined(); }); - test('accepts Draw.io and Drawnix render target preferences for artifact mode', () => { - for (const preferredDiagramRenderTarget of ['drawio', 'drawnix'] as const) { + test('accepts source-plus-preview render target preferences for artifact mode', () => { + for (const preferredDiagramRenderTarget of ['drawio', 'drawnix', 'circuitikz'] as const) { const input = buildDiagramOperationInput({ sourcePath: 'Notes/Topic.md', sourceMarkdown: '# Topic', diff --git a/src/tests/diagramPreviewModal.test.ts b/src/tests/diagramPreviewModal.test.ts index c71c220d..9df0088b 100644 --- a/src/tests/diagramPreviewModal.test.ts +++ b/src/tests/diagramPreviewModal.test.ts @@ -462,6 +462,35 @@ describe('diagram preview modal', () => { expect(buttons.some(button => button.text === 'Export PDF')).toBe(true); }); + test('renders circuitikz companion svg artifacts with svg png and pdf export actions', async () => { + (previewExport.renderPreviewArtifactSvg as jest.Mock).mockResolvedValueOnce('CMOS Inverter'); + const modal = mountModal(new DiagramPreviewModal(mockApp, createSession({ + target: 'circuitikz', + content: '\\usepackage{circuitikz}\n\\begin{document}\n\\begin{circuitikz}\n\\end{circuitikz}\n\\end{document}', + mimeType: 'text/x-tex', + sourceIntent: 'circuit', + previewSvg: { + content: 'CMOS Inverter', + mimeType: 'image/svg+xml' + } + }), 'en') as any); + + modal.onOpen(); + await flushPromises(); + + const iframe = findByTag(modal.contentEl, 'iframe'); + const sourcePreview = findByClass(modal.contentEl, 'notemd-diagram-preview-source-only-code'); + const svgPreview = findByClass(modal.contentEl, 'is-svg-preview'); + const buttons = collectButtons(modal.contentEl); + + expect(iframe).toBeNull(); + expect(sourcePreview).toBeNull(); + expect(svgPreview?.innerHTML).toContain('CMOS Inverter'); + expect(buttons.some(button => button.text === 'Export SVG')).toBe(true); + expect(buttons.some(button => button.text === 'Export PNG')).toBe(true); + expect(buttons.some(button => button.text === 'Export PDF')).toBe(true); + }); + test('hides save-source button when preview already points at saved artifact', async () => { const modal = mountModal(new DiagramPreviewModal(mockApp, { ...createSession(), diff --git a/src/tests/diagramSpecPrompt.test.ts b/src/tests/diagramSpecPrompt.test.ts index 59bf18ef..add46f2d 100644 --- a/src/tests/diagramSpecPrompt.test.ts +++ b/src/tests/diagramSpecPrompt.test.ts @@ -51,4 +51,18 @@ describe('diagram spec prompt builder', () => { expect(prompt).toMatch(/choose a non-dataChart intent/i); }); + + test('requires constrained CircuitSpec output for circuitikz render target', () => { + const prompt = buildDiagramSpecPrompt({ + preferredIntent: 'circuit', + preferredRenderTarget: 'circuitikz' + }); + + expect(prompt).toMatch(/intent:\s*circuit/i); + expect(prompt).toMatch(/circuitSpec/i); + expect(prompt).toMatch(/goldenReferenceId/i); + expect(prompt).toMatch(/style\.package/i); + expect(prompt).toMatch(/cmos-inverter-v1/i); + expect(prompt).toMatch(/Do not output raw TikZ/i); + }); }); diff --git a/src/tests/diagramSpecResponseParser.test.ts b/src/tests/diagramSpecResponseParser.test.ts index 274b213c..5bf9f711 100644 --- a/src/tests/diagramSpecResponseParser.test.ts +++ b/src/tests/diagramSpecResponseParser.test.ts @@ -54,6 +54,58 @@ describe('diagram spec response parser', () => { ]); }); + test('preserves embedded CircuitSpec payloads for circuitikz rendering', () => { + const raw = JSON.stringify({ + intent: 'circuit', + title: 'CMOS Inverter', + nodes: [], + circuitSpec: { + circuitKind: 'cmos-inverter', + title: 'CMOS Inverter', + goldenReferenceId: 'cmos-inverter-v1', + style: { + package: 'circuitikz', + voltageConvention: 'american voltages' + }, + nets: ['VDD', 'GND', 'vin', 'vout', 'shared_gate', 'shared_drain'], + components: [ + { + id: 'MP', + type: 'pmos', + label: '$M_P$', + terminals: { S: 'VDD', G: 'shared_gate', D: 'shared_drain' } + }, + { + id: 'MN', + type: 'nmos', + label: '$M_N$', + terminals: { D: 'shared_drain', G: 'shared_gate', S: 'GND' } + } + ], + connections: [ + { from: 'VDD', to: 'MP.S' }, + { from: 'MP.D', to: 'MN.D' }, + { from: 'MN.S', to: 'GND' }, + { from: 'vin', to: 'MP.G' }, + { from: 'vin', to: 'MN.G' }, + { from: 'MP.D', to: 'vout' }, + { from: 'MN.D', to: 'vout' } + ], + layoutHints: { + inputSide: 'left', + outputSide: 'right', + routingStyle: 'orthogonal' + } + } + }); + + const spec = parseDiagramSpecResponse(raw); + + expect(spec.intent).toBe('circuit'); + expect(spec.circuitSpec?.circuitKind).toBe('cmos-inverter'); + expect(spec.circuitSpec?.goldenReferenceId).toBe('cmos-inverter-v1'); + }); + test('throws when no valid json object can be extracted', () => { expect(() => parseDiagramSpecResponse('not valid json')).toThrow(/Unable to parse DiagramSpec/i); }); diff --git a/src/tests/diagramSpecValidation.test.ts b/src/tests/diagramSpecValidation.test.ts index 63de9dff..566d8fe0 100644 --- a/src/tests/diagramSpecValidation.test.ts +++ b/src/tests/diagramSpecValidation.test.ts @@ -203,4 +203,68 @@ describe('diagram spec validation', () => { expect(result.errors.join(' ')).toMatch(/pie/i); expect(result.errors.join(' ')).toMatch(/non-negative/i); }); + + test('accepts circuit specs without generic flowchart nodes', () => { + const spec: DiagramSpec = { + intent: 'circuit', + title: 'CMOS Inverter', + nodes: [], + circuitSpec: { + circuitKind: 'cmos-inverter', + title: 'CMOS Inverter', + goldenReferenceId: 'cmos-inverter-v1', + style: { + package: 'circuitikz', + voltageConvention: 'american voltages' + }, + nets: ['VDD', 'GND', 'vin', 'vout', 'shared_gate', 'shared_drain'], + components: [ + { + id: 'MP', + type: 'pmos', + label: '$M_P$', + terminals: { S: 'VDD', G: 'shared_gate', D: 'shared_drain' } + }, + { + id: 'MN', + type: 'nmos', + label: '$M_N$', + terminals: { D: 'shared_drain', G: 'shared_gate', S: 'GND' } + } + ], + connections: [ + { from: 'VDD', to: 'MP.S' }, + { from: 'MP.D', to: 'MN.D' }, + { from: 'MN.S', to: 'GND' }, + { from: 'vin', to: 'MP.G' }, + { from: 'vin', to: 'MN.G' }, + { from: 'MP.D', to: 'vout' }, + { from: 'MN.D', to: 'vout' } + ], + layoutHints: { + inputSide: 'left', + outputSide: 'right', + routingStyle: 'orthogonal' + } + } + }; + + const result = validateDiagramSpec(spec); + + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + test('rejects circuit intent without CircuitSpec topology', () => { + const spec: DiagramSpec = { + intent: 'circuit', + title: 'Missing Circuit', + nodes: [] + }; + + const result = validateDiagramSpec(spec); + + expect(result.valid).toBe(false); + expect(result.errors.join(' ')).toMatch(/CircuitSpec/i); + }); }); diff --git a/src/ui/NotemdSettingTab.ts b/src/ui/NotemdSettingTab.ts index 1f47e68e..14fba5d0 100644 --- a/src/ui/NotemdSettingTab.ts +++ b/src/ui/NotemdSettingTab.ts @@ -2486,6 +2486,7 @@ export class NotemdSettingTab extends PluginSettingTab { dropdown.addOption('classDiagram', experimentalDiagramI18n.intentClassDiagram); dropdown.addOption('erDiagram', experimentalDiagramI18n.intentErDiagram); dropdown.addOption('stateDiagram', experimentalDiagramI18n.intentStateDiagram); + dropdown.addOption('circuit', experimentalDiagramI18n.intentCircuit); dropdown.addOption('dataChart', experimentalDiagramI18n.intentDataChart); dropdown .setValue(this.plugin.settings.preferredDiagramIntent || 'auto') @@ -2507,6 +2508,7 @@ export class NotemdSettingTab extends PluginSettingTab { dropdown.addOption('editable-html-svg', experimentalDiagramI18n.renderTargetEditableHtmlSvg); dropdown.addOption('drawio', experimentalDiagramI18n.renderTargetDrawio); dropdown.addOption('drawnix', experimentalDiagramI18n.renderTargetDrawnix); + dropdown.addOption('circuitikz', experimentalDiagramI18n.renderTargetCircuitikz); dropdown .setValue(this.plugin.settings.preferredDiagramRenderTarget || 'auto') .onChange(async (value: string) => { diff --git a/src/ui/NotemdSidebarView.ts b/src/ui/NotemdSidebarView.ts index 84f33beb..481a8f8f 100644 --- a/src/ui/NotemdSidebarView.ts +++ b/src/ui/NotemdSidebarView.ts @@ -1696,6 +1696,7 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter { { value: 'classDiagram', label: i18n.settings.developer.experimentalDiagramPipeline.intentClassDiagram }, { value: 'erDiagram', label: i18n.settings.developer.experimentalDiagramPipeline.intentErDiagram }, { value: 'stateDiagram', label: i18n.settings.developer.experimentalDiagramPipeline.intentStateDiagram }, + { value: 'circuit', label: i18n.settings.developer.experimentalDiagramPipeline.intentCircuit }, { value: 'dataChart', label: i18n.settings.developer.experimentalDiagramPipeline.intentDataChart }, ]; @@ -1726,6 +1727,7 @@ export class NotemdSidebarView extends ItemView implements ProgressReporter { { value: 'editable-html-svg', label: i18n.settings.developer.experimentalDiagramPipeline.renderTargetEditableHtmlSvg }, { value: 'drawio', label: i18n.settings.developer.experimentalDiagramPipeline.renderTargetDrawio }, { value: 'drawnix', label: i18n.settings.developer.experimentalDiagramPipeline.renderTargetDrawnix }, + { value: 'circuitikz', label: i18n.settings.developer.experimentalDiagramPipeline.renderTargetCircuitikz }, ]; renderTargets.forEach(item => { diff --git a/website/docs/features/diagrams.mdx b/website/docs/features/diagrams.mdx index 2eabf14c..0053ac99 100644 --- a/website/docs/features/diagrams.mdx +++ b/website/docs/features/diagrams.mdx @@ -1,7 +1,7 @@ --- id: diagrams title: Diagrams -description: Generate Mermaid, JSON Canvas, Vega-Lite, HTML, editable figure artifacts, and future constrained circuitikz diagrams from your notes using a spec-first architecture +description: Generate Mermaid, JSON Canvas, Vega-Lite, HTML, editable figure artifacts, Draw.io, Drawnix, and constrained circuitikz diagrams from your notes using a spec-first architecture author: '@id': 'https://jacobinwwey.github.io/obsidian-NotEMD/#person-jacobinwwey' keywords: [diagrams, mermaid, vega-lite, canvas, visualization, spec-first, drawio, drawnix, editable svg, circuitikz, TikZJax] @@ -18,7 +18,7 @@ import TLDR from '@site/src/components/TLDR'; # Diagrams -**Notemd generates diagrams from your notes through a spec-first pipeline.** The LLM produces a renderer-agnostic `DiagramSpec` JSON, then dedicated adapters translate it into Mermaid, JSON Canvas, Vega-Lite, HTML, or editable HTML/SVG output. Supports 8 intent types, automatic fallback chains, live preview with SVG/PNG export, semantic verification, and local-knowledge-augmented generation. +**Notemd generates diagrams from your notes through a spec-first pipeline.** The LLM produces a renderer-agnostic `DiagramSpec` JSON, then dedicated adapters translate it into Mermaid, JSON Canvas, Vega-Lite, HTML, editable HTML/SVG, Draw.io, Drawnix, or constrained circuitikz output. Supports 9 intent types, automatic fallback chains, live preview with SVG/PNG/PDF export, semantic verification, and local-knowledge-augmented generation. This is part of the [Obsidian AI Knowledge Management Guide](/docs/pillar-ai-knowledge). @@ -54,6 +54,7 @@ graph LR | `stateDiagram` | Mermaid | HTML | State machines, lifecycle models | | `canvasMap` | JSON Canvas | Mermaid → HTML | Concept maps, knowledge graphs | | `dataChart` | Vega-Lite | Mermaid → HTML | Bar, line, area, scatter, pie, tables | +| `circuit` | circuitikz | none | Constrained circuit diagrams from validated `CircuitSpec` payloads | ## Intent Detection @@ -67,6 +68,7 @@ Notemd infers the best diagram type from your note's content using keyword scori | `stateDiagram` | State, transition, pending, running, failed (3+ matches) | 0.76 | | `flowchart` | Numbered steps (2+) or if/then/else/workflow vocab | 0.74 | | `canvasMap` | Concept map, knowledge graph, spatial, cluster | 0.72 | +| `circuit` | circuitikz, TikZJax, circuit, schematic, CMOS, NMOS, PMOS, MOSFET, VDD/GND, `vin`/`vout` | 0.78 | | `mindmap` | Default fallback | 0.55 | Override with the **Preferred diagram type** setting, the sidebar selector, or an explicit command palette option. @@ -80,9 +82,9 @@ The experimental spec-first pipeline now has two independent controls: | Preferred diagram type | `preferredDiagramIntent` | Guides the semantic shape of the generated `DiagramSpec` | | Preferred render target | `preferredDiagramRenderTarget` | Chooses the artifact renderer for **Generate diagram** and **Preview diagram** | -Set **Preferred render target** to **Auto** for the planner default, or choose Mermaid, JSON Canvas, Vega-Lite, HTML, or Editable HTML/SVG explicitly. The override applies only to artifact and preview commands. The standard **Summarise as Mermaid diagram** command remains pinned to Mermaid-compatible output so existing Markdown workflows do not silently switch formats. +Set **Preferred render target** to **Auto** for the planner default, or choose Mermaid, JSON Canvas, Vega-Lite, HTML, Editable HTML/SVG, Draw.io, Drawnix, or Circuitikz explicitly. The override applies only to artifact and preview commands. The standard **Summarise as Mermaid diagram** command remains pinned to Mermaid-compatible output so existing Markdown workflows do not silently switch formats. -This separation matters because a `flowchart` intent can now be rendered as Mermaid for Markdown notes, HTML for robust fallback, or Editable HTML/SVG for downstream editing. Draw.io and Drawnix remain CLI artifact exporters rather than in-plugin render targets. +This separation matters because a `flowchart` intent can now be rendered as Mermaid for Markdown notes, HTML for robust fallback, Editable HTML/SVG for downstream editing, or Draw.io/Drawnix source artifacts with SVG review companions. A `circuit` intent routes to Circuitikz and requires a validated `CircuitSpec`; it is not a request for arbitrary TikZ text. ## Usage @@ -101,16 +103,21 @@ This separation matters because a `flowchart` intent can now be rendered as Merm | Vega-Lite | `.json` | `{note}_diagram.json` | | HTML | `.html` | `{note}_diagram.html` | | Editable HTML/SVG | `.html` | `{note}_diagram.html` | +| Draw.io | `.drawio` + `.drawio.svg` + `.drawio.md` | `{note}_diagram.drawio` plus review companions | +| Drawnix | `.drawnix` + `.drawnix.svg` + `.drawnix.md` | `{note}_diagram.drawnix` plus review companions | +| Circuitikz | `.tex` + `.tex.svg` + `.tex.md` | `{note}_diagram.tex` plus review companions | ### Preview a Diagram 1. Run **"Notemd: Preview diagram"** 2. A modal opens with the rendered diagram -3. Export as SVG or PNG using the toolbar buttons +3. Export as SVG, PNG, or PDF using the toolbar buttons **Auto-open preview** is available in settings — after generation, the preview modal launches automatically. -The preview modal also has an artifact diagnostics panel. Renderers and smoke checks can attach `RenderArtifact.diagnostics`; the modal shows a diagnostic summary with error/warning/info counts, then severity, diagnostic kind, message, and repair advice next to the preview. The same summary is shown in preview history entries, so repeated circuitikz smoke attempts can be compared without opening every entry. For artifacts that have source content but cannot be rendered inline or through the HTML iframe path, the modal now falls back to a source-only preview instead of forcing an empty iframe. This gives circuitikz compile/render smoke, SVG text-token checks, PNG blank-screenshot checks, and future overlap reports a visible UI surface without making TikZJax or LaTeX a hard plugin runtime dependency or pretending source text is a verified visual render. +PNG and PDF preview export use the configured preview PPI. The default is 300 PPI and values above 600 PPI are clamped to 600. SVG remains vector-sized. Source artifacts such as `.drawio`, `.drawnix`, and `.tex` can provide a `previewSvg` companion so Obsidian can display and export reviewable images without embedding diagrams.net, Drawnix, LaTeX, or TikZJax in the plugin runtime. + +The preview modal also has an artifact diagnostics panel. Renderers and smoke checks can attach `RenderArtifact.diagnostics`; the modal shows a diagnostic summary with error/warning/info counts, then severity, diagnostic kind, message, and repair advice next to the preview. The same summary is shown in diagnostics-aware history entries, so repeated circuitikz smoke attempts can be compared without opening every entry. For artifacts that have source content but cannot be rendered inline or through the HTML iframe path, the modal now falls back to a source-only preview instead of forcing an empty iframe. This gives circuitikz compile/render smoke, SVG text-token checks, PNG blank-screenshot checks, path-only glyph overlap reports, and future overlap reports a visible UI surface without making TikZJax or LaTeX a hard plugin runtime dependency or pretending source text is a verified visual render. ### Legacy Mermaid Mode @@ -164,12 +171,12 @@ This target is intentionally not the default planner route yet. It is available ### Draw.io and Drawnix Export Boundaries -The current implementation keeps third-party editor support at the artifact boundary: +The current implementation keeps third-party editor support at the artifact boundary while still exposing explicit render targets: | Target | Contract | Runtime Dependency | |--------|----------|--------------------| -| Draw.io | deterministic uncompressed `mxfile` XML from `SemanticFigureModel` | none in plugin runtime or CI | -| Drawnix | minimal `.drawnix` JSON subset using `geometry` and `arrow-line` elements | none in plugin runtime or CI | +| Draw.io | deterministic uncompressed `mxfile` XML from `SemanticFigureModel`, plus SVG/PNG/PDF review companions | none in plugin runtime or CI | +| Drawnix | minimal `.drawnix` JSON subset using `geometry` and `arrow-line` elements, plus SVG/PNG/PDF review companions | none in plugin runtime or CI | The tradeoff is deliberate: Notemd can verify visible labels, stable IDs, and supported primitive coverage without embedding diagrams.net Desktop, Drawnix, Plait, or browser-only editor state into the plugin. @@ -213,7 +220,11 @@ Notemd now includes the first constrained repository prototype for this directio npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex ``` -The prototype adds a separate `CircuitSpec` boundary and deterministic exporter for six golden-reference families: +The prototype adds a constrained `CircuitSpec` boundary and deterministic exporter for six golden-reference families: + +In the experimental diagram pipeline, this is now also reachable through `intent: "circuit"` and render target `circuitikz`. The generated `DiagramSpec` may embed `circuitSpec` only for circuit intent. `CircuitikzRenderer` writes the same deterministic `.tex` source and attaches an SVG preview companion derived from that validated circuit topology, enabling Obsidian preview plus SVG/PNG/PDF export. The companion is not a LaTeX/TikZJax compile result; real renderer evidence still belongs to the explicit smoke commands below. + +For supported golden templates, `layoutHints.inputSide` and `layoutHints.outputSide` remain presentation-only controls. They can move deterministic input/output port placement, but they do not change the topology signature or allow a repair pass to rewire the circuit. | Circuit kind | Golden reference | Current guarantee | |--------------|------------------|-------------------| @@ -224,7 +235,7 @@ The prototype adds a separate `CircuitSpec` boundary and deterministic exporter | `cmos-nand2` | `cmos-nand2-v1` | validates parallel PMOS pull-up, series NMOS pull-down, dual inputs `va` / `vb`, and `vout` before writing LaTeX | | `cmos-nor2` | `cmos-nor2-v1` | validates series PMOS pull-up, parallel NMOS pull-down, dual inputs `va` / `vb`, and `vout` before writing LaTeX | -This is not a general TikZ generator yet. It does not compile LaTeX, call TikZJax, inspect screenshots, or run automated image-feedback repair. Those remain later gates. +This is not a general TikZ generator. It does not accept arbitrary TikZ, compile LaTeX, call TikZJax, inspect screenshots in the plugin runtime, or run automated image-feedback repair. Those remain later gates. The Preview diagram command can reopen saved circuitikz source artifacts directly when the file extension is `.tex` or `.tikz` and the source contains `\usepackage{circuitikz}` or `\begin{circuitikz}`. That route is a circuitikz source-only preview: the modal shows the source, diagnostics, copy/save controls, and history metadata, but it does not compile LaTeX or call TikZJax inside the plugin runtime. @@ -350,8 +361,8 @@ 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, CMOS inverter, `cmos-buffer` / `cmos-buffer-v1`, `cmos-transmission-gate` / `cmos-transmission-gate-v1`, `cmos-nand2` / `cmos-nand2-v1`, and `cmos-nor2` / `cmos-nor2-v1` golden templates, projects `layoutHints.inputSide` and `layoutHints.outputSide` into deterministic input/output port placement without changing topology, rejects repair topology drift through `--topology-reference`, emits topology-preserving repair briefs through `--repair-brief-output` and schema `notemd.circuitikz.repair-brief.v1`, includes structured `repairPrompt` handoff content with `diagnosticFocus`, `acceptanceCriteria`, and role `topology-preserving-circuitikz-repair`, validates repair candidates through `--repair-brief`, returns `repairAcceptance` gate evidence through schema `notemd.circuitikz.repair-acceptance.v1` with `readyForVisualAcceptance` and `remainingChecks`, persists that evidence through `--repair-acceptance-output`, parses compile logs, can run explicit local renderers plus `--expected-artifact`, SVG `--expected-svg-text`, accessibility metadata checks through `aria-label`, ``, and `<desc>`, hidden/transparent SVG element exclusion, `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`, close-path current-point handling for `Z/z`, 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, `text-anchor`-aware positioned text 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, grayscale/RGB tRNS transparent samples, and format-specific `render-png-unsupported` guidance for Adam7 interlaced PNGs and indexed bit-depth failures, 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`, records missing renderer configuration through `rendererAvailability.status: "missing-configuration"` and `compile-executable-invalid`, 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 for path-only visual text, precise pixel-level overlap checks, broader SVG path coverage where needed, automatic renderer installation/discovery only if it can remain optional, and automated topology-preserving repair execution | +| CLI support | `npm run diagram:export-artifact` exports editable HTML/SVG, Draw.io, Drawnix, Circuitikz, and SVG/PNG/PDF review evidence from one validated `DiagramSpec` | Add target-specific smoke fixtures when new targets ship | +| circuitikz | `DiagramSpec(intent: "circuit", circuitSpec) -> CircuitikzRenderer -> circuitikz` exports common-source, CMOS inverter, `cmos-buffer` / `cmos-buffer-v1`, `cmos-transmission-gate` / `cmos-transmission-gate-v1`, `cmos-nand2` / `cmos-nand2-v1`, and `cmos-nor2` / `cmos-nor2-v1` golden templates, exposes UI intent/render-target options, writes TeX plus SVG/PNG/PDF preview companions, validates topology before output, parses compile logs, can run explicit local renderers plus `--expected-artifact`, and keeps source-only fallback plus preview diagnostics visible through `RenderArtifact.diagnostics` and the preview modal | Add OCR-level label recognition for path-only visual text, precise pixel-level overlap checks, broader SVG path coverage where needed, automatic renderer installation/discovery only if it can remain optional, 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 @@ -361,6 +372,7 @@ For a CMOS inverter, the prompt should request an explicit topology plus layout | `enableExperimentalDiagramPipeline` | `false` | Toggle between spec-first and legacy Mermaid | | `experimentalDiagramCompatibilityMode` | `'legacy-mermaid'` | `'legacy-mermaid'` = Mermaid only; `'best-fit'` = native targets + fallbacks | | `preferredDiagramIntent` | `undefined` (auto) | Override automatic intent detection | +| `preferredDiagramRenderTarget` | `undefined` (auto) | Override artifact renderer, including Draw.io, Drawnix, and Circuitikz | | `summarizeToMermaidLanguage` | `'en'` | Target language for diagram labels | | `summarizeToMermaidProvider` / `Model` | DeepSeek | Per-task LLM for diagram generation | | `autoMermaidFixAfterGenerate` | (from constants) | Auto-run legacy fixer on Mermaid output | @@ -380,10 +392,11 @@ When enabled, Notemd retrieves relevant context snippets from your vault's local | Action | Method | |--------|--------| | SVG export | `mermaid.render()` / `vega.View.toSVG()` / SVG builder for Canvas | -| PNG export | SVG → Image → Canvas (device pixel ratio 1x-3x) → PNG ArrayBuffer | +| PNG export | SVG → Image → Canvas / preview rasterizer at configured PPI → PNG ArrayBuffer | +| PDF export | SVG → raster image at configured PPI → single-page PDF | | Source save | Raw artifact content saved with target-specific extension | | Source-only preview | Non-inline artifacts with source content shown as code plus diagnostics, without iframe rendering | -| Semantic audit | Mermaid, JSON Canvas, Vega-Lite, and editable HTML/SVG checked by `scripts/diagram-semantic-verification.js` | +| Semantic audit | Mermaid, JSON Canvas, Vega-Lite, editable HTML/SVG, Draw.io, Drawnix, and constrained circuitikz checked by renderer/CLI tests | **Caching**: RenderCache uses deterministic JSON key of `{spec, target, theme}`. In-flight deduplication prevents duplicate renders. diff --git a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/diagrams.mdx b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/diagrams.mdx index b831c717..f43d42a7 100644 --- a/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/diagrams.mdx +++ b/website/i18n/zh-CN/docusaurus-plugin-content-docs/current/features/diagrams.mdx @@ -1,7 +1,7 @@ --- id: diagrams title: 图表 -description: 利用以规范优先的架构,根据您的笔记生成 Mermaid、JSON Canvas、Vega-Lite、HTML 这些可编辑的图表文件,以及未来所需的受限版 circuitikz 图表。 +description: 利用规范优先架构,根据笔记生成 Mermaid、JSON Canvas、Vega-Lite、HTML、可编辑图形 artifact、Draw.io、Drawnix,以及受约束的 circuitikz 图表。 author: '@id': 'https://jacobinwwey.github.io/obsidian-NotEMD/#person-jacobinwwey' keywords: [diagrams, mermaid, vega-lite, canvas, visualization, spec-first, drawio, drawnix, editable svg, circuitikz, TikZJax] @@ -18,7 +18,7 @@ import TLDR from '@site/src/components/TLDR'; # 图表 <TLDR> -**Notemd** 通过以规范为先的流程,根据您的笔记生成图表。LLM 会先创建一个与渲染器无关的 `DiagramSpec` JSON,随后专门的适配器会将其转换为 Mermaid、JSON Canvas、Vega-Lite、HTML,或是可编辑的 HTML/SVG 格式输出。该工具支持 8 种意图类型、自动回退机制、带有 SVG/PNG 导出功能的实时预览、语义验证,以及基于本地知识的增强型生成功能。 +**Notemd** 通过规范优先流程,根据您的笔记生成图表。LLM 会先创建与渲染器无关的 `DiagramSpec` JSON,随后专门的适配器会将其转换为 Mermaid、JSON Canvas、Vega-Lite、HTML、可编辑 HTML/SVG、Draw.io、Drawnix,或受约束的 circuitikz 输出。该工具支持 9 种意图类型、自动回退机制、带有 SVG/PNG/PDF 导出功能的实时预览、语义验证,以及基于本地知识的增强型生成功能。 </TLDR> 这是[Obsidian AI知识管理指南](/docs/pillar-ai-knowledge)的一部分。 @@ -54,6 +54,7 @@ graph LR | `stateDiagram` | Mermaid | HTML | 状态机、生命周期模型 | | `canvasMap` | JSON Canvas | Mermaid → HTML | 概念图,知识图谱 | | `dataChart` | Vega-Lite | Mermaid → HTML | 柱状图、折线图、面积图、散点图、饼图、表格 | +| `circuit` | circuitikz | 无 | 从已验证的 `CircuitSpec` payload 生成受约束电路图 | ## 意图检测 @@ -67,6 +68,7 @@ Notemd 会通过关键词评分从您的笔记内容中推断出最佳的图表 | `stateDiagram` | 状态、过渡、待处理、运行中、失败(匹配3次及以上) | 0.76 | | `flowchart` | 编号步骤(2个以上)或 if/then/else/工作流相关术语 | 0.74 | | `canvasMap` | 概念图、知识图谱、空间结构、聚类 | 0.72 | +| `circuit` | circuitikz、TikZJax、电路、原理图、CMOS、NMOS、PMOS、MOSFET、VDD/GND、`vin`/`vout` | 0.78 | | `mindmap` | 默认回退方案 | 0.55 | 可以使用**首选图表类型**设置、侧边栏选择器或明确的命令面板选项来覆盖它。 @@ -80,9 +82,9 @@ Notemd 会通过关键词评分从您的笔记内容中推断出最佳的图表 | 首选的图表类型 | `preferredDiagramIntent` | 为生成的 `DiagramSpec` 指导语义结构 | | 首选渲染目标 | `preferredDiagramRenderTarget` | 为“生成图表”和“预览图表”选择工件渲染器 | -将规划器的默认**首选渲染目标**设置为**自动**,或者直接选择 Mermaid、JSON Canvas、Vega-Lite、HTML,或是可编辑的 HTML/SVG。此覆盖设置仅适用于生成结果和预览命令。标准的**汇总为 Mermaid 图表**命令仍会保持与 Mermaid 兼容的输出格式,从而避免现有的 Markdown 工作流在后台自动切换格式。 +将**首选渲染目标**设置为**自动**可使用规划器默认值;也可以显式选择 Mermaid、JSON Canvas、Vega-Lite、HTML、可编辑 HTML/SVG、Draw.io、Drawnix 或 Circuitikz。此覆盖设置仅适用于 artifact 生成与预览命令。标准的**汇总为 Mermaid 图表**命令仍会保持与 Mermaid 兼容的输出格式,从而避免现有 Markdown 工作流在后台自动切换格式。 -这种区分很重要,因为现在 `flowchart` 类型的意图可以被渲染为用于 Markdown 笔记的 Mermaid、用于可靠备用的 HTML,或是用于后续编辑的可编辑版本 HTML/SVG。而 Draw.io 和 Drawnix 仍然只是 CLI 类型的内容导出工具,而非插件内的渲染目标。 +这种区分很重要,因为 `flowchart` 意图现在可以渲染为用于 Markdown 笔记的 Mermaid、用于可靠备用的 HTML、用于后续编辑的可编辑 HTML/SVG,或带 SVG 审查 companion 的 Draw.io/Drawnix 源 artifact。`circuit` 意图会路由到 Circuitikz,并要求提供已验证的 `CircuitSpec`;它不是让模型自由输出任意 TikZ 文本。 ## 使用方法 @@ -101,15 +103,20 @@ Notemd 会通过关键词评分从您的笔记内容中推断出最佳的图表 | Vega-Lite | `.json` | `{note}_diagram.json` | | HTML | `.html` | `{note}_diagram.html` | | 可编辑的 HTML/SVG | `.html` | `{note}_diagram.html` | +| Draw.io | `.drawio` + `.drawio.svg` + `.drawio.md` | `{note}_diagram.drawio` 及其审查 companion | +| Drawnix | `.drawnix` + `.drawnix.svg` + `.drawnix.md` | `{note}_diagram.drawnix` 及其审查 companion | +| Circuitikz | `.tex` + `.tex.svg` + `.tex.md` | `{note}_diagram.tex` 及其审查 companion | ### 预览图表 1. 运行 **“Notemd: 预览图表”** 2. 一个弹窗打开,显示已渲染的图表。 -3. 使用工具栏按钮导出为 SVG 或 PNG 格式 +3. 使用工具栏按钮导出为 SVG、PNG 或 PDF 格式 在设置中可开启**自动打开预览**功能——生成完成后,预览弹窗会自动显示。 +PNG 与 PDF 预览导出会使用配置的 preview PPI。默认值为 300 PPI,超过 600 PPI 的值会被夹到 600。SVG 保持矢量尺寸。`.drawio`、`.drawnix` 与 `.tex` 等源 artifact 可以提供 `previewSvg` companion,让 Obsidian 在不嵌入 diagrams.net、Drawnix、LaTeX 或 TikZJax 运行时的情况下显示并导出可审查图像。 + 预览模态框还包含一个缺陷诊断面板。渲染器和烟雾测试可以附加 `RenderArtifact.diagnostics`;该模态框会在预览旁边显示诊断摘要,包括错误/警告/信息数量,以及严重程度、诊断类型、消息和修复建议。同样的摘要也会显示在预览历史记录中,因此无需逐一查看每条记录即可对比重复的 circuitikz 烟雾测试结果。对于那些有源代码内容但无法通过内联方式或 HTML iframe 路径进行渲染的缺陷,该模态框现在会转而使用仅显示源代码的预览方式,而不会强制使用空 iframe。这样一来,circuitikz 编译/渲染烟雾测试、SVG 文本标记检查、PNG 空白截图检查以及未来的重叠报告都能有可见的 UI 展示界面,同时无需让 TikZJax 或 LaTeX 成为必须的插件运行时依赖,也不会将源代码文本当作已验证的可视化渲染结果。 ### 传统 Mermaid 模式 @@ -164,12 +171,12 @@ Notemd 会通过关键词评分从您的笔记内容中推断出最佳的图表 ### Draw.io 和 Drawnix 导出边界 -当前的实现将第三方编辑器支持限制在构建产物边界内: +当前实现仍然把第三方编辑器支持限制在 artifact 边界内,但已经暴露为显式渲染目标: | 目标 | 合同 | 运行时依赖项 | |--------|----------|--------------------| -| Draw.io | 来自 `SemanticFigureModel` 的确定性未压缩 `mxfile` XML | 插件运行时或 CI 中均无。 | -| Drawnix | 使用 `geometry` 和 `arrow-line` 元素构成的最小 `.drawnix` JSON 子集 | 插件运行时或 CI 中均无。 | +| Draw.io | 来自 `SemanticFigureModel` 的确定性未压缩 `mxfile` XML,并提供 SVG/PNG/PDF 审查 companion | 插件运行时或 CI 中均无。 | +| Drawnix | 使用 `geometry` 和 `arrow-line` 元素构成的最小 `.drawnix` JSON 子集,并提供 SVG/PNG/PDF 审查 companion | 插件运行时或 CI 中均无。 | 这种权衡是经过刻意设计的:Notemd能够验证可见标签、稳定标识以及受支持的原始数据覆盖情况,而无需将 diagrams.net Desktop、Drawnix、Plait 或仅适用于浏览器的编辑器状态嵌入到插件中。 @@ -213,9 +220,13 @@ Notemd 现已包含该方向首个受限仓库原型。它被刻意设置为离 npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex ``` -该原型为六个黄金参考系列添加了独立的 `CircuitSpec` 边界以及确定的导出器: +该原型为六个 golden-reference family 添加了受约束的 `CircuitSpec` 边界以及确定性导出器: -| 电路类型 | 金色参考手册 | 当前保修期 | +在实验性图表流水线中,这条路径现在也可以通过 `intent: "circuit"` 与 `circuitikz` render target 触发。生成的 `DiagramSpec` 只有在 circuit intent 下才允许嵌入 `circuitSpec`。`CircuitikzRenderer` 会写出同一份确定性 `.tex` 源文件,并附加从已验证电路拓扑派生出的 SVG preview companion,从而支持 Obsidian 预览以及 SVG/PNG/PDF 导出。该 companion 不是 LaTeX/TikZJax 编译结果;真实 renderer 证据仍属于下文显式 smoke 命令。 + +对于已支持的 golden templates,`layoutHints.inputSide` 与 `layoutHints.outputSide` 仍然只是展示层控制。它们可以移动确定性的输入/输出端口位置,但不会改变 topology signature,也不能让 repair pass 重接电路。 + +| 电路类型 | Golden reference | 当前保证 | |--------------|------------------|-------------------| | `common-source-amplifier` | `common-source-nmos-v1` | 在编写 LaTeX 之前,会先验证 `VDD -> R_D -> M1.D`、`vin -> M1.G`、`M1.S -> GND` 和 `M1.D -> vout`。 | | `cmos-inverter` | `cmos-inverter-v1` | 在生成 LaTeX 代码之前,会验证 PMOS-over-NMOS 结构、共享栅极输入、共享漏极输出、`VDD -> MP.S` 以及 `MN.S -> GND` 是否正确。 | @@ -224,7 +235,7 @@ npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-in | `cmos-nand2` | `cmos-nand2-v1` | 在写入 LaTeX 之前,会验证并联的 PMOS 上拉电阻、串联的 NMOS 下拉电阻、双输入 `va` / `vb` 以及 `vout` 的功能。 | | `cmos-nor2` | `cmos-nor2-v1` | 在写入 LaTeX 之前,会验证串联 PMOS 上拉、并联 NMOS 下拉、双输入 `va` / `vb` 以及 `vout` 的功能。 | -这还不是一个通用的 TikZ 生成器。它无法编译 LaTeX、调用 TikZJax、查看截图,也无法执行自动图像反馈修复功能。这些功能仍是后续需要实现的环节。 +这不是通用 TikZ 生成器。它不接受任意 TikZ,不会在插件运行时编译 LaTeX、调用 TikZJax、检查截图,也不会执行自动图像反馈修复。这些仍然是后续 gate。 当文件扩展名为 `.tex` 或 `.tikz`,且源代码包含 `\usepackage{circuitikz}` 或 `\begin{circuitikz}` 时,Preview diagram 命令可以直接重新打开已保存的 circuitikz 源文件。这种预览方式属于仅显示源代码的 circuitikz 预览模式:弹窗会展示源代码、诊断信息、复制/保存控件以及历史记录元数据,但不会在插件运行时编译 LaTeX 代码或调用 TikZJax。 @@ -350,8 +361,8 @@ npm run diagram:smoke-circuitikz -- --output-dir docs/export/circuitikz-smoke -- |------|----------------|-----------| | 通用图表 | 已为 Mermaid、JSON Canvas、Vega-Lite、HTML 实现以规格优先的流水线 | 持续扩大语义验证的覆盖范围 | | 可编辑的图表 | 已实现 `editable-html-svg`、Draw.io XML 以及 Drawnix JSON 的构件边界划分。 | 只有在测试证明可编辑之后,才添加更丰富的原始数据类型。 | -| CLI 支持功能 | `npm run diagram:export-artifact` 从一个 `DiagramSpec` 导出可编辑的 HTML/SVG、Draw.io 和 Drawnix | 在新的目标版本发布时,添加针对该目标的烟雾装置。 | -| circuitikz | `CircuitSpec -> circuitikz`原型导出了公共源、CMOS反相器、`cmos-buffer` / `cmos-buffer-v1`、`cmos-transmission-gate` / `cmos-transmission-gate-v1`、`cmos-nand2` / `cmos-nand2-v1`以及`cmos-nor2` / `cmos-nor2-v1`这些标准模板,将项目`layoutHints.inputSide`和`layoutHints.outputSide`以确定性的方式布置输入/输出端口,同时不会改变拓扑结构;通过`--topology-reference`防止拓扑结构出现偏差,通过`--repair-brief-output`和架构`notemd.circuitikz.repair-brief.v1`生成能够保留拓扑结构的修复方案;包含带有`diagnosticFocus`、`acceptanceCriteria`以及角色 `topology-preserving-circuitikz-repair`的结构性`repairPrompt`交接内容;通过`--repair-brief`验证候选修复方案,通过架构`notemd.circuitikz.repair-acceptance.v1`并借助`readyForVisualAcceptance`和`remainingChecks`返回`repairAcceptance`级联电路的相关证据,通过`--repair-acceptance-output`保存该证据,可解析编译日志,能够运行特定的本地渲染器以及`--expected-artifact`、SVG `--expected-svg-text`,通过`aria-label`、`<title>`和`<desc>`检查无障碍功能相关的元数据,可排除隐藏/透明类型的SVG元素,对仅包含路径的标签进行`render-svg-text-path-only` / `pathOnlyGlyphUseCount`分类,针对`<use href="#...">`检查仅包含路径的图形放置情况,通过`render-svg-path-glyph-overlap`诊断仅包含路径的图形重叠问题,为`Z/z`处理闭合路径上的电流点,精确确定A/a弧线的端点范围,精确确定C/S/Q/T曲线端点的贝塞尔曲线范围,考虑笔触宽度因素进行SVG范围及标签重叠检查,进行`polyline` / `polygon`绘制几何结构检查,处理已定位的`tspan`标签几何结构,根据`text-anchor`调整已定位文本的几何形状,针对SVG有限画布/文本重叠以及标签与图形重叠问题,结合变换功能进行几何结构处理,同时检查PNG图像的非空白/裁剪/密集前景部分的截图情况,包括索引颜色调色板中的透明度值、灰度/RGB模式下的tRNS透明样本,以及针对Adam7交错式PNG格式和索引位深错误提供的特定格式的`render-png-unsupported`指导,通过`foregroundBounds`、`foregroundDensity`、`render-png-content-clipped`和`render-png-foreground-dense`实现这些功能且无需进行shell解析;包含用于综合测试的维护人员专用测试用例,通过`npm run diagram:smoke-circuitikz`记录缺失的渲染器配置信息,以及通过`rendererAvailability.status: "missing-configuration"`和`compile-executable-invalid`进行相关记录,同时还具备通用的预览诊断功能、诊断摘要统计、基于诊断结果的历史记录功能,以及通过`RenderArtifact.diagnostics`和预览模式实现的仅基于源代码的备用方案。 | 为仅包含路径的视觉文本添加OCR级标签识别功能,实现精确的像素级重叠检测,在需要时覆盖更广泛的SVG路径范围;仅在保持可选性的前提下自动安装或发现渲染器,并自动执行能够保留拓扑结构的修复操作。 | +| CLI 支持功能 | `npm run diagram:export-artifact` 可以从一个已验证 `DiagramSpec` 导出可编辑 HTML/SVG、Draw.io、Drawnix、Circuitikz,以及 SVG/PNG/PDF 审查证据 | 在新的目标版本发布时,添加针对该目标的 smoke fixture。 | +| circuitikz | `DiagramSpec(intent: "circuit", circuitSpec) -> CircuitikzRenderer -> circuitikz` 可导出 common-source、CMOS inverter、`cmos-buffer` / `cmos-buffer-v1`、`cmos-transmission-gate` / `cmos-transmission-gate-v1`、`cmos-nand2` / `cmos-nand2-v1` 和 `cmos-nor2` / `cmos-nor2-v1` golden templates;已暴露 UI intent/render-target 选项;会写出 TeX 以及 SVG/PNG/PDF preview companion;会在输出前验证拓扑;可解析编译日志、运行显式本地 renderer 与 `--expected-artifact`;并通过 `RenderArtifact.diagnostics` 和预览模态框保持 source-only fallback 与预览诊断可见 | 为 path-only visual text 增加 OCR 级标签识别、精确像素级重叠检测,并在需要时扩展 SVG path 覆盖;只有在能保持可选性的前提下,才考虑自动安装/发现 renderer 和自动执行 topology-preserving repair。 | | TikZJax 集成 | 用于 Obsidian 端显示的候选渲染主机 | 保持其可选性,不要让 TikZJax 成为插件运行时的强制依赖项。 | ## 配置 @@ -361,6 +372,7 @@ npm run diagram:smoke-circuitikz -- --output-dir docs/export/circuitikz-smoke -- | `enableExperimentalDiagramPipeline` | `false` | 在以规范优先和传统模式之间切换 Mermaid | | `experimentalDiagramCompatibilityMode` | `'legacy-mermaid'` | `'legacy-mermaid'` 只能等于 Mermaid;`'best-fit'` 等于原生目标及备用选项 | | `preferredDiagramIntent` | `undefined`(自动) | 覆盖自动意图检测功能 | +| `preferredDiagramRenderTarget` | `undefined`(自动) | 覆盖 artifact renderer,包括 Draw.io、Drawnix 和 Circuitikz | | `summarizeToMermaidLanguage` | `'en'` | 图表标签的目标语言 | | `summarizeToMermaidProvider` / `Model` | DeepSeek | 用于图表生成的每任务 LLM | | `autoMermaidFixAfterGenerate` | (来自常量) | 在 Mermaid 的输出上自动运行旧版本修复工具 | @@ -380,10 +392,11 @@ npm run diagram:smoke-circuitikz -- --output-dir docs/export/circuitikz-smoke -- | 操作 | 方法 | |--------|--------| | SVG export | Canvas 的 `mermaid.render()` / `vega.View.toSVG()` / SVG 构建工具 | -| 导出为 PNG 格式 | SVG → 图像 → 画布(设备像素比 1x-3x)→ PNG ArrayBuffer | +| 导出为 PNG 格式 | SVG → 图像 → Canvas / 按配置 PPI 的预览栅格化器 → PNG ArrayBuffer | +| 导出为 PDF 格式 | SVG → 按配置 PPI 栅格化图像 → 单页 PDF | | 源文件保存 | 原始工件内容已使用特定目标扩展名保存 | | 仅源代码预览 | 非内联的工件,其源代码内容以代码形式显示并附带诊断信息,不会使用 iframe 渲染。 | -| 语义审计 | Mermaid、JSON Canvas、Vega-Lite,以及可编辑的 HTML/SVG,已由 `scripts/diagram-semantic-verification.js` 进行检查 | +| 语义审计 | Mermaid、JSON Canvas、Vega-Lite、可编辑 HTML/SVG、Draw.io、Drawnix 与受约束 circuitikz 由 renderer/CLI tests 覆盖 | **缓存**:RenderCache 使用 `{spec, target, theme}` 的确定性 JSON 键。传输过程中的去重功能可避免重复渲染。