@windypro-rourou/dsh-eda 0.1.0

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.
@@ -0,0 +1,158 @@
1
+ // ============================================================================
2
+ // dsh-lichuang-eda · 自然语言 → 结构化电路设计(翻译层 v0)
3
+ // ============================================================================
4
+ // 面向 0 基础用户:把「中文需求 / 模板 ID」翻译成 json-gen.js 能消费的
5
+ // design 对象({ name, components, wires, nets })。
6
+ //
7
+ // v0 为规则版(模板 + 关键词匹配),生成器目前只支持 resistor / led,
8
+ // 所以可生成的模板有限;其余能力如实返回 ok:false + 说明(不编造)。
9
+ // 后续接入 LLM 后可这里挂一个"语义翻译"钩子(见 TODO 注释)。
10
+ // ============================================================================
11
+
12
+ import { coordKey } from './validate.js'
13
+
14
+ /**
15
+ * 模板目录(供 UI 卡片墙 / agent 展示)。
16
+ * supported=false 的模板:真实生成暂不支持,但会给出解释。
17
+ */
18
+ export const TEMPLATE_CATALOG = [
19
+ {
20
+ id: 'led-blink',
21
+ ico: '💡',
22
+ name: 'LED 点亮最小电路',
23
+ desc: '限流电阻 + LED,5V 供电,电阻接 5V、LED 接 GND',
24
+ supported: true,
25
+ },
26
+ {
27
+ id: 'voltage-divider',
28
+ ico: '⚡',
29
+ name: '电阻分压(降压基准)',
30
+ desc: '两电阻分压:5V → R1(1k) → R2(2k) → GND,中点 3.3V(近似)',
31
+ supported: true,
32
+ },
33
+ {
34
+ id: 'buck',
35
+ ico: '🔋',
36
+ name: '5V→3.3V 降压电路',
37
+ desc: '专业降压需电感/稳压芯片,v0 提供电阻分压近似 + 说明',
38
+ supported: 'approx',
39
+ },
40
+ {
41
+ id: 'esp32-min',
42
+ ico: '🛰️',
43
+ name: 'ESP32 最小系统',
44
+ desc: '需要主控芯片符号,v0 暂不支持该元件类型',
45
+ supported: false,
46
+ },
47
+ {
48
+ id: 'motor-drive',
49
+ ico: '🟢',
50
+ name: '电机驱动',
51
+ desc: '需要 MOSFET/驱动芯片符号,v0 暂不支持',
52
+ supported: false,
53
+ },
54
+ {
55
+ id: 'uart-convert',
56
+ ico: '🔌',
57
+ name: '接口转换',
58
+ desc: '需要电平转换/USB 芯片符号,v0 暂不支持',
59
+ supported: false,
60
+ },
61
+ ]
62
+
63
+ /** 规则:生成 LED 点亮最小电路(R + LED 串联手拉手)。 */
64
+ function buildLedDesign() {
65
+ return {
66
+ name: 'led-blink',
67
+ components: [
68
+ { ref: 'R1', type: 'resistor', value: '220', pos: { x: 400, y: 300 } },
69
+ { ref: 'LED1', type: 'led', value: 'red', pos: { x: 700, y: 300 } },
70
+ ],
71
+ // R1 右引脚(450,300) → LED1 阳极(740,300) 直连
72
+ wires: [[450, 300, 740, 300]],
73
+ nets: [
74
+ { name: '5V', points: [[350, 300]] },
75
+ { name: 'GND', points: [[660, 300]] },
76
+ ],
77
+ }
78
+ }
79
+
80
+ /** 规则:电阻分压(R1+R2 串联,中点网络标签 3.3V)。 */
81
+ function buildDividerDesign() {
82
+ return {
83
+ name: 'voltage-divider',
84
+ components: [
85
+ { ref: 'R1', type: 'resistor', value: '1k', pos: { x: 400, y: 300 } },
86
+ { ref: 'R2', type: 'resistor', value: '2k', pos: { x: 900, y: 300 } },
87
+ ],
88
+ wires: [[450, 300, 850, 300]],
89
+ nets: [
90
+ { name: '5V', points: [[350, 300]] },
91
+ { name: '3.3V', points: [[650, 300]] },
92
+ { name: 'GND', points: [[950, 300]] },
93
+ ],
94
+ }
95
+ }
96
+
97
+ const TEMPLATE_BUILDERS = {
98
+ 'led-blink': buildLedDesign,
99
+ 'voltage-divider': buildDividerDesign,
100
+ buck: buildDividerDesign, // 近似:分压 + 说明
101
+ }
102
+
103
+ /**
104
+ * 按模板 ID 生成设计。
105
+ * @returns {{ ok:boolean, design?:object, note?:string, title:string }}
106
+ */
107
+ export function templateById(id) {
108
+ const meta = TEMPLATE_CATALOG.find((t) => t.id === id)
109
+ if (meta === undefined) return { ok: false, title: id, note: `未知模板:${id}` }
110
+ if (meta.supported === false) {
111
+ return { ok: false, title: meta.name, note: `模板「${meta.name}」需要其他元件符号(${meta.desc}),v0 生成器暂仅支持 电阻/ LED。` }
112
+ }
113
+ const builder = TEMPLATE_BUILDERS[id]
114
+ if (builder === undefined) return { ok: false, title: meta.name, note: `模板「${meta.name}」尚未实现规则。` }
115
+ const design = builder()
116
+ const note = id === 'buck' ? '提示:电阻分压只适合小电流基准;真正的 5V→3.3V 降压建议用降压芯片(后续版本支持)。' : undefined
117
+ return { ok: true, design, title: meta.name, note }
118
+ }
119
+
120
+ /** 简单关键词匹配(规则版 NLU)。返回 { ok, design?, note?, title?, matched? } */
121
+ function matchByKeywords(text) {
122
+ const t = String(text ?? '').toLowerCase()
123
+ if (/led|发光|灯.?闪|点.?亮/.test(t)) {
124
+ return templateById('led-blink')
125
+ }
126
+ if (/分压|降压|3\.3v|3v3|基准电压/.test(t)) {
127
+ return templateById('buck')
128
+ }
129
+ return null
130
+ }
131
+
132
+ /**
133
+ * 中文需求 → design。规则版:先模板匹配,未命中则提示。
134
+ * @param {string} text 用户的中文描述
135
+ * @param {object} opts { template?: string } 可指定模板 ID
136
+ * @returns {{ ok:boolean, design?:object, title:string, note?:string, matched?:string }}
137
+ */
138
+ export function translateRequest(text, opts = {}) {
139
+ if (opts.template) return templateById(opts.template)
140
+ const byKw = matchByKeywords(text)
141
+ if (byKw !== null) return { ...byKw, matched: true }
142
+ return {
143
+ ok: false,
144
+ title: '待支持的电路',
145
+ note:
146
+ 'v0 规则版只能识别「LED 点亮 / 电阻分压 / 降压」等简单需求。' +
147
+ '请用这些词(例:一个 LED 点亮电路、电阻分压)或从模板卡选择;' +
148
+ '完整自然语言理解将在接入 LLM 翻译层后支持。',
149
+ }
150
+ }
151
+
152
+ /** 给 UI/agent 用的设计摘要。 */
153
+ export function describeDesign(design) {
154
+ if (design === undefined) return '(无设计)'
155
+ const comps = design.components.map((c) => `${c.ref}(${c.type}) ${c.value}@${coordKey(c.pos?.x, c.pos?.y)}`).join('、')
156
+ const nets = (design.nets ?? []).map((n) => n.name).join('、')
157
+ return `元件: ${comps || '无'}; 导线: ${(design.wires ?? []).length} 条; 网络: ${nets || '无'}`
158
+ }
@@ -0,0 +1,159 @@
1
+ // ============================================================================
2
+ // dsh-lichuang-eda · 结构校验 + 网表推导 + 连通性检查(供测试与插件调用)
3
+ // ============================================================================
4
+ // 纯 ESM、零 npm 依赖。基于 src/json-gen.js 的生成物与输入 design 做校验:
5
+ // - validateSchematic(obj) : 结构校验(顶层/head/canvas/shape/BBox/colors)
6
+ // - deriveNetlist(design) : 从 design(元件引脚/导线/网络)推导网表
7
+ // - checkConnectivity(design): 连通性检查(悬空引脚 / 孤立网络 / 未闭合)
8
+ //
9
+ // 说明:本次只支持 resistor / led 两种符号(与 SYMBOL_BUILDERS 一致),
10
+ // 引脚几何与 buildResistorLib / buildLedLib 硬编码一致(旋转 0/180)。
11
+ // ============================================================================
12
+
13
+ /** 坐标归一化为键(保留 2 位小数),用于去重/连通。 */
14
+ export function coordKey(x, y) {
15
+ return `${Math.round(Number(x) * 100) / 100},${Math.round(Number(y) * 100) / 100}`
16
+ }
17
+
18
+ /**
19
+ * 元件引脚点(resistor: 右+50/左-50;led: 阳极+40/阴极-40;与生成器一致)。
20
+ * @returns {Array<{x,y,ref,pin}>}
21
+ */
22
+ export function pinPoints(comp) {
23
+ const cx = Number(comp.pos?.x ?? NaN)
24
+ const cy = Number(comp.pos?.y ?? NaN)
25
+ if (Number.isNaN(cx) || Number.isNaN(cy)) return []
26
+ if (comp.type === 'resistor') {
27
+ return [
28
+ { x: cx + 50, y: cy, ref: comp.ref, pin: 1 },
29
+ { x: cx - 50, y: cy, ref: comp.ref, pin: 2 },
30
+ ]
31
+ }
32
+ if (comp.type === 'led') {
33
+ return [
34
+ { x: cx + 40, y: cy, ref: comp.ref, pin: 1 },
35
+ { x: cx - 40, y: cy, ref: comp.ref, pin: 2 },
36
+ ]
37
+ }
38
+ return []
39
+ }
40
+
41
+ /** 极简 union-find。 */
42
+ function makeUnionFind(keys) {
43
+ const parent = new Map()
44
+ for (const k of keys) parent.set(k, k)
45
+ const find = (x) => {
46
+ while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x) }
47
+ return x
48
+ }
49
+ const union = (a, b) => { const ra = find(a), rb = find(b); if (ra !== rb) parent.set(ra, rb) }
50
+ return { find, union }
51
+ }
52
+
53
+ /**
54
+ * 从 design 推导网表:把导线顶点、元件引脚、网络标签/接地标志按坐标合并成
55
+ * 连通网络。返回 { nets: [ { name, pins:[{ref,pin}], points:[coords] } ],
56
+ * dangling: [ {ref,pin} ] }。
57
+ * @param {object} design { components, wires, nets, ... }
58
+ */
59
+ export function deriveNetlist(design = {}) {
60
+ const components = design.components ?? []
61
+ const wires = design.wires ?? []
62
+ const nets = design.nets ?? []
63
+
64
+ // 收集所有节点坐标
65
+ const nodeKeys = new Set()
66
+ const wireLinks = [] // [ [key,key], ... ]
67
+ const pinList = [] // { key, ref, pin }
68
+ const labelPoints = [] // { key, name } (network label / GND flag)
69
+
70
+ for (const comp of components) {
71
+ for (const p of pinPoints(comp)) {
72
+ const k = coordKey(p.x, p.y)
73
+ nodeKeys.add(k)
74
+ pinList.push({ key: k, ref: comp.ref, pin: p.pin })
75
+ }
76
+ }
77
+ for (const w of wires) {
78
+ for (let i = 0; i + 1 < w.length; i += 2) {
79
+ const a = coordKey(w[i], w[i + 1])
80
+ const b = coordKey(w[i + 2], w[i + 3])
81
+ nodeKeys.add(a); nodeKeys.add(b)
82
+ wireLinks.push([a, b])
83
+ }
84
+ }
85
+ for (const net of nets) {
86
+ for (const [nx, ny] of net.points) {
87
+ const k = coordKey(nx, ny)
88
+ nodeKeys.add(k)
89
+ labelPoints.push({ key: k, name: net.name })
90
+ }
91
+ }
92
+
93
+ const uf = makeUnionFind([...nodeKeys])
94
+ for (const [a, b] of wireLinks) uf.union(a, b)
95
+
96
+ // 分组:root -> { pins:[], labels:Set, points:Set }
97
+ const groups = new Map()
98
+ const attach = (key) => {
99
+ const root = uf.find(key)
100
+ let g = groups.get(root)
101
+ if (g === undefined) { g = { pins: [], labels: new Set(), points: new Set() }; groups.set(root, g) }
102
+ g.points.add(key)
103
+ return g
104
+ }
105
+ for (const p of pinList) { const g = attach(p.key); g.pins.push({ ref: p.ref, pin: p.pin }) }
106
+ for (const l of labelPoints) { const g = attach(l.key); g.labels.add(l.name) }
107
+
108
+ // 每个连通组是 1 个"物理网";网络名 = 组内标签名(GND 优先,取首个)。
109
+ const netsOut = []
110
+ const dangling = []
111
+ for (const [, g] of groups) {
112
+ const name = g.labels.size > 0 ? [...g.labels][0] : null // 网络标签/接地名(命名网)
113
+ netsOut.push({ name, pins: g.pins, points: [...g.points] })
114
+ // 悬空:某引脚所在的组只有它自己这一个引脚,且没有命名标签 → 未接入任何其他元件
115
+ if (g.pins.length === 1 && name === null) dangling.push(...g.pins)
116
+ }
117
+ return { nets: netsOut, dangling }
118
+ }
119
+
120
+ /**
121
+ * 结构校验:对生成物(平面单片对象)做字段级检查。
122
+ * @param {object} obj generateSchematic 的返回值
123
+ * @returns {{ ok:boolean, errors:string[] }}
124
+ */
125
+ export function validateSchematic(obj) {
126
+ const errors = []
127
+ if (obj === null || typeof obj !== 'object') { return { ok: false, errors: ['not an object'] } }
128
+ const mustHave = ['head', 'canvas', 'shape', 'BBox', 'colors']
129
+ for (const k of mustHave) if (!(k in obj)) errors.push(`missing top-level key: ${k}`)
130
+ if (obj.head) {
131
+ for (const k of ['docType', 'editorVersion', 'newgId', 'c_para', 'c_spiceCmd', 'hasIdFlag', 'uuid', 'x', 'y', 'portOfADImportHack', 'importFlag', 'transformList']) {
132
+ if (!(k in obj.head)) errors.push(`missing head key: ${k}`)
133
+ }
134
+ if (obj.head.docType !== '1') errors.push(`head.docType expected "1", got ${obj.head.docType}`)
135
+ }
136
+ if (typeof obj.canvas === 'string') {
137
+ if (!obj.canvas.startsWith('CA~')) errors.push('canvas must start with CA~')
138
+ if (obj.canvas.split('~').length !== 15) errors.push(`canvas expected 15 segments, got ${obj.canvas.split('~').length}`)
139
+ } else errors.push('canvas must be a string')
140
+ if (Array.isArray(obj.shape)) {
141
+ if (obj.shape.length === 0) errors.push('shape is empty')
142
+ } else errors.push('shape must be an array')
143
+ if (!('BBox' in obj) || typeof obj.BBox !== 'object') errors.push('BBox missing or not object')
144
+ if (!('colors' in obj) || typeof obj.colors !== 'object') errors.push('colors missing or not object')
145
+ return { ok: errors.length === 0, errors }
146
+ }
147
+
148
+ /**
149
+ * 连通性检查:网表推导 + 悬空引脚判定。
150
+ * @param {object} design
151
+ * @returns {{ ok:boolean, errors:string[], netlist:{nets,dangling} }}
152
+ */
153
+ export function checkConnectivity(design = {}) {
154
+ const { nets, dangling } = deriveNetlist(design)
155
+ const errors = []
156
+ // 每个元件引脚都必须落在某一命名网,或与其它元件引脚同网(即 ≥2 脚)。
157
+ for (const d of dangling) errors.push(`引脚 ${d.ref}.${d.pin} 悬空(未连线/未打网络标签)`)
158
+ return { ok: errors.length === 0, errors, netlist: { nets, dangling } }
159
+ }