From 69042210cb67e81043f5d1f545286e03d2694361 Mon Sep 17 00:00:00 2001 From: Ferraz Date: Mon, 29 Jun 2026 21:11:33 -0300 Subject: [PATCH 1/4] feat(components): add diode variants and potentiometer/rheostat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the standard library with the most-reached-for variable resistors and diode-family parts (closes #25, closes #26). #26 — Diode variants: - ZenerDiode, SchottkyDiode, Photodiode, all reusing the Diode A/C terminals and anode/cathode roles. - Refactor drawDiode into a shared drawDiodeBody helper; each variant adds a distinct cathode treatment (Zener lip bar, Schottky squared-S hooks, Photodiode inward "incident light" arrows). #25 — Potentiometer + Rheostat: - Rheostat: two-terminal variable resistor (resistor body + diagonal arrow). - Potentiometer: three-terminal (1, W, 2) with a wiper role. Adds a layout geometry branch placing the wiper as a perpendicular midpoint tap and an absolute-coordinate drawer, the same approach used for the transistor. The wiper auto-derives its side from position, so orientation hints rotate it correctly. Tests: parts-catalog resolution for the new two-terminal parts, a potentiometer block covering the wiper role and alias, and a snapshot. Docs: MVP component table, CONTEXT wiper role, the authoring skill, plus two new gallery examples (pot-divider, diode-variants). --- docs/CONTEXT.md | 2 +- docs/EXAMPLES.md | 12 ++ docs/MVP.md | 25 ++-- docs/assets/diode-variants.png | Bin 0 -> 11331 bytes docs/assets/diode-variants.svg | 19 +++ docs/assets/pot-divider.png | Bin 0 -> 17868 bytes docs/assets/pot-divider.svg | 19 +++ examples/diode-variants.wire | 19 +++ examples/pot-divider.wire | 14 ++ packages/core/src/layout/geometry.ts | 27 ++++ packages/core/src/library/standard-library.ts | 55 ++++++++ packages/core/src/library/symbols.ts | 5 + packages/core/src/render/symbols.ts | 129 ++++++++++++++++-- .../__snapshots__/parts-catalog.test.ts.snap | 21 +++ packages/core/test/parts-catalog.test.ts | 68 +++++++++ scripts/update-examples.mjs | 2 + .../wire-lang/references/component-library.md | 16 +++ 17 files changed, 410 insertions(+), 23 deletions(-) create mode 100644 docs/assets/diode-variants.png create mode 100644 docs/assets/diode-variants.svg create mode 100644 docs/assets/pot-divider.png create mode 100644 docs/assets/pot-divider.svg create mode 100644 examples/diode-variants.wire create mode 100644 examples/pot-divider.wire diff --git a/docs/CONTEXT.md b/docs/CONTEXT.md index 6aa68b9..634f025 100644 --- a/docs/CONTEXT.md +++ b/docs/CONTEXT.md @@ -61,7 +61,7 @@ The visual convention followed by the standard schematic symbols. _Avoid_: formal standards compliance claim, copied standard catalog **Symbol Terminal Role**: -A named connection role expected by a **Schematic Symbol**, such as `anode`, `cathode`, `collector`, `base`, `emitter`, `positive`, or `negative`. +A named connection role expected by a **Schematic Symbol**, such as `anode`, `cathode`, `collector`, `base`, `emitter`, `positive`, `negative`, `wiper`, or `reference`. _Avoid_: terminal name, instance ID **Symbol Terminal Mapping**: diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 245be57..f0e552a 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -48,3 +48,15 @@ Source: [examples/kitchen-sink.wire](../examples/kitchen-sink.wire) Source: [examples/bus-rail.wire](../examples/bus-rail.wire) ![Bus-rail layout demo](./assets/bus-rail.svg) + +## Potentiometer Voltage Divider + +Source: [examples/pot-divider.wire](../examples/pot-divider.wire) + +![Potentiometer voltage divider](./assets/pot-divider.svg) + +## Diode Variants + +Source: [examples/diode-variants.wire](../examples/diode-variants.wire) + +![Diode variants](./assets/diode-variants.svg) diff --git a/docs/MVP.md b/docs/MVP.md index 8ab188e..ab32483 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -274,8 +274,13 @@ The MVP ships a small standard component library. | `Capacitor` | `1`, `2` | recommended `capacitance: capacitance` | `id`, `capacitance` | `capacitor` | | `PolarizedCapacitor` | `+`, `-` | recommended `capacitance: capacitance` | `id`, `capacitance` | `polarized-capacitor` | | `Inductor` | `1`, `2` | recommended `inductance: inductance` | `id`, `inductance` | `inductor` | +| `Potentiometer` | `1`, `W`, `2` | recommended `value: resistance` | `id`, `value` | `potentiometer` | +| `Rheostat` | `1`, `2` | recommended `value: resistance` | `id`, `value` | `rheostat` | | `Diode` | `A`, `C` | none | `id` | `diode` | | `LED` | `A`, `C` | optional `color: enum(red, green, blue, yellow, white, amber)` | `id` | `led` | +| `ZenerDiode` | `A`, `C` | none | `id` | `zener-diode` | +| `SchottkyDiode` | `A`, `C` | none | `id` | `schottky-diode` | +| `Photodiode` | `A`, `C` | none | `id` | `photodiode` | | `NPNTransistor` | `C`, `B`, `E` | none | `id` | `npn-transistor` | | `PNPTransistor` | `C`, `B`, `E` | none | `id` | `pnp-transistor` | | `Battery` | `+`, `-` | recommended `voltage: voltage` | `id`, `voltage` | `battery` | @@ -292,14 +297,18 @@ The MVP ships a small standard component library. | `PowerFlag` | `1` | recommended `name: string` | none | `power-flag` | | `IC` | from `pins=[...]` | recommended `pins: ic-pin-list` | `id` | `ic` | -Designator prefixes: `FerriteBead` → `FB`/`L`, `TVSDiode` → `D`/`TVS`, `Speaker` → -`LS`/`SP`, `Antenna` → `ANT`/`E`, `TestPoint` → `TP`, `PTC` → `F`/`RT`, -`PowerFlag` → `PWR`/`PR`/`PF`, `IC` → `U`/`IC`. - -`TVSDiode` maps `anode`→`A` and `cathode`→`C`; `Speaker` maps `positive`→`+` and -`negative`→`-`. `PowerFlag` draws its `name` (e.g. `VBAT`, `5V`, `3V3`, `VCC`) -inside the flag glyph; it is a visual rail flag only and does not create a hidden -global net. +Designator prefixes: `Potentiometer` → `R`/`RV`/`VR`, `Rheostat` → +`R`/`RV`/`VR`/`RH`, `ZenerDiode` → `D`/`ZD`, `SchottkyDiode` → `D`/`SD`, +`Photodiode` → `D`/`PD`, `FerriteBead` → `FB`/`L`, `TVSDiode` → `D`/`TVS`, +`Speaker` → `LS`/`SP`, `Antenna` → `ANT`/`E`, `TestPoint` → `TP`, `PTC` → +`F`/`RT`, `PowerFlag` → `PWR`/`PR`/`PF`, `IC` → `U`/`IC`. + +`Potentiometer` maps `wiper`→`W` (the two track ends `1`/`2` are +interchangeable); `ZenerDiode`, `SchottkyDiode`, and `Photodiode` map `anode`→`A` +and `cathode`→`C` like `Diode`. `TVSDiode` maps `anode`→`A` and `cathode`→`C`; +`Speaker` maps `positive`→`+` and `negative`→`-`. `PowerFlag` draws its `name` +(e.g. `VBAT`, `5V`, `3V3`, `VCC`) inside the flag glyph; it is a visual rail flag +only and does not create a hidden global net. MOSFETs and complex board modules such as Arduino boards are outside the MVP. diff --git a/docs/assets/diode-variants.png b/docs/assets/diode-variants.png new file mode 100644 index 0000000000000000000000000000000000000000..f621f6698e023ba5e3f69c0e8d094a7e7f71e6d2 GIT binary patch literal 11331 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%0DQAh?AD!z1xE$9nCf}^Th3Y|Ix!HrvFcGz?QUx zp4LNfADAQZ+4BvIsN88Syk0?*f!yBz)KC6zB2=v#dKWmmaB>e9GbXaEgPhq9?#4@B z7Eu;9LjCD|iSkooPL^^qym|J_LW{73w3hSy$ddGBT$}$+8TLi}0!2VHfwo`J6gUh? zM|S~{wXJ?{_;v<1JOvWTagC2pdM{bW)eLt6%Cwyng!k$Hb?cT;{RPusA@TnYbKdxl zvuA`9uzBonzoE)D&*kycP?!t%#1XZ=pIm%ax82Ci;$|~$M}o&0npWlO^fv3diPdiQ zMDGNA{yMT=wYUaC9Aq9&-=_Wc`PruAwg?>P^&I_z)5>p{e<(F;pAKUm*<QQt;dQF%CP2{1|kyAH6ZHfyAqU49cW;On&21;-hC z6?; Zpn=rNoRBu)-CDjYm(4DfUA+0={{Z?e0 +Diode variants +A Schottky rectifier feeds a node clamped by a Zener, plus a photodiode sense leg. + + + +5VD1D2D3R11k + +Zener clamp + \ No newline at end of file diff --git a/docs/assets/pot-divider.png b/docs/assets/pot-divider.png new file mode 100644 index 0000000000000000000000000000000000000000..1629280bf9fed343a802ff37fdbc54f6cd277f08 GIT binary patch literal 17868 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 +Potentiometer voltage divider +A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg. + + + +5VRV110kRH14k7 + +Wiper output + \ No newline at end of file diff --git a/examples/diode-variants.wire b/examples/diode-variants.wire new file mode 100644 index 0000000..a84b23c --- /dev/null +++ b/examples/diode-variants.wire @@ -0,0 +1,19 @@ +schematic + title "Diode variants" + description "A Schottky rectifier feeds a node clamped by a Zener, plus a photodiode sense leg." + + component PWR1 PowerFlag name=5V + component D1 SchottkyDiode + component D2 ZenerDiode + component D3 Photodiode + component R1 Resistor value=1k + component G1 GroundReference + component G2 GroundReference + + net VIN: PWR1.1, D1.A + net NODE: D1.C, D2.C, R1.1 + net GNDZ: D2.A, G1.GND + net SENSE: R1.2, D3.A + net GNDP: D3.C, G2.GND + + annotation "Zener clamp" near D2 diff --git a/examples/pot-divider.wire b/examples/pot-divider.wire new file mode 100644 index 0000000..d397eb0 --- /dev/null +++ b/examples/pot-divider.wire @@ -0,0 +1,14 @@ +schematic + title "Potentiometer voltage divider" + description "A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg." + + component PWR1 PowerFlag name=5V + component RV1 Potentiometer value=10k + component RH1 Rheostat value=4k7 + component G1 GroundReference + + net VCC: PWR1.1, RV1.1 + net OUT: RV1.W, RH1.1 + net GND: RV1.2, RH1.2, G1.GND + + annotation "Wiper output" near RV1 diff --git a/packages/core/src/layout/geometry.ts b/packages/core/src/layout/geometry.ts index b056ee8..cde2331 100644 --- a/packages/core/src/layout/geometry.ts +++ b/packages/core/src/layout/geometry.ts @@ -35,6 +35,11 @@ const TWO_TERM_CROSS = 22; const TRANSISTOR_MAIN = 54; const TRANSISTOR_CROSS = 44; +// Potentiometer: a two-terminal resistor body whose wiper taps the midpoint on a +// perpendicular stub. The wiper sits this far off the body centerline; the cross +// span is symmetric around the body so the box encloses the tap on either flow. +const POT_WIPER_REACH = 24; + // Single-terminal glyphs (ground, power flag, test point, antenna) hang off a // short stub; the terminal exits one end toward a rail. const SINGLE_TERM_MAIN = 36; @@ -59,6 +64,10 @@ const TWO_TERMINAL_SYMBOLS = new Set([ "inductor", "diode", "led", + "zener-diode", + "schottky-diode", + "photodiode", + "rheostat", "spst-switch", "push-button", "battery", @@ -111,6 +120,24 @@ export function componentGeometry(instance: ComponentInstance): ComponentGeom { }; } + if (symbol === "potentiometer") { + // Body runs end-to-end like a resistor; the wiper taps the midpoint on a + // perpendicular stub. Resolve the wiper by role so the two track ends keep + // their declared order regardless of which terminal is the tap. + const wiper = roleTerminal(instance, "wiper") ?? instance.terminals[1] ?? "W"; + const ends = instance.terminals.filter((name) => name !== wiper); + const [a, b] = ends.length >= 2 ? ends : instance.terminals; + return { + mainSpan: TWO_TERM_MAIN, + crossSpan: 2 * POT_WIPER_REACH, + terminals: [ + { name: a ?? "1", main: 0, cross: 0 }, + { name: b ?? "2", main: TWO_TERM_MAIN, cross: 0 }, + { name: wiper, main: TWO_TERM_MAIN / 2, cross: -POT_WIPER_REACH }, + ], + }; + } + if (TWO_TERMINAL_SYMBOLS.has(symbol) && instance.terminals.length >= 2) { const [a, b] = instance.terminals; return { diff --git a/packages/core/src/library/standard-library.ts b/packages/core/src/library/standard-library.ts index d2d5c1d..21a4557 100644 --- a/packages/core/src/library/standard-library.ts +++ b/packages/core/src/library/standard-library.ts @@ -48,6 +48,25 @@ const DEFINITIONS: readonly ComponentTypeDef[] = [ symbol: "inductor", designatorPrefixes: ["L"], }, + { + name: "Potentiometer", + // Three terminals: the two track ends (`1`, `2`) and the `W`iper tap. + terminals: ["1", "W", "2"], + properties: [{ name: "value", kind: "quantity", dimension: "resistance", recommended: true }], + defaultLabels: ["id", "value"], + symbol: "potentiometer", + roleMappings: [{ role: "wiper", terminal: "W" }], + designatorPrefixes: ["R", "RV", "VR"], + }, + { + name: "Rheostat", + // Two-terminal variable resistor: a resistor body with a wiper arrow. + terminals: ["1", "2"], + properties: [{ name: "value", kind: "quantity", dimension: "resistance", recommended: true }], + defaultLabels: ["id", "value"], + symbol: "rheostat", + designatorPrefixes: ["R", "RV", "VR", "RH"], + }, { name: "Diode", terminals: ["A", "C"], @@ -78,6 +97,42 @@ const DEFINITIONS: readonly ComponentTypeDef[] = [ ], designatorPrefixes: ["D", "LED"], }, + { + name: "ZenerDiode", + terminals: ["A", "C"], + properties: [], + defaultLabels: ["id"], + symbol: "zener-diode", + roleMappings: [ + { role: "anode", terminal: "A" }, + { role: "cathode", terminal: "C" }, + ], + designatorPrefixes: ["D", "ZD"], + }, + { + name: "SchottkyDiode", + terminals: ["A", "C"], + properties: [], + defaultLabels: ["id"], + symbol: "schottky-diode", + roleMappings: [ + { role: "anode", terminal: "A" }, + { role: "cathode", terminal: "C" }, + ], + designatorPrefixes: ["D", "SD"], + }, + { + name: "Photodiode", + terminals: ["A", "C"], + properties: [], + defaultLabels: ["id"], + symbol: "photodiode", + roleMappings: [ + { role: "anode", terminal: "A" }, + { role: "cathode", terminal: "C" }, + ], + designatorPrefixes: ["D", "PD"], + }, { name: "NPNTransistor", terminals: ["C", "B", "E"], diff --git a/packages/core/src/library/symbols.ts b/packages/core/src/library/symbols.ts index 80fd81d..3f51eb4 100644 --- a/packages/core/src/library/symbols.ts +++ b/packages/core/src/library/symbols.ts @@ -10,6 +10,11 @@ const SYMBOL_REQUIRED_ROLES: Record = { inductor: [], diode: ["anode", "cathode"], led: ["anode", "cathode"], + "zener-diode": ["anode", "cathode"], + "schottky-diode": ["anode", "cathode"], + photodiode: ["anode", "cathode"], + potentiometer: ["wiper"], + rheostat: [], "npn-transistor": ["collector", "base", "emitter"], "pnp-transistor": ["collector", "base", "emitter"], battery: ["positive", "negative"], diff --git a/packages/core/src/render/symbols.ts b/packages/core/src/render/symbols.ts index 9054bda..a8b16cd 100644 --- a/packages/core/src/render/symbols.ts +++ b/packages/core/src/render/symbols.ts @@ -224,26 +224,32 @@ function drawInductor(pen: Pen, length: number): string { return pen.wire({ points }); } -function drawDiode(pen: Pen, length: number, component: LayoutComponent, led: boolean): string { +const DIODE_TIP = 7; // half-length of the body: flat anode at -tip, point at +tip +const DIODE_HEIGHT = 16; // height of the body and the cathode bar + +/** Diode leads plus the filled triangle body, shared by every diode variant. */ +function drawDiodeBody(pen: Pen, length: number, fill: string): string { const center = length / 2; - const tipDistance = 7; // half-length of the body: flat anode at -tip, point at +tip - const bodyHeight = 16; // height of the body and the cathode bar - const fill = led ? (ledColor(component) ?? "#9ca3af") : "#1f2937"; - const parts = [ - pen.lead({ from: { along: 0, across: 0 }, to: { along: center - tipDistance, across: 0 } }), // anode lead - pen.lead({ - from: { along: center + tipDistance, across: 0 }, - to: { along: length, across: 0 }, - }), // cathode lead + return group( + pen.lead({ from: { along: 0, across: 0 }, to: { along: center - DIODE_TIP, across: 0 } }), // anode lead + pen.lead({ from: { along: center + DIODE_TIP, across: 0 }, to: { along: length, across: 0 } }), // cathode lead pen.triangle({ fill, points: [ - { along: center - tipDistance, across: -bodyHeight / 2 }, - { along: center - tipDistance, across: bodyHeight / 2 }, - { along: center + tipDistance, across: 0 }, + { along: center - DIODE_TIP, across: -DIODE_HEIGHT / 2 }, + { along: center - DIODE_TIP, across: DIODE_HEIGHT / 2 }, + { along: center + DIODE_TIP, across: 0 }, ], }), - pen.bar({ at: center + tipDistance, height: bodyHeight }), // cathode bar + ); +} + +function drawDiode(pen: Pen, length: number, component: LayoutComponent, led: boolean): string { + const center = length / 2; + const fill = led ? (ledColor(component) ?? "#9ca3af") : "#1f2937"; + const parts = [ + drawDiodeBody(pen, length, fill), + pen.bar({ at: center + DIODE_TIP, height: DIODE_HEIGHT }), // straight cathode bar ]; if (led) { // Two parallel "emitted light" arrows pointing away from the diode. @@ -258,6 +264,68 @@ function drawDiode(pen: Pen, length: number, component: LayoutComponent, led: bo return group(...parts); } +function drawZenerDiode(pen: Pen, length: number): string { + const center = length / 2; + const bar = center + DIODE_TIP; + const h = DIODE_HEIGHT / 2; + // Cathode bar with both ends bent back toward the anode (the Zener "lip"). + return group( + drawDiodeBody(pen, length, "#1f2937"), + pen.wire({ + points: [ + { along: bar - 4, across: -h }, + { along: bar, across: -h }, + { along: bar, across: h }, + { along: bar - 4, across: h }, + ], + }), + ); +} + +function drawSchottkyDiode(pen: Pen, length: number): string { + const center = length / 2; + const bar = center + DIODE_TIP; + const h = DIODE_HEIGHT / 2; + // Cathode bar with squared "S" hooks: a forward tick on top, a back tick below. + return group( + drawDiodeBody(pen, length, "#1f2937"), + pen.wire({ + points: [ + { along: bar + 4, across: -h + 4 }, + { along: bar + 4, across: -h }, + { along: bar, across: -h }, + { along: bar, across: h }, + { along: bar - 4, across: h }, + { along: bar - 4, across: h - 4 }, + ], + }), + ); +} + +function drawPhotodiode(pen: Pen, length: number): string { + const center = length / 2; + // A plain diode plus two arrows pointing *into* the body (incident light), + // the inverse of the LED's outward-emitting arrows. + return group( + drawDiodeBody(pen, length, "#1f2937"), + pen.bar({ at: center + DIODE_TIP, height: DIODE_HEIGHT }), + pen.arrow({ from: { along: center + 4, across: -16 }, to: { along: center, across: -10 } }), + pen.arrow({ from: { along: center + 8, across: -14 }, to: { along: center + 4, across: -8 } }), + ); +} + +function drawRheostat(pen: Pen, length: number): string { + const center = length / 2; + // A resistor body with a diagonal wiper arrow drawn across it. + return group( + drawResistor(pen, length), + pen.arrow({ + from: { along: center - 12, across: 13 }, + to: { along: center + 12, across: -13 }, + }), + ); +} + function drawBattery(pen: Pen, length: number): string { const center = length / 2; const tallPlate = 22; // long cell plate height (the + terminal) @@ -684,6 +752,28 @@ function drawModule(component: LayoutComponent): string { return parts.join(""); } +function drawPotentiometer(component: LayoutComponent): string { + // The body is a resistor between the two track ends; the wiper taps the + // midpoint with an arrow. Resolve the wiper by role, then draw the body in the + // end-to-end frame and the arrow in a fresh frame from the wiper to the body. + const wiperName = component.roleMappings.find((mapping) => mapping.role === "wiper")?.terminal; + const wiper = + component.terminals.find((terminal) => terminal.name === wiperName) ?? component.terminals[1]; + const ends = component.terminals.filter((terminal) => terminal !== wiper); + const [a, b] = ends.length >= 2 ? ends : component.terminals; + if (!a || !b) return ""; + const { frame, length } = makeFrame(a.point, b.point); + const body = drawResistor(makePen(frame), length); + if (!wiper) return body; + const bodyCenter = frame({ along: length / 2, across: 0 }); + const { frame: wiperFrame, length: wiperLength } = makeFrame(wiper.point, bodyCenter); + const arrow = makePen(wiperFrame).arrow({ + from: { along: 0, across: 0 }, + to: { along: Math.max(wiperLength - 9, wiperLength * 0.6), across: 0 }, + }); + return group(body, arrow); +} + function drawTwoTerminal(component: LayoutComponent): string | null { const [first, second] = component.terminals; if (!first || !second) return null; @@ -702,6 +792,14 @@ function drawTwoTerminal(component: LayoutComponent): string | null { return drawDiode(p, length, component, false); case "led": return drawDiode(p, length, component, true); + case "zener-diode": + return drawZenerDiode(p, length); + case "schottky-diode": + return drawSchottkyDiode(p, length); + case "photodiode": + return drawPhotodiode(p, length); + case "rheostat": + return drawRheostat(p, length); case "battery": return drawBattery(p, length); case "spst-switch": @@ -756,6 +854,9 @@ export function renderComponent(component: LayoutComponent): string { case "pnp-transistor": glyph = drawTransistor(component, true); break; + case "potentiometer": + glyph = drawPotentiometer(component); + break; case "ic": glyph = drawIc(component); break; diff --git a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap index ea6d4c5..c766a7e 100644 --- a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap +++ b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap @@ -21,3 +21,24 @@ exports[`parts catalog: snapshot > renders a mixed-parts schematic stably 1`] = " `; + +exports[`parts catalog: variable resistors & diode variants snapshot > renders the new discrete parts stably 1`] = ` +" +Variable resistors and diode variants +A schematic with 5 components. + + + +RV110kRH14k7D1D2D3 + +" +`; diff --git a/packages/core/test/parts-catalog.test.ts b/packages/core/test/parts-catalog.test.ts index 4ae096e..05b4fcc 100644 --- a/packages/core/test/parts-catalog.test.ts +++ b/packages/core/test/parts-catalog.test.ts @@ -15,6 +15,10 @@ describe("parts catalog: new discrete components", () => { { type: "TVSDiode", id: "D1", symbol: "tvs-diode", terminals: ["A", "C"] }, { type: "Speaker", id: "LS1", symbol: "speaker", terminals: ["+", "-"] }, { type: "PTC", id: "F1", symbol: "ptc", terminals: ["1", "2"] }, + { type: "Rheostat", id: "RV1", symbol: "rheostat", terminals: ["1", "2"] }, + { type: "ZenerDiode", id: "D2", symbol: "zener-diode", terminals: ["A", "C"] }, + { type: "SchottkyDiode", id: "D3", symbol: "schottky-diode", terminals: ["A", "C"] }, + { type: "Photodiode", id: "D4", symbol: "photodiode", terminals: ["A", "C"] }, ]; for (const part of twoTerminal) { @@ -83,6 +87,70 @@ describe("parts catalog: new discrete components", () => { }); }); +describe("parts catalog: potentiometer", () => { + const DIVIDER = `schematic + component RV1 Potentiometer value=10k + component R1 Resistor value=1k + component R2 Resistor value=2k + component R3 Resistor value=3k + net TOP: RV1.1, R1.1 + net WIP: RV1.W, R2.1 + net BOT: RV1.2, R3.1 + net RET: R1.2, R2.2, R3.2 +`; + + it("resolves three terminals with a wiper role mapping", () => { + const { model, ok } = compile(DIVIDER); + expect(ok).toBe(true); + const rv = model.components.find((c) => c.id === "RV1")!; + expect(rv.symbol).toBe("potentiometer"); + expect(rv.terminals).toEqual(["1", "W", "2"]); + expect(rv.roleMappings).toContainEqual({ role: "wiper", terminal: "W" }); + }); + + it("accepts the `wiper` role as a terminal alias on a net", () => { + const source = `schematic + component RV1 Potentiometer value=10k + component R1 Resistor value=1k + component R2 Resistor value=2k + component R3 Resistor value=3k + net TOP: RV1.1, R1.1 + net WIP: RV1.wiper, R2.1 + net BOT: RV1.2, R3.1 + net RET: R1.2, R2.2, R3.2 +`; + expect(errorCodes(source)).toEqual([]); + const { model } = compile(source); + const wip = model.nets.find((n) => n.name === "WIP")!; + expect(wip.members).toContainEqual({ component: "RV1", terminal: "W" }); + }); + + it("renders the potentiometer symbol without error", () => { + expect(() => renderSvg(DIVIDER)).not.toThrow(); + expect(renderSvg(DIVIDER)).toContain('data-wire-symbol="potentiometer"'); + }); +}); + +describe("parts catalog: variable resistors & diode variants snapshot", () => { + it("renders the new discrete parts stably", () => { + const svg = renderSvg(`schematic + title "Variable resistors and diode variants" + component RV1 Potentiometer value=10k + component RH1 Rheostat value=4k7 + component D1 ZenerDiode + component D2 SchottkyDiode + component D3 Photodiode + net A: RV1.1, RH1.1, D3.C + net W: RV1.W, D1.A + net B: RV1.2, D1.C + net C: RH1.2, D2.A + net D: D2.C, D3.A +`); + expect(svg.startsWith(" { const IC = `schematic component U1 IC pins=[1:VCC@left, 2:GND@left, 3:OUT@right] diff --git a/scripts/update-examples.mjs b/scripts/update-examples.mjs index cf37b77..d49e8a1 100644 --- a/scripts/update-examples.mjs +++ b/scripts/update-examples.mjs @@ -21,6 +21,8 @@ const EXAMPLES = [ ["examples/npn-led-driver.wire", "docs/assets/npn-led-driver"], ["examples/kitchen-sink.wire", "docs/assets/kitchen-sink"], ["examples/bus-rail.wire", "docs/assets/bus-rail"], + ["examples/pot-divider.wire", "docs/assets/pot-divider"], + ["examples/diode-variants.wire", "docs/assets/diode-variants"], ]; execFileSync("pnpm", ["--filter", "@wire-lang/core", "run", "build:js"], { diff --git a/skills/wire-lang/references/component-library.md b/skills/wire-lang/references/component-library.md index 955db9d..2f067c7 100644 --- a/skills/wire-lang/references/component-library.md +++ b/skills/wire-lang/references/component-library.md @@ -9,8 +9,13 @@ Lang source. | `Capacitor` | `1`, `2` | recommended `capacitance` | `id`, `capacitance` | | `PolarizedCapacitor` | `+`, `-` | recommended `capacitance` | `id`, `capacitance` | | `Inductor` | `1`, `2` | recommended `inductance` | `id`, `inductance` | +| `Potentiometer` | `1`, `W`, `2` | recommended `value` | `id`, `value` | +| `Rheostat` | `1`, `2` | recommended `value` | `id`, `value` | | `Diode` | `A`, `C` | none | `id` | | `LED` | `A`, `C` | optional `color` | `id` | +| `ZenerDiode` | `A`, `C` | none | `id` | +| `SchottkyDiode` | `A`, `C` | none | `id` | +| `Photodiode` | `A`, `C` | none | `id` | | `NPNTransistor` | `C`, `B`, `E` | none | `id` | | `PNPTransistor` | `C`, `B`, `E` | none | `id` | | `Battery` | `+`, `-` | recommended `voltage` | `id`, `voltage` | @@ -30,10 +35,21 @@ Lang source. `PTC` is the resettable-fuse / polyfuse variant. `PowerFlag` draws its `name` (e.g. `5V`, `3V3`, `VBAT`) as a rail flag and is not a hidden global net. +`Potentiometer` is a three-terminal variable resistor: the two track ends `1` +and `2` are interchangeable and the `W` wiper taps the middle (connect it by name +or via the `wiper` role alias, e.g. `net OUT: RV1.W, ...`). `Rheostat` is the +two-terminal form. `ZenerDiode`, `SchottkyDiode`, and `Photodiode` reuse the +`Diode` `A`/`C` terminals. + ## Property Examples ```wire component R1 Resistor value=10k +component RV1 Potentiometer value=10k +component RH1 Rheostat value=4k7 +component D2 ZenerDiode +component D3 SchottkyDiode +component D4 Photodiode component C1 Capacitor capacitance=100nF component BT1 Battery voltage=5V component D1 LED color=red From ae5795c644fa94c5c29cb39a1e2735cf6b56b1b1 Mon Sep 17 00:00:00 2001 From: Ferraz Date: Mon, 29 Jun 2026 23:17:47 -0300 Subject: [PATCH 2/4] fix(render): use the canonical Z-bar for the Zener cathode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Zener used a non-standard bracket-shaped cathode bar (both ends bent toward the anode) to avoid resembling the existing TVS mark. That reads as wrong. Use the canonical Zener "Z" bar instead — top end bent toward the anode, bottom toward the cathode. A unidirectional TVS shares this standard mark by design; the bidirectional TVS remains the two-triangle form. --- docs/assets/diode-variants.png | Bin 11331 -> 11343 bytes docs/assets/diode-variants.svg | 2 +- packages/core/src/render/symbols.ts | 6 ++++-- .../__snapshots__/parts-catalog.test.ts.snap | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/assets/diode-variants.png b/docs/assets/diode-variants.png index f621f6698e023ba5e3f69c0e8d094a7e7f71e6d2..fec39f46c91635225d4d8a9e1c9fad2bcf015ea1 100644 GIT binary patch literal 11343 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 literal 11331 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%0DQAh?AD!z1xE$9nCf}^Th3Y|Ix!HrvFcGz?QUx zp4LNfADAQZ+4BvIsN88Syk0?*f!yBz)KC6zB2=v#dKWmmaB>e9GbXaEgPhq9?#4@B z7Eu;9LjCD|iSkooPL^^qym|J_LW{73w3hSy$ddGBT$}$+8TLi}0!2VHfwo`J6gUh? zM|S~{wXJ?{_;v<1JOvWTagC2pdM{bW)eLt6%Cwyng!k$Hb?cT;{RPusA@TnYbKdxl zvuA`9uzBonzoE)D&*kycP?!t%#1XZ=pIm%ax82Ci;$|~$M}o&0npWlO^fv3diPdiQ zMDGNA{yMT=wYUaC9Aq9&-=_Wc`PruAwg?>P^&I_z)5>p{e<(F;pAKUm*<QQt;dQF%CP2{1|kyAH6ZHfyAqU49cW;On&21;-hC z6?; Zpn=rNoRBu)-CDjYm(4DfUA+0={{Z?e0 -5VD1D2D3R11k +5VD1D2D3R11k Zener clamp \ No newline at end of file diff --git a/packages/core/src/render/symbols.ts b/packages/core/src/render/symbols.ts index a8b16cd..b7d7b3a 100644 --- a/packages/core/src/render/symbols.ts +++ b/packages/core/src/render/symbols.ts @@ -268,7 +268,9 @@ function drawZenerDiode(pen: Pen, length: number): string { const center = length / 2; const bar = center + DIODE_TIP; const h = DIODE_HEIGHT / 2; - // Cathode bar with both ends bent back toward the anode (the Zener "lip"). + // Canonical Zener cathode bar: a "Z" with the top end bent back toward the + // anode and the bottom end bent forward toward the cathode. (A unidirectional + // TVS shares this standard mark; the bidirectional TVS is the two-triangle form.) return group( drawDiodeBody(pen, length, "#1f2937"), pen.wire({ @@ -276,7 +278,7 @@ function drawZenerDiode(pen: Pen, length: number): string { { along: bar - 4, across: -h }, { along: bar, across: -h }, { along: bar, across: h }, - { along: bar - 4, across: h }, + { along: bar + 4, across: h }, ], }), ); diff --git a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap index c766a7e..4af283a 100644 --- a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap +++ b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap @@ -38,7 +38,7 @@ exports[`parts catalog: variable resistors & diode variants snapshot > renders t .wire-annotation { fill: #6b7280; font: italic 10px var(--wire-font, system-ui, sans-serif); } -RV110kRH14k7D1D2D3 +RV110kRH14k7D1D2D3 " `; From d3a18282fc30bc9e521e61a9159d936069573838 Mon Sep 17 00:00:00 2001 From: Ferraz Date: Sun, 6 Sep 2026 18:12:13 -0300 Subject: [PATCH 3/4] fix: clear potentiometer labels and connect wiper to track --- docs/MVP.md | 4 +- docs/assets/diode-variants.svg | 9 +- docs/assets/pot-divider.svg | 5 +- docs/reviews/pr-67-v031.md | 46 +++++++++- examples/diode-variants.wire | 2 - examples/pot-divider.wire | 2 - packages/core/src/render/symbols.ts | 24 +++++- .../__snapshots__/parts-catalog.test.ts.snap | 2 +- packages/core/test/issue-67.test.ts | 85 +++++++++++++++++++ .../wire-lang/references/component-library.md | 7 +- 10 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 packages/core/test/issue-67.test.ts diff --git a/docs/MVP.md b/docs/MVP.md index f5ef3af..f19ade4 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -306,7 +306,9 @@ Designator prefixes: `Potentiometer` → `R`/`RV`/`VR`, `Rheostat` → `Potentiometer` maps `wiper`→`W` (the two track ends `1`/`2` are interchangeable); `ZenerDiode`, `SchottkyDiode`, and `Photodiode` map `anode`→`A` -and `cathode`→`C` like `Diode`. `TVSDiode` maps `anode`→`A` and `cathode`→`C`; +and `cathode`→`C` like `Diode`. Potentiometer labels sit opposite the wiper, +using final terminal coordinates so orientation and direction hints preserve +clearance from the terminal paths. The wiper arrow touches the track midpoint. `TVSDiode` maps `anode`→`A` and `cathode`→`C`; `Speaker` maps `positive`→`+` and `negative`→`-`. `PowerFlag` draws its `name` (e.g. `VBAT`, `5V`, `3V3`, `VCC`) inside the flag glyph; it is a visual rail flag only and does not create a hidden global net. diff --git a/docs/assets/diode-variants.svg b/docs/assets/diode-variants.svg index fcf0573..d6c4ff3 100644 --- a/docs/assets/diode-variants.svg +++ b/docs/assets/diode-variants.svg @@ -1,4 +1,4 @@ - + Diode variants A Schottky rectifier feeds a node clamped by a Zener, plus a photodiode sense leg. - -5VD1D2D3R11k - -Zener clamp + +5VD1D2D3R11k + \ No newline at end of file diff --git a/docs/assets/pot-divider.svg b/docs/assets/pot-divider.svg index ff3225a..9da2357 100644 --- a/docs/assets/pot-divider.svg +++ b/docs/assets/pot-divider.svg @@ -1,4 +1,4 @@ - + Potentiometer voltage divider A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg. -5VRV110kRH14k7 +5VRV110kRH14k7 -Wiper output \ No newline at end of file diff --git a/docs/reviews/pr-67-v031.md b/docs/reviews/pr-67-v031.md index 269db25..c421948 100644 --- a/docs/reviews/pr-67-v031.md +++ b/docs/reviews/pr-67-v031.md @@ -1,4 +1,48 @@ -# PR #67: 0.3.1 overlap investigation +# PR #67: overlap fix and 0.3.1 investigation + +## Follow-up fix + +The PR now places potentiometer labels on the side opposite the wiper using +final terminal coordinates, and extends the wiper arrow to the resistor track +midpoint. Both changes follow orientation and direction hints. The gallery +examples omit the redundant `Wiper output` and `Zener clamp` annotations that +intersected wires. General annotation collision avoidance remains unchanged. + +The new `issue-67.test.ts` checks label rectangles against actual net segments, +checks that labels stay inside the canvas, and verifies that the rendered arrow +tip lies on a rendered resistor segment. Before the fix, 12 of its 16 cases failed. +All 16 now pass across four directions and two orientations. All 170 project +tests pass, as do typecheck and lint. The existing Diode control is unchanged. + +The comparisons below under “Original investigation” are historical evidence +that merging 0.3.1 alone did not fix these problems. Current fix images live in +`docs/assets/pr-67-v031/fix/` on `demo/pr-67-v031-assets`. + +### Potentiometer divider + +Same source on pre-fix PR `b4578b2` and the fixed renderer. + +| Before fix | After fix | +| ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| ![Before](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/pot-divider-before.png) | ![After](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/pot-divider-after.png) | + +### Vertical potentiometer + +Same source on pre-fix PR `b4578b2` and the fixed renderer. + +| Before fix | After fix | +| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| ![Before](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/pot-vertical-before.png) | ![After](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/pot-vertical-after.png) | + +### Component catalog + +Same source on pre-fix PR `b4578b2` and the fixed renderer. + +| Before fix | After fix | +| ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| ![Before](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/catalog-before.png) | ![After](https://raw.githubusercontent.com/eduardozf/wire-lang/demo/pr-67-v031-assets/docs/assets/pr-67-v031/fix/catalog-after.png) | + +## Original investigation Release 0.3.1 does not fix the visible collisions in PR #67's new components. The four component PNGs are byte-identical before and after merging the release. diff --git a/examples/diode-variants.wire b/examples/diode-variants.wire index a84b23c..6fc07a2 100644 --- a/examples/diode-variants.wire +++ b/examples/diode-variants.wire @@ -15,5 +15,3 @@ schematic net GNDZ: D2.A, G1.GND net SENSE: R1.2, D3.A net GNDP: D3.C, G2.GND - - annotation "Zener clamp" near D2 diff --git a/examples/pot-divider.wire b/examples/pot-divider.wire index d397eb0..6ac22c8 100644 --- a/examples/pot-divider.wire +++ b/examples/pot-divider.wire @@ -10,5 +10,3 @@ schematic net VCC: PWR1.1, RV1.1 net OUT: RV1.W, RH1.1 net GND: RV1.2, RH1.2, G1.GND - - annotation "Wiper output" near RV1 diff --git a/packages/core/src/render/symbols.ts b/packages/core/src/render/symbols.ts index dadf8ec..46e64c7 100644 --- a/packages/core/src/render/symbols.ts +++ b/packages/core/src/render/symbols.ts @@ -779,7 +779,7 @@ function drawPotentiometer(component: LayoutComponent): string { const { frame: wiperFrame, length: wiperLength } = makeFrame(wiper.point, bodyCenter); const arrow = makePen(wiperFrame).arrow({ from: { along: 0, across: 0 }, - to: { along: Math.max(wiperLength - 9, wiperLength * 0.6), across: 0 }, + to: { along: wiperLength, across: 0 }, }); return group(body, arrow); } @@ -833,6 +833,28 @@ function drawLabels(component: LayoutComponent): string { const labels = component.labels.filter((label) => label !== ""); if (labels.length === 0) return ""; const parts: string[] = []; + if (component.symbol === "potentiometer") { + const wiperName = component.roleMappings.find((mapping) => mapping.role === "wiper")?.terminal; + const wiper = component.terminals.find((terminal) => terminal.name === wiperName); + if (wiper) { + // Keep text opposite the wiper, beside the track rather than in any of + // the three terminal paths. Use final coordinates so hints rotate it. + const dx = wiper.point.x - component.center.x; + const dy = wiper.point.y - component.center.y; + const vertical = Math.abs(dx) > Math.abs(dy); + const side = (vertical ? dx : dy) < 0 ? 1 : -1; + const x = component.center.x + (vertical ? side * 18 : 0); + const anchor = vertical ? (side > 0 ? "start" : "end") : "middle"; + let y = vertical + ? component.center.y + 4 - ((labels.length - 1) * 13) / 2 + : component.center.y + (side > 0 ? 22 : -16); + for (const label of labels) { + parts.push(text(label, { x, y }, anchor, "wire-label")); + y += vertical ? 13 : side * 13; + } + return parts.join(""); + } + } // A vertical two-terminal part has wires entering top and bottom, so labels // above would sit in the wire's path; put them beside the body instead. const [first, second] = component.terminals; diff --git a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap index 3ac60d8..078b0d8 100644 --- a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap +++ b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap @@ -38,7 +38,7 @@ exports[`parts catalog: variable resistors & diode variants snapshot > renders t .wire-annotation { fill: #6b7280; font: italic 10px var(--wire-font, system-ui, sans-serif); } -RV110kRH14k7D1D2D3 +RV110kRH14k7D1D2D3 " `; diff --git a/packages/core/test/issue-67.test.ts b/packages/core/test/issue-67.test.ts new file mode 100644 index 0000000..ee57e5b --- /dev/null +++ b/packages/core/test/issue-67.test.ts @@ -0,0 +1,85 @@ +import { readFileSync } from "node:fs"; +import { compile, layout, renderSvg } from "@wire-lang/core"; +import { describe, expect, it } from "vitest"; + +const divider = readFileSync( + new URL("../../../examples/pot-divider.wire", import.meta.url), + "utf8", +); + +function coordinates(value: string): number[] { + return value.match(/-?\d+(?:\.\d+)?/g)!.map(Number); +} + +describe("issue #67: potentiometer contact and labels", () => { + for (const direction of ["left-to-right", "right-to-left", "top-to-bottom", "bottom-to-top"]) { + for (const orientation of ["horizontal", "vertical"]) { + const source = `${divider}\nrender direction=${direction}\nrender RV1 orientation=${orientation}\n`; + const name = `${direction}, ${orientation}`; + + it(`keeps labels clear of connections: ${name}`, () => { + const model = layout(compile(source).model); + const svg = renderSvg(source); + const component = svg.match( + /]*data-wire-id="RV1"[^>]*>(.*?)<\/g>/s, + )![1]!; + const labels = [ + ...component.matchAll( + /([^<]+)<\/text>/g, + ), + ]; + expect(labels).toHaveLength(2); + for (const [, x, y, anchor, text] of labels) { + const width = text!.length * 7; + const left = Number(x) - (anchor === "middle" ? width / 2 : anchor === "end" ? width : 0); + const box = { + left: left - 2, + right: left + width + 2, + top: Number(y) - 11, + bottom: Number(y) + 2, + }; + for (const wire of model.wires) { + for (const { from, to } of wire.segments) { + const intersects = + Math.max(from.x, to.x) > box.left && + Math.min(from.x, to.x) < box.right && + Math.max(from.y, to.y) > box.top && + Math.min(from.y, to.y) < box.bottom; + expect(intersects, `${text} intersects ${wire.net}`).toBe(false); + } + } + expect(box.left).toBeGreaterThanOrEqual(0); + expect(box.right).toBeLessThanOrEqual(model.size.width); + expect(box.top).toBeGreaterThanOrEqual(0); + expect(box.bottom).toBeLessThanOrEqual(model.size.height); + } + }); + + it(`puts the arrow tip on the resistor track: ${name}`, () => { + const svg = renderSvg(source); + const component = svg.match( + /]*data-wire-id="RV1"[^>]*>(.*?)<\/g>/s, + )![1]!; + const track = coordinates(component.match(/]* d="([^"]+)"/)![1]!); + const [x, y] = coordinates(component.match(/]* points="([^"]+)"/)![1]!); + let touches = false; + for (let i = 2; i < track.length; i += 2) { + const ax = track[i - 2]!; + const ay = track[i - 1]!; + const bx = track[i]!; + const by = track[i + 1]!; + const cross = (x! - ax) * (by - ay) - (y! - ay) * (bx - ax); + if ( + Math.abs(cross) < 0.01 && + x! >= Math.min(ax, bx) && + x! <= Math.max(ax, bx) && + y! >= Math.min(ay, by) && + y! <= Math.max(ay, by) + ) + touches = true; + } + expect(touches).toBe(true); + }); + } + } +}); diff --git a/skills/wire-lang/references/component-library.md b/skills/wire-lang/references/component-library.md index 2f067c7..7d81b02 100644 --- a/skills/wire-lang/references/component-library.md +++ b/skills/wire-lang/references/component-library.md @@ -38,7 +38,12 @@ Lang source. `Potentiometer` is a three-terminal variable resistor: the two track ends `1` and `2` are interchangeable and the `W` wiper taps the middle (connect it by name or via the `wiper` role alias, e.g. `net OUT: RV1.W, ...`). `Rheostat` is the -two-terminal form. `ZenerDiode`, `SchottkyDiode`, and `Photodiode` reuse the +two-terminal form. Potentiometer labels are placed opposite the wiper and follow +orientation hints. The arrow touches the resistor track. Avoid redundant +annotations beside these parts when the component and net names already explain +the circuit; annotations are not automatically routed around wires. + +`ZenerDiode`, `SchottkyDiode`, and `Photodiode` reuse the `Diode` `A`/`C` terminals. ## Property Examples From 753539f29af37f8505efed3da4da2df1d1d5f48e Mon Sep 17 00:00:00 2001 From: Ferraz Date: Sun, 6 Sep 2026 19:29:26 -0300 Subject: [PATCH 4/4] fix: correct example circuits and simplify discrete wiring --- docs/EXAMPLES.md | 2 +- docs/MVP.md | 16 +++- docs/assets/diode-variants.svg | 13 +-- docs/assets/pot-divider.svg | 14 +-- docs/reviews/pr-67-v031.md | 23 +++++ examples/diode-variants.wire | 24 +++-- examples/pot-divider.wire | 12 ++- packages/core/src/layout/engine.ts | 92 ++++++++++++++++++- packages/core/src/layout/geometry.ts | 10 +- packages/core/src/library/standard-library.ts | 4 +- packages/core/src/render/symbols.ts | 3 +- .../__snapshots__/parts-catalog.test.ts.snap | 8 +- .../render-features.test.ts.snap | 8 +- packages/core/test/ground-routing.test.ts | 27 ++++++ packages/core/test/issue-67.test.ts | 19 +++- packages/core/test/pr-67-examples.test.ts | 43 +++++++++ .../wire-lang/references/component-library.md | 13 ++- 17 files changed, 280 insertions(+), 51 deletions(-) create mode 100644 packages/core/test/ground-routing.test.ts create mode 100644 packages/core/test/pr-67-examples.test.ts diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index f0e552a..552ac7c 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -53,7 +53,7 @@ Source: [examples/bus-rail.wire](../examples/bus-rail.wire) Source: [examples/pot-divider.wire](../examples/pot-divider.wire) -![Potentiometer voltage divider](./assets/pot-divider.svg) +![Loaded potentiometer divider](./assets/pot-divider.svg) ## Diode Variants diff --git a/docs/MVP.md b/docs/MVP.md index f19ade4..f3146d0 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -279,7 +279,7 @@ The MVP ships a small standard component library. | `Rheostat` | `1`, `2` | recommended `value: resistance` | `id`, `value` | `rheostat` | | `Diode` | `A`, `C` | none | `id` | `diode` | | `LED` | `A`, `C` | optional `color: enum(red, green, blue, yellow, white, amber)` | `id` | `led` | -| `ZenerDiode` | `A`, `C` | none | `id` | `zener-diode` | +| `ZenerDiode` | `A`, `C` | optional `voltage: voltage` | `id`, `voltage` | `zener-diode` | | `SchottkyDiode` | `A`, `C` | none | `id` | `schottky-diode` | | `Photodiode` | `A`, `C` | none | `id` | `photodiode` | | `NPNTransistor` | `C`, `B`, `E` | none | `id` | `npn-transistor` | @@ -718,3 +718,17 @@ Later extensions: - [ADR 0016](./adr/0016-browser-auto-render-post-mvp.md) records the browser auto-render scope decision. - [ADR 0017](./adr/0017-esm-only-node-20.md) records the runtime and module-format decision. - [ADR 0019](./adr/0019-build-time-markdown-integration.md) records the original static Markdown integration, superseded in part by ADR 0020. + +### Discrete branch routing + +In horizontal flows, vertical two-terminal parts can flip to put earlier +connections above later connections, with explicit power and ground symbols +taking priority. Potentiometers can mirror to face their connected load. +Adjacent facing terminals on the same axis connect directly when no component +lies between them. Ground connections routed to a lower rail first leave the +terminal outward and pass around the symbol bars. + +The diode gallery uses a series resistor ahead of a Zener bias rail, a +reverse-biased photodiode, and one explicit shared GND net. `ZenerDiode` accepts +an optional `voltage` quantity and displays it. The divider gallery shows a +rheostat loading the wiper output, with ground connections represented by labels. diff --git a/docs/assets/diode-variants.svg b/docs/assets/diode-variants.svg index d6c4ff3..708d97c 100644 --- a/docs/assets/diode-variants.svg +++ b/docs/assets/diode-variants.svg @@ -1,6 +1,6 @@ - + Diode variants -A Schottky rectifier feeds a node clamped by a Zener, plus a photodiode sense leg. +A Schottky-protected 5V input feeds a current-limited 3.3V Zener rail and a reverse-biased photodiode with a sense resistor. - - -5VD1D2D3R11k - + + +5VD1R11kD23.3VD3R2100k + +SENSESENSEGNDGNDGND \ No newline at end of file diff --git a/docs/assets/pot-divider.svg b/docs/assets/pot-divider.svg index 9da2357..d3033aa 100644 --- a/docs/assets/pot-divider.svg +++ b/docs/assets/pot-divider.svg @@ -1,6 +1,6 @@ - -Potentiometer voltage divider -A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg. + +Loaded potentiometer divider +A 10k potentiometer divides 5V; a 4k7 rheostat loads its OUT node to ground. - - -5VRV110kRH14k7 - + + +5VRV110kRH14k7 +GNDGNDGNDOUT \ No newline at end of file diff --git a/docs/reviews/pr-67-v031.md b/docs/reviews/pr-67-v031.md index c421948..0b55013 100644 --- a/docs/reviews/pr-67-v031.md +++ b/docs/reviews/pr-67-v031.md @@ -1,5 +1,28 @@ # PR #67: overlap fix and 0.3.1 investigation +## Current electrical and routing corrections + +The five follow-up review findings are now addressed. The diode gallery places +1 kΩ ahead of the shared 3.3 V Zener bias, connects the photodiode cathode to +that bias, and senses its anode through 100 kΩ to the same explicit GND net. +The divider description identifies the rheostat as an output load. Its vertical +potentiometer faces the load and `OUT` is visible. + +Both galleries use named ground connections to remove long return loops. +Adjacent facing terminals now connect directly. Vertical branches can flip +using connection order and explicit power/ground references. Lower-rail ground +connections leave the terminal outward and pass beside the bars. Vertical optical +component labels reserve room for the light arrows. + +All 173 tests pass, including the retained original collision fixture, a ground +approach regression, and checks of the example net memberships, polarity, +straight connection, visible output label, and absence of collinear overlaps or +foreign-terminal hits. Typecheck, lint, docs formatting, build, and example CLI +checks pass. The render comparisons for this revision are in `realistic/` on +the `demo/pr-67-v031-assets` branch. + +The sections below record earlier investigation and fix stages. + ## Follow-up fix The PR now places potentiometer labels on the side opposite the wiper using diff --git a/examples/diode-variants.wire b/examples/diode-variants.wire index 6fc07a2..221f1f6 100644 --- a/examples/diode-variants.wire +++ b/examples/diode-variants.wire @@ -1,17 +1,21 @@ schematic title "Diode variants" - description "A Schottky rectifier feeds a node clamped by a Zener, plus a photodiode sense leg." - + description "A Schottky-protected 5V input feeds a current-limited 3.3V Zener rail and a reverse-biased photodiode with a sense resistor." component PWR1 PowerFlag name=5V component D1 SchottkyDiode - component D2 ZenerDiode - component D3 Photodiode component R1 Resistor value=1k + component D2 ZenerDiode voltage=3.3V + component D3 Photodiode + component R2 Resistor value=100k component G1 GroundReference - component G2 GroundReference + net 5V: PWR1.1, D1.A + net LIMITED: D1.C, R1.1 + net BIAS: R1.2, D2.C, D3.C + net SENSE: D3.A, R2.1 + net GND: D2.A, R2.2, G1.GND + render D2 orientation=vertical + render D3 orientation=vertical + render R2 orientation=vertical - net VIN: PWR1.1, D1.A - net NODE: D1.C, D2.C, R1.1 - net GNDZ: D2.A, G1.GND - net SENSE: R1.2, D3.A - net GNDP: D3.C, G2.GND + render net GND style=label + render net SENSE style=label diff --git a/examples/pot-divider.wire b/examples/pot-divider.wire index 6ac22c8..165a6b0 100644 --- a/examples/pot-divider.wire +++ b/examples/pot-divider.wire @@ -1,12 +1,14 @@ schematic - title "Potentiometer voltage divider" - description "A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg." - + title "Loaded potentiometer divider" + description "A 10k potentiometer divides 5V; a 4k7 rheostat loads its OUT node to ground." component PWR1 PowerFlag name=5V component RV1 Potentiometer value=10k component RH1 Rheostat value=4k7 component G1 GroundReference - - net VCC: PWR1.1, RV1.1 + net 5V: PWR1.1, RV1.1 net OUT: RV1.W, RH1.1 net GND: RV1.2, RH1.2, G1.GND + render RV1 orientation=vertical + render RH1 orientation=vertical + render net GND style=label + annotation "OUT" near net OUT diff --git a/packages/core/src/layout/engine.ts b/packages/core/src/layout/engine.ts index 1c052e4..c738fc9 100644 --- a/packages/core/src/layout/engine.ts +++ b/packages/core/src/layout/engine.ts @@ -35,6 +35,7 @@ interface MC { /** A net member's terminal, tagged with its body center so drops can fan away from it. */ interface DropPoint extends MC { + component: string; centerMain: number; } @@ -55,6 +56,7 @@ interface Drop { railCross: number; centerMain: number; lane: number; + escapeCross?: number; } /** A multi-terminal net awaiting rail-track assignment and segment emission. */ @@ -192,6 +194,44 @@ export function layout(model: SchematicModel): LayoutModel { }; const mirrored = mirrorGeometry(entry.geom); if (cost(mirrored) < cost(entry.geom)) entry.geom = mirrored; + // In horizontal flows, vertical two-terminal branches read top to bottom: + // earlier partners feed the top and later partners receive the bottom. + // Explicit ground/power glyphs take precedence over declaration order. + if ( + !vertical && + entry.geom.terminals.length === 2 && + entry.geom.terminals[0]!.main === entry.geom.terminals[1]!.main + ) { + const crossCost = (geom: ComponentGeom): number => { + let total = 0; + for (const net of model.nets) { + const own = net.members.find((member) => member.component === entry.instance.id); + const terminal = geom.terminals.find((term) => term.name === own?.terminal); + if (!terminal) continue; + const ground = net.members.some( + (member) => placedById.get(member.component)?.instance.symbol === "ground-reference", + ); + const supply = net.members.some( + (member) => placedById.get(member.component)?.instance.symbol === "power-flag", + ); + if (ground || supply) { + total += (ground ? -terminal.cross : terminal.cross) * model.components.length; + continue; + } + for (const partner of net.members) { + if (partner.component === entry.instance.id) continue; + const other = partnerMain(partner.component, partner.terminal); + if (other !== undefined) total += terminal.cross * (other < entry.centerMain ? 1 : -1); + } + } + return total; + }; + const flipped = { + ...entry.geom, + terminals: entry.geom.terminals.map((term) => ({ ...term, cross: -term.cross })), + }; + if (crossCost(flipped) < crossCost(entry.geom)) entry.geom = flipped; + } } for (const entry of placed) { @@ -282,7 +322,11 @@ export function layout(model: SchematicModel): LayoutModel { .map((member): DropPoint | undefined => { const point = terminalPoints.get(`${member.component}.${member.terminal}`); if (!point) return undefined; - return { ...point, centerMain: centerByComponent.get(member.component) ?? point.main }; + return { + ...point, + component: member.component, + centerMain: centerByComponent.get(member.component) ?? point.main, + }; }) .filter((point): point is DropPoint => point !== undefined); if (points.length === 0) continue; @@ -317,6 +361,27 @@ export function layout(model: SchematicModel): LayoutModel { continue; } + // Adjacent facing terminals on one axis need only a straight connection. + const [left, right] = [...points].sort((a, b) => a.main - b.main); + if ( + points.length === 2 && + left && + right && + left.cross === right.cross && + left.main > left.centerMain && + right.main < right.centerMain && + !placed.some((entry) => entry.centerMain > left.main && entry.centerMain < right.main) + ) { + wires.push({ + net: net.name, + anonymous: net.anonymous, + style: "wire", + segments: [{ from: left, to: right }], + junctions: [], + }); + continue; + } + const avg = points.reduce((sum, point) => sum + point.cross, 0) / points.length; const side: "top" | "bottom" = avg < 0 ? "top" : "bottom"; const railCross = side === "top" ? topProvisional : bottomProvisional; @@ -326,12 +391,22 @@ export function layout(model: SchematicModel): LayoutModel { railCross, centerMain: point.centerMain, lane: 0, + escapeCross: + placedById.get(point.component)?.instance.symbol === "ground-reference" && + point.cross < 0 && + railCross > point.cross + ? point.cross - STUB + : undefined, })); pendingNets.push({ net, side, railCross, minMain: 0, maxMain: 0, drops }); allDrops.push(...drops); } assignDropLanes(allDrops); + // Leave the ground terminal outward before passing around its bars. + for (const drop of allDrops) { + if (drop.escapeCross !== undefined) drop.lane = Math.max(24, drop.lane); + } // With lanes fixed, record each rail's true main extent, then pack the rails // into shared tracks per side so unrelated nets stop stacking up the margins. @@ -356,6 +431,21 @@ export function layout(model: SchematicModel): LayoutModel { } for (const drop of drops) { const lm = laneMain(drop); + if (drop.escapeCross !== undefined) { + segments.push( + { + from: { main: drop.main, cross: drop.cross }, + to: { main: drop.main, cross: drop.escapeCross }, + }, + { + from: { main: drop.main, cross: drop.escapeCross }, + to: { main: lm, cross: drop.escapeCross }, + }, + { from: { main: lm, cross: drop.escapeCross }, to: { main: lm, cross: railCross } }, + ); + if (lm > minMain && lm < maxMain) junctions.push({ main: lm, cross: railCross }); + continue; + } // Step sideways into the assigned lane before running down to the rail. if (drop.lane !== 0) { segments.push({ diff --git a/packages/core/src/layout/geometry.ts b/packages/core/src/layout/geometry.ts index debe8a6..cc92ed3 100644 --- a/packages/core/src/layout/geometry.ts +++ b/packages/core/src/layout/geometry.ts @@ -102,13 +102,17 @@ const SINGLE_TERMINAL_SYMBOLS = new Set([ ]); /** - * Symbols the layout may mirror to face a wire. Only two-terminal parts qualify: + * Symbols the layout may mirror to face a wire. Two-terminal parts qualify: * their glyphs draw between the terminal points, so swapped terminals mirror the * drawing for free, and a reversed diode/LED/battery is legitimate schematic - * practice. Modules and ICs draw from `side` and never mirror; transistors draw + * practice. Potentiometers also draw from role positions, so the wiper can face + * its load while the interchangeable track ends swap. Modules and ICs draw from `side` and never mirror; transistors draw * from role positions and are excluded for the same reason. */ -export const MIRRORABLE_SYMBOLS: ReadonlySet = TWO_TERMINAL_SYMBOLS; +export const MIRRORABLE_SYMBOLS: ReadonlySet = new Set([ + ...TWO_TERMINAL_SYMBOLS, + "potentiometer", +]); export function isTwoTerminalSymbol(symbol: string): boolean { return TWO_TERMINAL_SYMBOLS.has(symbol); diff --git a/packages/core/src/library/standard-library.ts b/packages/core/src/library/standard-library.ts index 21a4557..a7d0223 100644 --- a/packages/core/src/library/standard-library.ts +++ b/packages/core/src/library/standard-library.ts @@ -100,8 +100,8 @@ const DEFINITIONS: readonly ComponentTypeDef[] = [ { name: "ZenerDiode", terminals: ["A", "C"], - properties: [], - defaultLabels: ["id"], + properties: [{ name: "voltage", kind: "quantity", dimension: "voltage" }], + defaultLabels: ["id", "voltage"], symbol: "zener-diode", roleMappings: [ { role: "anode", terminal: "A" }, diff --git a/packages/core/src/render/symbols.ts b/packages/core/src/render/symbols.ts index 46e64c7..533532d 100644 --- a/packages/core/src/render/symbols.ts +++ b/packages/core/src/render/symbols.ts @@ -864,7 +864,8 @@ function drawLabels(component: LayoutComponent): string { second && Math.abs(second.point.y - first.point.y) > Math.abs(second.point.x - first.point.x); if (vertical) { - const x = component.position.x + component.size.width + 6; + const optical = component.symbol === "photodiode" || component.symbol === "led"; + const x = component.position.x + component.size.width + (optical ? 18 : 6); let y = component.center.y + 4 - ((labels.length - 1) * 13) / 2; for (const label of labels) { parts.push( diff --git a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap index 078b0d8..dbd6abf 100644 --- a/packages/core/test/__snapshots__/parts-catalog.test.ts.snap +++ b/packages/core/test/__snapshots__/parts-catalog.test.ts.snap @@ -15,7 +15,7 @@ exports[`parts catalog: snapshot > renders a mixed-parts schematic stably 1`] = .wire-net-label { fill: #2563eb; font: 600 10px var(--wire-font, system-ui, sans-serif); } .wire-annotation { fill: #6b7280; font: italic 10px var(--wire-font, system-ui, sans-serif); } - + VCC1GND2OUT3EN4NC5U1FB1D1LS1ANT1TP1F15V @@ -37,8 +37,8 @@ exports[`parts catalog: variable resistors & diode variants snapshot > renders t .wire-net-label { fill: #2563eb; font: 600 10px var(--wire-font, system-ui, sans-serif); } .wire-annotation { fill: #6b7280; font: italic 10px var(--wire-font, system-ui, sans-serif); } - -RV110kRH14k7D1D2D3 - + +RV110kRH14k7D1D2D3 + " `; diff --git a/packages/core/test/__snapshots__/render-features.test.ts.snap b/packages/core/test/__snapshots__/render-features.test.ts.snap index 9e0d101..69ebf19 100644 --- a/packages/core/test/__snapshots__/render-features.test.ts.snap +++ b/packages/core/test/__snapshots__/render-features.test.ts.snap @@ -35,7 +35,7 @@ exports[`layout/render features > produces stable SVG output (snapshot) 1`] = ` .wire-net-label { fill: #2563eb; font: 600 10px var(--wire-font, system-ui, sans-serif); } .wire-annotation { fill: #6b7280; font: italic 10px var(--wire-font, system-ui, sans-serif); } - + BT15VR1220ohmD1 Current limiting resistor " @@ -62,7 +62,7 @@ exports[`layout/render features > renders a component rotated by orientation (sn `; exports[`layout/render features > renders the complex symbol set (transistor, ground, header, switch, inductor, polarized cap) 1`] = ` -" +" Symbol coverage A schematic with 7 components. - - + + Q1R11kL110mH+C1100uFSW1VCCGNDJ1 " diff --git a/packages/core/test/ground-routing.test.ts b/packages/core/test/ground-routing.test.ts new file mode 100644 index 0000000..b3dfa21 --- /dev/null +++ b/packages/core/test/ground-routing.test.ts @@ -0,0 +1,27 @@ +import { compile, layout } from "@wire-lang/core"; +import { expect, it } from "vitest"; + +it("approaches a ground terminal from outside its glyph on a lower return rail", () => { + const result = layout( + compile(`schematic + component R1 Resistor value=1k + component G1 GroundReference + net GND: R1.2, G1.GND + render R1 orientation=vertical + `).model, + ); + const ground = result.components.find((c) => c.id === "G1")!; + const terminal = ground.terminals[0]!.point; + const connections = result.wires + .flatMap((wire) => wire.segments) + .filter( + ({ from, to }) => + (from.x === terminal.x && from.y === terminal.y) || + (to.x === terminal.x && to.y === terminal.y), + ); + expect(connections).toHaveLength(1); + const segment = connections[0]!; + const other = + segment.from.x === terminal.x && segment.from.y === terminal.y ? segment.to : segment.from; + expect(other.y).toBeLessThan(terminal.y); +}); diff --git a/packages/core/test/issue-67.test.ts b/packages/core/test/issue-67.test.ts index ee57e5b..80aaf0e 100644 --- a/packages/core/test/issue-67.test.ts +++ b/packages/core/test/issue-67.test.ts @@ -1,11 +1,20 @@ -import { readFileSync } from "node:fs"; import { compile, layout, renderSvg } from "@wire-lang/core"; import { describe, expect, it } from "vitest"; -const divider = readFileSync( - new URL("../../../examples/pot-divider.wire", import.meta.url), - "utf8", -); +// Keep the original collision fixture independent of gallery edits. +const divider = `schematic + title "Potentiometer voltage divider" + description "A 10k potentiometer taps a fraction of the 5V rail; a rheostat trims the return leg." + + component PWR1 PowerFlag name=5V + component RV1 Potentiometer value=10k + component RH1 Rheostat value=4k7 + component G1 GroundReference + + net VCC: PWR1.1, RV1.1 + net OUT: RV1.W, RH1.1 + net GND: RV1.2, RH1.2, G1.GND +`; function coordinates(value: string): number[] { return value.match(/-?\d+(?:\.\d+)?/g)!.map(Number); diff --git a/packages/core/test/pr-67-examples.test.ts b/packages/core/test/pr-67-examples.test.ts new file mode 100644 index 0000000..cf2bc33 --- /dev/null +++ b/packages/core/test/pr-67-examples.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from "node:fs"; +import { compile, layout, renderSvg } from "@wire-lang/core"; +import { expect, it } from "vitest"; +import { collinearOverlaps, foreignTerminalHits } from "./helpers/geometry.js"; + +const source = (name: string) => + readFileSync(new URL(`../../../examples/${name}.wire`, import.meta.url), "utf8"); + +it("connects a current-limited Zener bias and reverse-biased photodiode to one ground", () => { + const result = compile(source("diode-variants")); + expect(result.diagnostics).toEqual([]); + const members = (net: string) => + result.model.nets + .find((n) => n.name === net)! + .members.map((m) => `${m.component}.${m.terminal}`) + .sort(); + expect(members("LIMITED")).toEqual(["D1.C", "R1.1"]); + expect(members("BIAS")).toEqual(["D2.C", "D3.C", "R1.2"]); + expect(members("SENSE")).toEqual(["D3.A", "R2.1"]); + expect(members("GND")).toEqual(["D2.A", "G1.GND", "R2.2"]); + const drawing = layout(result.model); + for (const id of ["D2", "D3"]) { + const diode = drawing.components.find((c) => c.id === id)!; + expect(diode.terminals.find((t) => t.name === "C")!.point.y).toBeLessThan( + diode.terminals.find((t) => t.name === "A")!.point.y, + ); + } + expect(drawing.wires.find((w) => w.net === "LIMITED")!.segments).toHaveLength(1); + expect(renderSvg(result.model)).toContain(">3.3V"); + expect(collinearOverlaps(drawing)).toEqual([]); + expect(foreignTerminalHits(drawing, result.model)).toEqual([]); +}); + +it("shows a right-facing divider wiper and its load with a visible OUT label", () => { + const result = compile(source("pot-divider")); + expect(result.diagnostics).toEqual([]); + const drawing = layout(result.model); + const pot = drawing.components.find((c) => c.id === "RV1")!; + expect(pot.terminals.find((t) => t.name === "W")!.point.x).toBeGreaterThan(pot.center.x); + expect(drawing.labels.some((label) => label.text === "OUT")).toBe(true); + expect(collinearOverlaps(drawing)).toEqual([]); + expect(foreignTerminalHits(drawing, result.model)).toEqual([]); +}); diff --git a/skills/wire-lang/references/component-library.md b/skills/wire-lang/references/component-library.md index 7d81b02..cefc7f1 100644 --- a/skills/wire-lang/references/component-library.md +++ b/skills/wire-lang/references/component-library.md @@ -13,7 +13,7 @@ Lang source. | `Rheostat` | `1`, `2` | recommended `value` | `id`, `value` | | `Diode` | `A`, `C` | none | `id` | | `LED` | `A`, `C` | optional `color` | `id` | -| `ZenerDiode` | `A`, `C` | none | `id` | +| `ZenerDiode` | `A`, `C` | optional `voltage` | `id`, `voltage` | | `SchottkyDiode` | `A`, `C` | none | `id` | | `Photodiode` | `A`, `C` | none | `id` | | `NPNTransistor` | `C`, `B`, `E` | none | `id` | @@ -83,3 +83,14 @@ MOSFETs, op-amps, relays, motors, displays, sensors, Arduino boards, ESP32 boards, and custom component libraries are outside the MVP standard library. Use a local `define component ... symbol module` block when a simple module placeholder is enough. + +## Practical diode and divider examples + +Use `ZenerDiode voltage=3.3V` to show its nominal breakdown voltage. A Zener +shunt bias needs current limiting ahead of its branch, not just a resistor in +another load branch. For a reverse-biased photodiode, connect the cathode toward +the positive bias and its anode toward the sensing resistor and ground. + +GroundReference glyphs do not merge nets. Put all intended common returns in +one `GND` net, even when using `render net GND style=label` to shorten the drawing. +A rheostat between a potentiometer wiper and ground is a load on the output.