@huaqiu/dsh-tool-kicad-schematic-gen 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +6 -2
  2. package/README.zh.md +11 -7
  3. package/lib/client.js +4 -0
  4. package/lib/index.js +101 -74
  5. package/package.json +3 -3
  6. package/skills/kicad-schematic-design/SKILL.md +14 -4
  7. package/skills/kicad-schematic-design/assets/drawing-patterns/LICENSE.kicad-symbols.md +27 -0
  8. package/skills/kicad-schematic-design/assets/drawing-patterns/crystal-load.mjs +35 -0
  9. package/skills/kicad-schematic-design/assets/drawing-patterns/crystal-load.png +0 -0
  10. package/skills/kicad-schematic-design/assets/drawing-patterns/dcdc-boost.mjs +46 -0
  11. package/skills/kicad-schematic-design/assets/drawing-patterns/dcdc-boost.png +0 -0
  12. package/skills/kicad-schematic-design/assets/drawing-patterns/dcdc-buck.mjs +44 -0
  13. package/skills/kicad-schematic-design/assets/drawing-patterns/dcdc-buck.png +0 -0
  14. package/skills/kicad-schematic-design/assets/drawing-patterns/dcdc-geometry.mjs +23 -0
  15. package/skills/kicad-schematic-design/assets/drawing-patterns/decoupling-bank.png +0 -0
  16. package/skills/kicad-schematic-design/assets/drawing-patterns/generate-supplement.mjs +80 -0
  17. package/skills/kicad-schematic-design/assets/drawing-patterns/generate-systems.mjs +71 -0
  18. package/skills/kicad-schematic-design/assets/drawing-patterns/generate.mjs +10 -2
  19. package/skills/kicad-schematic-design/assets/drawing-patterns/geometry.mjs +1 -1
  20. package/skills/kicad-schematic-design/assets/drawing-patterns/i2c-pullups.mjs +23 -0
  21. package/skills/kicad-schematic-design/assets/drawing-patterns/i2c-pullups.png +0 -0
  22. package/skills/kicad-schematic-design/assets/drawing-patterns/ldo-fixed-on.png +0 -0
  23. package/skills/kicad-schematic-design/assets/drawing-patterns/ldo-three-pin.mjs +49 -0
  24. package/skills/kicad-schematic-design/assets/drawing-patterns/ldo-three-pin.png +0 -0
  25. package/skills/kicad-schematic-design/assets/drawing-patterns/led-power-indicator.png +0 -0
  26. package/skills/kicad-schematic-design/assets/drawing-patterns/mcu-external-crystal.mjs +6 -0
  27. package/skills/kicad-schematic-design/assets/drawing-patterns/mcu-external-crystal.png +0 -0
  28. package/skills/kicad-schematic-design/assets/drawing-patterns/mcu-minimum.mjs +90 -0
  29. package/skills/kicad-schematic-design/assets/drawing-patterns/mcu-minimum.png +0 -0
  30. package/skills/kicad-schematic-design/assets/drawing-patterns/mosfet-low-side.mjs +44 -0
  31. package/skills/kicad-schematic-design/assets/drawing-patterns/mosfet-low-side.png +0 -0
  32. package/skills/kicad-schematic-design/assets/drawing-patterns/pullup-button.png +0 -0
  33. package/skills/kicad-schematic-design/assets/drawing-patterns/reference-symbols.kicad_sym +3790 -0
  34. package/skills/kicad-schematic-design/assets/drawing-patterns/reference-symbols.md +34 -0
  35. package/skills/kicad-schematic-design/assets/drawing-patterns/render.mjs +7 -1
  36. package/skills/kicad-schematic-design/assets/drawing-patterns/reset-rc.mjs +27 -0
  37. package/skills/kicad-schematic-design/assets/drawing-patterns/reset-rc.png +0 -0
  38. package/skills/kicad-schematic-design/assets/drawing-patterns/swd-header.mjs +24 -0
  39. package/skills/kicad-schematic-design/assets/drawing-patterns/swd-header.png +0 -0
  40. package/skills/kicad-schematic-design/assets/drawing-patterns/usb-c-device.mjs +35 -0
  41. package/skills/kicad-schematic-design/assets/drawing-patterns/usb-c-device.png +0 -0
  42. package/skills/kicad-schematic-design/references/component-pcb.md +6 -0
  43. package/skills/kicad-schematic-design/references/drawing-patterns/crystal-load.md +19 -0
  44. package/skills/kicad-schematic-design/references/drawing-patterns/dcdc-boost.md +15 -0
  45. package/skills/kicad-schematic-design/references/drawing-patterns/dcdc-buck.md +15 -0
  46. package/skills/kicad-schematic-design/references/drawing-patterns/i2c-pullups.md +19 -0
  47. package/skills/kicad-schematic-design/references/drawing-patterns/ldo-three-pin.md +19 -0
  48. package/skills/kicad-schematic-design/references/drawing-patterns/mcu-external-crystal.md +11 -0
  49. package/skills/kicad-schematic-design/references/drawing-patterns/mcu-minimum.md +17 -0
  50. package/skills/kicad-schematic-design/references/drawing-patterns/mosfet-low-side.md +17 -0
  51. package/skills/kicad-schematic-design/references/drawing-patterns/reset-enable.md +1 -1
  52. package/skills/kicad-schematic-design/references/drawing-patterns/reset-rc.md +19 -0
  53. package/skills/kicad-schematic-design/references/drawing-patterns/swd-header.md +15 -0
  54. package/skills/kicad-schematic-design/references/drawing-patterns/usb-c-device.md +17 -0
  55. package/skills/kicad-schematic-design/references/drawing-patterns.md +20 -1
  56. package/skills/kicad-schematic-design/references/layout-delivery.md +1 -1
  57. package/skills/kicad-schematic-design/references/troubleshooting.md +22 -0
  58. package/src/client/overview-result.ts +1 -0
  59. package/src/components/identity.ts +3 -5
  60. package/src/components/import-component.ts +3 -5
  61. package/src/components/import-symbol.ts +2 -4
  62. package/src/components/models.ts +3 -15
  63. package/src/components/project-footprint.ts +4 -6
  64. package/src/schematic/overview.ts +6 -15
  65. package/src/schematic/tools.ts +37 -16
  66. package/src/skills.ts +17 -1
  67. package/LICENSE +0 -21
package/README.md CHANGED
@@ -12,7 +12,7 @@ Imports remain workspace scoped and use DSH sandbox policy. Model inspection rep
12
12
 
13
13
  Build, typecheck and test from the repository workspace. Restart the KiCad assistant after replacing the old bundles. See [merge notes](../../docs/SCHEMATIC-MERGE.md).
14
14
 
15
- Paired import: `import_kicad_component({manufacturer_id, mpn, projectDir})` downloads a symbol and footprint, registers `sym-lib-table` and `fp-lib-table`, and returns `footprintId`. Place with `addSymbol({libraryPath, libId, footprint: footprintId, ...})`. Success requires both usable models and matching unique pin/pad number sets. Public tool arguments use required `manufacturer_id` and `mpn`, matching `@huaqiu/dsh-tool-part-search`; imports also accept optional `projectDir` (defaults to the workspace root). `mfgId` is retained only by the internal identity parser for legacy direct callers; schema-validated tool calls must use `manufacturer_id`. See the skill's [component and PCB reference](skills/kicad-schematic-design/references/component-pcb.md).
15
+ Paired import: `import_kicad_component({manufacturer_id, mpn, projectDir})` downloads a symbol and footprint, registers `sym-lib-table` and `fp-lib-table`, and returns `footprintId`. Place with `addSymbol({libraryPath, libId, footprint: footprintId, ...})`. Success requires both usable models and matching unique pin/pad number sets. Public tool arguments use required `manufacturer_id` and `mpn`, matching `@huaqiu/dsh-tool-part-search`; imports also accept optional `projectDir` (defaults to the workspace root). The obsolete `mfgId` parameter is rejected; all callers must use `manufacturer_id`. See the skill's [component and PCB reference](skills/kicad-schematic-design/references/component-pcb.md).
16
16
 
17
17
  Power symbols: declare actual shared nets with the builder's `requiredPowerSymbols` option. `powerLabelCatalog(nets)` returns current-build label UUIDs, coordinates and imported-module names for reproducible placement. `checkPowerSymbols()` and render/save reject missing symbols or unconverted labels for declared nets. Layout warnings still require visual review. See [power-symbol delivery](skills/kicad-schematic-design/references/power-symbols.md).
18
18
 
@@ -20,10 +20,14 @@ Power symbols: declare actual shared nets with the builder's `requiredPowerSymbo
20
20
 
21
21
  Standalone common-only symbols with pins (e.g. SW-PTB-2_0_1) are automatically normalized in project copies to unit 1 without changing pins or graphics. Model summaries and import results report builderCompatibility.normalization=common_only_to_unit_1. Inherited, empty, malformed or alternate-only definitions are not normalized. Unsupported imports report part, symbol, sections and source URL.
22
22
 
