From 2084b09b4971993294922dbead4c3b6b7f2f3bb6 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Wed, 30 Sep 2026 23:46:14 +0200 Subject: [PATCH 01/13] Let a part draw its body with a Dart painter, and answer to aliases PAINTER draws a part's body with a painter the app registers under that name, in place of an SVG. Only the drawing is code: pins, properties, behaviour and physics stay in the .pdl, and VISUALS still draw over it. It is for artwork that arrives as vector paths from a design tool rather than as an SVG file. A part now needs an SVG, a PAINTER or VISUALS, and SVG with PAINTER is rejected since both would draw the body. ALIAS "", repeatable, is another name a part answers to, so a renamed part's old .cdl type token still resolves. PDL.md documents both, and its complete example is the LDR again, now drawn by a PAINTER and aliased to the Photoresistor it shipped as. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 75 ++++++++++++++++++++++++++++-------- lib/src/part_definition.dart | 24 +++++++++--- lib/src/pdl_parser.dart | 26 ++++++++++++- test/pdl_parser_test.dart | 27 +++++++++++++ 4 files changed, 128 insertions(+), 24 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index 6989e82..6fc58ac 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -4,6 +4,8 @@ A `.pdl` file describes a circuit part as data: its artwork, its pins, the properties a user can edit, and how it behaves in a simulation. Parts written this way need no Dart and no rebuild — drop the file and its SVG into `packages/pinbench_parts/assets/parts//` and it appears in the palette. +A part can also take its drawing from a Dart painter instead of an SVG, and +keep everything else as data — see [PAINTER](#painter). This exists because most parts do not need bespoke drawing code. A sensor, a module, a passive component: a photograph-accurate body from an SVG, a few @@ -21,7 +23,7 @@ shipped parts and draws them. | Part | Exercises | | --- | --- | -| `ldr` — photoresistor | `PHYSICS resistor` driven by a property | +| `ldr` — photoresistor | `PHYSICS resistor` driven by a property; a `PAINTER` body and an `ALIAS` | | `thermistor` — 10k NTC | an exponential curve (`exp`) over two steps of state | | `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape | | `cr2032` — coin cell | `PHYSICS voltageSource` | @@ -29,7 +31,7 @@ shipped parts and draws them. | `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock | | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | | `soil_moisture` — capacitive probe | an inverted, offset output range in two steps of state | -| `battery_9v` | `PHYSICS voltageSource`, like the coin cell | +| `battery_9v` | `PHYSICS voltageSource`, like the coin cell; a `PAINTER` body | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | | `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property | @@ -37,24 +39,26 @@ shipped parts and draws them. ## A complete example -This is the part shipped at `assets/parts/ldr/ldr.pdl`. Nothing else is -needed to make a working photoresistor. +This is the part shipped at `assets/parts/ldr/ldr.pdl`. Everything about how +it behaves is here; the only Dart it leans on is its drawing, the `ldr` +painter. ```pdl -# Photoresistor (LDR) — resistance falls as light rises. +# LDR (photoresistor) — resistance falls as light rises. -PART "Photoresistor" +PART "LDR" +ALIAS "Photoresistor" ID ldr CATEGORY sensors DESCRIPTION "Light-dependent resistor" -SIZE 12.7mm 6.35mm -SVG "ldr.svg" +SIZE 56px 52px +PAINTER ldr PINS # id x y role label - a 4px 20px passive "A" - b 68px 20px passive "B" + a 20px 52px passive "A" + b 36px 52px passive "B" PROPERTIES darkResistance number 1000000 unit:"Ω" label:"Dark resistance" @@ -104,13 +108,20 @@ body sizes so the part is drawn the size the real thing is. | `ID ` | yes | Stable key. **Permanent** — see below. | | `SIZE ` | yes | Footprint. | | `SVG ""` | yes¹ | Artwork, relative to this file. | +| `PAINTER ` | yes¹ | Draws the body with a registered Dart painter — see [PAINTER](#painter). | +| `ALIAS ""` | no | Another name the part answers to. Repeatable. | | `CATEGORY ` | no | Palette group (`sensors`, `basic`, `actuators`, …). | | `DESCRIPTION ""` | no | Palette tooltip. | | `ORIGIN ` | no | Part origin inside the footprint. Defaults to the centre. | | `LOGIC ` | no | Names a Dart behaviour — see [LOGIC](#logic). | -¹ Either `SVG` or a `VISUALS` section. A part with neither draws nothing and -is rejected. +¹ One of `SVG`, `PAINTER` or a `VISUALS` section. A part with none draws +nothing and is rejected; `SVG` and `PAINTER` together are rejected too, since +both would draw the body. + +`ALIAS` is for renames. The palette search matches an alias, and so does a +`.cdl` type token, so a file saved under the old name still loads — and is +written back under the new one. The LDR shipped as `Photoresistor`. > **`ID` is permanent.** It is stored as `definitionId` on every placed > component in every saved `.cdl`. Changing it orphans them all. The name can @@ -246,8 +257,9 @@ bad rule stops the part loading rather than surfacing as a wrong number later. ## VISUALS -Vector shapes drawn **over** the SVG. Use these only for what changes; a -static body belongs in the artwork. +Vector shapes drawn **over** the body, whether that is an SVG or a +`PAINTER`. Use these only for what changes; a static body belongs in the +artwork. ``` VISUALS @@ -281,6 +293,35 @@ An attribute value cannot contain spaces unless it is quoted, because attributes are single tokens — so a multi-term expression needs quoting: `color:"lerp(#000, #fff, state.x)"`. +## PAINTER + +A body can be drawn by Dart instead of an SVG, while the part stays a `.pdl`: + +``` +PAINTER ldr +``` + +The name is looked up in `PartPainterRegistry` in `pinbench_parts`, which maps +it to a painter. That painter draws only the body: it is scaled onto `SIZE`, +`VISUALS` still draw over it, and its pins, properties, behaviour and physics +are all this file's. Only the drawing is code. + +This is the shape to reach for when the artwork comes from a design tool as +vector paths rather than as an SVG file. `battery_9v` and `ldr` were drawn in +Figma, one layer per colour, and converted to Flutter paths with the Flutter +Da Vinci plugin; their painters (`battery_9v_painter.dart`, `ldr_painter.dart`) +are those paths and nothing else, drawn through the `PathArtPainter` mixin. +The part itself is still the `.pdl` beside them. + +A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so +the part is hit on the body and not across its whole bounds. An unregistered +name is not fatal — the part loads and draws its `VISUALS` — and the +bundled-parts test fails on it. + +Use `PAINTER` for a static body. A part whose look is the simulation — a +servo horn, a display's pixels — is still better as a built-in part with a +hand-written painter. + ## PHYSICS How the part participates in the analog solve. @@ -504,7 +545,8 @@ mechanism replaced a per-part frame updater in the engine with. ## Assets -A part is a directory: `assets/parts//.pdl` plus its SVG. Add the +A part is a directory: `assets/parts//.pdl` plus its SVG, if it draws +with one. Add the directory to `packages/pinbench_parts/pubspec.yaml` under `flutter: assets:`. A relative `SVG "foo.svg"` resolves next to the `.pdl` that named it, so a part @@ -554,7 +596,8 @@ Honest about what is not there yet: 1. `mkdir packages/pinbench_parts/assets/parts//` 2. Draw the body as an SVG. Its viewBox is scaled onto `SIZE`, so the units - inside it do not matter — only the aspect. + inside it do not matter — only the aspect. Or, for vector art exported as + paths, register a painter and name it with [`PAINTER`](#painter). 3. Write `.pdl`. Put pins on the lattice (`4, 20, 36, …`). 4. Declare the directory in `packages/pinbench_parts/pubspec.yaml`. 5. `cd packages/pinbench_parts && flutter test` — the bundled-parts test will tell diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index 746971e..7f821cd 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -5,8 +5,8 @@ import 'geometry.dart'; import 'pin.dart'; import 'resistance.dart'; -/// A part described entirely by data — a `.pdl` file — rather than by a -/// hand-written Dart painter. +/// A part described by data — a `.pdl` file — rather than as a built-in part +/// in Dart. /// /// This is how the catalog grows without code. A definition carries everything /// needed to place, draw, wire and simulate a component: its [visual] (an SVG @@ -38,6 +38,12 @@ class const PartDefinition({ /// Palette grouping, matching `PartCategory` names. final String? category, + + /// Other names the part goes by — `Photoresistor` for an `LDR`. The palette + /// search matches them, and so does a `.cdl` type token, which is what keeps + /// a file written under a part's old name loading after a rename. The part + /// is always written under [name]. + final List aliases = const [], final Map properties = const {}, final Map state = const {}, @@ -75,10 +81,11 @@ class const PartDefinition({ /// How a [PartDefinition] is drawn. /// -/// Either an [svgPath] — the common case for the static parts this format -/// exists to make cheap — or a list of vector [shapes] for artwork that has to -/// react to state. Both may be present: shapes draw over the SVG, which is how -/// a static body gets a lit LED or a moving needle on top. +/// The body comes from an [svgPath] — the common case for the static parts this +/// format exists to make cheap — or from a [painter], a Dart painter the app +/// registers under that name. Vector [shapes] draw over either, for artwork +/// that has to react to state: that is how a static body gets a lit LED or a +/// moving needle on top. A part may also be shapes alone. @immutable class const VisualDef({ /// Footprint in canvas pixels. Authored in millimetres and converted at @@ -89,6 +96,11 @@ class const VisualDef({ /// Path of an SVG, resolved against the `assetDirectory` the file was /// parsed with — for the bundled parts, a Flutter asset key. final String? svgPath, + + /// Names a painter registered in the app that draws the body instead of an + /// SVG — vector paths generated from a design tool, say. Only the drawing + /// is Dart; pins, properties and behaviour stay in the `.pdl`. + final String? painter, final List shapes = const [], /// Where the part's origin sits inside that footprint. diff --git a/lib/src/pdl_parser.dart b/lib/src/pdl_parser.dart index 40aad47..a4173bd 100644 --- a/lib/src/pdl_parser.dart +++ b/lib/src/pdl_parser.dart @@ -90,6 +90,8 @@ class PdlParser { double? height; PdlPoint? origin; String? svgPath; + String? painter; + final aliases = []; final pins = []; final properties = {}; @@ -144,6 +146,21 @@ class PdlParser { case 'LOGIC': logic = tokens.length > 1 ? _unquote(tokens[1]) : null; continue; + case 'PAINTER': + if (tokens.length < 2) { + error(lineNumber, 'PAINTER needs a name'); + } else { + painter = _unquote(tokens[1]); + } + continue; + case 'ALIAS': + final alias = _unquote(tokens.skip(1).join(' ')); + if (alias.isEmpty) { + error(lineNumber, 'ALIAS needs a name'); + } else { + aliases.add(alias); + } + continue; case 'SIZE': if (tokens.length < 3) { error(lineNumber, 'SIZE needs a width and a height'); @@ -219,8 +236,11 @@ class PdlParser { error(0, 'missing ID — it is the stable key saved circuits refer to'); } if (width == null || height == null) error(0, 'missing SIZE'); - if (svgPath == null && shapes.isEmpty) { - error(0, 'a part needs either an SVG or a VISUALS section — nothing would be drawn'); + if (svgPath == null && painter == null && shapes.isEmpty) { + error(0, 'a part needs an SVG, a PAINTER or a VISUALS section — nothing would be drawn'); + } + if (svgPath != null && painter != null) { + error(0, 'SVG and PAINTER both draw the body — a part has one or the other'); } if (pins.isEmpty) warn(0, 'no PINS — this part cannot be wired to anything'); @@ -243,9 +263,11 @@ class PdlParser { name: name!, description: description, category: category, + aliases: aliases, logic: logic, visual: VisualDef( svgPath: svgPath, + painter: painter, shapes: shapes, width: width!, height: height!, diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index 0887520..893d159 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -143,6 +143,14 @@ PHYSICS resistor expect(result.errors.map((e) => e.message).join(), contains('nothing would be drawn')); }); + test('both an SVG and a PAINTER, which would fight over the body', () { + final result = parse( + pdl(head: 'PART "Thing"\nID thing\nSIZE 32px 16px\nSVG "t.svg"\nPAINTER thing'), + ); + expect(result.errors.map((e) => e.message).join(), contains('one or the other')); + expect(result.definition, isNull); + }); + test('a behaviour rule writing undeclared state', () { final result = parse(pdl(body: 'PINS\n a 4px 4px\n\nBEHAVIOR\n state.ghost = 1')); expect(result.errors.map((e) => e.message).join(), contains('no STATE section declares')); @@ -231,6 +239,25 @@ PHYSICS resistor expect(shapes[1].text.evaluate(context), 'state.x', reason: 'quoted stays literal'); }); + test('a PAINTER draws the body in place of an SVG', () { + final result = parse(pdl(head: 'PART "Thing"\nID thing\nSIZE 32px 16px\nPAINTER thing')); + expect(result.diagnostics, isEmpty, reason: result.diagnostics.join('\n')); + expect(result.definition!.visual.painter, 'thing'); + expect(result.definition!.visual.svgPath, isNull); + }); + + test('ALIAS may repeat, and keeps spaces inside quotes', () { + final result = parse( + pdl( + head: + 'PART "LDR"\nID ldr\nSIZE 32px 16px\nSVG "t.svg"\n' + 'ALIAS "Photoresistor"\nALIAS "Light sensor"', + ), + ); + expect(result.diagnostics, isEmpty, reason: result.diagnostics.join('\n')); + expect(result.definition!.aliases, ['Photoresistor', 'Light sensor']); + }); + test('a relative SVG path resolves next to the .pdl that named it', () { final result = PdlParser.parse( pdl(), From 83da0e4c0da5cc06415adb09216c0152e53b1178 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 00:01:18 +0200 Subject: [PATCH 02/13] List the coin cell among the PAINTER-drawn parts Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index 6fc58ac..f2ba3ec 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -26,7 +26,7 @@ shipped parts and draws them. | `ldr` — photoresistor | `PHYSICS resistor` driven by a property; a `PAINTER` body and an `ALIAS` | | `thermistor` — 10k NTC | an exponential curve (`exp`) over two steps of state | | `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape | -| `cr2032` — coin cell | `PHYSICS voltageSource` | +| `cr2032` — coin cell | `PHYSICS voltageSource`; a `PAINTER` body | | `diode_1n4148` | `PHYSICS` only — no rules at all | | `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock | | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | @@ -307,10 +307,11 @@ it to a painter. That painter draws only the body: it is scaled onto `SIZE`, are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as -vector paths rather than as an SVG file. `battery_9v` and `ldr` were drawn in -Figma, one layer per colour, and converted to Flutter paths with the Flutter -Da Vinci plugin; their painters (`battery_9v_painter.dart`, `ldr_painter.dart`) -are those paths and nothing else, drawn through the `PathArtPainter` mixin. +vector paths rather than as an SVG file. `battery_9v`, `cr2032` and `ldr` were +drawn in Figma, one layer per colour, and converted to Flutter paths with the +Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`, +`cr2032_painter.dart`, `ldr_painter.dart`) are those paths and nothing else, +drawn through the `PathArtPainter` mixin. The part itself is still the `.pdl` beside them. A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so From 037f076ed0cbcf787f077da2c897e774b459a33f Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 00:07:41 +0200 Subject: [PATCH 03/13] List the 1N4148 among the PAINTER-drawn parts Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index f2ba3ec..e7c19fd 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -27,7 +27,7 @@ shipped parts and draws them. | `thermistor` — 10k NTC | an exponential curve (`exp`) over two steps of state | | `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape | | `cr2032` — coin cell | `PHYSICS voltageSource`; a `PAINTER` body | -| `diode_1n4148` | `PHYSICS` only — no rules at all | +| `diode_1n4148` | `PHYSICS` only — no rules at all; a `PAINTER` body | | `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock | | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | | `soil_moisture` — capacitive probe | an inverted, offset output range in two steps of state | @@ -307,11 +307,12 @@ it to a painter. That painter draws only the body: it is scaled onto `SIZE`, are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as -vector paths rather than as an SVG file. `battery_9v`, `cr2032` and `ldr` were -drawn in Figma, one layer per colour, and converted to Flutter paths with the -Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`, -`cr2032_painter.dart`, `ldr_painter.dart`) are those paths and nothing else, -drawn through the `PathArtPainter` mixin. +vector paths rather than as an SVG file. `battery_9v`, `cr2032`, +`diode_1n4148` and `ldr` were drawn in Figma, one layer per colour, and +converted to Flutter paths with the Flutter Da Vinci plugin; their painters +(`battery_9v_painter.dart`, `cr2032_painter.dart`, `diode_1n4148_painter.dart`, +`ldr_painter.dart`) are those paths and nothing else, drawn through the +`PathArtPainter` mixin. The part itself is still the `.pdl` beside them. A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so From 0f1d6a1d5acb778789fab7baf518fded74728c21 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 00:37:00 +0200 Subject: [PATCH 04/13] Add transistor PHYSICS types: npn, pnp, nmos, pmos A bipolar transistor maps `c`, `b`, `e` and a MOSFET `d`, `g`, `s`; a part's numeric PHYSICS parameters are SPICE model parameters over the engine's defaults, so `bf = 300` makes a higher-gain part. PDL.md documents them, lists the four transistor parts, and notes that one PAINTER can serve several parts, as the transistors' does. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 37 +++++++++++++++++++++++++++++++----- lib/src/part_definition.dart | 14 ++++++++++++++ test/pdl_parser_test.dart | 18 ++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index e7c19fd..a807879 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -32,6 +32,8 @@ shipped parts and draws them. | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | | `soil_moisture` — capacitive probe | an inverted, offset output range in two steps of state | | `battery_9v` | `PHYSICS voltageSource`, like the coin cell; a `PAINTER` body | +| `transistor_npn`, `transistor_pnp` | `PHYSICS npn`/`pnp` — three pins; one `PAINTER` shared by four parts | +| `transistor_nmos`, `transistor_pmos` | `PHYSICS nmos`/`pmos` | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | | `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property | @@ -308,11 +310,13 @@ are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as vector paths rather than as an SVG file. `battery_9v`, `cr2032`, -`diode_1n4148` and `ldr` were drawn in Figma, one layer per colour, and -converted to Flutter paths with the Flutter Da Vinci plugin; their painters -(`battery_9v_painter.dart`, `cr2032_painter.dart`, `diode_1n4148_painter.dart`, -`ldr_painter.dart`) are those paths and nothing else, drawn through the -`PathArtPainter` mixin. +`diode_1n4148`, `ldr` and the four transistors were drawn in Figma, one layer +per colour, and converted to Flutter paths with the Flutter Da Vinci plugin; +their painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, +`diode_1n4148_painter.dart`, `ldr_painter.dart`, `transistor_painter.dart`) +are those paths and nothing else, drawn through the `PathArtPainter` mixin. +One painter can serve several parts: the transistors share a body and register +a `transistor_` name each. The part itself is still the `.pdl` beside them. A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so @@ -345,12 +349,35 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `capacitor` | `C_…` | `capacitance` | yes | | `diode` | `D_…` | — | no | | `currentSource` (`isource`) | — | — | not yet built | +| `npn`, `pnp` | `Q_…` | model parameters | no | +| `nmos`, `pmos` | `M_…` | model parameters | no | A matching `physics.` rule overrides the static parameter and is pushed into ngspice each frame via `alter`, the same mechanism the Arduino's pin sources use. That avoids rebuilding the circuit, which would reset every node voltage mid-run. +### Transistors + +A transistor maps three nodes: `c`, `b`, `e` for a bipolar one, `d`, `g`, `s` +for a MOSFET, whose body is tied to its source. Its numeric parameters are the +SPICE `.model` parameters, applied over the engine's defaults — a 2N3904 for +`npn`, a 2N3906 for `pnp`, and level-1 MOSFETs near a 2N7000 and its +P-channel counterpart: + +``` +PHYSICS npn + c = collector + b = base + e = emitter + bf = 300 # a higher-gain part than the default +``` + +The solve is an operating point, so only DC parameters matter (`is`, `bf`, +`br`, `vaf`, … and `vto`, `kp`, `lambda`). A MOSFET's gate is tied to its +source through 1 GΩ so that a gate left unwired is off rather than an +unsolvable floating node. + ## LOGIC Some parts cannot be a rule. `BEHAVIOR` is total by design — no loops, no diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index 7f821cd..85aaeb8 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -248,6 +248,16 @@ enum SpiceComponentType() { /// something a part could assemble. potentiometer, + /// Bipolar transistors, mapped `c`, `b`, `e`. Numeric `PHYSICS` parameters + /// are SPICE model parameters (`bf = 300`) over the engine's defaults. + npn, + pnp, + + /// Enhancement MOSFETs, mapped `d`, `g`, `s`, the body tied to the source. + /// Numeric parameters are level-1 SPICE model parameters (`vto`, `kp`). + nmos, + pmos, + none, } @@ -302,6 +312,10 @@ class const SpiceModelDef({ 'voltageSource' || 'vsource' => SpiceComponentType.voltageSource, 'capacitor' => SpiceComponentType.capacitor, 'currentSource' || 'isource' => SpiceComponentType.currentSource, + 'npn' => SpiceComponentType.npn, + 'pnp' => SpiceComponentType.pnp, + 'nmos' => SpiceComponentType.nmos, + 'pmos' => SpiceComponentType.pmos, _ => SpiceComponentType.none, }; } diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index 893d159..fbb8516 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -239,6 +239,24 @@ PHYSICS resistor expect(shapes[1].text.evaluate(context), 'state.x', reason: 'quoted stays literal'); }); + test('a transistor maps three pins and keeps its model parameters', () { + final result = parse( + pdl( + body: + 'PINS\n c 4px 4px\n b 20px 4px\n e 36px 4px\n\n' + 'PHYSICS npn\n c = c\n b = b\n e = e\n bf = 300', + ), + ); + expect(result.diagnostics, isEmpty, reason: result.diagnostics.join('\n')); + final model = result.definition!.spiceModel!; + expect(model.type, SpiceComponentType.npn); + expect(model.pinMapping, {'c': 'c', 'b': 'b', 'e': 'e'}); + expect(model.parameters, {'bf': 300}); + for (final name in ['pnp', 'nmos', 'pmos']) { + expect(SpiceModelDef.typeFromName(name).name, name); + } + }); + test('a PAINTER draws the body in place of an SVG', () { final result = parse(pdl(head: 'PART "Thing"\nID thing\nSIZE 32px 16px\nPAINTER thing')); expect(result.diagnostics, isEmpty, reason: result.diagnostics.join('\n')); From f29198a729cb6004b8048dca2b7a3a3bac369784 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 00:51:12 +0200 Subject: [PATCH 05/13] Add Darlington PHYSICS types: npnDarlington, pnpDarlington A Darlington maps `c`, `b`, `e` like one bipolar transistor and is built as two, the first driving the second's base, with a resistor across each base-emitter junction as in a TIP120 (`r1`, `r2`). PDL.md documents them, notes that the power MOSFET parts are plain `nmos`/`pmos` with their own `vto` and `kp`, and lists the TO-220 parts and their shared PAINTER. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 27 ++++++++++++++++++++------- lib/src/part_definition.dart | 8 ++++++++ test/pdl_parser_test.dart | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index a807879..b4351a2 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -34,6 +34,8 @@ shipped parts and draws them. | `battery_9v` | `PHYSICS voltageSource`, like the coin cell; a `PAINTER` body | | `transistor_npn`, `transistor_pnp` | `PHYSICS npn`/`pnp` — three pins; one `PAINTER` shared by four parts | | `transistor_nmos`, `transistor_pmos` | `PHYSICS nmos`/`pmos` | +| `power_nmos`, `power_pmos` — TO-220 | `PHYSICS nmos`/`pmos` with their own `vto` and `kp` | +| `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | | `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property | @@ -310,13 +312,14 @@ are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as vector paths rather than as an SVG file. `battery_9v`, `cr2032`, -`diode_1n4148`, `ldr` and the four transistors were drawn in Figma, one layer -per colour, and converted to Flutter paths with the Flutter Da Vinci plugin; -their painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, -`diode_1n4148_painter.dart`, `ldr_painter.dart`, `transistor_painter.dart`) -are those paths and nothing else, drawn through the `PathArtPainter` mixin. -One painter can serve several parts: the transistors share a body and register -a `transistor_` name each. +`diode_1n4148`, `ldr` and the transistors were drawn in Figma, one layer per +colour, and converted to Flutter paths with the Flutter Da Vinci plugin; their +painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, +`diode_1n4148_painter.dart`, `ldr_painter.dart`, `transistor_painter.dart`, +`to220_painter.dart`) are those paths and nothing else, drawn through the +`PathArtPainter` mixin. One painter can serve several parts: the TO-92 +transistors share a body and register a `transistor_` name each, and the +TO-220 parts a `to220_`. The part itself is still the `.pdl` beside them. A painter whose body is smaller than `SIZE` reports it through `bodyRect`, so @@ -351,6 +354,7 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `currentSource` (`isource`) | — | — | not yet built | | `npn`, `pnp` | `Q_…` | model parameters | no | | `nmos`, `pmos` | `M_…` | model parameters | no | +| `npnDarlington`, `pnpDarlington` | two `Q_…` | model parameters, `r1`, `r2` | no | A matching `physics.` rule overrides the static parameter and is pushed into ngspice each frame via `alter`, the same mechanism the Arduino's pin @@ -373,6 +377,15 @@ PHYSICS npn bf = 300 # a higher-gain part than the default ``` +A Darlington pair maps the same three nodes as one bipolar transistor and is +built as two, the first driving the second's base, with a resistor across each +base-emitter junction as in a TIP120: `r1` (8 kΩ) across the driver, `r2` +(120 Ω) across the output. Its other parameters apply to both transistors. + +A power MOSFET is the same `nmos`/`pmos` element with bigger numbers: the +`power_nmos` and `power_pmos` parts declare `vto` and `kp` near an IRLZ44N and +an IRF9540N rather than adding a type. + The solve is an operating point, so only DC parameters matter (`is`, `bf`, `br`, `vaf`, … and `vto`, `kp`, `lambda`). A MOSFET's gate is tied to its source through 1 GΩ so that a gate left unwired is off rather than an diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index 85aaeb8..f5496d5 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -253,6 +253,12 @@ enum SpiceComponentType() { npn, pnp, + /// Darlington pairs — two bipolar transistors, the first driving the + /// second's base, with the TIP120's internal base-emitter resistors (`r1` + /// across the driver, `r2` across the output). Mapped `c`, `b`, `e`. + npnDarlington, + pnpDarlington, + /// Enhancement MOSFETs, mapped `d`, `g`, `s`, the body tied to the source. /// Numeric parameters are level-1 SPICE model parameters (`vto`, `kp`). nmos, @@ -314,6 +320,8 @@ class const SpiceModelDef({ 'currentSource' || 'isource' => SpiceComponentType.currentSource, 'npn' => SpiceComponentType.npn, 'pnp' => SpiceComponentType.pnp, + 'npnDarlington' => SpiceComponentType.npnDarlington, + 'pnpDarlington' => SpiceComponentType.pnpDarlington, 'nmos' => SpiceComponentType.nmos, 'pmos' => SpiceComponentType.pmos, _ => SpiceComponentType.none, diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index fbb8516..06d0397 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -252,7 +252,7 @@ PHYSICS resistor expect(model.type, SpiceComponentType.npn); expect(model.pinMapping, {'c': 'c', 'b': 'b', 'e': 'e'}); expect(model.parameters, {'bf': 300}); - for (final name in ['pnp', 'nmos', 'pmos']) { + for (final name in ['pnp', 'nmos', 'pmos', 'npnDarlington', 'pnpDarlington']) { expect(SpiceModelDef.typeFromName(name).name, name); } }); From 5946134afdf08311c7c4b1c7ea7ac5d7f2375a0a Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 01:21:32 +0200 Subject: [PATCH 06/13] Add the rgbLed PHYSICS type; list the capacitor and RGB LED parts `rgbLed` maps three anodes, `r`, `g`, `b`, and their shared cathode `k`: a common-cathode RGB LED built as three measured dies, so a LOGIC reads each colour's current at its pin. PDL.md documents it, lists the electrolytic capacitor and the RGB LED among the bundled parts, and notes that a PAINTER receives the part's state, which is how the RGB LED's lights up. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 25 +++++++++++++++++++------ lib/src/part_definition.dart | 6 ++++++ test/pdl_parser_test.dart | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index b4351a2..161ba31 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -27,6 +27,7 @@ shipped parts and draws them. | `thermistor` — 10k NTC | an exponential curve (`exp`) over two steps of state | | `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape | | `cr2032` — coin cell | `PHYSICS voltageSource`; a `PAINTER` body | +| `capacitor_electrolytic` — polarized | `PHYSICS capacitor` driven by a µF property; a `PAINTER` body | | `diode_1n4148` | `PHYSICS` only — no rules at all; a `PAINTER` body | | `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock | | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | @@ -36,6 +37,7 @@ shipped parts and draws them. | `transistor_nmos`, `transistor_pmos` | `PHYSICS nmos`/`pmos` | | `power_nmos`, `power_pmos` — TO-220 | `PHYSICS nmos`/`pmos` with their own `vto` and `kp` | | `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | +| `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | | `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property | @@ -312,12 +314,15 @@ are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as vector paths rather than as an SVG file. `battery_9v`, `cr2032`, -`diode_1n4148`, `ldr` and the transistors were drawn in Figma, one layer per -colour, and converted to Flutter paths with the Flutter Da Vinci plugin; their -painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, -`diode_1n4148_painter.dart`, `ldr_painter.dart`, `transistor_painter.dart`, -`to220_painter.dart`) are those paths and nothing else, drawn through the -`PathArtPainter` mixin. One painter can serve several parts: the TO-92 +`capacitor_electrolytic`, `diode_1n4148`, `ldr`, `rgb_led` and the transistors were drawn +in Figma, one layer per colour, and converted to Flutter paths with the +Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`, +`cr2032_painter.dart`, `electrolytic_capacitor_painter.dart`, +`diode_1n4148_painter.dart`, `ldr_painter.dart`, `rgb_led_painter.dart`, +`transistor_painter.dart`, `to220_painter.dart`) are those paths, drawn through +the `PathArtPainter` mixin. A painter also receives the placed part's +properties and state, so it can draw what changes too: `rgb_led` mixes its +glow from the `red`, `green` and `blue` state its logic writes. One painter can serve several parts: the TO-92 transistors share a body and register a `transistor_` name each, and the TO-220 parts a `to220_`. The part itself is still the `.pdl` beside them. @@ -355,6 +360,7 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `npn`, `pnp` | `Q_…` | model parameters | no | | `nmos`, `pmos` | `M_…` | model parameters | no | | `npnDarlington`, `pnpDarlington` | two `Q_…` | model parameters, `r1`, `r2` | no | +| `rgbLed` | three `D_…` | — | no | A matching `physics.` rule overrides the static parameter and is pushed into ngspice each frame via `alter`, the same mechanism the Arduino's pin @@ -382,6 +388,13 @@ built as two, the first driving the second's base, with a resistor across each base-emitter junction as in a TIP120: `r1` (8 kΩ) across the driver, `r2` (120 Ω) across the output. Its other parameters apply to both transistors. +An `rgbLed` maps each colour's anode — `r`, `g`, `b` — and the shared cathode +`k`. The red die drops about 2 V at 20 mA, green and blue about 3 V, and each +is measured like a single LED, so a `LOGIC` reads a colour's current with +`context.spice.pinCurrent('')`. The bundled part's `rgb_led` +logic turns those into `state.red`, `state.green` and `state.blue`, which its +painter reads to light the lens. + A power MOSFET is the same `nmos`/`pmos` element with bigger numbers: the `power_nmos` and `power_pmos` parts declare `vto` and `kp` near an IRLZ44N and an IRF9540N rather than adding a type. diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index f5496d5..e9c2a0b 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -259,6 +259,11 @@ enum SpiceComponentType() { npnDarlington, pnpDarlington, + /// A common-cathode RGB LED: three diodes, anodes mapped `r`, `g`, `b` and + /// their shared cathode `k`, each measured like a single LED so a logic can + /// read the current through every colour at its pin. + rgbLed, + /// Enhancement MOSFETs, mapped `d`, `g`, `s`, the body tied to the source. /// Numeric parameters are level-1 SPICE model parameters (`vto`, `kp`). nmos, @@ -322,6 +327,7 @@ class const SpiceModelDef({ 'pnp' => SpiceComponentType.pnp, 'npnDarlington' => SpiceComponentType.npnDarlington, 'pnpDarlington' => SpiceComponentType.pnpDarlington, + 'rgbLed' => SpiceComponentType.rgbLed, 'nmos' => SpiceComponentType.nmos, 'pmos' => SpiceComponentType.pmos, _ => SpiceComponentType.none, diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index 06d0397..e2bbacd 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -252,7 +252,7 @@ PHYSICS resistor expect(model.type, SpiceComponentType.npn); expect(model.pinMapping, {'c': 'c', 'b': 'b', 'e': 'e'}); expect(model.parameters, {'bf': 300}); - for (final name in ['pnp', 'nmos', 'pmos', 'npnDarlington', 'pnpDarlington']) { + for (final name in ['pnp', 'nmos', 'pmos', 'npnDarlington', 'pnpDarlington', 'rgbLed']) { expect(SpiceModelDef.typeFromName(name).name, name); } }); From 30b9ee63fa6be7cf88cbc8eb1ecf50a3f2e9cce7 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 01:42:01 +0200 Subject: [PATCH 07/13] Document diode model parameters and the three axial diodes A `diode` part's numeric PHYSICS parameters (`is`, `n`) are now its SPICE model numbers. PDL.md lists the 1N4007 rectifier and 1N5819 Schottky beside the 1N4148, and their shared axial-diode PAINTER. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index 161ba31..620a496 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -28,7 +28,8 @@ shipped parts and draws them. | `slide_switch` — SPST | a boolean property, and `visual.*` driving a shape | | `cr2032` — coin cell | `PHYSICS voltageSource`; a `PAINTER` body | | `capacitor_electrolytic` — polarized | `PHYSICS capacitor` driven by a µF property; a `PAINTER` body | -| `diode_1n4148` | `PHYSICS` only — no rules at all; a `PAINTER` body | +| `diode_1n4148` — switching | `PHYSICS` only — no rules at all; a `PAINTER` body | +| `diode_1n4007` — rectifier, `diode_1n5819` — Schottky | `PHYSICS diode` with each part's own model numbers; one `PAINTER` shared by three parts | | `pir_sensor` — PIR motion | `LOGIC` — a one-shot needing a clock | | `tmp36`, `lm35` — analog temperature | `PHYSICS voltageSource` driven by a property, read by `analogRead` | | `soil_moisture` — capacitive probe | an inverted, offset output range in two steps of state | @@ -314,11 +315,11 @@ are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as vector paths rather than as an SVG file. `battery_9v`, `cr2032`, -`capacitor_electrolytic`, `diode_1n4148`, `ldr`, `rgb_led` and the transistors were drawn +`capacitor_electrolytic`, the diodes, `ldr`, `rgb_led` and the transistors were drawn in Figma, one layer per colour, and converted to Flutter paths with the Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, `electrolytic_capacitor_painter.dart`, -`diode_1n4148_painter.dart`, `ldr_painter.dart`, `rgb_led_painter.dart`, +`axial_diode_painter.dart`, `ldr_painter.dart`, `rgb_led_painter.dart`, `transistor_painter.dart`, `to220_painter.dart`) are those paths, drawn through the `PathArtPainter` mixin. A painter also receives the placed part's properties and state, so it can draw what changes too: `rgb_led` mixes its @@ -355,7 +356,7 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `resistor` | `R_…` | `resistance` | yes | | `voltageSource` (`vsource`) | `V_…` | `voltage` | yes | | `capacitor` | `C_…` | `capacitance` | yes | -| `diode` | `D_…` | — | no | +| `diode` | `D_…` | model parameters (`is`, `n`) | no | | `currentSource` (`isource`) | — | — | not yet built | | `npn`, `pnp` | `Q_…` | model parameters | no | | `nmos`, `pmos` | `M_…` | model parameters | no | From 2ffaa467ace6f1dccf31bde95a33d911eaa75d0b Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 01:54:57 +0200 Subject: [PATCH 08/13] Add the spdt PHYSICS type and the SPDT slide switch `spdt` maps a common pole `c` and throws `a` and `b`: two resistors, one closed and one open, swapped by a single alterable `position` (0 for a, 1 for b), so a `physics.position` rule throws it mid-run. PDL.md lists it and the slide_switch_spdt part. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 2 ++ lib/src/part_definition.dart | 7 +++++++ test/pdl_parser_test.dart | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/doc/PDL.md b/doc/PDL.md index 620a496..14a13b7 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -38,6 +38,7 @@ shipped parts and draws them. | `transistor_nmos`, `transistor_pmos` | `PHYSICS nmos`/`pmos` | | `power_nmos`, `power_pmos` — TO-220 | `PHYSICS nmos`/`pmos` with their own `vto` and `kp` | | `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | +| `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match | | `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | @@ -362,6 +363,7 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `nmos`, `pmos` | `M_…` | model parameters | no | | `npnDarlington`, `pnpDarlington` | two `Q_…` | model parameters, `r1`, `r2` | no | | `rgbLed` | three `D_…` | — | no | +| `spdt` | two `R_…` | `position` (0 = `a`, 1 = `b`) | yes | A matching `physics.` rule overrides the static parameter and is pushed into ngspice each frame via `alter`, the same mechanism the Arduino's pin diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index e9c2a0b..490eb0c 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -264,6 +264,12 @@ enum SpiceComponentType() { /// read the current through every colour at its pin. rgbLed, + /// A single-pole double-throw switch: the common `c` connected to `a` or to + /// `b`. Two resistors, closed and open, swapped by one alterable value, + /// `position` — 0 for `a`, 1 for `b` — so a `physics.position` rule throws + /// it mid-run. + spdt, + /// Enhancement MOSFETs, mapped `d`, `g`, `s`, the body tied to the source. /// Numeric parameters are level-1 SPICE model parameters (`vto`, `kp`). nmos, @@ -328,6 +334,7 @@ class const SpiceModelDef({ 'npnDarlington' => SpiceComponentType.npnDarlington, 'pnpDarlington' => SpiceComponentType.pnpDarlington, 'rgbLed' => SpiceComponentType.rgbLed, + 'spdt' => SpiceComponentType.spdt, 'nmos' => SpiceComponentType.nmos, 'pmos' => SpiceComponentType.pmos, _ => SpiceComponentType.none, diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index e2bbacd..dc43ef8 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -252,7 +252,15 @@ PHYSICS resistor expect(model.type, SpiceComponentType.npn); expect(model.pinMapping, {'c': 'c', 'b': 'b', 'e': 'e'}); expect(model.parameters, {'bf': 300}); - for (final name in ['pnp', 'nmos', 'pmos', 'npnDarlington', 'pnpDarlington', 'rgbLed']) { + for (final name in [ + 'pnp', + 'nmos', + 'pmos', + 'npnDarlington', + 'pnpDarlington', + 'rgbLed', + 'spdt', + ]) { expect(SpiceModelDef.typeFromName(name).name, name); } }); From 2f95f49db8578056cb6fc81839eaf1059b39ba42 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 02:09:07 +0200 Subject: [PATCH 09/13] Add the ledArray PHYSICS type and the 7-segment display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ledArray` is any number of LED dies on one cathode: every mapped role but `k` is a die from its pin to the cathode, measured like a single LED, and another `k…` role is a second cathode pin tied inside. `is` and `n` set the dies' model; red by default. PDL.md documents it and lists the seven_segment part. Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 8 ++++++++ lib/src/part_definition.dart | 8 ++++++++ test/pdl_parser_test.dart | 1 + 3 files changed, 17 insertions(+) diff --git a/doc/PDL.md b/doc/PDL.md index 14a13b7..0e270f2 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -39,6 +39,7 @@ shipped parts and draws them. | `power_nmos`, `power_pmos` — TO-220 | `PHYSICS nmos`/`pmos` with their own `vto` and `kp` | | `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | | `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match | +| `seven_segment` — common cathode | `PHYSICS ledArray` of eight dies and two tied cathode pins; `LOGIC` into `STATE` per segment; a `PAINTER` that lights each | | `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | @@ -364,6 +365,7 @@ SPICE node name to a pin id. Mapping to an undeclared pin is a parse error. | `npnDarlington`, `pnpDarlington` | two `Q_…` | model parameters, `r1`, `r2` | no | | `rgbLed` | three `D_…` | — | no | | `spdt` | two `R_…` | `position` (0 = `a`, 1 = `b`) | yes | +| `ledArray` | one `D_…` per mapped die | model parameters (`is`, `n`) | no | A matching `physics.` rule overrides the static parameter and is pushed into ngspice each frame via `alter`, the same mechanism the Arduino's pin @@ -398,6 +400,12 @@ is measured like a single LED, so a `LOGIC` reads a colour's current with logic turns those into `state.red`, `state.green` and `state.blue`, which its painter reads to light the lens. +An `ledArray` generalises that to any number of dies on one cathode: each +role but `k` maps a die's anode — a 7-segment display maps `a` to `g` and +`dp` — and a second cathode pin maps `k2`, tied to `k` inside the part as it +is in the real one. The `led_array` logic writes each die's brightness into +`STATE` under its pin's id. + A power MOSFET is the same `nmos`/`pmos` element with bigger numbers: the `power_nmos` and `power_pmos` parts declare `vto` and `kp` near an IRLZ44N and an IRF9540N rather than adding a type. diff --git a/lib/src/part_definition.dart b/lib/src/part_definition.dart index 490eb0c..ccbe9f2 100644 --- a/lib/src/part_definition.dart +++ b/lib/src/part_definition.dart @@ -270,6 +270,13 @@ enum SpiceComponentType() { /// it mid-run. spdt, + /// Any number of LEDs sharing a cathode — a 7-segment display, a bar graph. + /// Every mapped role but `k` is a die from that pin to the cathode `k`, + /// measured like a single LED; any other role starting `k` (`k2`) is a + /// second cathode pin, tied to the first. Numeric `is` and `n` set the + /// dies' model; without them they drop about 2 V, as red ones do. + ledArray, + /// Enhancement MOSFETs, mapped `d`, `g`, `s`, the body tied to the source. /// Numeric parameters are level-1 SPICE model parameters (`vto`, `kp`). nmos, @@ -335,6 +342,7 @@ class const SpiceModelDef({ 'pnpDarlington' => SpiceComponentType.pnpDarlington, 'rgbLed' => SpiceComponentType.rgbLed, 'spdt' => SpiceComponentType.spdt, + 'ledArray' => SpiceComponentType.ledArray, 'nmos' => SpiceComponentType.nmos, 'pmos' => SpiceComponentType.pmos, _ => SpiceComponentType.none, diff --git a/test/pdl_parser_test.dart b/test/pdl_parser_test.dart index dc43ef8..0592d0b 100644 --- a/test/pdl_parser_test.dart +++ b/test/pdl_parser_test.dart @@ -260,6 +260,7 @@ PHYSICS resistor 'pnpDarlington', 'rgbLed', 'spdt', + 'ledArray', ]) { expect(SpiceModelDef.typeFromName(name).name, name); } From 7246f5cddb2a57846e799a0f2f985454ba70ba49 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 02:22:00 +0200 Subject: [PATCH 10/13] List the IR receiver among the bundled parts Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/PDL.md b/doc/PDL.md index 0e270f2..41c120e 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -40,6 +40,7 @@ shipped parts and draws them. | `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | | `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match | | `seven_segment` — common cathode | `PHYSICS ledArray` of eight dies and two tied cathode pins; `LOGIC` into `STATE` per segment; a `PAINTER` that lights each | +| `ir_receiver` — VS1838B | `PHYSICS resistor` only: its internal pull-up, the honest model with no infrared source; a `PAINTER` body | | `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | @@ -317,7 +318,7 @@ are all this file's. Only the drawing is code. This is the shape to reach for when the artwork comes from a design tool as vector paths rather than as an SVG file. `battery_9v`, `cr2032`, -`capacitor_electrolytic`, the diodes, `ldr`, `rgb_led` and the transistors were drawn +`capacitor_electrolytic`, the diodes, `ir_receiver`, `ldr`, `rgb_led` and the transistors were drawn in Figma, one layer per colour, and converted to Flutter paths with the Flutter Da Vinci plugin; their painters (`battery_9v_painter.dart`, `cr2032_painter.dart`, `electrolytic_capacitor_painter.dart`, From 2dfe718c2289f13e551ef0c5f75f34bb093ff57d Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 02:57:41 +0200 Subject: [PATCH 11/13] Note the IR receiver hears the IR Remote part Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/PDL.md b/doc/PDL.md index 41c120e..c735d30 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -40,7 +40,7 @@ shipped parts and draws them. | `tip120` — Darlington, TO-220 | `PHYSICS npnDarlington` | | `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match | | `seven_segment` — common cathode | `PHYSICS ledArray` of eight dies and two tied cathode pins; `LOGIC` into `STATE` per segment; a `PAINTER` that lights each | -| `ir_receiver` — VS1838B | `PHYSICS resistor` only: its internal pull-up, the honest model with no infrared source; a `PAINTER` body | +| `ir_receiver` — VS1838B | `PHYSICS resistor` only — its internal pull-up; the IR Remote part's presses arrive on OUT as timed NEC codes, played in by the engine | | `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | From 98b6ecf02cac02a9ebeb55b35b4f215e0abbaad0 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 03:16:47 +0200 Subject: [PATCH 12/13] Note the RGB LED now uses the led_array logic Co-Authored-By: Claude Opus 5.5 (1M context) --- doc/PDL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/PDL.md b/doc/PDL.md index c735d30..9fbcd1a 100644 --- a/doc/PDL.md +++ b/doc/PDL.md @@ -41,7 +41,7 @@ shipped parts and draws them. | `slide_switch_spdt` — SPDT | `PHYSICS spdt` thrown by an `enum` property; a `PAINTER` that slides its lever to match | | `seven_segment` — common cathode | `PHYSICS ledArray` of eight dies and two tied cathode pins; `LOGIC` into `STATE` per segment; a `PAINTER` that lights each | | `ir_receiver` — VS1838B | `PHYSICS resistor` only — its internal pull-up; the IR Remote part's presses arrive on OUT as timed NEC codes, played in by the engine | -| `rgb_led` — common cathode | `PHYSICS rgbLed`, `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | +| `rgb_led` — common cathode | `PHYSICS rgbLed`, the `led_array` `LOGIC` reading a current per pin into `STATE`, and a `PAINTER` that lights up from that state | | `bh1750` — light, I²C | `LOGIC` serving a pointer-less register bank; opcodes tracked in `STATE` | | `mpu6050` — accelerometer + gyro, I²C | `LOGIC` serving registers the sketch configures; an address set by wiring AD0 | | `ds1307` — real-time clock, I²C | `LOGIC` keeping time on the simulated clock; an `enum` property | @@ -397,9 +397,9 @@ base-emitter junction as in a TIP120: `r1` (8 kΩ) across the driver, `r2` An `rgbLed` maps each colour's anode — `r`, `g`, `b` — and the shared cathode `k`. The red die drops about 2 V at 20 mA, green and blue about 3 V, and each is measured like a single LED, so a `LOGIC` reads a colour's current with -`context.spice.pinCurrent('')`. The bundled part's `rgb_led` -logic turns those into `state.red`, `state.green` and `state.blue`, which its -painter reads to light the lens. +`context.spice.pinCurrent('')`. The bundled part uses the +`led_array` logic below, which turns those into `state.red`, `state.green` +and `state.blue`, and its painter reads them to light the lens. An `ledArray` generalises that to any number of dies on one cathode: each role but `k` maps a die's anode — a 7-segment display maps `a` to `g` and From 8d84e3b1dc0308a8d39075ba365aaa9aecf45392 Mon Sep 17 00:00:00 2001 From: burhankhanzada Date: Thu, 1 Oct 2026 12:14:18 +0200 Subject: [PATCH 13/13] Let one .pdl describe a part in several configurations A CONFIGURATION "