mirror of
https://github.com/jacobinwwey/obsidian-NotEMD.git
synced 2026-07-22 05:48:27 +00:00
feat: add constrained circuitikz export prototype
This commit is contained in:
parent
2f6dc8c36a
commit
2d688db7c1
24 changed files with 1122 additions and 23 deletions
|
|
@ -39,6 +39,7 @@ This directory contains repository-level documentation for maintainers and contr
|
|||
|
||||
- [Diagram Reference Integration And Figure Generation Plan](./brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.md)
|
||||
- [circuitikz Figure Generation Roadmap](./maintainer/circuitikz-figure-generation-roadmap.md)
|
||||
- [circuitikz Export Prototype](./maintainer/circuitikz-export-prototype.md)
|
||||
- [Diagram Platform Phase 2 Requirements](./brainstorms/2026-04-14-diagram-platform-phase-2-requirements.md)
|
||||
- [Local KB Retrieval Decision And Quality Truth](./brainstorms/2026-06-09-local-kb-retrieval-decision-and-quality-truth.md)
|
||||
- [Local KB RAG Quality And Execution Truth](./brainstorms/2026-06-09-local-kb-rag-quality-and-execution-truth.md)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
- [图形参考项目集成与 Figure 生成扩展方案](./brainstorms/2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md)
|
||||
- [circuitikz Figure Generation Roadmap](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)
|
||||
- [circuitikz 导出原型](./maintainer/circuitikz-export-prototype.zh-CN.md)
|
||||
- [Diagram Platform Phase 2 Requirements](./brainstorms/2026-04-14-diagram-platform-phase-2-requirements.zh-CN.md)
|
||||
- [Local KB Retrieval 方案决策与质量真值](./brainstorms/2026-06-09-local-kb-retrieval-decision-and-quality-truth.zh-CN.md)
|
||||
- [Local KB RAG 质量与执行链路真值](./brainstorms/2026-06-09-local-kb-rag-quality-and-execution-truth.zh-CN.md)
|
||||
|
|
|
|||
|
|
@ -189,6 +189,17 @@ Phase D implementation status on 2026-07-05: the Drawnix spike is implemented as
|
|||
|
||||
Phase E implementation status on 2026-07-05: `scripts/export-diagram-artifact.js` and `npm run diagram:export-artifact` now provide an offline artifact exporter. The CLI reads a `DiagramSpec` JSON file, validates the requested target, builds a temporary internal `esbuild` bundle from the existing TypeScript exporters, and writes `editable-html-svg`, `drawio`, or `drawnix` output. `src/tests/diagramArtifactExportCli.test.ts` verifies real HTML/XML/JSON output, normalized-id collision handling, Draw.io visible-label preservation, Drawnix `geometry`/`arrow-line` output, and unsupported-target failure semantics. `docs/maintainer/diagram-artifact-export-cli.md` and `docs/maintainer/diagram-artifact-export-cli.zh-CN.md` document the command contract and explicitly state that no Obsidian runtime is required.
|
||||
|
||||
### Phase F: Constrained circuitikz Prototype
|
||||
|
||||
- [x] Keep circuit diagrams outside `DiagramSpec` until the circuit-specific topology/layout fields prove reusable.
|
||||
- [x] Define a minimal `CircuitSpec` with `circuitKind`, named nets, typed components, terminal references, layout hints, style, and `goldenReferenceId`.
|
||||
- [x] Add deterministic golden-template export for `common-source-amplifier` and `cmos-inverter`.
|
||||
- [x] Add an offline CLI that writes `.tex` without Obsidian, TikZJax, LaTeX, or browser runtime dependencies.
|
||||
- [x] Add topology-rejection tests so invalid circuits fail before output creation.
|
||||
- [x] Add bilingual maintainer docs and website progress notes.
|
||||
|
||||
Phase F implementation status on 2026-07-05: `src/diagram/adapters/circuitikz/circuitSpec.ts` and `src/diagram/adapters/circuitikz/circuitikzExporter.ts` now implement a constrained `CircuitSpec -> circuitikz` prototype. `scripts/export-circuitikz.js` and `npm run diagram:export-circuitikz` export deterministic LaTeX for `common-source-nmos-v1` and `cmos-inverter-v1`. `src/tests/circuitikzExporter.test.ts` and `src/tests/circuitikzExportCli.test.ts` verify deterministic output, topology rejection, package-script exposure, UTF-8 BOM input handling, and no output write for invalid topology. The implementation deliberately stops before TikZJax/LaTeX compilation, screenshot inspection, and visual repair loops.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
| Decision | Benefit | Cost |
|
||||
|
|
@ -196,6 +207,7 @@ Phase E implementation status on 2026-07-05: `scripts/export-diagram-artifact.js
|
|||
| HTML/SVG target before Drawnix | Fastest path to richer figures and editable export | Requires owning SVG layout and text fitting |
|
||||
| Drawnix as export target only | Avoids full whiteboard host complexity | No in-plugin board editing |
|
||||
| Keep `DiagramSpec` primary | Preserves semantic quality and testability | Requires writing target adapters instead of reusing converters blindly |
|
||||
| Separate `CircuitSpec` for circuitikz | Keeps terminal/topology rules explicit and testable | Requires a second spec boundary until common fields prove reusable |
|
||||
| Manual/release visual gate | Avoids flaky CI from diagrams.net/font differences | Maintainer must run richer checks before release claims |
|
||||
|
||||
## Pitfalls To Avoid
|
||||
|
|
@ -206,6 +218,7 @@ Phase E implementation status on 2026-07-05: `scripts/export-diagram-artifact.js
|
|||
- Do not add Google Fonts as a hard runtime dependency inside Obsidian preview. Use system fallbacks and keep external font loading optional.
|
||||
- Do not put heavy export code in `main.js` until packaging isolation is real.
|
||||
- Do not widen `DiagramSpec` prematurely. Use an internal `SemanticFigureModel` first; only promote fields after two or more targets need them.
|
||||
- Do not treat `diagram:export-circuitikz` as proof of visual rendering. It proves topology-constrained LaTeX export, not TikZJax compilation or screenshot quality.
|
||||
|
||||
## Best-Practice Boundary
|
||||
|
||||
|
|
@ -223,12 +236,12 @@ Cloudy is valuable because it proves a disciplined HTML/SVG + Draw.io export con
|
|||
|
||||
## Recommended Next Move
|
||||
|
||||
Do not start by adding dependencies. Start by adding a minimal editable HTML/SVG renderer behind the experimental diagram path and prove:
|
||||
The HTML/SVG, Draw.io, Drawnix, and first circuitikz export boundaries now exist. The next move should still avoid adding runtime dependencies by default:
|
||||
|
||||
1. current `DiagramSpec` can express a useful architecture/runtime mechanism figure;
|
||||
2. the renderer can generate a self-contained nonblank HTML artifact;
|
||||
3. visible text fits and remains searchable/editable;
|
||||
4. Draw.io export coverage can be audited structurally;
|
||||
5. the existing Mermaid, JSON Canvas, Vega-Lite, and HTML renderers are unaffected.
|
||||
1. add optional local TikZJax/LaTeX smoke verification for the two circuitikz golden families;
|
||||
2. parse compile logs into actionable diagnostics without requiring TikZJax as a plugin runtime dependency;
|
||||
3. add screenshot-level checks for nonblank render, expected labels, bounded canvas, and obvious overlap;
|
||||
4. keep topology locked during any repair prompt so visual repair cannot change the circuit;
|
||||
5. only then consider more circuit families or a plugin-side circuit preview target.
|
||||
|
||||
Only after those claims are verified should Notemd consider a `.drawnix` target. Drawnix is a good future export format, not a reason to embed a whiteboard product in the plugin.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -189,6 +189,17 @@ Phase D 于 2026-07-05 的实现状态:Drawnix spike 以 `src/diagram/adapters
|
|||
|
||||
Phase E 于 2026-07-05 的实现状态:`scripts/export-diagram-artifact.js` 与 `npm run diagram:export-artifact` 已提供离线 artifact exporter。CLI 读取 `DiagramSpec` JSON 文件,先验证 target,再用临时内部 `esbuild` bundle 复用现有 TypeScript exporters,写出 `editable-html-svg`、`drawio` 或 `drawnix` 输出。`src/tests/diagramArtifactExportCli.test.ts` 验证真实 HTML/XML/JSON 输出、normalized id collision handling、Draw.io visible-label preservation、Drawnix `geometry`/`arrow-line` 输出,以及 unsupported-target failure semantics。`docs/maintainer/diagram-artifact-export-cli.md` 与 `docs/maintainer/diagram-artifact-export-cli.zh-CN.md` 记录命令契约,并明确 no Obsidian runtime is required。
|
||||
|
||||
### Phase F:受约束 circuitikz 原型
|
||||
|
||||
- [x] 在 circuit-specific topology/layout 字段证明可复用前,不把电路图塞进 `DiagramSpec`。
|
||||
- [x] 定义最小 `CircuitSpec`,包含 `circuitKind`、命名 nets、typed components、terminal references、layout hints、style 和 `goldenReferenceId`。
|
||||
- [x] 为 `common-source-amplifier` 与 `cmos-inverter` 增加 deterministic golden-template export。
|
||||
- [x] 增加离线 CLI,在不依赖 Obsidian、TikZJax、LaTeX 或 browser runtime 的情况下写出 `.tex`。
|
||||
- [x] 增加 topology-rejection tests,确保无效电路在创建输出前失败。
|
||||
- [x] 增加双语 maintainer docs 与 website progress notes。
|
||||
|
||||
Phase F 于 2026-07-05 的实现状态:`src/diagram/adapters/circuitikz/circuitSpec.ts` 与 `src/diagram/adapters/circuitikz/circuitikzExporter.ts` 已实现受约束的 `CircuitSpec -> circuitikz` 原型。`scripts/export-circuitikz.js` 与 `npm run diagram:export-circuitikz` 可以为 `common-source-nmos-v1` 和 `cmos-inverter-v1` 导出确定性 LaTeX。`src/tests/circuitikzExporter.test.ts` 与 `src/tests/circuitikzExportCli.test.ts` 验证确定性输出、拓扑拒绝、package-script 暴露、UTF-8 BOM 输入处理,以及无效拓扑不会写出 output file。实现仍有意停在 TikZJax/LaTeX compilation、screenshot inspection 和 visual repair loop 之前。
|
||||
|
||||
## 权衡
|
||||
|
||||
| 决策 | 收益 | 成本 |
|
||||
|
|
@ -196,6 +207,7 @@ Phase E 于 2026-07-05 的实现状态:`scripts/export-diagram-artifact.js`
|
|||
| HTML/SVG target 先于 Drawnix | 最快获得更丰富 figure 与可编辑导出路径 | 必须自己拥有 SVG layout 与 text fitting |
|
||||
| Drawnix 仅作为 export target | 避免完整白板 host 复杂度 | 不提供插件内 board editing |
|
||||
| 保持 `DiagramSpec` 为主边界 | 保留语义质量与可测试性 | 需要编写 target adapters,不能盲目复用 converters |
|
||||
| 为 circuitikz 单独使用 `CircuitSpec` | 让 terminal/topology 规则保持显式且可测试 | 在公共字段证明可复用前需要维护第二个 spec 边界 |
|
||||
| manual/release visual gate | 避免 diagrams.net/font 差异导致 flaky CI | release 前维护者需要跑更重的检查 |
|
||||
|
||||
## 需要避免的坑
|
||||
|
|
@ -206,6 +218,7 @@ Phase E 于 2026-07-05 的实现状态:`scripts/export-diagram-artifact.js`
|
|||
- 不要在 Obsidian preview 里把 Google Fonts 做成硬运行时依赖。使用 system fallback,外部 font loading 保持可选。
|
||||
- 不要在 packaging isolation 成立前,把重型导出代码放进 `main.js`。
|
||||
- 不要过早扩宽 `DiagramSpec`。先使用内部 `SemanticFigureModel`;只有两个以上 target 都需要同一字段时,再提升为公共字段。
|
||||
- 不要把 `diagram:export-circuitikz` 当成视觉渲染证明。它证明的是 topology-constrained LaTeX export,不是 TikZJax compilation 或 screenshot quality。
|
||||
|
||||
## 最佳实践边界
|
||||
|
||||
|
|
@ -223,12 +236,12 @@ Cloudy 的价值在于证明严格的 HTML/SVG + Draw.io export contract 可以
|
|||
|
||||
## 建议的下一步
|
||||
|
||||
不要从增加依赖开始。先在 experimental diagram path 后面增加最小可编辑 HTML/SVG renderer,并证明:
|
||||
HTML/SVG、Draw.io、Drawnix 和第一个 circuitikz export 边界现在都已经存在。下一步仍然不应默认增加 runtime dependencies:
|
||||
|
||||
1. 当前 `DiagramSpec` 能表达有用的 architecture / runtime mechanism figure;
|
||||
2. renderer 能生成自包含、非空 HTML artifact;
|
||||
3. 可见文本能正确 fit,并保持 searchable / editable;
|
||||
4. Draw.io export coverage 可结构化审计;
|
||||
5. 现有 Mermaid、JSON Canvas、Vega-Lite 与 HTML renderer 不受影响。
|
||||
1. 为两个 circuitikz golden families 增加可选本地 TikZJax/LaTeX smoke verification;
|
||||
2. 将 compile logs 解析为 actionable diagnostics,且不要求 TikZJax 成为插件 runtime 硬依赖;
|
||||
3. 增加 screenshot-level 检查:nonblank render、expected labels、bounded canvas、obvious overlap;
|
||||
4. repair prompt 中锁定拓扑,避免视觉修复改变电路;
|
||||
5. 只有到这一步之后,再考虑更多 circuit families 或插件侧 circuit preview target。
|
||||
|
||||
只有这些 claim 被验证后,Notemd 才应该考虑 `.drawnix` target。Drawnix 是好的未来导出格式,不是把白板产品嵌入插件的理由。
|
||||
Drawnix 仍然是好的导出格式,不是把白板产品嵌入插件的理由。circuitikz 仍然是受约束 circuit target,不是接受 LLM 任意 TikZ 输出的理由。
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ Repository docs preview for `obsidian-NotEMD`.
|
|||
- [Diagram Artifact Export CLI (zh-CN)](./maintainer/diagram-artifact-export-cli.zh-CN.md)
|
||||
- [circuitikz Figure Generation Roadmap (EN)](./maintainer/circuitikz-figure-generation-roadmap.md)
|
||||
- [circuitikz Figure Generation Roadmap (zh-CN)](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)
|
||||
- [circuitikz Export Prototype (EN)](./maintainer/circuitikz-export-prototype.md)
|
||||
- [circuitikz Export Prototype (zh-CN)](./maintainer/circuitikz-export-prototype.zh-CN.md)
|
||||
- [Draw.io Export Visual Regression (EN)](./maintainer/drawio-export-visual-regression.md)
|
||||
- [Draw.io Export Visual Regression (zh-CN)](./maintainer/drawio-export-visual-regression.zh-CN.md)
|
||||
- [Drawnix Export Spike (EN)](./maintainer/drawnix-export-spike.md)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@
|
|||
- [Diagram Artifact Export CLI(中文)](./maintainer/diagram-artifact-export-cli.zh-CN.md)
|
||||
- [circuitikz Figure Generation Roadmap(英文)](./maintainer/circuitikz-figure-generation-roadmap.md)
|
||||
- [circuitikz Figure Generation Roadmap(中文)](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)
|
||||
- [circuitikz 导出原型(英文)](./maintainer/circuitikz-export-prototype.md)
|
||||
- [circuitikz 导出原型(中文)](./maintainer/circuitikz-export-prototype.zh-CN.md)
|
||||
- [draw.io 导出视觉回归 Runbook(英文)](./maintainer/drawio-export-visual-regression.md)
|
||||
- [draw.io 导出视觉回归 Runbook(中文)](./maintainer/drawio-export-visual-regression.zh-CN.md)
|
||||
- [Drawnix 导出 Spike(英文)](./maintainer/drawnix-export-spike.md)
|
||||
|
|
|
|||
80
docs/maintainer/circuitikz-export-prototype.md
Normal file
80
docs/maintainer/circuitikz-export-prototype.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
date: 2026-07-05
|
||||
topic: circuitikz-export-prototype
|
||||
---
|
||||
|
||||
# circuitikz Export Prototype
|
||||
|
||||
Language: **English** | [简体中文](./circuitikz-export-prototype.zh-CN.md)
|
||||
|
||||
## Contract
|
||||
|
||||
`scripts/export-circuitikz.js` is the offline export boundary for the first constrained circuit-diagram prototype.
|
||||
|
||||
It accepts a checked `CircuitSpec` JSON file and writes deterministic `circuitikz` LaTeX without requiring Obsidian, TikZJax, a LaTeX installation, or a browser runtime:
|
||||
|
||||
```bash
|
||||
npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex
|
||||
```
|
||||
|
||||
Direct entrypoint:
|
||||
|
||||
```bash
|
||||
node scripts/export-circuitikz.js --input common-source.json --output common-source.tex
|
||||
```
|
||||
|
||||
Input may be UTF-8 with or without a BOM, so JSON written from Windows PowerShell remains usable.
|
||||
|
||||
## Supported Circuit Families
|
||||
|
||||
This is not a generic TikZ generator. The current prototype supports only golden-reference families whose topology and layout can be validated before export:
|
||||
|
||||
| `circuitKind` | `goldenReferenceId` | Template |
|
||||
|---|---|---|
|
||||
| `common-source-amplifier` | `common-source-nmos-v1` | NMOS common-source amplifier with `R_D`, `VDD`, `vin`, `vout`, and grounded source |
|
||||
| `cmos-inverter` | `cmos-inverter-v1` | PMOS-over-NMOS CMOS inverter with shared gate input and shared drain output |
|
||||
|
||||
The adapter validates the structural invariant first, then emits a fixed layout. For example, the CMOS inverter requires:
|
||||
|
||||
- `MP` as `pmos` and `MN` as `nmos`;
|
||||
- `VDD -> MP.S`;
|
||||
- `MN.S -> GND`;
|
||||
- `vin -> MP.G` and `vin -> MN.G`;
|
||||
- `MP.D` and `MN.D` on the shared output drain path;
|
||||
- both transistor drains connected to `vout`.
|
||||
|
||||
## Why `CircuitSpec` Is Separate
|
||||
|
||||
`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.
|
||||
|
||||
The boundary is:
|
||||
|
||||
```text
|
||||
CircuitSpec
|
||||
-> topology/template validation
|
||||
-> deterministic golden-reference circuitikz adapter
|
||||
-> .tex artifact
|
||||
```
|
||||
|
||||
This keeps the model-facing contract narrow and makes topology drift testable. The exporter rejects invalid topology before writing an output file.
|
||||
|
||||
## Verification
|
||||
|
||||
Canonical regression commands:
|
||||
|
||||
```bash
|
||||
npm test -- --runInBand src/tests/circuitikzExporter.test.ts src/tests/circuitikzExportCli.test.ts --runTestsByPath
|
||||
```
|
||||
|
||||
The tests verify:
|
||||
|
||||
- deterministic common-source LaTeX;
|
||||
- constrained CMOS inverter output;
|
||||
- topology rejection before export;
|
||||
- CLI exposure through `package.json`;
|
||||
- UTF-8 BOM input handling;
|
||||
- no output file is written for invalid topology.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This prototype does not compile LaTeX, call TikZJax, run screenshot inspection, or use rendered-image feedback. Those are later gates. It also does not accept arbitrary natural-language circuit requests. The important current claim is narrower: validated `CircuitSpec` input can produce stable, readable circuitikz for two high-value golden families.
|
||||
80
docs/maintainer/circuitikz-export-prototype.zh-CN.md
Normal file
80
docs/maintainer/circuitikz-export-prototype.zh-CN.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
date: 2026-07-05
|
||||
topic: circuitikz-export-prototype
|
||||
---
|
||||
|
||||
# circuitikz 导出原型
|
||||
|
||||
语言: [English](./circuitikz-export-prototype.md) | **简体中文**
|
||||
|
||||
## 契约
|
||||
|
||||
`scripts/export-circuitikz.js` 是第一个强约束电路图原型的离线导出边界。
|
||||
|
||||
它接收经过校验的 `CircuitSpec` JSON 文件,并写出确定性的 `circuitikz` LaTeX;不需要 Obsidian、TikZJax、LaTeX 安装或浏览器 runtime:
|
||||
|
||||
```bash
|
||||
npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex
|
||||
```
|
||||
|
||||
直接入口:
|
||||
|
||||
```bash
|
||||
node scripts/export-circuitikz.js --input common-source.json --output common-source.tex
|
||||
```
|
||||
|
||||
输入可以是带 BOM 或不带 BOM 的 UTF-8,因此 Windows PowerShell 写出的 JSON 不需要额外归一化。
|
||||
|
||||
## 已支持的电路族
|
||||
|
||||
这不是通用 TikZ 生成器。当前原型只支持能够在导出前验证拓扑与布局约束的 golden-reference families:
|
||||
|
||||
| `circuitKind` | `goldenReferenceId` | Template |
|
||||
|---|---|---|
|
||||
| `common-source-amplifier` | `common-source-nmos-v1` | 带 `R_D`、`VDD`、`vin`、`vout` 和源极接地的 NMOS 共源放大器 |
|
||||
| `cmos-inverter` | `cmos-inverter-v1` | PMOS 在上、NMOS 在下,共用 gate 输入、共用 drain 输出的 CMOS 反相器 |
|
||||
|
||||
adapter 会先验证结构 invariant,再输出固定布局。例如 CMOS 反相器要求:
|
||||
|
||||
- `MP` 是 `pmos`,`MN` 是 `nmos`;
|
||||
- `VDD -> MP.S`;
|
||||
- `MN.S -> GND`;
|
||||
- `vin -> MP.G` 且 `vin -> MN.G`;
|
||||
- `MP.D` 与 `MN.D` 位于共享输出 drain path;
|
||||
- 两个晶体管 drain 都连接到 `vout`。
|
||||
|
||||
## 为什么 `CircuitSpec` 独立存在
|
||||
|
||||
当前不会为了电路图过早扩展 `DiagramSpec`。电路图需要拓扑、terminal references、layout lanes 和 package conventions;这些字段并不属于普通 flowchart 或 data chart。
|
||||
|
||||
当前边界是:
|
||||
|
||||
```text
|
||||
CircuitSpec
|
||||
-> topology/template validation
|
||||
-> deterministic golden-reference circuitikz adapter
|
||||
-> .tex artifact
|
||||
```
|
||||
|
||||
这能让 model-facing contract 保持窄边界,并让拓扑漂移可测试。exporter 会在写出文件前拒绝无效拓扑。
|
||||
|
||||
## 验证
|
||||
|
||||
标准回归命令:
|
||||
|
||||
```bash
|
||||
npm test -- --runInBand src/tests/circuitikzExporter.test.ts src/tests/circuitikzExportCli.test.ts --runTestsByPath
|
||||
```
|
||||
|
||||
测试覆盖:
|
||||
|
||||
- 确定性的 common-source LaTeX;
|
||||
- 强约束 CMOS inverter 输出;
|
||||
- 导出前拒绝拓扑错误;
|
||||
- `package.json` 中的 CLI 暴露;
|
||||
- UTF-8 BOM 输入处理;
|
||||
- 无效拓扑不会写出 output file。
|
||||
|
||||
## 非目标
|
||||
|
||||
这个原型不编译 LaTeX、不调用 TikZJax、不做截图检查,也不使用渲染图像反馈。这些是后续 gate。它也不接受任意自然语言电路请求。当前重要声明更窄:经过验证的 `CircuitSpec` 输入可以为两个高价值 golden families 生成稳定、可读的 circuitikz。
|
||||
|
|
@ -135,6 +135,17 @@ Screenshot feedback can be manual first. Automated screenshot checks should star
|
|||
| D. render feedback | Add optional local TikZJax/LaTeX smoke path | compile failures return actionable diagnostics |
|
||||
| E. visual repair loop | Feed rendered image or overlap report back into repair prompt | repeated layout errors are corrected without changing topology |
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Phase A is documented. Phase B/C now have a constrained repository prototype:
|
||||
|
||||
- `src/diagram/adapters/circuitikz/circuitSpec.ts` defines the separate circuit-only spec boundary.
|
||||
- `src/diagram/adapters/circuitikz/circuitikzExporter.ts` validates topology and emits deterministic `circuitikz` LaTeX for `common-source-amplifier` and `cmos-inverter`.
|
||||
- `scripts/export-circuitikz.js` and `npm run diagram:export-circuitikz` provide the offline export command.
|
||||
- `src/tests/circuitikzExporter.test.ts` and `src/tests/circuitikzExportCli.test.ts` verify deterministic output, topology rejection, package-script exposure, and UTF-8 BOM handling.
|
||||
|
||||
The implementation still deliberately stops before Phase D/E. It does not compile LaTeX, call TikZJax, inspect screenshots, or run a visual repair loop.
|
||||
|
||||
## Best Current Practice
|
||||
|
||||
Until `CircuitSpec` exists, use constrained prompts:
|
||||
|
|
|
|||
|
|
@ -135,6 +135,17 @@ screenshot feedback 可以先保持人工执行。自动 screenshot 检查应从
|
|||
| D. render feedback | 增加可选的本地 TikZJax/LaTeX smoke path | compile failures 返回 actionable diagnostics |
|
||||
| E. visual repair loop | 将 rendered image 或 overlap report 反馈给 repair prompt | 不改变拓扑的前提下修正重复 layout errors |
|
||||
|
||||
## 实现状态
|
||||
|
||||
Phase A 已经文档化。Phase B/C 现在有了受约束的仓库内原型:
|
||||
|
||||
- `src/diagram/adapters/circuitikz/circuitSpec.ts` 定义独立的 circuit-only spec 边界。
|
||||
- `src/diagram/adapters/circuitikz/circuitikzExporter.ts` 会验证拓扑,并为 `common-source-amplifier` 与 `cmos-inverter` 输出确定性的 `circuitikz` LaTeX。
|
||||
- `scripts/export-circuitikz.js` 与 `npm run diagram:export-circuitikz` 提供离线导出命令。
|
||||
- `src/tests/circuitikzExporter.test.ts` 与 `src/tests/circuitikzExportCli.test.ts` 验证确定性输出、拓扑拒绝、package-script 暴露和 UTF-8 BOM 处理。
|
||||
|
||||
实现仍有意停在 Phase D/E 之前。它不会编译 LaTeX、调用 TikZJax、检查截图或运行视觉修复闭环。
|
||||
|
||||
## Best Current Practice
|
||||
|
||||
在 `CircuitSpec` 尚未存在前,使用强约束 prompt:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"cli:help": "node scripts/invoke-maintainer-cli-operation.js --help",
|
||||
"cli:invoke": "node scripts/invoke-maintainer-cli-operation.js",
|
||||
"diagram:export-artifact": "node scripts/export-diagram-artifact.js",
|
||||
"diagram:export-circuitikz": "node scripts/export-circuitikz.js",
|
||||
"chronicle:sync-repo-saga": "node scripts/repo-saga/update-quarterly-saga.mjs --sync-only",
|
||||
"chronicle:update": "node scripts/repo-saga/update-quarterly-saga.mjs",
|
||||
"audit:i18n-ui": "node scripts/audit-hardcoded-ui-strings.js",
|
||||
|
|
|
|||
148
scripts/export-circuitikz.js
Normal file
148
scripts/export-circuitikz.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { buildSync } = require('esbuild');
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Notemd circuitikz export
|
||||
|
||||
Usage:
|
||||
node scripts/export-circuitikz.js --input <circuit-spec.json> --output <circuit.tex>
|
||||
|
||||
Example:
|
||||
node scripts/export-circuitikz.js --input cmos-inverter.json --output cmos-inverter.tex
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
switch (token) {
|
||||
case '--input':
|
||||
args.input = argv[++index];
|
||||
break;
|
||||
case '--output':
|
||||
args.output = argv[++index];
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
args.help = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function assertRequiredArgs(args) {
|
||||
if (!args.input) {
|
||||
throw new Error('Missing required --input.');
|
||||
}
|
||||
if (!args.output) {
|
||||
throw new Error('Missing required --output.');
|
||||
}
|
||||
}
|
||||
|
||||
function loadCircuitSpec(inputPath) {
|
||||
const source = fs.readFileSync(inputPath, 'utf8').replace(/^\uFEFF/, '');
|
||||
const spec = JSON.parse(source);
|
||||
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
||||
throw new Error('CircuitSpec JSON must be an object.');
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
function ensureOutputDirectory(outputPath) {
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
}
|
||||
|
||||
function buildExporterBundle(repoRoot) {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-exporter-'));
|
||||
const outfile = path.join(tempRoot, 'circuitikz-exporter.cjs');
|
||||
const entrySource = `
|
||||
import { exportCircuitSpecToCircuitikz } from './src/diagram/adapters/circuitikz/circuitikzExporter';
|
||||
|
||||
export function exportCircuitikz(spec) {
|
||||
return exportCircuitSpecToCircuitikz(spec);
|
||||
}
|
||||
`;
|
||||
|
||||
buildSync({
|
||||
stdin: {
|
||||
contents: entrySource,
|
||||
resolveDir: repoRoot,
|
||||
loader: 'ts',
|
||||
sourcefile: 'circuitikz-exporter.ts'
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile,
|
||||
logLevel: 'silent'
|
||||
});
|
||||
|
||||
return {
|
||||
outfile,
|
||||
cleanup() {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function run(args, repoRoot = path.resolve(__dirname, '..')) {
|
||||
assertRequiredArgs(args);
|
||||
const inputPath = path.resolve(args.input);
|
||||
const outputPath = path.resolve(args.output);
|
||||
const spec = loadCircuitSpec(inputPath);
|
||||
const bundle = buildExporterBundle(repoRoot);
|
||||
|
||||
try {
|
||||
const { exportCircuitikz } = require(bundle.outfile);
|
||||
const content = exportCircuitikz(spec);
|
||||
ensureOutputDirectory(outputPath);
|
||||
fs.writeFileSync(outputPath, content, 'utf8');
|
||||
|
||||
return {
|
||||
circuitKind: spec.circuitKind,
|
||||
goldenReferenceId: spec.goldenReferenceId,
|
||||
inputPath,
|
||||
outputPath,
|
||||
componentCount: Array.isArray(spec.components) ? spec.components.length : 0,
|
||||
connectionCount: Array.isArray(spec.connections) ? spec.connections.length : 0
|
||||
};
|
||||
} finally {
|
||||
bundle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await run(args);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseArgs,
|
||||
run
|
||||
};
|
||||
61
src/diagram/adapters/circuitikz/circuitSpec.ts
Normal file
61
src/diagram/adapters/circuitikz/circuitSpec.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
export const SUPPORTED_CIRCUIT_KINDS = [
|
||||
'common-source-amplifier',
|
||||
'cmos-inverter'
|
||||
] as const;
|
||||
|
||||
export type CircuitKind = typeof SUPPORTED_CIRCUIT_KINDS[number];
|
||||
|
||||
export const SUPPORTED_CIRCUIT_COMPONENT_TYPES = [
|
||||
'nmos',
|
||||
'pmos',
|
||||
'resistor'
|
||||
] as const;
|
||||
|
||||
export type CircuitComponentType = typeof SUPPORTED_CIRCUIT_COMPONENT_TYPES[number];
|
||||
|
||||
export type CircuitVoltageConvention = 'american voltages' | 'european voltages';
|
||||
|
||||
export interface CircuitComponent {
|
||||
id: string;
|
||||
type: CircuitComponentType;
|
||||
label?: string;
|
||||
terminals: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface CircuitConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface CircuitStyle {
|
||||
package: 'circuitikz';
|
||||
voltageConvention: CircuitVoltageConvention;
|
||||
}
|
||||
|
||||
export interface CircuitLayoutHints {
|
||||
inputSide?: 'left' | 'right';
|
||||
outputSide?: 'left' | 'right';
|
||||
routingStyle?: 'orthogonal';
|
||||
}
|
||||
|
||||
export interface CircuitSpec {
|
||||
circuitKind: CircuitKind;
|
||||
title: string;
|
||||
goldenReferenceId: string;
|
||||
style: CircuitStyle;
|
||||
nets: string[];
|
||||
components: CircuitComponent[];
|
||||
connections: CircuitConnection[];
|
||||
layoutHints?: CircuitLayoutHints;
|
||||
}
|
||||
|
||||
export function isSupportedCircuitKind(value: unknown): value is CircuitKind {
|
||||
return typeof value === 'string'
|
||||
&& (SUPPORTED_CIRCUIT_KINDS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function isSupportedCircuitComponentType(value: unknown): value is CircuitComponentType {
|
||||
return typeof value === 'string'
|
||||
&& (SUPPORTED_CIRCUIT_COMPONENT_TYPES as readonly string[]).includes(value);
|
||||
}
|
||||
289
src/diagram/adapters/circuitikz/circuitikzExporter.ts
Normal file
289
src/diagram/adapters/circuitikz/circuitikzExporter.ts
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
import { ValidationError } from '../../../types';
|
||||
import {
|
||||
CircuitComponent,
|
||||
CircuitConnection,
|
||||
CircuitSpec,
|
||||
isSupportedCircuitComponentType,
|
||||
isSupportedCircuitKind,
|
||||
SUPPORTED_CIRCUIT_KINDS
|
||||
} from './circuitSpec';
|
||||
|
||||
export interface CircuitSpecValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
function normalizeLabel(value: string | undefined, fallback: string): string {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
function collectComponentIds(components: CircuitComponent[], errors: string[]): Map<string, CircuitComponent> {
|
||||
const componentById = new Map<string, CircuitComponent>();
|
||||
|
||||
for (const component of components) {
|
||||
const id = component.id?.trim();
|
||||
if (!id) {
|
||||
errors.push('Circuit component is missing an id.');
|
||||
continue;
|
||||
}
|
||||
if (componentById.has(id)) {
|
||||
errors.push(`Circuit component id "${id}" is duplicated.`);
|
||||
continue;
|
||||
}
|
||||
if (!isSupportedCircuitComponentType(component.type)) {
|
||||
errors.push(`Circuit component "${id}" uses unsupported type "${String(component.type)}".`);
|
||||
}
|
||||
if (!component.terminals || Object.keys(component.terminals).length === 0) {
|
||||
errors.push(`Circuit component "${id}" is missing terminals.`);
|
||||
}
|
||||
componentById.set(id, component);
|
||||
}
|
||||
|
||||
return componentById;
|
||||
}
|
||||
|
||||
function parseTerminalReference(reference: string): { componentId: string; terminal: string } | null {
|
||||
const trimmed = reference.trim();
|
||||
const separatorIndex = trimmed.indexOf('.');
|
||||
if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
componentId: trimmed.slice(0, separatorIndex),
|
||||
terminal: trimmed.slice(separatorIndex + 1)
|
||||
};
|
||||
}
|
||||
|
||||
function validateReference(
|
||||
reference: string,
|
||||
componentById: Map<string, CircuitComponent>,
|
||||
nets: Set<string>,
|
||||
errors: string[]
|
||||
): void {
|
||||
const terminalReference = parseTerminalReference(reference);
|
||||
if (!terminalReference) {
|
||||
if (!nets.has(reference.trim())) {
|
||||
errors.push(`Circuit connection references unknown net "${reference}".`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const component = componentById.get(terminalReference.componentId);
|
||||
if (!component) {
|
||||
errors.push(`Circuit connection references unknown component "${terminalReference.componentId}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(component.terminals, terminalReference.terminal)) {
|
||||
errors.push(
|
||||
`Circuit connection references missing terminal "${terminalReference.terminal}" `
|
||||
+ `on component "${terminalReference.componentId}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hasConnection(connections: CircuitConnection[], first: string, second: string): boolean {
|
||||
return connections.some(connection =>
|
||||
(connection.from === first && connection.to === second)
|
||||
|| (connection.from === second && connection.to === first)
|
||||
);
|
||||
}
|
||||
|
||||
function requireComponent(
|
||||
componentById: Map<string, CircuitComponent>,
|
||||
id: string,
|
||||
type: CircuitComponent['type'],
|
||||
errors: string[]
|
||||
): void {
|
||||
const component = componentById.get(id);
|
||||
if (!component) {
|
||||
errors.push(`Circuit kind requires component "${id}".`);
|
||||
return;
|
||||
}
|
||||
if (component.type !== type) {
|
||||
errors.push(`Circuit component "${id}" must use type "${type}".`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCommonSourceTemplate(
|
||||
spec: CircuitSpec,
|
||||
componentById: Map<string, CircuitComponent>,
|
||||
errors: string[]
|
||||
): void {
|
||||
if (spec.goldenReferenceId !== 'common-source-nmos-v1') {
|
||||
errors.push('Common-source amplifier requires goldenReferenceId "common-source-nmos-v1".');
|
||||
}
|
||||
|
||||
requireComponent(componentById, 'M1', 'nmos', errors);
|
||||
requireComponent(componentById, 'RD', 'resistor', errors);
|
||||
|
||||
if (!hasConnection(spec.connections, 'vin', 'M1.G')) {
|
||||
errors.push('Common-source amplifier requires vin to connect to M1.G.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'M1.D', 'vout')) {
|
||||
errors.push('Common-source amplifier requires M1.D to connect to vout.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'M1.S', 'GND')) {
|
||||
errors.push('Common-source amplifier requires M1.S to connect to GND.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'VDD', 'RD.top') || !hasConnection(spec.connections, 'RD.bottom', 'M1.D')) {
|
||||
errors.push('Common-source amplifier requires VDD -> RD -> M1.D drain path.');
|
||||
}
|
||||
}
|
||||
|
||||
function validateCmosInverterTemplate(
|
||||
spec: CircuitSpec,
|
||||
componentById: Map<string, CircuitComponent>,
|
||||
errors: string[]
|
||||
): void {
|
||||
if (spec.goldenReferenceId !== 'cmos-inverter-v1') {
|
||||
errors.push('CMOS inverter requires goldenReferenceId "cmos-inverter-v1".');
|
||||
}
|
||||
|
||||
requireComponent(componentById, 'MP', 'pmos', errors);
|
||||
requireComponent(componentById, 'MN', 'nmos', errors);
|
||||
|
||||
if (!hasConnection(spec.connections, 'MP.D', 'MN.D')) {
|
||||
errors.push('CMOS inverter requires MP.D and MN.D to share the output drain path.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'vin', 'MP.G') || !hasConnection(spec.connections, 'vin', 'MN.G')) {
|
||||
errors.push('CMOS inverter requires vin to drive both MP.G and MN.G.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'VDD', 'MP.S')) {
|
||||
errors.push('CMOS inverter requires VDD to connect to MP.S.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'MN.S', 'GND')) {
|
||||
errors.push('CMOS inverter requires MN.S to connect to GND.');
|
||||
}
|
||||
if (!hasConnection(spec.connections, 'MP.D', 'vout') || !hasConnection(spec.connections, 'MN.D', 'vout')) {
|
||||
errors.push('CMOS inverter requires both transistor drains to connect to vout.');
|
||||
}
|
||||
}
|
||||
|
||||
export function validateCircuitSpec(spec: CircuitSpec): CircuitSpecValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!isSupportedCircuitKind(spec.circuitKind)) {
|
||||
errors.push(
|
||||
`Circuit spec uses unsupported circuitKind "${String(spec.circuitKind)}". `
|
||||
+ `Supported kinds: ${SUPPORTED_CIRCUIT_KINDS.join(', ')}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!spec.title?.trim()) {
|
||||
errors.push('Circuit spec title is required.');
|
||||
}
|
||||
if (spec.style?.package !== 'circuitikz') {
|
||||
errors.push('Circuit spec style.package must be "circuitikz".');
|
||||
}
|
||||
if (spec.style?.voltageConvention !== 'american voltages' && spec.style?.voltageConvention !== 'european voltages') {
|
||||
errors.push('Circuit spec style.voltageConvention must be "american voltages" or "european voltages".');
|
||||
}
|
||||
if (spec.layoutHints?.routingStyle && spec.layoutHints.routingStyle !== 'orthogonal') {
|
||||
errors.push('CircuitSpec circuitikz prototype only supports orthogonal routing.');
|
||||
}
|
||||
|
||||
const nets = new Set((spec.nets ?? []).map(net => net.trim()).filter(Boolean));
|
||||
for (const requiredNet of ['VDD', 'GND', 'vin', 'vout']) {
|
||||
if (!nets.has(requiredNet)) {
|
||||
errors.push(`Circuit spec requires net "${requiredNet}".`);
|
||||
}
|
||||
}
|
||||
|
||||
const componentById = collectComponentIds(spec.components ?? [], errors);
|
||||
for (const connection of spec.connections ?? []) {
|
||||
validateReference(connection.from, componentById, nets, errors);
|
||||
validateReference(connection.to, componentById, nets, errors);
|
||||
}
|
||||
|
||||
if (spec.circuitKind === 'common-source-amplifier') {
|
||||
validateCommonSourceTemplate(spec, componentById, errors);
|
||||
} else if (spec.circuitKind === 'cmos-inverter') {
|
||||
validateCmosInverterTemplate(spec, componentById, errors);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function assertValidCircuitSpec(spec: CircuitSpec): CircuitSpec {
|
||||
const result = validateCircuitSpec(spec);
|
||||
if (!result.valid) {
|
||||
throw new ValidationError(result.errors.join(' '), 'INVALID_INPUT');
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
function renderCommonSourceTemplate(spec: CircuitSpec): string {
|
||||
const m1 = spec.components.find(component => component.id === 'M1');
|
||||
const rd = spec.components.find(component => component.id === 'RD');
|
||||
const m1Label = normalizeLabel(m1?.label, '$M_1$');
|
||||
const rdLabel = normalizeLabel(rd?.label, '$R_D$');
|
||||
|
||||
return `\\usepackage{circuitikz}
|
||||
\\begin{document}
|
||||
\\begin{circuitikz}[${spec.style.voltageConvention}]
|
||||
\\draw
|
||||
(3,5) node[vcc]{$V_{DD}$}
|
||||
to [R, l=${rdLabel}] (3,3)
|
||||
to [short, *-o] (5,3) node[right]{$v_{out}$}
|
||||
(3,3) to [short] (3,2.2)
|
||||
node[nmos, anchor=D] (M1) {${m1Label}}
|
||||
(M1.S) to [short] (3,0.5)
|
||||
node[ground]{}
|
||||
(M1.G) to [short, -o] (0.8,2.2)
|
||||
node[left]{$v_{in}$};
|
||||
\\draw
|
||||
(3,0.5) node[below right]{$S$};
|
||||
\\end{circuitikz}
|
||||
\\end{document}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCmosInverterTemplate(spec: CircuitSpec): string {
|
||||
const mp = spec.components.find(component => component.id === 'MP');
|
||||
const mn = spec.components.find(component => component.id === 'MN');
|
||||
const mpLabel = normalizeLabel(mp?.label, '$M_P$');
|
||||
const mnLabel = normalizeLabel(mn?.label, '$M_N$');
|
||||
|
||||
return `\\usepackage{circuitikz}
|
||||
\\begin{document}
|
||||
\\begin{circuitikz}[${spec.style.voltageConvention}]
|
||||
\\draw
|
||||
(3,5) node[vcc]{$V_{DD}$}
|
||||
to [short] (3,4.4)
|
||||
node[pmos, anchor=S] (MP) {${mpLabel}}
|
||||
(MP.D) to [short] (3,2.75)
|
||||
to [short] (3,2.6)
|
||||
node[nmos, anchor=D] (MN) {${mnLabel}}
|
||||
(MN.S) to [short] (3,0.8)
|
||||
node[ground]{};
|
||||
\\draw
|
||||
(MP.G) to [short] (1.5,3.5)
|
||||
(MN.G) to [short] (1.5,2.0)
|
||||
(1.5,3.5) to [short] (1.5,2.0)
|
||||
to [short, -o] (0.8,2.75)
|
||||
node[left]{$v_{in}$};
|
||||
\\draw
|
||||
(3,2.75) to [short, *-o] (5,2.75) node[right]{$v_{out}$};
|
||||
\\end{circuitikz}
|
||||
\\end{document}
|
||||
`;
|
||||
}
|
||||
|
||||
export function exportCircuitSpecToCircuitikz(spec: CircuitSpec): string {
|
||||
assertValidCircuitSpec(spec);
|
||||
|
||||
switch (spec.circuitKind) {
|
||||
case 'common-source-amplifier':
|
||||
return renderCommonSourceTemplate(spec);
|
||||
case 'cmos-inverter':
|
||||
return renderCmosInverterTemplate(spec);
|
||||
default:
|
||||
throw new ValidationError('Unsupported circuitKind.', 'INVALID_INPUT');
|
||||
}
|
||||
}
|
||||
146
src/tests/circuitikzExportCli.test.ts
Normal file
146
src/tests/circuitikzExportCli.test.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { execFileSync, spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CircuitSpec } from '../diagram/adapters/circuitikz/circuitSpec';
|
||||
|
||||
function createSpec(): CircuitSpec {
|
||||
return {
|
||||
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 export CLI', () => {
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const packageJsonPath = path.join(repoRoot, 'package.json');
|
||||
const scriptPath = path.join(repoRoot, 'scripts', 'export-circuitikz.js');
|
||||
|
||||
test('is exposed through package scripts', () => {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
expect(packageJson.scripts['diagram:export-circuitikz']).toBe('node scripts/export-circuitikz.js');
|
||||
});
|
||||
|
||||
test('exports circuitikz LaTeX from a BOM-prefixed CircuitSpec file', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-cli-'));
|
||||
const specPath = path.join(tempRoot, 'circuit.json');
|
||||
const outputPath = path.join(tempRoot, 'circuit.tex');
|
||||
fs.writeFileSync(specPath, `\uFEFF${JSON.stringify(createSpec(), null, 2)}`, 'utf8');
|
||||
|
||||
try {
|
||||
const stdout = execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
scriptPath,
|
||||
'--input', specPath,
|
||||
'--output', outputPath
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
);
|
||||
|
||||
const result = JSON.parse(stdout);
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
circuitKind: 'cmos-inverter',
|
||||
goldenReferenceId: 'cmos-inverter-v1',
|
||||
outputPath,
|
||||
componentCount: 2,
|
||||
connectionCount: 7
|
||||
}));
|
||||
|
||||
const tex = fs.readFileSync(outputPath, 'utf8');
|
||||
expect(tex).toContain('\\usepackage{circuitikz}');
|
||||
expect(tex).toContain('node[pmos, anchor=S] (MP) {$M_P$}');
|
||||
expect(tex).toContain('node[nmos, anchor=D] (MN) {$M_N$}');
|
||||
expect(tex).toContain('node[right]{$v_{out}$};');
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
test('fails before writing output for invalid topology', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-cli-invalid-'));
|
||||
const specPath = path.join(tempRoot, 'circuit.json');
|
||||
const outputPath = path.join(tempRoot, 'circuit.tex');
|
||||
const spec = createSpec();
|
||||
spec.connections = spec.connections.filter(connection => connection.from !== 'MP.D' || connection.to !== 'MN.D');
|
||||
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf8');
|
||||
|
||||
try {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
scriptPath,
|
||||
'--input', specPath,
|
||||
'--output', outputPath
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe('');
|
||||
expect(result.stderr).toContain('CMOS inverter requires MP.D and MN.D');
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('fails before writing output when the input JSON is not an object', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'notemd-circuitikz-cli-malformed-'));
|
||||
const specPath = path.join(tempRoot, 'circuit.json');
|
||||
const outputPath = path.join(tempRoot, 'circuit.tex');
|
||||
fs.writeFileSync(specPath, 'null', 'utf8');
|
||||
|
||||
try {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
scriptPath,
|
||||
'--input', specPath,
|
||||
'--output', outputPath
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe('');
|
||||
expect(result.stderr).toContain('CircuitSpec JSON must be an object.');
|
||||
expect(fs.existsSync(outputPath)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
54
src/tests/circuitikzExportDocsContract.test.ts
Normal file
54
src/tests/circuitikzExportDocsContract.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('circuitikz export docs contract', () => {
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const englishDoc = fs.readFileSync(
|
||||
path.join(repoRoot, 'docs', 'maintainer', 'circuitikz-export-prototype.md'),
|
||||
'utf8'
|
||||
);
|
||||
const chineseDoc = fs.readFileSync(
|
||||
path.join(repoRoot, 'docs', 'maintainer', 'circuitikz-export-prototype.zh-CN.md'),
|
||||
'utf8'
|
||||
);
|
||||
const progressDoc = fs.readFileSync(
|
||||
path.join(repoRoot, 'docs', 'brainstorms', '2026-07-04-diagram-reference-integration-and-figure-generation-plan.md'),
|
||||
'utf8'
|
||||
);
|
||||
const progressDocZh = fs.readFileSync(
|
||||
path.join(repoRoot, 'docs', 'brainstorms', '2026-07-04-diagram-reference-integration-and-figure-generation-plan.zh-CN.md'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('documents the CLI contract, supported families, and explicit non-goals in both languages', () => {
|
||||
for (const doc of [englishDoc, chineseDoc]) {
|
||||
expect(doc).toContain('scripts/export-circuitikz.js');
|
||||
expect(doc).toContain('npm run diagram:export-circuitikz');
|
||||
expect(doc).toContain('CircuitSpec');
|
||||
expect(doc).toContain('common-source-amplifier');
|
||||
expect(doc).toContain('common-source-nmos-v1');
|
||||
expect(doc).toContain('cmos-inverter');
|
||||
expect(doc).toContain('cmos-inverter-v1');
|
||||
expect(doc).toContain('UTF-8');
|
||||
expect(doc).toContain('BOM');
|
||||
expect(doc).toContain('src/tests/circuitikzExporter.test.ts');
|
||||
expect(doc).toContain('src/tests/circuitikzExportCli.test.ts');
|
||||
}
|
||||
|
||||
expect(englishDoc).toContain('This is not a generic TikZ generator.');
|
||||
expect(chineseDoc).toContain('这不是通用 TikZ 生成器。');
|
||||
});
|
||||
|
||||
test('records Phase F implementation status in the bilingual progress plan', () => {
|
||||
for (const doc of [progressDoc, progressDocZh]) {
|
||||
expect(doc).toContain('Phase F');
|
||||
expect(doc).toContain('CircuitSpec -> circuitikz');
|
||||
expect(doc).toContain('scripts/export-circuitikz.js');
|
||||
expect(doc).toContain('npm run diagram:export-circuitikz');
|
||||
expect(doc).toContain('src/tests/circuitikzExporter.test.ts');
|
||||
expect(doc).toContain('src/tests/circuitikzExportCli.test.ts');
|
||||
expect(doc).toContain('TikZJax/LaTeX');
|
||||
expect(doc).toContain('screenshot');
|
||||
}
|
||||
});
|
||||
});
|
||||
127
src/tests/circuitikzExporter.test.ts
Normal file
127
src/tests/circuitikzExporter.test.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
assertValidCircuitSpec,
|
||||
exportCircuitSpecToCircuitikz,
|
||||
validateCircuitSpec
|
||||
} from '../diagram/adapters/circuitikz/circuitikzExporter';
|
||||
import { CircuitSpec } from '../diagram/adapters/circuitikz/circuitSpec';
|
||||
|
||||
function createCommonSourceSpec(): CircuitSpec {
|
||||
return {
|
||||
circuitKind: 'common-source-amplifier',
|
||||
title: 'Common Source Amplifier',
|
||||
goldenReferenceId: 'common-source-nmos-v1',
|
||||
style: {
|
||||
package: 'circuitikz',
|
||||
voltageConvention: 'american voltages'
|
||||
},
|
||||
nets: ['VDD', 'GND', 'vin', 'vout', 'drain', 'source'],
|
||||
components: [
|
||||
{ id: 'M1', type: 'nmos', label: '$M_1$', terminals: { D: 'drain', G: 'vin', S: 'source' } },
|
||||
{ id: 'RD', type: 'resistor', label: '$R_D$', terminals: { top: 'VDD', bottom: 'drain' } }
|
||||
],
|
||||
connections: [
|
||||
{ from: 'VDD', to: 'RD.top' },
|
||||
{ from: 'RD.bottom', to: 'M1.D' },
|
||||
{ from: 'M1.D', to: 'vout' },
|
||||
{ from: 'M1.S', to: 'GND' },
|
||||
{ from: 'vin', to: 'M1.G' }
|
||||
],
|
||||
layoutHints: {
|
||||
inputSide: 'left',
|
||||
outputSide: 'right',
|
||||
routingStyle: 'orthogonal'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createCmosInverterSpec(): CircuitSpec {
|
||||
return {
|
||||
circuitKind: 'cmos-inverter',
|
||||
title: 'CMOS Inverter',
|
||||
goldenReferenceId: 'cmos-inverter-v1',
|
||||
style: {
|
||||
package: 'circuitikz',
|
||||
voltageConvention: 'american voltages'
|
||||
},
|
||||
nets: ['VDD', 'GND', 'vin', 'vout', 'p_source', 'n_source', '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 exporter', () => {
|
||||
test('validates and exports the common-source golden reference template deterministically', () => {
|
||||
const spec = createCommonSourceSpec();
|
||||
|
||||
expect(validateCircuitSpec(spec)).toEqual({ valid: true, errors: [] });
|
||||
expect(exportCircuitSpecToCircuitikz(spec)).toBe(`\\usepackage{circuitikz}
|
||||
\\begin{document}
|
||||
\\begin{circuitikz}[american voltages]
|
||||
\\draw
|
||||
(3,5) node[vcc]{$V_{DD}$}
|
||||
to [R, l=$R_D$] (3,3)
|
||||
to [short, *-o] (5,3) node[right]{$v_{out}$}
|
||||
(3,3) to [short] (3,2.2)
|
||||
node[nmos, anchor=D] (M1) {$M_1$}
|
||||
(M1.S) to [short] (3,0.5)
|
||||
node[ground]{}
|
||||
(M1.G) to [short, -o] (0.8,2.2)
|
||||
node[left]{$v_{in}$};
|
||||
\\draw
|
||||
(3,0.5) node[below right]{$S$};
|
||||
\\end{circuitikz}
|
||||
\\end{document}
|
||||
`);
|
||||
});
|
||||
|
||||
test('validates and exports a constrained CMOS inverter template deterministically', () => {
|
||||
const spec = createCmosInverterSpec();
|
||||
const output = exportCircuitSpecToCircuitikz(spec);
|
||||
|
||||
expect(assertValidCircuitSpec(spec)).toBe(spec);
|
||||
expect(output).toContain('\\begin{circuitikz}[american voltages]');
|
||||
expect(output).toContain('(3,5) node[vcc]{$V_{DD}$}');
|
||||
expect(output).toContain('node[pmos, anchor=S] (MP) {$M_P$}');
|
||||
expect(output).toContain('node[nmos, anchor=D] (MN) {$M_N$}');
|
||||
expect(output).toContain('(MP.G) to [short] (1.5,3.5)');
|
||||
expect(output).toContain('(MN.G) to [short] (1.5,2.0)');
|
||||
expect(output).toContain('(3,2.75) to [short, *-o] (5,2.75) node[right]{$v_{out}$};');
|
||||
expect(output).toContain('node[left]{$v_{in}$};');
|
||||
expect(output).toContain('node[ground]{};');
|
||||
});
|
||||
|
||||
test('rejects CMOS inverter specs that do not lock shared drain topology', () => {
|
||||
const spec = createCmosInverterSpec();
|
||||
spec.connections = spec.connections.filter(connection => connection.from !== 'MP.D' || connection.to !== 'MN.D');
|
||||
|
||||
expect(validateCircuitSpec(spec).errors).toContain(
|
||||
'CMOS inverter requires MP.D and MN.D to share the output drain path.'
|
||||
);
|
||||
expect(() => exportCircuitSpecToCircuitikz(spec)).toThrow(/MP\.D and MN\.D/);
|
||||
});
|
||||
|
||||
test('rejects common-source specs that do not connect the input to the NMOS gate', () => {
|
||||
const spec = createCommonSourceSpec();
|
||||
spec.connections = spec.connections.filter(connection => connection.to !== 'M1.G');
|
||||
|
||||
expect(validateCircuitSpec(spec).errors).toContain(
|
||||
'Common-source amplifier requires vin to connect to M1.G.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -120,20 +120,38 @@ describe('docs hub contract', () => {
|
|||
expect(docsHub).toContain(
|
||||
'[circuitikz Figure Generation Roadmap](./maintainer/circuitikz-figure-generation-roadmap.md)'
|
||||
);
|
||||
expect(docsHub).toContain(
|
||||
'[circuitikz Export Prototype](./maintainer/circuitikz-export-prototype.md)'
|
||||
);
|
||||
expect(docsHubZh).toContain(
|
||||
'[circuitikz Figure Generation Roadmap](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)'
|
||||
);
|
||||
expect(docsHubZh).toContain(
|
||||
'[circuitikz 导出原型](./maintainer/circuitikz-export-prototype.zh-CN.md)'
|
||||
);
|
||||
expect(docsIndex).toContain(
|
||||
'[circuitikz Figure Generation Roadmap (EN)](./maintainer/circuitikz-figure-generation-roadmap.md)'
|
||||
);
|
||||
expect(docsIndex).toContain(
|
||||
'[circuitikz Figure Generation Roadmap (zh-CN)](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)'
|
||||
);
|
||||
expect(docsIndex).toContain(
|
||||
'[circuitikz Export Prototype (EN)](./maintainer/circuitikz-export-prototype.md)'
|
||||
);
|
||||
expect(docsIndex).toContain(
|
||||
'[circuitikz Export Prototype (zh-CN)](./maintainer/circuitikz-export-prototype.zh-CN.md)'
|
||||
);
|
||||
expect(docsIndexZh).toContain(
|
||||
'[circuitikz Figure Generation Roadmap(英文)](./maintainer/circuitikz-figure-generation-roadmap.md)'
|
||||
);
|
||||
expect(docsIndexZh).toContain(
|
||||
'[circuitikz Figure Generation Roadmap(中文)](./maintainer/circuitikz-figure-generation-roadmap.zh-CN.md)'
|
||||
);
|
||||
expect(docsIndexZh).toContain(
|
||||
'[circuitikz 导出原型(英文)](./maintainer/circuitikz-export-prototype.md)'
|
||||
);
|
||||
expect(docsIndexZh).toContain(
|
||||
'[circuitikz 导出原型(中文)](./maintainer/circuitikz-export-prototype.zh-CN.md)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,20 +42,25 @@ describe('website documentation contract', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('diagram pages document circuitikz as a constrained future target in both languages', () => {
|
||||
test('diagram pages document circuitikz as a constrained prototype in both languages', () => {
|
||||
expect(markdownHeadings(chineseDiagrams)).toHaveLength(markdownHeadings(englishDiagrams).length);
|
||||
expect(englishDiagrams).toContain('Current circuitikz Prototype');
|
||||
expect(chineseDiagrams).toContain('当前 circuitikz 原型');
|
||||
|
||||
for (const content of [englishDiagrams, chineseDiagrams]) {
|
||||
expect(content).toContain('circuitikz / TikZJax Direction');
|
||||
expect(content).toContain('Golden Reference Prompt Shape');
|
||||
expect(content).toContain('Current Progress And Next Phases');
|
||||
expect(content).toContain('CircuitSpec');
|
||||
expect(content).toContain('common-source-amplifier');
|
||||
expect(content).toContain('cmos-inverter-v1');
|
||||
expect(content).toContain('Golden Reference Template');
|
||||
expect(content).toContain('TikZJax Render');
|
||||
expect(content).toContain('\\usepackage{circuitikz}');
|
||||
expect(content).toContain('pmos');
|
||||
expect(content).toContain('nmos');
|
||||
expect(content).toContain('npm run diagram:export-artifact');
|
||||
expect(content).toContain('npm run diagram:export-circuitikz');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,23 @@ The first-class model should describe circuit topology and layout separately:
|
|||
| Style | package, voltage convention, labels, anchors | `\begin{circuitikz}[american voltages]` |
|
||||
| Validation | compile log, missing anchors, overlap/screenshot checks | TikZJax/LaTeX diagnostics plus visual review |
|
||||
|
||||
### Current circuitikz Prototype
|
||||
|
||||
Notemd now includes the first constrained repository prototype for this direction. It is intentionally offline and template-bound:
|
||||
|
||||
```bash
|
||||
npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex
|
||||
```
|
||||
|
||||
The prototype adds a separate `CircuitSpec` boundary and deterministic exporter for two golden-reference families:
|
||||
|
||||
| Circuit kind | Golden reference | Current guarantee |
|
||||
|--------------|------------------|-------------------|
|
||||
| `common-source-amplifier` | `common-source-nmos-v1` | validates `VDD -> R_D -> M1.D`, `vin -> M1.G`, `M1.S -> GND`, and `M1.D -> vout` before writing LaTeX |
|
||||
| `cmos-inverter` | `cmos-inverter-v1` | validates PMOS-over-NMOS topology, shared gate input, shared drain output, `VDD -> MP.S`, and `MN.S -> GND` before writing LaTeX |
|
||||
|
||||
This is not a general TikZ generator yet. It does not compile LaTeX, call TikZJax, inspect screenshots, or run image-feedback repair. Those remain later gates.
|
||||
|
||||
### Golden Reference Prompt Shape
|
||||
|
||||
For near-term usage, provide a renderable golden reference before asking for a circuit variant. A constrained prompt should preserve the preamble, coordinate scale, anchor style, and routing conventions:
|
||||
|
|
@ -235,8 +252,6 @@ For a CMOS inverter, the prompt should request an explicit topology plus layout
|
|||
- use named anchors (`PM1.G`, `NM1.G`, `PM1.D`, `NM1.D`) instead of visually inferred coordinates;
|
||||
- avoid diagonal or crossing wires unless electrically required.
|
||||
|
||||
This is not yet a shipped renderer target. It is the recommended constraint system for the next implementation phase.
|
||||
|
||||
### Current Progress And Next Phases
|
||||
|
||||
| Area | Current status | Next move |
|
||||
|
|
@ -244,7 +259,7 @@ This is not yet a shipped renderer target. It is the recommended constraint syst
|
|||
| 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 | Researched as a constrained future target, not a free-form generation path | Define `CircuitSpec`, golden templates, compile-log parsing, and screenshot feedback loop |
|
||||
| circuitikz | `CircuitSpec -> circuitikz` prototype exports common-source and CMOS inverter golden templates through `npm run diagram:export-circuitikz` | Add compile/screenshot feedback and topology-preserving repair |
|
||||
| TikZJax integration | Candidate render host for Obsidian-side display | Keep it optional; do not make TikZJax a hard plugin runtime dependency |
|
||||
|
||||
## Configuration
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ graph LR
|
|||
E --> F[Preview or Export]
|
||||
```
|
||||
|
||||
The current implementation already supports Mermaid, JSON Canvas, Vega-Lite, HTML fallback, editable HTML/SVG, Draw.io XML artifacts, and a minimal Drawnix JSON subset. Circuit diagrams are a harder class: circuitikz can express accurate electrical topology, but unconstrained LLM output often produces unreadable routing or non-rendering LaTeX. The planned direction is to treat circuitikz as a constrained target with golden-reference templates, node-grid layout rules, render diagnostics, and screenshot feedback loops.
|
||||
The current implementation already supports Mermaid, JSON Canvas, Vega-Lite, HTML fallback, editable HTML/SVG, Draw.io XML artifacts, a minimal Drawnix JSON subset, and an offline `CircuitSpec -> circuitikz` prototype for common-source and CMOS inverter golden templates. Circuit diagrams are a harder class: circuitikz can express accurate electrical topology, but unconstrained LLM output often produces unreadable routing or non-rendering LaTeX. The next direction is to keep circuitikz constrained with golden-reference templates, node-grid layout rules, render diagnostics, and screenshot feedback loops.
|
||||
|
||||
Read the details in [Diagrams](./features/diagrams).
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,23 @@ graph LR
|
|||
| Style | package、电压约定、labels、anchors | `\begin{circuitikz}[american voltages]` |
|
||||
| Validation | compile log、missing anchors、overlap/screenshot checks | TikZJax/LaTeX diagnostics plus visual review |
|
||||
|
||||
### 当前 circuitikz 原型
|
||||
|
||||
Notemd 现在已经包含这个方向的第一个受约束仓库内原型。它有意保持为离线、template-bound 的导出能力:
|
||||
|
||||
```bash
|
||||
npm run diagram:export-circuitikz -- --input cmos-inverter.json --output cmos-inverter.tex
|
||||
```
|
||||
|
||||
这个原型新增了独立的 `CircuitSpec` 边界,并为两个 golden-reference families 提供确定性 exporter:
|
||||
|
||||
| Circuit kind | Golden reference | Current guarantee |
|
||||
|--------------|------------------|-------------------|
|
||||
| `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 拓扑、shared gate input、shared drain output、`VDD -> MP.S` 和 `MN.S -> GND` |
|
||||
|
||||
这还不是通用 TikZ 生成器。它不会编译 LaTeX、调用 TikZJax、检查截图或运行图像反馈修复;这些仍是后续 gate。
|
||||
|
||||
### Golden Reference Prompt Shape
|
||||
|
||||
近期使用时,应该在要求生成电路变体前给出一个可渲染 golden reference。强约束 prompt 应保留 preamble、coordinate scale、anchor style 和 routing conventions:
|
||||
|
|
@ -235,8 +252,6 @@ graph LR
|
|||
- 使用 named anchors(`PM1.G`、`NM1.G`、`PM1.D`、`NM1.D`),不要依赖视觉推断坐标;
|
||||
- 除非电气上必要,避免 diagonal 或 crossing wires。
|
||||
|
||||
这还不是已发布的 renderer target。它是下一阶段实现的推荐约束系统。
|
||||
|
||||
### Current Progress And Next Phases
|
||||
|
||||
| Area | Current status | Next move |
|
||||
|
|
@ -244,7 +259,7 @@ graph LR
|
|||
| General diagrams | Mermaid、JSON Canvas、Vega-Lite、HTML 的 spec-first pipeline 已实现 | 继续扩展 semantic verification coverage |
|
||||
| Editable figures | `editable-html-svg`、Draw.io XML、Drawnix JSON artifact boundaries 已实现 | 只有在测试证明 editability 后再加入更丰富 primitives |
|
||||
| CLI support | `npm run diagram:export-artifact` 可从同一个 `DiagramSpec` 导出 editable HTML/SVG、Draw.io、Drawnix | 新 target 发布时增加 target-specific smoke fixtures |
|
||||
| circuitikz | 已作为强约束 future target 研究,不作为自由生成路径 | 定义 `CircuitSpec`、golden templates、compile-log parsing 与 screenshot feedback loop |
|
||||
| circuitikz | `CircuitSpec -> circuitikz` 原型已经可通过 `npm run diagram:export-circuitikz` 导出 common-source 与 CMOS inverter golden templates | 增加 compile/screenshot feedback 与 topology-preserving repair |
|
||||
| TikZJax integration | Obsidian-side display 的候选 render host | 保持可选,不把 TikZJax 变成硬 runtime 依赖 |
|
||||
|
||||
## Configuration
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ graph LR
|
|||
E --> F[Preview or Export]
|
||||
```
|
||||
|
||||
当前实现已经支持 Mermaid、JSON Canvas、Vega-Lite、HTML fallback、editable HTML/SVG、Draw.io XML artifact,以及最小 Drawnix JSON subset。电路图是更难的一类:circuitikz 可以准确表达电路拓扑,但不受约束的 LLM 输出经常产生不可读的布线或无法渲染的 LaTeX。后续方向是把 circuitikz 作为强约束 target:使用 golden-reference 模板、节点网格布局规则、渲染诊断和截图反馈闭环。
|
||||
当前实现已经支持 Mermaid、JSON Canvas、Vega-Lite、HTML fallback、editable HTML/SVG、Draw.io XML artifact、最小 Drawnix JSON subset,以及面向 common-source 和 CMOS inverter golden templates 的离线 `CircuitSpec -> circuitikz` 原型。电路图是更难的一类:circuitikz 可以准确表达电路拓扑,但不受约束的 LLM 输出经常产生不可读的布线或无法渲染的 LaTeX。下一步方向是继续把 circuitikz 作为强约束 target:使用 golden-reference 模板、节点网格布局规则、渲染诊断和截图反馈闭环。
|
||||
|
||||
详细内容见 [图表与可编辑 Figure](./features/diagrams)。
|
||||
|
||||
|
|
|
|||
|
|
@ -355,6 +355,9 @@ function auditDiagramDocs() {
|
|||
assertContains(englishDiagrams, 'TikZJax', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'preferredDiagramRenderTarget', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'Render Target Selection', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'diagram:export-circuitikz', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'CircuitSpec', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'cmos-inverter-v1', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'Golden Reference Prompt Shape', 'English diagrams doc');
|
||||
assertContains(englishDiagrams, 'scripts/diagram-semantic-verification.js', 'English diagrams doc');
|
||||
|
||||
|
|
@ -366,6 +369,9 @@ function auditDiagramDocs() {
|
|||
assertContains(zhCnDiagrams, 'TikZJax', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'preferredDiagramRenderTarget', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, '渲染目标选择', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'diagram:export-circuitikz', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'CircuitSpec', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'cmos-inverter-v1', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'Golden Reference Prompt Shape', 'zh-CN diagrams doc');
|
||||
assertContains(zhCnDiagrams, 'SemanticFigureModel', 'zh-CN diagrams doc');
|
||||
assertNotContains(zhCnDiagrams, 'content="noindex,follow"', 'zh-CN diagrams doc');
|
||||
|
|
|
|||
Loading…
Reference in a new issue