23
- 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,待本轮正常完成后,在最终回复后的轮次尾部显示页面切换、缩放、ZIP 下载;工具执行期间不展示正式卡片。正式概览必须是本轮最后一个工具操作,之后调用其他工具或普通预览时须重新正式交付;失败、取消、中断的轮次不显示正式卡片。卡片从所属轮次的工具记录恢复,切换会话无需全局缓存。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer、conversation.chat.turnTail 和 useChat(ChatSnapshot 的 locations/nodes)支持;不可用时仍返回本地预览路径。快照保存在插件 `.overview-cache/`,重启后可读取;移除该目录后历史卡片下载将失效。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
23
+ 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,待本轮正常完成后,在最终回复后的轮次尾部显示页面切换、缩放、ZIP 下载;工具执行期间不展示正式卡片。正式概览必须是本轮最后一个工具操作,之后调用其他工具或普通预览时须重新正式交付;失败、取消、中断的轮次不显示正式卡片。卡片从所属轮次的工具记录恢复,切换会话无需全局缓存。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer、conversation.chat.turnTail 和 useChat(ChatSnapshot 的 locations/nodes)支持;不可用时仍返回本地预览路径。持久快照默认保存在当前用户家目录的 `.hq-edge/plugin-data/dsh-tool-kicad-schematic-gen/overview-cache/`,家目录通过 `os.homedir()` 动态获取;可用绝对路径 `HQ_EDGE_HOME` 覆盖 `.hq-edge` 根目录,不随 `DSH_HOME` 或插件版本变化。重启后可读取,手动删除快照后对应历史卡片下载将失效。工程、中间产物和预览保存在当前工作区;插件安装目录仅用于读取代码和模板,不读写概览快照。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
24
24
 
25
25
  Module analysis queries use `analyze_kicad_module({schematicPath, view, reference?, pin?, net?, offset?, kicadCliPath?})`, with explicit `view`. Downloads already return a summary: query only needed components, grouped nets or grouped BOM. CLI/project options are resolved internally; optional absolute `kicadCliPath` is for recovery using the existing `get_eda_host_info` result. On `CLI_START_FAILED`, retry analysis with that path and `view:summary`, without downloading again. Successful paths are reused; default CLI command names preserve automatic discovery. Probe errors return `error:{code,executable}` and retain causes; capability failures are distinct from startup failures. Matching same-stem projects and downloaded `.source.kicad_pro` files are discovered. Detail output contains `status`, `items`, and optional `nextOffset`/`warnings`; missing `nextOffset` means completion. Component custom fields require a reference filter. Partial results are reused for detail queries; after fixing configuration use `view:summary` to retry. Full local artifacts are preserved. This replaces the previous analysis query arguments and response contract; existing callers must migrate. See [module analysis](skills/kicad-schematic-design/references/module-analysis.md).
26
26
 
27
27
  方案先行:先保存硬件方案,用 `scripts/run-design.mjs --read-plan <方案.md>` 读回,再按文档编写脚本。运行时必须传 `--plan <path> --plan-sha256 <sha256>`;构建库直接调用也要求 `designPlan:{path,sha256}`。平铺 create 与层次生成工具同样检查方案。缺失、空文档或版本变化时阻止生成;只读模块分析和预览不受影响。构建报告保存方案全文、方案 hash 和脚本 hash。此检查保证文件和版本前置,不代替方案电气完整性及文图一致性审查。
28
28
 
29
29
  电气栅格:默认 50 mil(1.27 mm)。构建器吸附器件放置、导线坐标、标签和模块平移量;`gridPoint/snapGrid` 可用于脚本规划。引脚库必须兼容同一格点,原理图生成与正式交付检查实际引脚及连接点,偏离时阻止交付。已有工程需从脚本重新构建,不通过独立舍入引脚/连线修补。
30
+
31
+ 候选预览与交付状态:普通 `render_kicad_module_preview` 可传 `projectDir`,在首版预览时校验全部页的本地封装,无需先复制库到候选目录。`footprintCheck` 返回逐页的实际解析结果,缺库时顶层 `status:error`,已渲染图片仍保留。`renderStatus` 单独表示渲染结果;`deliveryStatus` 为 `not_requested`、`ready` 或 `blocked`。正式交付失败不会返回整体成功;只有 `deliveryStatus:ready` 和有效概览才代表正式快照已创建,这仍不代表电气验证通过。
32
+
33
+ 技能加载时提供 `pluginDir`、`skillDir`、`runtimePath`、`runnerPath`、`geometryPath` 等只读资源绝对路径,避免混淆插件根目录与技能根目录。资源安装目录不用于保存项目输出。失败恢复、有限重试及候选检查顺序见技能的 `references/troubleshooting.md`。
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
  | 电路模块搜索下载 | eda_module_rag_search、eda_module_category_search、eda_module_precise_search、download_eda_module_schematic |
9
9
  | 原理图工具 | get_kicad_module_ports、render_kicad_module_preview、create_kicad_hierarchy、edit_kicad_flat_schematic |
10
10
 
11
- 同时注册 kicad-schematic-design 技能。元件搜索由KiCad 内置的 @huaqiu/dsh-tool-part-search 提供,调用 search_hqsch_parts。该搜索插件随 KiCad 提供,可在插件管理器中启用;原理图插件本身不注册它的四个查询工具。两者间将搜索结果的 manufacturer.id 映射为 manufacturer_id,mpn 保持不变;公开工具统一要求 manufacturer_id 和 mpn,不再暴露 mfgId;两个导入工具额外支持可选 projectDir,默认工作区根目录。内部身份解析保留旧调用兼容,经过工具 Schema 校验的调用必须使用 manufacturer_id。
11
+ 同时注册 kicad-schematic-design 技能。元件搜索由KiCad 内置的 @huaqiu/dsh-tool-part-search 提供,调用 search_hqsch_parts。该搜索插件随 KiCad 提供,可在插件管理器中启用;原理图插件本身不注册它的四个查询工具。两者间将搜索结果的 manufacturer.id 映射为 manufacturer_id,mpn 保持不变;公开工具统一要求 manufacturer_id 和 mpn,不再暴露 mfgId;两个导入工具额外支持可选 projectDir,默认工作区根目录。内部身份解析同样拒绝旧参数 mfgId,所有调用统一使用 manufacturer_id。
12
12
 
13
13
  配置分为 components、modules.search、modules.download、schematic,使用原配置的默认限额。flatTemplateDir 默认指向本包技能模板。Node 22.19+,KiCad 10 CLI 路径可通过 schematic.kicadCliPath 配置。
14
14
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  公开脚本入口是 lib/builder.js 或包的 ./builder 导出。设计技能、资源与模板位于 skills/kicad-schematic-design/。
18
18
 
19
- 新导入器件库默认保存到 `project/libraries/symbols/` 和 `project/libraries/footprints/`,库表保留在 project 根目录,使用 `${KIPRJMOD}` 相对路径注册。相同器件重复导入复用原文件;旧项目已有根目录库时保持兼容,不自动移动。脚本使用导入返回的实际 libraryPath 和 footprintId;概览 ZIP 会递归收集 libraries 下的器件库。
19
+ 新导入器件库默认保存到 `project/libraries/symbols/` 和 `project/libraries/footprints/`,库表保留在 project 根目录,使用 `${KIPRJMOD}` 相对路径注册。相同器件重复导入复用原文件;不再复用工程根目录中的旧库;同名库表仍指向旧目录时明确报冲突,不自动迁移或覆盖。脚本使用导入返回的实际 libraryPath 和 footprintId;概览 ZIP 会递归收集 libraries 下的器件库。
20
20
 
21
21
  复用模块的封装通过 `localize_kicad_module_footprints` 的 list/import/check 流程本地化。模块下载现在保留 ZIP 中的 `.kicad_mod` 并返回 footprints 列表;缺失时使用明确选择的 manufacturer_id/mpn 查询在线封装。原嵌入符号、引脚和布线不替换,编号集合不匹配时拒绝导入。映射存入 `libraries/mappings/`,在设计脚本中使用 `importModule({...,footprintMappingPath})` 持久应用;来源变化后旧映射会拒绝使用。正式 delivery 在生成概览前验证根图和子页的本地封装引用。映射和封装一并进入项目 ZIP;尺寸、电气功能及 3D 完整性仍需单独确认。已有工程不会在插件加载时自动选择或替换器件。
22
22
 
@@ -24,11 +24,11 @@ KiCad CLI 自动从当前运行时安装位置或 PATH 查找,也可通过 KIC
24
24
 
25
25
  路径发现由配置与 skill 共用缓存,环境变量变化后重新识别。模型已有 get_eda_host_info 返回结果时,可以把其中的 kicad-cli 绝对路径传给 render_kicad_module_preview 的可选 kicadCliPath;只影响当前调用,不修改默认配置。原理图文件生成和预览不要求 PCB IPC 诊断或 Python 路径查询。
26
26
 
27
- 正式概览快照保存到当前 DSH_HOME/plugin-data/dsh-tool-kicad-schematic-gen/overview-cache;没有 DSH_HOME 时使用用户数据目录,不要求插件安装目录可写。快照完整写入后才发布,旧会话仍可读取插件目录中原有的 .overview-cache。不会自动删除历史快照;手动清理对应快照后,旧会话中的下载和放置操作将失效。
27
+ 正式概览快照默认保存到 `~/.hq-edge/plugin-data/dsh-tool-kicad-schematic-gen/overview-cache/`(`~` 为当前用户家目录,使用 `os.homedir()` 获取);设置绝对路径 `HQ_EDGE_HOME` 时保存到该根目录下同一子目录。不会使用 `DSH_HOME`、`LOCALAPPDATA` 或插件安装目录作为新快照的默认存储位置。快照完整写入后才发布;不再兼容读取插件目录中的旧快照,相关旧会话卡片需重新生成正式概览。不会自动删除历史快照;手动清理对应快照后,旧会话中的下载和放置操作将失效。
28
28
 
29
- 在工作区根目录执行 pnpm --filter @huaqiu/dsh-tool-kicad-schematic-gen build、typecheck 或 test。使用配套 DSH CLI 注册本目录,替换旧四个插件,并重新打开 KiCad AI 助手。历史备份和旧脚本兼容见 [合并说明](../../docs/SCHEMATIC-MERGE.md)。
29
+ 在工作区根目录执行 pnpm --filter @huaqiu/dsh-tool-kicad-schematic-gen build、typecheck 或 test。使用配套 DSH CLI 注册本目录,替换旧四个插件,并重新打开 KiCad AI 助手。旧脚本须更新为当前接口,不提供历史调用兼容。
30
30
 
31
- 成套器件导入:`import_kicad_component({manufacturer_id, mpn, projectDir})` 同时下载符号与封装,登记两个项目库表,返回 `footprintId`。放置时传 `footprint: imported.footprintId`。两种实际模型可用且引脚/焊盘编号集合匹配才成功;仍需手册核对尺寸与功能。公开调用只使用 `manufacturer_id`,旧 `mfgId` 仅在内部解析层为历史直接调用保留。
31
+ 成套器件导入:`import_kicad_component({manufacturer_id, mpn, projectDir})` 同时下载符号与封装,登记两个项目库表,返回 `footprintId`。放置时传 `footprint: imported.footprintId`。两种实际模型可用且引脚/焊盘编号集合匹配才成功;仍需手册核对尺寸与功能。公开调用只使用 `manufacturer_id`,内部解析层同样拒绝旧 `mfgId` 参数。
32
32
 
33
33
  电源符号:在构建选项中声明实际共享网络 `requiredPowerSymbols`。`powerLabelCatalog(nets)` 返回本次标签 UUID、网络、坐标和导入模块名,按这些条件定位后生成 placements,不引用上一轮 UUID。`checkPowerSymbols()` 与 render/save 检查声明网络的标准符号和残留标签;未完成时拒绝保存。遮挡提示仍需局部预览确认,详见 [电源符号交付](skills/kicad-schematic-design/references/power-symbols.md)。
34
34
 
@@ -36,14 +36,18 @@ KiCad CLI 自动从当前运行时安装位置或 PATH 查找,也可通过 KIC
36
36
 
37
37
  含引脚的独立公共单元符号(如 SW-PTB-2_0_1)在项目副本中自动规范为单元 1,保留引脚与图形。模型摘要和导入结果通过 builderCompatibility.normalization=common_only_to_unit_1 报告;继承、空单元、异常命名及仅替代图形的符号不做自动规范。不支持的导入错误包含器件、符号、单元名称及源文件 URL。
38
38
 
39
- 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,显示页面切换、缩放、ZIP 下载。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer 与前端加载支持;不可用时仍返回本地预览路径。快照保存在插件 `.overview-cache/`,重启后可读取;移除该目录后历史卡片下载将失效。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
39
+ 最终概览卡片:正式预览传入 `delivery: true` 和包含全部本地库的 `projectDir`,显示页面切换、缩放、ZIP 下载。复用已有 PNG,不重新生成电路,无重新生成/详情按钮。需 KiCad Harness 的 webServer 与前端加载支持;不可用时仍返回本地预览路径。持久快照默认保存在当前用户家目录的 `.hq-edge/plugin-data/dsh-tool-kicad-schematic-gen/overview-cache/`,家目录通过 `os.homedir()` 动态获取;可用绝对路径 `HQ_EDGE_HOME` 覆盖 `.hq-edge` 根目录,不随 `DSH_HOME` 或插件版本变化。重启后可读取,手动删除快照后对应历史卡片下载将失效。工程、中间产物和预览保存在当前工作区;插件安装目录仅用于读取代码和模板,不读写概览快照。ZIP 为工程目录内相关设计文件与库的快照,不收集脚本、预览或外部库。
40
40
  # 生成流程耗时优化
41
41
 
42
42
  - 模块封装 `import` 返回 `stage: prepared`,仅表示本地库和映射准备完成;源文件默认不改写。`check_mapping` 验证来源应用映射后的状态,`check` 检查实际候选或最终原理图,避免混淆。
43
43
  - 封装准备最多 4 并发;项目库表按项目串行写入。批次验证失败不写库,错误返回失败 assignment 索引;已验证下载在当前进程缓存 15 分钟(最多 128 项、16 MiB),退出进程后清空。瞬时网络/429/5xx 错误内部最多重试一次,格式和编号错误不自动重试。
44
- - 使用现有供应商符号缓存对齐逻辑读取模块,支持旧式 `(module ...)` 封装的焊盘元数据,不改写源封装。编号核对不等于尺寸或电气验证。
44
+ - 使用现有供应商符号缓存对齐逻辑读取模块;封装仅支持当前 `(footprint ...)` 格式,旧式 `(module ...)` 明确拒绝,不自动转换。编号核对不等于尺寸或电气验证。
45
45
  - builder 新增 `attachPinLabel`,支持显式方向、引线长度与通道错列;不推断引脚方向或自动避障。
46
46
  - 技能目录 `scripts/run-design.mjs` 使用参数数组启动生成脚本,将退出码、执行时间和完整输出保存为 UTF-8 JSON;终端仅返回摘要,避免 PowerShell 转义、输出截断和 BOM 返工。模块封装准备提前进行,文档与脚本复用连接计划。
47
47
 
48
48
 
49
49
  模块分析查询:`analyze_kicad_module({schematicPath, view, reference?, pin?, net?, offset?, kicadCliPath?})`。`view` 必填;下载已给分析摘要,直接查询所需 `components`、`nets` 或 `bom`。CLI/源工程默认内部解析;路径或启动失败 `CLI_START_FAILED` 时,复用 `get_eda_host_info` 返回的 kicad-cli 绝对路径,通过可选 `kicadCliPath` + `view:summary` 直接重试分析,不重新下载。成功路径后续复用,默认命令名不会覆盖自动发现;探测失败返回 `error:{code,executable}` 并区分启动、能力不支持、超时和命令失败。任意本地图采用同名 `.kicad_pro`,下载模块重启后可识别同名 `.source.kicad_pro`。详情返回 `status`、`items`、可选 `nextOffset`/`warnings`;网络按组分块,BOM 按全部物料字段与装配状态合并,器件自定义字段在指定 `reference` 时返回。部分失败结果供详情查询复用,修复后用 `view:summary` 重试。缺失 `nextOffset` 表示结束;不可用数据会明确警告。完整本地分析文件仍保留,详见 [模块分析](skills/kicad-schematic-design/references/module-analysis.md)。此接口替换旧版分析查询参数及返回结构,旧调用需同步更新。
50
+
51
+ 候选预览与交付状态:普通 `render_kicad_module_preview` 可传 `projectDir`,在首版预览时校验全部页的本地封装,无需先复制库到候选目录。`footprintCheck` 返回逐页的实际解析结果,缺库时顶层 `status:error`,已渲染图片仍保留。`renderStatus` 单独表示渲染结果;`deliveryStatus` 为 `not_requested`、`ready` 或 `blocked`。正式交付失败不会返回整体成功;只有 `deliveryStatus:ready` 和有效概览才代表正式快照已创建,这仍不代表电气验证通过。
52
+
53
+ 技能加载时提供 `pluginDir`、`skillDir`、`runtimePath`、`runnerPath`、`geometryPath` 等只读资源绝对路径,避免混淆插件根目录与技能根目录。资源安装目录不用于保存项目输出。失败恢复、有限重试及候选检查顺序见技能的 `references/troubleshooting.md`。
package/lib/client.js CHANGED
@@ -19,6 +19,10 @@
19
19
  message: String(o.overviewError),
20
20
  formal: true
21
21
  };
22
+ if (o.footprintCheck?.status === "blocked") return {
23
+ kind: "error",
24
+ message: String(o.footprintCheck.error || "工程本地封装检查未通过")
25
+ };
22
26
  if (!v) return o.status === "error" ? {
23
27
  kind: "error",
24
28
  message: "未能生成预览"
package/lib/index.js CHANGED
@@ -2494,30 +2494,7 @@ async function fetchSymbolSource(mfgId, mpn, options, signal, fetcher = fetch) {
2494
2494
  };
2495
2495
  }
2496
2496
  function summarizeFootprint(text) {
2497
- const root = parse(text);
2498
- if (root.head === "module") {
2499
- const pads = children(root, "pad").map((pad) => {
2500
- const at = children(pad, "at")[0], size = children(pad, "size")[0], layers = children(pad, "layers")[0];
2501
- if (!at || !size || !layers || layers.items.length < 2 || ![1, 2].every((i) => Number.isFinite(Number(value(at, i))) && Number(value(size, i)) > 0 && Number.isFinite(Number(value(size, i))))) throw new Error("Legacy footprint pad requires finite position, positive size and layers");
2502
- value(pad, 3);
2503
- return {
2504
- number: value(pad),
2505
- type: value(pad, 2)
2506
- };
2507
- });
2508
- if (pads.some((pad) => ![
2509
- "smd",
2510
- "thru_hole",
2511
- "np_thru_hole",
2512
- "connect"
2513
- ].includes(pad.type))) throw new Error("Unsupported legacy footprint pad type");
2514
- return {
2515
- status: "parsed",
2516
- name: value(root),
2517
- padCount: pads.length,
2518
- pads
2519
- };
2520
- }
2497
+ if (parse(text).head !== "footprint") throw new Error("Unsupported footprint format: expected (footprint ...)");
2521
2498
  const footprint = new BoardParser().parse(`(kicad_pcb (version 20240108) (generator dsh-tool-kicad-library) ${text})`).footprints[0];
2522
2499
  if (!footprint) throw new Error("Huaqiu footprint file: no KiCad footprint found");
2523
2500
  const pads = footprint.pads.map((pad) => ({
@@ -2547,7 +2524,7 @@ async function getComponentModels(mfgId, mpn, options, signal, fetcher = fetch)
2547
2524
  };
2548
2525
  }
2549
2526
  function inspectModelText(text, url, type) {
2550
- const validHeader = type === "symbol" ? /^\s*\(kicad_symbol_lib(?=\s|\()/u.test(text) : /^\s*\((?:footprint|module)(?=\s|\()/u.test(text);
2527
+ const validHeader = type === "symbol" ? /^\s*\(kicad_symbol_lib(?=\s|\()/u.test(text) : /^\s*\(footprint(?=\s|\()/u.test(text);
2551
2528
  try {
2552
2529
  if (!validHeader) throw new Error(`Downloaded ${type} is not a KiCad model; content begins: ${text.slice(0, 160)}`);
2553
2530
  return type === "symbol" ? summarizeSymbol(text) : summarizeFootprint(text);
@@ -2566,7 +2543,7 @@ function inspectModelText(text, url, type) {
2566
2543
  //#region src/components/project-footprint.ts
2567
2544
  function footprintModel(raw) {
2568
2545
  const form = parse(raw), name = value(form);
2569
- if (!["footprint", "module"].includes(form.head) || !name || /[<>:"/\\|?*\x00-\x1f]/u.test(name) || /[. ]$/u.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name)) throw new Error("Invalid KiCad footprint or unsafe footprint name");
2546
+ if (form.head !== "footprint" || !name || /[<>:"/\\|?*\x00-\x1f]/u.test(name) || /[. ]$/u.test(name) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name)) throw new Error("Invalid KiCad footprint or unsafe footprint name");
2570
2547
  const summary = summarizeFootprint(raw);
2571
2548
  if (summary.status !== "parsed" || !summary.padCount) throw new Error("Footprint must have parsed pads");
2572
2549
  return {
@@ -2575,18 +2552,16 @@ function footprintModel(raw) {
2575
2552
  text: raw.endsWith("\n") ? raw : `${raw}\n`
2576
2553
  };
2577
2554
  }
2578
- /** Preflight all destinations before writing; preserve existing tables and legacy layouts. */
2555
+ /** Preflight all destinations before writing; preserve existing tables and use the current project library layout. */
2579
2556
  async function prepareProjectFootprint(project, nickname, raw, fs, signal) {
2580
2557
  const model = footprintModel(raw);
2581
- const legacy = await fs.resolve(join(project.displayPath, `${nickname}.pretty`), { signal });
2582
- const legacyExists = await fs.stat(legacy, signal) !== void 0;
2583
- const footprint = await fs.resolve(join(project.displayPath, ...legacyExists ? [] : ["libraries", "footprints"], `${nickname}.pretty`, `${model.name}.kicad_mod`), { signal });
2558
+ const footprint = await fs.resolve(join(project.displayPath, "libraries", "footprints", `${nickname}.pretty`, `${model.name}.kicad_mod`), { signal });
2584
2559
  const table = await fs.resolve(join(project.displayPath, "fp-lib-table"), { signal });
2585
2560
  if (!fs.contains(project, footprint) || !fs.contains(project, table)) throw new Error("Footprint targets must stay inside projectDir");
2586
2561
  const info = await fs.stat(footprint, signal), tableInfo = await fs.stat(table, signal);
2587
2562
  if (info && (info.type !== "file" || await fs.readText(footprint, signal) !== model.text)) throw new Error("Footprint already exists with different content");
2588
2563
  if (tableInfo && tableInfo.type !== "file") throw new Error("fp-lib-table must be a file");
2589
- const uri = `\${KIPRJMOD}/${legacyExists ? "" : "libraries/footprints/"}${nickname}.pretty`;
2564
+ const uri = `\${KIPRJMOD}/libraries/footprints/${nickname}.pretty`;
2590
2565
  const entry = ` (lib (name ${quote(nickname)})(type "KiCad")(uri ${quote(uri)})(options "")(descr ""))`;
2591
2566
  const old = tableInfo ? await fs.readText(table, signal) : null;
2592
2567
  let next = null;
@@ -3399,10 +3374,8 @@ async function importSymbol(mfgId, mpn, projectDir, workspaceRoot, options, fs,
3399
3374
  const source = preparedSource ?? await fetchSymbolSource(mfgId, mpn, options, signal, fetcher);
3400
3375
  if (source.symbol.name.length === 0 || source.symbol.name.includes(":")) throw new Error("upstream Symbol name must be non-empty and must not contain a colon");
3401
3376
  const nickname = libraryName(mfgId, mpn);
3402
- const legacy = await fs.resolve(join(project.displayPath, `${nickname}.kicad_sym`), { signal });
3403
- const legacyExists = await fs.stat(legacy, signal) !== void 0;
3404
- const library = legacyExists ? legacy : await fs.resolve(join(project.displayPath, "libraries", "symbols", `${nickname}.kicad_sym`), { signal });
3405
- const uri = `\${KIPRJMOD}/${legacyExists ? "" : "libraries/symbols/"}${nickname}.kicad_sym`;
3377
+ const library = await fs.resolve(join(project.displayPath, "libraries", "symbols", `${nickname}.kicad_sym`), { signal });
3378
+ const uri = `\${KIPRJMOD}/libraries/symbols/${nickname}.kicad_sym`;
3406
3379
  const table = await fs.resolve(join(project.displayPath, "sym-lib-table"), { signal });
3407
3380
  if (!fs.contains(project, library) || !fs.contains(project, table)) throw new Error("project library targets must stay inside projectDir");
3408
3381
  const rawText = source.text.endsWith("\n") ? source.text : `${source.text}\n`;
@@ -3444,10 +3417,10 @@ async function importSymbol(mfgId, mpn, projectDir, workspaceRoot, options, fs,
3444
3417
  }
3445
3418
  //#endregion
3446
3419
  //#region src/components/identity.ts
3447
- /** Canonical identity; legacy direct callers may still use mfgId outside tool schemas. */
3420
+ /** Parse the current manufacturer_id/MPN contract. */
3448
3421
  function partIdentity(args) {
3449
- const id = args.manufacturer_id ?? args.mfgId;
3450
- if (args.manufacturer_id !== void 0 && args.mfgId !== void 0 && args.manufacturer_id !== args.mfgId) throw new Error("manufacturer_id and mfgId must identify the same manufacturer");
3422
+ if ("mfgId" in args) throw new Error("Unsupported parameter mfgId; use manufacturer_id");
3423
+ const id = args.manufacturer_id;
3451
3424
  if (typeof id !== "string" || !/^\d{1,20}$/.test(id)) throw new Error("manufacturer_id must contain 1 to 20 digits");
3452
3425
  if (typeof args.mpn !== "string" || !args.mpn.trim() || args.mpn.length > 120) throw new Error("mpn must contain 1 to 120 characters");
3453
3426
  return {
@@ -3486,7 +3459,7 @@ async function importComponent(mfgId, mpn, projectDir, workspaceRoot, options, f
3486
3459
  if (!urls.symbol || !urls.footprint) throw new Error("COMPONENT_MODELS_REQUIRED: both symbol and footprint URLs are required");
3487
3460
  const [source, rawFootprint] = await Promise.all([fetchSymbolSource(mfgId, mpn, options, signal, cachedFetch), fetchModelText(urls.footprint, "footprint", options, signal, cachedFetch)]);
3488
3461
  const form = parse(rawFootprint);
3489
- if (!["footprint", "module"].includes(form.head)) throw new Error("Downloaded footprint is not a KiCad footprint");
3462
+ if (form.head !== "footprint") throw new Error("Downloaded footprint is not a KiCad footprint");
3490
3463
  const footprintName = value(form);
3491
3464
  if (!footprintName || /[<>:"/\\|?*\x00-\x1f]/u.test(footprintName) || /[. ]$/u.test(footprintName) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(footprintName)) throw new Error("Footprint name is not a safe local filename");
3492
3465
  const summary = summarizeFootprint(rawFootprint);
@@ -3500,9 +3473,7 @@ async function importComponent(mfgId, mpn, projectDir, workspaceRoot, options, f
3500
3473
  if (compatibility.status !== "ready") throw new Error(`SYMBOL_UNITS_UNSUPPORTED: ${mfgId}:${mpn}; symbol ${source.symbol.name}; sections [${(source.symbol.children ?? []).map((child) => child.name).join(", ")}]; source ${source.sourceUrl}; ${compatibility.nextAction}`);
3501
3474
  const nickname = libraryName(mfgId, mpn);
3502
3475
  const footprintText = rawFootprint.endsWith("\n") ? rawFootprint : `${rawFootprint}\n`;
3503
- const legacy = await fs.resolve(join(project.displayPath, `${nickname}.pretty`), { signal });
3504
- const legacyExists = await fs.stat(legacy, signal) !== void 0;
3505
- const footprint = await fs.resolve(join(project.displayPath, ...legacyExists ? [] : ["libraries", "footprints"], `${nickname}.pretty`, `${footprintName}.kicad_mod`), { signal });
3476
+ const footprint = await fs.resolve(join(project.displayPath, "libraries", "footprints", `${nickname}.pretty`, `${footprintName}.kicad_mod`), { signal });
3506
3477
  const table = await fs.resolve(join(project.displayPath, "fp-lib-table"), { signal });
3507
3478
  if (!fs.contains(project, footprint) || !fs.contains(project, table)) throw new Error("Footprint targets must stay inside projectDir");
3508
3479
  const info = await fs.stat(footprint, signal);
@@ -3510,7 +3481,7 @@ async function importComponent(mfgId, mpn, projectDir, workspaceRoot, options, f
3510
3481
  const tableInfo = await fs.stat(table, signal);
3511
3482
  if (tableInfo && tableInfo.type !== "file") throw new Error("fp-lib-table must be a regular file");
3512
3483
  const oldTable = tableInfo ? await fs.readText(table, signal) : null;
3513
- const uri = `\${KIPRJMOD}/${legacyExists ? "" : "libraries/footprints/"}${nickname}.pretty`;
3484
+ const uri = `\${KIPRJMOD}/libraries/footprints/${nickname}.pretty`;
3514
3485
  const entry = ` (lib (name ${quote(nickname)})(type "KiCad")(uri ${quote(uri)})(options "")(descr ""))`;
3515
3486
  let nextTable;
3516
3487
  if (oldTable === null) nextTable = `(fp_lib_table\n (version 7)\n${entry}\n)\n`;
@@ -5491,13 +5462,10 @@ function inspectPinConnections(objects, reference, number) {
5491
5462
  //#region src/schematic/overview.ts
5492
5463
  /** Immutable, bounded snapshots for the browser; never accepts browser file paths. */
5493
5464
  const OVERVIEW_ROUTE = "/api/v1/kicad-schematic-overview";
5494
- const legacyCache = fileURLToPath(new URL("../.overview-cache/", import.meta.url));
5495
- /** Store generated data outside installed code, including read-only plugin installations. */
5465
+ /** Persist snapshots across sessions and runtime upgrades under the current user's HQ Edge home. */
5496
5466
  function overviewCacheDirectory(env = process.env) {
5497
- const home = env.DSH_HOME;
5498
- if (home && isAbsolute(home)) return join(home, "plugin-data", "dsh-tool-kicad-schematic-gen", "overview-cache");
5499
- const userData = env.LOCALAPPDATA && isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : join(homedir(), ".local", "share");
5500
- return join(userData, "deepseek-harness", "dsh-tool-kicad-schematic-gen", "overview-cache");
5467
+ const home = env.HQ_EDGE_HOME && isAbsolute(env.HQ_EDGE_HOME) ? env.HQ_EDGE_HOME : join(homedir(), ".hq-edge");
5468
+ return join(home, "plugin-data", "dsh-tool-kicad-schematic-gen", "overview-cache");
5501
5469
  }
5502
5470
  const MAX_BYTES = 67108864;
5503
5471
  async function createOverview(input, store = overviewCacheDirectory()) {
@@ -5588,7 +5556,7 @@ async function createOverview(input, store = overviewCacheDirectory()) {
5588
5556
  size: zip.length
5589
5557
  };
5590
5558
  }
5591
- function overviewHandler(store = overviewCacheDirectory(), legacyStore = store === overviewCacheDirectory() ? legacyCache : void 0) {
5559
+ function overviewHandler(store = overviewCacheDirectory()) {
5592
5560
  return async (req, res) => {
5593
5561
  if (req.method !== "GET" && req.method !== "HEAD") {
5594
5562
  res.writeHead(405);
@@ -5603,13 +5571,7 @@ function overviewHandler(store = overviewCacheDirectory(), legacyStore = store =
5603
5571
  return;
5604
5572
  }
5605
5573
  try {
5606
- let bytes;
5607
- try {
5608
- bytes = await readFile(join(store, match[1], match[2]));
5609
- } catch (error) {
5610
- if (error.code !== "ENOENT" || !legacyStore) throw error;
5611
- bytes = await readFile(join(legacyStore, match[1], match[2]));
5612
- }
5574
+ const bytes = await readFile(join(store, match[1], match[2]));
5613
5575
  res.writeHead(200, {
5614
5576
  "content-type": match[2] === "project.zip" ? "application/zip" : match[2] === "placement.json" ? "application/json" : "image/png",
5615
5577
  "content-length": bytes.length,
@@ -5929,7 +5891,7 @@ function registerSchematicTools(ctx, config) {
5929
5891
  },
5930
5892
  projectDir: {
5931
5893
  type: "string",
5932
- description: "With delivery:true, project folder containing the schematic, local symbol/footprint libraries and library tables. Defaults to the schematic folder. Must stay inside workspace."
5894
+ description: "Project folder containing local libraries and library tables. On ordinary candidate previews, pass this to check footprints against the prepared project before publishing. With delivery:true defaults to the schematic folder and must also contain all sheets. Must stay inside workspace."
5933
5895
  },
5934
5896
  regions: {
5935
5897
  type: "array",
@@ -5990,6 +5952,26 @@ function registerSchematicTools(ctx, config) {
5990
5952
  ]
5991
5953
  }] },
5992
5954
  overviewError: { oneOf: [{ type: "string" }, { type: "null" }] },
5955
+ renderStatus: {
5956
+ type: "string",
5957
+ enum: [
5958
+ "ok",
5959
+ "partial",
5960
+ "error"
5961
+ ]
5962
+ },
5963
+ deliveryStatus: {
5964
+ type: "string",
5965
+ enum: [
5966
+ "not_requested",
5967
+ "ready",
5968
+ "blocked"
5969
+ ]
5970
+ },
5971
+ footprintCheck: { oneOf: [{ type: "null" }, {
5972
+ type: "object",
5973
+ additionalProperties: true
5974
+ }] },
5993
5975
  manifestPath: { type: "string" },
5994
5976
  cached: { type: "boolean" },
5995
5977
  regionManifestPath: { type: "string" },
@@ -6081,6 +6063,9 @@ function registerSchematicTools(ctx, config) {
6081
6063
  "pages",
6082
6064
  "failures",
6083
6065
  "status",
6066
+ "renderStatus",
6067
+ "deliveryStatus",
6068
+ "footprintCheck",
6084
6069
  "rootImagePath",
6085
6070
  "omittedPages",
6086
6071
  "readImageInstruction",
@@ -6105,7 +6090,6 @@ function registerSchematicTools(ctx, config) {
6105
6090
  ], "render_kicad_module_preview");
6106
6091
  const renderConfig = previewConfig(config, input.kicadCliPath);
6107
6092
  if (input.delivery !== void 0 && typeof input.delivery !== "boolean") throw new Error("delivery must be boolean");
6108
- if (input.projectDir !== void 0 && input.delivery !== true) throw new Error("projectDir requires delivery:true");
6109
6093
  const workspace = workspaceRoot(exec.agent);
6110
6094
  const source = await readWorkspaceSchematic(ctx.fs, workspace, requiredString(input, "schematicPath"), config.maxSchematicBytes, exec.signal);
6111
6095
  const requestedDir = input.outputDir === void 0 ? join(dirname(source.path), "previews") : requiredString(input, "outputDir");
@@ -6115,6 +6099,39 @@ function registerSchematicTools(ctx, config) {
6115
6099
  const pageNames = input.pageNames === void 0 ? [] : parseUnconnectedPorts(input.pageNames, "pageNames");
6116
6100
  if (pageNames.length > config.previewMaxPages) throw new Error(`code=PREVIEW_PAGE_LIMIT; retryUnchanged=false; request at most ${config.previewMaxPages} pageNames per call.`);
6117
6101
  const regions = resolvePreviewRegions(input.regions, source.text, basename(source.path, ".kicad_sch"), config.flatListLimit);
6102
+ let footprintCheck = null;
6103
+ let checkedProject;
6104
+ if (input.delivery === true || input.projectDir !== void 0) {
6105
+ footprintCheck = {
6106
+ status: "ready",
6107
+ sheets: [],
6108
+ error: null
6109
+ };
6110
+ try {
6111
+ const project = input.projectDir === void 0 ? dirname(source.path) : requiredString(input, "projectDir");
6112
+ const [root, resolved] = await Promise.all([ctx.fs.resolve(workspace, { signal: exec.signal }), ctx.fs.resolve(project, {
6113
+ cwd: workspace,
6114
+ signal: exec.signal
6115
+ })]);
6116
+ if (!ctx.fs.contains(root, resolved) || (await ctx.fs.stat(resolved, exec.signal))?.type !== "directory") throw new Error("projectDir must be an existing directory inside the workspace");
6117
+ checkedProject = resolved.displayPath;
6118
+ for (const item of sources) {
6119
+ const sheet = item.schematicPath === source.path ? source : await readWorkspaceSchematic(ctx.fs, workspace, item.schematicPath, config.maxSchematicBytes, exec.signal);
6120
+ if (input.delivery === true) assertSchematicGrid(sheet.text);
6121
+ const audit = await auditFootprints(sheet.text, checkedProject, ctx.fs, exec.signal);
6122
+ footprintCheck.sheets.push({
6123
+ schematicPath: sheet.path,
6124
+ ...audit
6125
+ });
6126
+ }
6127
+ const missing = footprintCheck.sheets.filter((sheet) => sheet.status !== "ready");
6128
+ if (missing.length) throw new Error(`FOOTPRINTS_UNRESOLVED: ${missing.map((sheet) => `${sheet.schematicPath}: ${sheet.unresolved.map((part) => part.reference).join(", ")}`).join("; ")}. Prepare project-local footprints and persist script mappings before publishing; do not rebuild unchanged geometry to repair missing libraries.`);
6129
+ } catch (error) {
6130
+ exec.signal.throwIfAborted();
6131
+ footprintCheck.status = "blocked";
6132
+ footprintCheck.error = error instanceof Error ? error.message : String(error);
6133
+ }
6134
+ }
6118
6135
  const rendered = await renderModulePreview(outputDir, source.path, revision, renderConfig, exec.signal, sources, void 0, pageNames);
6119
6136
  const selectedRegions = input.regions === void 0 ? regions.filter((r) => rendered.pages.some((p) => p.exportedPageName === r.pageName)) : regions;
6120
6137
  const crops = await renderPreviewRegions(rendered.pages, selectedRegions, revision, rendered.manifestPath, config, exec.signal);
@@ -6122,35 +6139,31 @@ function registerSchematicTools(ctx, config) {
6122
6139
  const omittedPages = sources.filter((item) => !rendered.pages.some((page) => page.exportedPageName === item.exportedPageName)).map((item) => item.exportedPageName);
6123
6140
  let overview = null, overviewError = null;
6124
6141
  if (input.delivery === true) {
6125
- if (!overviewAvailable) overviewError = "Preview card service unavailable; use returned local preview paths.";
6142
+ if (footprintCheck?.status === "blocked") overviewError = footprintCheck.error;
6143
+ else if (!overviewAvailable) overviewError = "Preview card service unavailable; use returned local preview paths.";
6126
6144
  else if (!rendered.pages.length) overviewError = "No rendered pages available for the overview.";
6127
6145
  else try {
6128
- const project = input.projectDir === void 0 ? dirname(source.path) : requiredString(input, "projectDir");
6129
- const resolved = await ctx.fs.resolve(project, {
6130
- cwd: workspace,
6131
- signal: exec.signal
6132
- });
6133
- for (const item of sources) {
6134
- const sheet = item.schematicPath === source.path ? source : await readWorkspaceSchematic(ctx.fs, workspace, item.schematicPath, config.maxSchematicBytes, exec.signal);
6135
- assertSchematicGrid(sheet.text);
6136
- const footprints = await auditFootprints(sheet.text, resolved.displayPath, ctx.fs, exec.signal);
6137
- if (footprints.status !== "ready") throw new Error(`FOOTPRINTS_UNRESOLVED: ${footprints.unresolved.length} of ${footprints.total} physical instances in ${item.schematicPath} lack project-local footprints. Run localize_kicad_module_footprints check/import and persist script mappings before delivery.`);
6138
- }
6139
6146
  overview = await createOverview({
6140
6147
  workspace,
6141
- projectDir: resolved.displayPath,
6148
+ projectDir: checkedProject,
6142
6149
  schematicPath: source.path,
6143
6150
  revision,
6144
6151
  pages: rendered.pages,
6145
6152
  sources
6146
6153
  });
6147
6154
  } catch (error) {
6155
+ exec.signal.throwIfAborted();
6148
6156
  overviewError = error instanceof Error ? error.message : String(error);
6149
6157
  }
6150
6158
  }
6159
+ const renderStatus = rendered.pages.length === 0 ? "error" : rendered.truncated || failures.length > 0 ? "partial" : "ok";
6160
+ const deliveryStatus = input.delivery !== true ? "not_requested" : overview ? "ready" : "blocked";
6151
6161
  return {
6152
6162
  overview,
6153
6163
  overviewError,
6164
+ renderStatus,
6165
+ deliveryStatus,
6166
+ footprintCheck,
6154
6167
  schematicPath: source.path,
6155
6168
  revision,
6156
6169
  outputDir,
@@ -6162,7 +6175,7 @@ function registerSchematicTools(ctx, config) {
6162
6175
  failures,
6163
6176
  regionImages: crops.regionImages,
6164
6177
  regionManifestPath: crops.regionManifestPath,
6165
- status: rendered.pages.length === 0 ? "error" : rendered.truncated || failures.length > 0 ? "partial" : "ok",
6178
+ status: deliveryStatus === "blocked" || footprintCheck?.status === "blocked" ? "error" : renderStatus,
6166
6179
  rootImagePath: rendered.pages.find((page) => page.pageSchematicPath === source.path || page.exportedPageName === basename(source.path, ".kicad_sch"))?.pngPath ?? null,
6167
6180
  omittedPages,
6168
6181
  readImageInstruction: "For module detail read exact regionImages[].pngPath first; each image includes millimetre bounds, source and revision in regionManifestPath. Read rootImagePath for overall layout, or an exact pages[].pngPath. Do not write crop/zoom scripts for existing region images. Only listed PNGs exist; never replace an SVG extension or construct a PNG path. Check failures first: do not repeat failed pages unchanged or export a netlist to repair a preview. For unrendered pages not listed in failures, call render_kicad_module_preview with the same schematicPath and pageNames from omittedPages. Match the image to revision and its pageSchematicPath using manifestPath, pageIndex, and exportedPageName. A null pageSchematicPath means the page name could not be matched; do not infer its child file. A preview is not netlist evidence."
@@ -6696,6 +6709,20 @@ const skillName = "kicad-schematic-design";
6696
6709
  const providerName = `bundled:${skillName}`;
6697
6710
  const directory = fileURLToPath(new URL(`../skills/${skillName}/`, import.meta.url));
6698
6711
  const skillFile = new URL(`../skills/${skillName}/SKILL.md`, import.meta.url);
6712
+ /** Read-only installed resources; project outputs must use the session workspace. */
6713
+ function skillResourcePaths() {
6714
+ return {
6715
+ pluginDir: fileURLToPath(new URL("../", import.meta.url)),
6716
+ skillDir: directory,
6717
+ referencesDir: join(directory, "references"),
6718
+ assetsDir: join(directory, "assets"),
6719
+ runtimePath: join(directory, "assets", "runtime.mjs"),
6720
+ runnerPath: join(directory, "scripts", "run-design.mjs"),
6721
+ drawingPatternsPath: join(directory, "references", "drawing-patterns.md"),
6722
+ geometryPath: join(directory, "assets", "drawing-patterns", "geometry.mjs"),
6723
+ templateDir: join(directory, "templates")
6724
+ };
6725
+ }
6699
6726
  function parseSkill(raw) {
6700
6727
  const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw);
6701
6728
  if (!frontmatter) throw new Error(`${skillName}: missing skill frontmatter`);
@@ -6746,7 +6773,7 @@ function apply$1(ctx, kicadCliPath) {
6746
6773
  provider: providerName,
6747
6774
  source: candidate.source,
6748
6775
  resourceBase,
6749
- content: `${loaded.content}\n\n## 当前安装路径(运行时生成)\n\n项目脚本设置 DSH_SCHEMATIC_PLUGIN_DIR=${JSON.stringify(fileURLToPath(new URL("../", import.meta.url)))}。CLI=${JSON.stringify(paths.kicadCliPath)}。${paths.symbolDir ? `KICAD_SYMBOL_DIR=${JSON.stringify(paths.symbolDir)}。` : "未找到标准符号库;使用项目符号或配置 KICAD_SYMBOL_DIR。"}`
6776
+ content: `${loaded.content}\n\n## 当前安装路径(运行时生成)\n\n只读资源路径清单(直接使用,不从 pluginDir 猜测技能资源路径;输出另存当前工作区):\n\n\`\`\`json\n${JSON.stringify(skillResourcePaths(), null, 2)}\n\`\`\`\n\n项目脚本设置 DSH_SCHEMATIC_PLUGIN_DIR=${JSON.stringify(skillResourcePaths().pluginDir)}。CLI=${JSON.stringify(paths.kicadCliPath)}。${paths.symbolDir ? `KICAD_SYMBOL_DIR=${JSON.stringify(paths.symbolDir)}。` : "未找到标准符号库;使用项目符号或配置 KICAD_SYMBOL_DIR。不要扫描或猜测其他安装目录;随包参考符号不提供实际器件的本地封装。"}`
6750
6777
  };
6751
6778
  }
6752
6779
  })));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-tool-kicad-schematic-gen",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "KiCad 原理图设计:电路模块搜索、器件模型与手册、符号导入、编辑预览、脚本构建及设计技能",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,8 +32,8 @@
32
32
  "pdfjs-dist": "6.3.289",
33
33
  "sharp": "0.35.4",
34
34
  "undici": "^8.10.0",
35
- "@huaqiu/dsh-plugin-log": "0.6.9",
36
- "@huaqiu/dsh-sidebar-entry": "0.6.9"
35
+ "@huaqiu/dsh-plugin-log": "0.6.10",
36
+ "@huaqiu/dsh-sidebar-entry": "0.6.10"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.2",
@@ -13,6 +13,16 @@ description: Before a new schematic, ask the user to confirm requirements and wa
13
13
 
14
14
  自动分析若报 CLI_START_FAILED,复用 get_eda_host_info 的 installation.executables 中名为 kicad-cli 的 path,传入 analyze_kicad_module({schematicPath,view:"summary",kicadCliPath}) 重试;没有已有宿主信息才获取一次。不要重新下载模块、扫描配置目录或把不支持的导出命令当作路径问题。后续查询复用成功路径,省略 kicadCliPath。
15
15
 
16
+ ## 执行主线
17
+
18
+ 需求确认 → 保存并读回硬件方案 → 检查模块与准备全部实际器件库 → 按连接计划编写脚本 → 候选预览与本地封装检查 → 集中修复并同步方案 → 发布与正式概览。方案阶段的选型核实照常进行,方案未保存读回前不编写生成脚本;不增加额外审批。
19
+
20
+ - 路径直接使用末尾运行时提供的 `skillDir/runtimePath/runnerPath/geometryPath` 等绝对路径。本文的 assets/、references/、scripts/ 均相对于 skillDir,不是 pluginDir。只读取本次需要的接口、所选画法及其直接依赖,不递归列出整个技能目录。
21
+ - 同一项目已读取的接口和成功查询沿用;独立的只读资料请求可批量执行。写入同一工程库表的导入按工具并发约定执行,不盲目并发修改库表。
22
+ - 首版前准备全部物理器件(含阻容与连接器)的真实本地符号、封装及库表。已有复用模块先核对映射;参考符号和非空 Footprint 名称都不代表本地封装已就绪。
23
+ - 第一版候选普通预览传 `projectDir` 指向已准备库表的正式工程目录,同时检查 `footprintCheck`;修正未解析引用后再发布。正式交付要求 `deliveryStatus:ready` 且 overview 非空;`renderStatus:ok` 只代表渲染成功。
24
+ - 读取或导入失败时按[错误分类与恢复](references/troubleshooting.md)改变下一步,不原样重复确定性错误。实际器件变更时同步方案/BOM,再读回版本并构建。已使用任务清单时,在最后一次正式概览之前更新实际完成状态;未完成项说明原因。
25
+
16
26
  ## 1. 明确需求与原理图形式
17
27
 
18
28
  新建原理图项目必须先执行 hardware-design-brief 的“方案前需求确认”:展示已知目标、交付范围、关键缺口和推荐选项,用 `ask_user_question` 提问并等待真实用户回复;即使能自行作假设也不能跳过。工具不可用时在对话中提问并结束本轮等待。未回复、取消、超时或默认选择不算确认,期间只可只读核实资料,不得写正式方案、生成脚本或电路副本。已确认的同一项目需求可沿用,不反复确认。需求回复应在现有 PRD/精简方案中记录实际来源;有文档和 hash 不等于需求已经确认。
@@ -45,9 +55,9 @@ description: Before a new schematic, ask the user to confirm requirements and wa
45
55
 
46
56
  对选定的复用模块完成[模块封装本地化](references/module-footprints.md):先 list 原实例,优先利用模块下载返回的 footprints,缺失时搜索精确器件并只下载对应封装;保存 mappingPath,并在设计脚本的 importModule 中传 footprintMappingPath。已有符号和电路保持不变。最终 project 中每个需要上板的实例都必须指向实际存在的本地封装,不能保留未注册的 kicad_api_lib 引用;电源符号不要求封装。来源内容变化时重新生成映射,布局重跑时复用记录。
47
57
 
48
- 封装准备从模块确定后开始,与布局工作交错推进,不留到交付前集中补齐。`import.stage=prepared` 只表示库与映射就绪,来源未改写时用 `check_mapping` 检查准备状态;`check` 留给已应用映射的实际候选/最终原理图,避免把源文件 resolved:0 当成下载失败。先利用 list 的 dnp、端口工具和器件引脚信息明确真实接法,再共用连接计划生成脚本与文档引脚表。
58
+ 封装准备从模块确定后开始,首版构建前完成实际器件库和来源映射,不留到交付前集中补齐。`import.stage=prepared` 只表示库与映射就绪,来源未改写时用 `check_mapping` 检查准备状态;`check` 留给已应用映射的实际候选/最终原理图,避免把源文件 resolved:0 当成下载失败。先利用 list 的 dnp、端口工具和器件引脚信息明确真实接法,再共用连接计划生成脚本与文档引脚表。
49
59
 
50
- 项目本地器件库统一归类:符号写入 `project/libraries/symbols/*.kicad_sym`,封装写入 `project/libraries/footprints/*.pretty/`;`sym-lib-table` 和 `fp-lib-table` 保留在 project 根目录,通过 `${KIPRJMOD}/libraries/...` 注册。复用导入工具返回的 libraryPath、libId、footprintId,不另在根目录创建副本。设计脚本使用相对于自身文件位置的路径定位 project,并保留整个 libraries 目录用于打包和后续 PCB。旧项目已有根目录库时,工具仍可复用;整理旧项目须同步库表和脚本路径,不改变库昵称或实例关联。
60
+ 项目本地器件库统一归类:符号写入 `project/libraries/symbols/*.kicad_sym`,封装写入 `project/libraries/footprints/*.pretty/`;`sym-lib-table` 和 `fp-lib-table` 保留在 project 根目录,通过 `${KIPRJMOD}/libraries/...` 注册。复用导入工具返回的 libraryPath、libId、footprintId,不另在根目录创建副本。设计脚本使用相对于自身文件位置的路径定位 project,并保留整个 libraries 目录用于打包和后续 PCB。不再自动复用根目录旧库;遇到同名旧库表路径冲突时明确修正库表及脚本,不依赖历史目录回退。
51
61
 
52
62
  专门的器件搜索由 KiCad 内置插件 `@huaqiu/dsh-tool-part-search` 提供。用 `search_hqsch_parts({query, page, page_size, require_eda_model: true, requirements: {symbol: true, footprint: true}})` 搜索型号或功能规格,结果在 `items` 中,支持分页。需要完整参数、分类、文档或标签时,用选定器件的 `manufacturer.id` 和精确型号调用 `get_hqsch_part({manufacturer_id, mpn})`。`get_hqsch_part_models({manufacturer_id, mpn})` 只返回符号、封装、3D 等模型链接,不下载或解析;检查实际引脚和焊盘仍用原理图插件的 `get_kicad_component_models({manufacturer_id, mpn})`。仅在需要采购信息时调用 `get_hqsch_supply_chain({parts: [{manufacturer_id, mpn}]})`,空报价不代表市场无库存。搜索中的模型标记可能由筛选条件推导,不能据此认定模型一定存在或不存在;以详情、模型链接及实际解析结果核对。搜索插件关闭或不可用时说明条件,不绕过开关;可继续检查已有模块,缺少在线符号的部分按本节回退规则处理。
53
63
 
@@ -74,7 +84,7 @@ description: Before a new schematic, ask the user to confirm requirements and wa
74
84
 
75
85
  从零编写或补充电路时读取 [编程接口](references/programming.md) 与 [模块脚本组织](references/modular-scripts.md)。模型决定器件、连接和布局,现有 builder.js 处理文件格式与真实引脚坐标。多功能电路采用 design.mjs 主入口和 modules/ 下每框独立脚本,共享器件数据与引用编号;先放置全部器件,再布线,最后统一保存。下载复用模块和新编写模块可同时进入同一个 Schematic;此组合优先按 [混合整合范例](references/mixed-design.md) 编写主入口,导入模块后将返回的目标引用编号登记到共享分配器,再放置新增器件。只修受影响模块和连接,避免重新读取全部代码。默认不创建 smoke.mjs 或单电阻测试工程;直接生成实际候选并调用预览。只有库接口发生变化或明确的内部错误才做最小复现。先用生成器返回的全部外框坐标检查页面范围与框间重叠,集中调整一次后再预览,不为打印坐标重复生成候选。
76
86
 
77
- 新增 LED、上拉按键、去耦电容或 LDO 外围等局部电路,或排列芯片/连接器的信号标签时,先按需读取 [局部电路画法索引](references/drawing-patterns.md),选择已确认拓扑匹配的简短代码;不熟悉画法才读取对应参考图。局部单元放在现有功能框内,共用编号和两阶段 place/connect,不各自建框或保存工程。代码依照实际导入符号的引脚、方向和属性调整,不能照抄范例取值或用画法替代电气设计。复用模块布局已经清楚时保留,不为套范例重画。绘制复位/使能 RC 或并列启动按键时,读取 [复位与启动画法](references/drawing-patterns/reset-enable.md):上拉与按键竖向对齐,对地电容放节点右下方、地向下,避免横放按键的回绕线;BOOT 不自动复制复位电容。LDO 输入电容放在 VIN 一侧、输出电容放在 VOUT 一侧,优先短导线直接连接,不把所属外围集中到框下方;芯片去耦同样归属到供电脚附近。先组织框内主信号流向、对齐器件和属性,再调整外框与整页位置;同一画法与图片在本轮已经读过就复用结论。
87
+ 新增 LED、上拉/复位 RC、去耦、三脚或带 EN 的 LDO、I²C 上拉、无源晶振负载电容、异步 Buck/Boost DC-DC、MCU 最小系统、USB Type-C 设备端、MOSFET 低侧开关、SWD 接口等局部电路,或排列芯片/连接器的信号标签时,先按需读取 [局部电路画法索引](references/drawing-patterns.md),选择已确认拓扑匹配的简短代码;不熟悉画法才读取对应参考图。局部单元放在现有功能框内,共用编号和两阶段 place/connect,不各自建框或保存工程。代码依照实际导入符号的引脚、方向和属性调整,不能照抄范例取值或用画法替代电气设计。复用模块布局已经清楚时保留,不为套范例重画。绘制复位/使能 RC 或并列启动按键时,读取 [复位与启动画法](references/drawing-patterns/reset-enable.md):上拉与按键竖向对齐,对地电容放节点右下方、地向下,避免横放按键的回绕线;BOOT 不自动复制复位电容。LDO 输入电容放在 VIN 一侧、输出电容放在 VOUT 一侧,优先短导线直接连接,不把所属外围集中到框下方;芯片去耦同样归属到供电脚附近。MCU 的复位 RC、BOOT 配置优先靠近所属引脚直接连线,避免用同名标签把紧邻外围拆成孤立支路;晶振位于单独支路时在两端及 MCU 对应脚使用成对同名标签,避免跨区长线;按已确认时钟方案选择内部 RC 或外部无源晶振范例。先组织框内主信号流向、对齐器件和属性,再调整外框与整页位置;同一画法与图片在本轮已经读过就复用结论。
78
88
 
79
89
  同侧多个信号优先用编程接口中的 attachPinLabels 批量短直引出:左右水平、上下竖排,导线外端同列/同行,名称默认排在引线上方或沿竖线排列;不随名称长度逐根改变线长,也不默认把上下引脚扇出成阶梯。电源、地、NC 和已有连接单独处理;普通单点或确需折线时才用 attachPinLabel。执行用 scripts/run-design.mjs 保存 UTF-8 构建报告,正常项目不读取编译库源码猜接口。同一问题连续两轮仍未改善时,改用更宽外框或不同引线通道,集中处理明确遮挡;不跳过电气问题或无法辨认的内容。
80
90
 
@@ -90,4 +100,4 @@ description: Before a new schematic, ask the user to confirm requirements and wa
90
100
 
91
101
  候选通过后发布到固定 project/,只替换本设计生成的工程文件和已列明子页,保留符号库、来源和其他工程文件。正式工程使用相对子页路径,再生成正式预览;安全清理本轮候选。交付正式原理图、工程配置、所需符号库、脚本、来源与预览,说明复用/修改/新设计的模块及未核实条件。允许为模块理解只读提取并缓存网表/BOM;默认不运行 ERC、不额外进行网表接线比对、不编写端点审计、不生成 PCB;用户明确要求时再执行。预览和连接计划不能称为电气验证通过。
92
102
 
93
- 先在候选上完成最后一次读图、修复、封装检查和发布,再生成正式概览;不要先 delivery 再读取图片继续布局修改。若发现新问题,使用普通预览修复完成后再正式交付。同一轮只展示最后一次有效正式预览,旧 ZIP 快照仍保留。正式概览必须是本轮最后一个工具操作;清理、检查和读图都在它之前完成。之后若继续调用任何工具,必须重新正式交付;完成正式交付后直接输出最终说明。最终正式工程的预览调用 `render_kicad_module_preview({schematicPath: 正式原理图路径, projectDir: 正式project目录, delivery: true})`,可复用同版本渲染缓存,在本轮正常完成后,于最终回复后的轮次尾部显示原理图概览、页面切换、缩放和工程 ZIP 下载卡片;执行期间仅保存工具结果,不立即展示卡片。中间布局检查不传 delivery。projectDir 必须同时包含根图、子页和工程所需本地库;ZIP 收集该目录内原理图、工程配置、符号、封装、库表及3D模型,不包含脚本、来源、预览或工作区外的库。返回 overviewError 时说明卡片或打包不可用,保留已生成工程和现有本地预览,不因此重建电路。卡片不提供重新生成、详情或自动放置按钮。概览不代表电气验证通过。
103
+ 先在候选上完成最后一次读图、修复、封装检查和发布,再生成正式概览;不要先 delivery 再读取图片继续布局修改。若发现新问题,使用普通预览修复完成后再正式交付。同一轮只展示最后一次有效正式预览,旧 ZIP 快照仍保留。正式概览必须是本轮最后一个工具操作;清理、检查和读图都在它之前完成。之后若继续调用任何工具,必须重新正式交付;完成正式交付后直接输出最终说明。最终正式工程的预览调用 `render_kicad_module_preview({schematicPath: 正式原理图路径, projectDir: 正式project目录, delivery: true})`,可复用同版本渲染缓存,在本轮正常完成后,于最终回复后的轮次尾部显示原理图概览、页面切换、缩放和工程 ZIP 下载卡片;执行期间仅保存工具结果,不立即展示卡片。中间布局检查不传 delivery。projectDir 必须同时包含根图、子页和工程所需本地库;ZIP 收集该目录内原理图、工程配置、符号、封装、库表及3D模型,不包含脚本、来源、预览或工作区外的库。交付返回 deliveryStatus:blocked 或 overviewError 时不得宣称正式交付成功;先依据 footprintCheck/error 修正缺库或服务问题,保留已有工程和预览,不为补库重画未变的电路。卡片不提供重新生成、详情或自动放置按钮。概览不代表电气验证通过。
@@ -0,0 +1,27 @@
1
+ ### KiCad Libraries License
2
+
3
+ The KiCad libraries are licensed under the [Creative Commons CC-BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/legalcode), with the following exception:
4
+
5
+ ---------
6
+
7
+ _To the extent that the creation of electronic designs that use 'Licensed Material' can be considered to be 'Adapted Material', then the copyright holder waives article 3 of the license with respect to these designs and any generated files which use data provided as part of the 'Licensed Material'._
8
+
9
+ ---------
10
+
11
+ You can read more about KiCad library licensing [here](http://kicad.org/libraries/license).
12
+
13
+ **What does this mean?**
14
+
15
+ KiCad libraries are licensed in such a way to ensure free use of library data for commercial, closed, and non-commercial projects without restriction. KiCad does not wish to exert any control over designs produced using the library data, or force users to reveal proprietary information contained in their designs. Neither do we wish to force users to attribute the KiCad libraries _within their design_.
16
+
17
+ Use of the library data in a project does not (by itself) require that the design or any files generated from the design are licensed under the CC-BY-SA 4.0 License. You are free to use the library data in your own projects without the obligation to share your project files under this or any other license agreement.
18
+
19
+ However, if you wish to redistribute the KiCad libraries, or parts thereof (including in modified form) as a collection then the exception above does not apply. Redistributed library collections must be shared under the same license agreement. Under these circumstances, the libraries must also retain attribution information, including the license documents which are distributed with the library files.
20
+
21
+ ----------------------
22
+
23
+ **Warranty**
24
+
25
+ The KiCad libraries are provided in the hope that they will be useful, but are provided without warranty of any kind, express or implied.
26
+
27
+ The KiCad libraries are compiled by the KiCad community - if you find an error in the library data, please help the community and contribute a fix!
@@ -0,0 +1,35 @@
1
+ /** Passive crystal with verified external load capacitors; not an oscillator design rule. */
2
+ import { vertical, wire, point } from './geometry.mjs';
3
+ export function place({ region, at: [x, y], parts, references: refs, nets, externalCrystalConfirmed }) {
4
+ if (externalCrystalConfirmed !== true) {
5
+ throw new Error('Verify an external passive crystal is used and requires these load capacitors');
6
+ }
7
+ const { pins, ...symbol } = parts.crystal;
8
+ if (!pins.left || !pins.right || parts.capacitors.length !== 2 || refs.capacitors.length !== 2) {
9
+ throw new Error('Provide two crystal terminal roles and two capacitors');
10
+ }
11
+ region.addSymbol({ ...symbol, reference: refs.crystal, at: point([x + 38.1, y + 15.24]),
12
+ referenceAt: point([x + 38.1, y + 2.54]), valueAt: point([x + 38.1, y + 6.35]) });
13
+ const left = point(region.pin(refs.crystal, pins.left)), right = point(region.pin(refs.crystal, pins.right));
14
+ if (left[0] >= right[0] || left[1] !== right[1]) {
15
+ throw new Error('Orient crystal terminals horizontally without exchanging physical roles');
16
+ }
17
+ const nodes = [[left[0] - 12.7, left[1]], [right[0] + 12.7, right[1]]];
18
+ const caps = parts.capacitors.map((part, i) => vertical(region, part, refs.capacitors[i],
19
+ point([nodes[i][0], left[1] + 15.24]), 'top', 'bottom'));
20
+ if (caps.some(c => c.top[1] <= left[1])) throw new Error('Increase crystal load-capacitor spacing');
21
+ return {
22
+ reservedBounds: { origin: [x, y], size: [81.28, 58.42] },
23
+ ports: { left: { reference: refs.crystal, pin: pins.left },
24
+ right: { reference: refs.crystal, pin: pins.right } },
25
+ connect() {
26
+ [left, right].forEach((p, i) => {
27
+ const node = nodes[i], c = caps[i], ground = [c.bottom[0], c.bottom[1] + 7.62];
28
+ wire(region, p, node); wire(region, node, c.top); region.marker('junction', node);
29
+ region.label(i === 0 ? nets.left : nets.right, node);
30
+ wire(region, c.bottom, ground); region.label(nets.ground, ground);
31
+ });
32
+ for (const pin of pins.caseGround ?? []) region.label(nets.ground, point(region.pin(refs.crystal, pin)));
33
+ },
34
+ };
35
+ }