app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
0.2.52 source (not yet released, needs signing): importing a part again replaces its 3D body instead of stacking a second one (add_body deletes the old bodies with select + PCB:Clear; verified live on AD 26.11.1)
12 files changed
+179−21
CHANGELOG.md+7@@ -1,5 +1,12 @@ # Changelog +## 0.2.52 (2026-10-20)+- **Importing a part again replaces its 3D body instead of stacking a second one.** `add_body` (and the `step` of `import_part`, which calls it) added a new body every call and never removed the old one, and `normalize_bodies` then centred both on the pads: the AMPLH5030S inductor reached a board (2026-10-16) with the old model and the fixed one inside each other, repaired only by `remove_footprint`, a re-import and `PCB:UpdateFootprints`. The agent now deletes the footprint's existing 3D bodies before it adds the new one, the way the editor's Edit > Delete does (it selects exactly those bodies and runs `PCB:Clear`; `RemovePCBObject` left them in the stored footprint after save), how `build_symbol_modes` replaces a symbol of the same LibReference. It removes nothing until the new STEP has loaded, so a bad path keeps the old body.+- The `add_body` reply says `replacedBodies` (also at the top level), `keptExisting`, `bodiesBefore`, `sameNameFootprints` and `bodiesNow`, and its `_hint` warns when bodies are still stacked or when several footprints share the name (only the first gets the body). `import_part`'s add_body step carries `replacedBodies`, `bodiesNow` and `sameNameFootprints`.+- New `add_body` argument `keepExisting` (default false): add next to the existing bodies, for a deliberate second body.+- Boards that already place the part keep the old body until `PCB:UpdateFootprints`.+- Verified live on Altium Designer 26.11.1: three repeated adds keep 1 body, `keepExisting` makes 2 and the next plain add makes 1 again, a bad STEP path keeps the old body, and the saved and reopened footprint has 1 body and its 2 pads.+ ## 0.2.51 (2026-10-19) - **IC symbols attach to their box again.** `altium-symbol-modes` (`tools/symbol_modes.py`, the spec behind `build_symbol_modes` / `import_part`) wrote each pin's outer end where Altium's `Pin.Location` wants the end on the body, so every pin of a 2-column or 4-side symbol floated one pin length (300 mil) off the box with its name in the gap. Pins now start on the body edge in all ten display modes. Small parts drawn from their KiCad symbol (resistors, capacitors, inductors, LEDs) were never affected. - `dev-skills/altium-bridge-test/test_symbol_modes.py`: every pin of every mode must start on its body edge (fails on 0.2.50, passes on 0.2.51).
SKILL.md+1−1@@ -60,7 +60,7 @@ Altium is a licensed host app. The bridge never installs it silently; it detects | `altium_check_dialogs` / `altium_dismiss_dialogs` | dialogs by PID with body shot; press a button in the background | `hwnd`, `button` | | `altium_create_footprint` / `altium_create_symbol` | author a footprint (pads) / symbol (pins) into a .PcbLib/.SchLib, creating the library if missing | `name`, `libraryPath`, `pads` / `pins` | | `altium_set_params` | parameters (LCSC Part #, MPN, …), comment, footprint link on a symbol | `libraryPath`, `symbolName`, `params`, `footprint` |-| `altium_add_body` | 3D body on a footprint — STEP model or extruded box | `libraryPath`, `footprintName`, `step` \| `bwMil,bhMil,hMil` |+| `altium_add_body` | 3D body on a footprint — STEP model or extruded box; REPLACES the footprint's existing bodies (from 0.2.52) unless `keepExisting:true` | `libraryPath`, `footprintName`, `step` \| `bwMil,bhMil,hMil`, `keepExisting` | | `altium_install_library` / `altium_compile_intlib` / `altium_extract_intlib` | Installed Libraries, .LibPkg → .IntLib, .IntLib → sources | `libraryPath` / `libpkgPath` / `intlibPath` | | `altium_list_installed_libraries` / `altium_uninstall_library` | read Installed Libraries / remove entries (from 0.2.45; files never touched, any open document for the library is closed first, nothing matching answers `notInstalled:true`). Close Altium with a plain `altium_close` afterwards: a discard close can bring the entry back | (none) / `libraryName` (file stem: SchLib AND PcbLib) or `libraryPath`, `dryRun` | | `altium_build_footprint` / `altium_build_symbol_modes` | author a footprint from a `altium-kicad2altium` spec (via pads, roundrect, mech layers) / ONE symbol with N display modes | `libraryPath`, `spec` |
bridge-rs/src/library.rs+78−4@@ -111,18 +111,59 @@ pub fn build_symbol_modes(args: &Value) -> Value { None, timeout_of(args, 240))) } +/// `keepExisting:true` (or "true"/"1") is the opt-out for a deliberate second body; anything else+/// means add_body REPLACES the footprint's existing 3D bodies.+fn keep_existing(args: &Value) -> bool {+ match args.get("keepExisting") {+ Some(Value::Bool(b)) => *b,+ Some(Value::String(t)) => matches!(t.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes"),+ Some(Value::Number(n)) => n.as_i64() == Some(1),+ _ => false,+ }+}++/// The add_body `_hint`, from the agent's reply data: says what was replaced, and flags the two+/// ways a footprint can still end up with stacked bodies (a replace that did not take, or several+/// footprints sharing the name so the body went to the first of them only).+fn body_hint(d: &Value, keep: bool) -> String {+ let n = |k: &str| d.get(k).and_then(|v| v.as_i64()).unwrap_or(0);+ let (replaced, now, same) = (n("replacedBodies"), n("bodiesNow"), n("sameNameFootprints"));+ let fp = d.get("footprint").and_then(|v| v.as_str()).unwrap_or("the footprint");+ let mut h = if keep {+ format!("keepExisting: the new body was ADDED next to the {} already on {fp}; it now has {now}.", n("bodiesBefore"))+ } else if replaced > 0 {+ format!("Replaced {replaced} existing 3D bod{} on {fp} with the new one (a re-import replaces, never stacks).",+ if replaced == 1 { "y" } else { "ies" })+ } else {+ format!("{fp} had no 3D body; added one.")+ };+ if !keep && now > 1 {+ h.push_str(&format!(" WARNING: {fp} still reads {now} bodies after a replace: altium_footprint_primitives to see them, then altium_remove_footprint + re-import."));+ }+ if same > 1 {+ h.push_str(&format!(" WARNING: {same} footprints in this library are named {fp}; the body went to the FIRST only. Remove the stale copies (altium_remove_footprint) and re-import."));+ }+ h.push_str(" Then altium_save {filePath: libraryPath}; placed parts pick the change up with PCB:UpdateFootprints.");+ h+}+ pub fn add_body(args: &Value) -> Value { let lib = match s(args, "libraryPath") { Some(l) if !l.is_empty() => l, _ => return json!({"success": false, "error": "libraryPath (.PcbLib) is required", "_hint": "step:'C:/…/part.step' for a real model, else bwMil/bhMil/hMil/soMil for an extruded box"}), };+ let keep = keep_existing(args); let mut r = lib_call(args, "add_body", vec![("name", s_or(args, "footprintName", "")), ("step", s_or(args, "step", "")), ("bw_mil", s_or(args, "bwMil", "100")), ("bh_mil", s_or(args, "bhMil", "100")),- ("h_mil", s_or(args, "hMil", "40")), ("so_mil", s_or(args, "soMil", "0"))],+ ("h_mil", s_or(args, "hMil", "40")), ("so_mil", s_or(args, "soMil", "0")),+ ("keep", if keep { "1".into() } else { "0".into() })], Some(&lib), 90); if r.get("success").and_then(|b| b.as_bool()) == Some(true) {+ let d = r.get("data").cloned().unwrap_or(json!({}));+ r["replacedBodies"] = d.get("replacedBodies").cloned().unwrap_or(Value::Null);+ r["_hint"] = json!(body_hint(&d, keep)); // canvas-added bodies land 50000 mil from the pads in the stored frame; realign let n = agent::call("normalize_bodies", &[("name", s_or(args, "footprintName", ""))], None, Duration::from_secs(90)); r["normalized"] = n.get("data").and_then(|d| d.get("shifts")).cloned().unwrap_or(Value::Null);@@ -571,11 +612,17 @@ pub fn import_part(args: &Value) -> Value { steps.push(json!({"build_footprint": fp_name})); } if let (Some(step), Some(fp)) = (s(args, "step").filter(|v| !v.is_empty()), fp_name.clone()) {+ // keep=0: a part imported again REPLACES its 3D body (it used to stack a second one on the+ // footprint: AMPLH5030S, 2026-10-16), the way build_symbol_modes replaces its symbol. let r = agent::call("add_body", &[("libraryPath", pcb_lib.clone()), ("name", fp.clone()), ("step", step),- ("bw_mil", "100".into()), ("bh_mil", "100".into()), ("h_mil", "40".into()), ("so_mil", "0".into())],+ ("bw_mil", "100".into()), ("bh_mil", "100".into()), ("h_mil", "40".into()), ("so_mil", "0".into()),+ ("keep", "0".into())], None, Duration::from_secs(180));- steps.push(json!({"add_body": r.get("success").and_then(|b| b.as_bool()).unwrap_or(false)}));+ let d = r.get("data").cloned().unwrap_or(json!({}));+ steps.push(json!({"add_body": r.get("success").and_then(|b| b.as_bool()).unwrap_or(false),+ "replacedBodies": d.get("replacedBodies"), "bodiesNow": d.get("bodiesNow"),+ "sameNameFootprints": d.get("sameNameFootprints")})); agent::call("normalize_bodies", &[("name", fp)], None, Duration::from_secs(90)); } let mut sym_name = s(args, "symbolName");@@ -625,7 +672,7 @@ pub fn import_part(args: &Value) -> Value { } } json!({"success": true, "symbol": sym_name, "footprint": fp_name, "steps": steps,- "_hint": "One-call part import (Barrett's import_part on the resident lane). Verify with altium_read_symbol + altium_pcb_report."})+ "_hint": "One-call part import (Barrett's import_part on the resident lane). Importing a part again REPLACES its symbol and its footprint's 3D body (steps[].replacedBodies says how many old bodies went; bodiesNow should be 1). Verify with altium_read_symbol + altium_pcb_report; placed parts pick the change up with PCB:UpdateFootprints."}) } // ---- install_library_bundle: the no-AI web-control lane ----------------------@@ -1002,6 +1049,33 @@ mod tests { assert_eq!(p.items[1].0, "unpack"); } + #[test]+ fn add_body_replaces_unless_keep_existing_is_set() {+ assert!(!keep_existing(&json!({})));+ assert!(!keep_existing(&json!({"keepExisting": false})));+ assert!(!keep_existing(&json!({"keepExisting": "no"})));+ assert!(keep_existing(&json!({"keepExisting": true})));+ assert!(keep_existing(&json!({"keepExisting": "true"})));+ assert!(keep_existing(&json!({"keepExisting": 1})));+ }++ #[test]+ fn add_body_hint_reports_replacement_and_flags_stacking() {+ let h = body_hint(&json!({"footprint": "AMPLH5030S", "replacedBodies": 1, "bodiesNow": 1, "sameNameFootprints": 1}), false);+ assert!(h.contains("Replaced 1 existing 3D body on AMPLH5030S"), "{h}");+ assert!(!h.contains("WARNING"), "{h}");+ let h = body_hint(&json!({"footprint": "F", "replacedBodies": 0, "bodiesNow": 1, "sameNameFootprints": 1}), false);+ assert!(h.contains("had no 3D body") && !h.contains("WARNING"), "{h}");+ // the replace did not take: two bodies remain+ let h = body_hint(&json!({"footprint": "F", "replacedBodies": 0, "bodiesNow": 2, "sameNameFootprints": 1}), false);+ assert!(h.contains("still reads 2 bodies"), "{h}");+ // a deliberate second body is not a warning+ let h = body_hint(&json!({"footprint": "F", "bodiesBefore": 1, "bodiesNow": 2, "sameNameFootprints": 1}), true);+ assert!(h.contains("ADDED") && !h.contains("WARNING"), "{h}");+ let h = body_hint(&json!({"footprint": "F", "replacedBodies": 1, "bodiesNow": 1, "sameNameFootprints": 2}), false);+ assert!(h.contains("2 footprints in this library are named F"), "{h}");+ }+ #[test] fn model_spec_separators_are_sanitised() { assert_eq!(spec_safe("a;b|c"), "a,b/c");
bridge-rs/src/main.rs+1−1@@ -31,7 +31,7 @@ use tiny_http::{Header, Method, Response, Server}; /// Attached to every failed reply (see the /command handler). const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend."; -pub const BRIDGE_VERSION: &str = "0.2.51";+pub const BRIDGE_VERSION: &str = "0.2.52"; fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> { let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/verbs.rs+9−9@@ -114,9 +114,9 @@ fn describe() -> Value { "PARTIAL results by design: skipped says which checks had no data; a clean result means the checks that RAN found nothing.", &["read_board", "cross_check"], &["Altium holds ONE current document - the review activates board and sheet in turn."], json!({"includeSchematic": true})), verb_doc("import_part", "One call imports a whole part: footprint spec + STEP + symbol modes + models + params + save.", json!({"schLib": "path", "pcbLib": "path", "footprintSpec": "path", "symbolSpec": "path", "step": "path", "params": "{}", "comment": "string", "description": "string"}),- json!({"symbol": "string", "footprint": "string", "steps": "[]"}), 300,- "Check the step counts: models:0 fails the call - a symbol with no footprint model places with nothing attached.",- &["read_symbol", "show_symbol"], &["add_models APPENDS on every call (not idempotent) - repair is remove_symbol + re-import.", "The first part into a NEW library can silently skip models+params if the bridge restarted mid-flight - verify with read_symbol."], json!({})),+ json!({"symbol": "string", "footprint": "string", "steps": "[] - the add_body step carries replacedBodies, bodiesNow, sameNameFootprints"}), 300,+ "Check the step counts: models:0 fails the call - a symbol with no footprint model places with nothing attached. Importing again REPLACES the symbol and the footprint's 3D body (add_body step: bodiesNow 1); boards that place the part need PCB:UpdateFootprints.",+ &["read_symbol", "show_symbol"], &["add_models APPENDS on every call (not idempotent) - repair is remove_symbol + re-import.", "The add_body step's sameNameFootprints > 1 means the library holds stale footprints of that name: the body went to the FIRST only.", "The first part into a NEW library can silently skip models+params if the bridge restarted mid-flight - verify with read_symbol."], json!({})), verb_doc("test_board", "Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the existing board when pcbPath already exists (issue 11).", json!({"libraryPath": "path (.PcbLib)", "footprintName": "string | 'A;B;C'", "wMm": "number (new board only)", "hMm": "number (new board only)", "pcbPath": "path (.PcbDoc) - existing = append, new = create+save", "assigns": "des=NET;...", "stubs": "des=x,y;..."}), json!({"placed": "string", "count": "int", "pcb": "path", "saved": "bool (checked on disk)", "appended": "bool", "existingParts": "int", "padsOnComp": "int", "bodiesOnComp": "int"}), 240,@@ -685,12 +685,12 @@ fn describe() -> Value { &["import_part", "add_symbol_art", "symbol_mode", "add_models"], &["libref comes from the spec's META line.", "Compare data.displayModeCount with data.modes before trusting the alternates.", "Library is left UNSAVED."], json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "spec": "C:/lib/specs/VL53L5CX.sym.txt"})),- verb_doc("add_body", "Add a 3D body to a footprint of a PcbLib: a STEP model, or an extruded box when no step is given. The bridge then re-aligns bodies onto the pads (normalize_bodies) and returns a frame.",- json!({"libraryPath": "path to a .PcbLib (required)", "footprintName": "string - omit for the FIRST footprint", "step": "Windows path to a .step - optional", "bwMil": "int, default 100 - box width", "bhMil": "int, default 100 - box depth", "hMil": "int, default 40 - box height", "soMil": "int, default 0 - standoff", "timeout": "seconds, default 90"}),- json!({"data": "{footprint, body, bodiesNow, bodiesOnCanvas}", "normalized": "string - the shifts applied", "shot": "{image: base64 png, width, height}"}), 90,- "altium_save {filePath: libraryPath}, then altium_show_3d_chip to look at it. Never engrave the MPN into the STEP.",- &["normalize_bodies", "show_3d_chip", "pcb_report", "import_part"],- &["It ADDS a body every call: check data.bodiesNow, a second call leaves two.", "Without footprintName it acts on the first footprint in the library.", "'could not load model' = the STEP path is not on THIS machine: stage it with send_files."],+ verb_doc("add_body", "Put a 3D body on a footprint of a PcbLib: a STEP model, or an extruded box when no step is given. It REPLACES the footprint's existing 3D bodies (a re-import never stacks a second model) unless keepExisting:true. The bridge then re-aligns bodies onto the pads (normalize_bodies) and returns a frame.",+ json!({"libraryPath": "path to a .PcbLib (required)", "footprintName": "string - required when the library holds more than one footprint", "step": "Windows path to a .step - optional", "bwMil": "int, default 100 - box width", "bhMil": "int, default 100 - box depth", "hMil": "int, default 40 - box height", "soMil": "int, default 0 - standoff", "keepExisting": "bool, default false - ADD next to the existing bodies instead of replacing them (a deliberate second body)", "timeout": "seconds, default 90"}),+ json!({"data": "{footprint, body, replacedBodies, keptExisting, bodiesBefore, sameNameFootprints, bodiesNow, bodiesOnCanvas}", "replacedBodies": "int - old bodies removed before the add", "normalized": "string - the shifts applied", "shot": "{image: base64 png, width, height}", "_hint": "string - what was replaced, and a WARNING when bodies are still stacked"}), 90,+ "altium_save {filePath: libraryPath}, then altium_show_3d_chip to look at it; boards that already place the part need PCB:UpdateFootprints. Never engrave the MPN into the STEP.",+ &["normalize_bodies", "show_3d_chip", "pcb_report", "import_part", "footprint_primitives"],+ &["It REPLACES by default (from 0.2.52): data.replacedBodies says how many old bodies went and data.bodiesNow should read 1. keepExisting:true is the only way to stack a second body.", "A bad step path ('could not load model') removes nothing: the old body stays.", "data.sameNameFootprints > 1 = several footprints share the name and only the FIRST got the body: altium_remove_footprint the stale copies.", "Without footprintName it acts on the only footprint in the library (name_required when there are several).", "'could not load model' = the STEP path is not on THIS machine: stage it with send_files."], json!({"libraryPath": "C:/lib/Adom_ToF.PcbLib", "footprintName": "VL53L5CX", "step": "C:/lib/VL53L5CX.step"})), verb_doc("add_models", "Attach one or more PCBLIB footprint models, parameters, comment and description to a SchLib component (Altium's several-footprints-per-symbol mechanism).", json!({"libraryPath": "path to a .SchLib (required)", "symbolName": "string (required)", "models": "[{name, pcblib, description}]", "params": "{key: value}", "comment": "string", "description": "string", "timeout": "seconds, default 120"}),
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.51",+ "version": "0.2.52", "type": "app", "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
page.json+1−1@@ -4,7 +4,7 @@ "type": "app", "title": "Altium - the Altium Bridge", "brief": "Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.",- "version": "0.2.51",+ "version": "0.2.52", "hero": { "type": "image", "path": "screenshots/hero.png"
skills/altium-libraries/SKILL.md+11@@ -100,6 +100,17 @@ offset — this was the CH340G "orange sliver"); `add_body` auto-runs `normalize centre each body on its pad cluster. Note: the PCB **Library** editor's 3D view does not render bodies — the board 3D does; prove bodies on a board (`altium_test_board`). +**A body REPLACES, never stacks (from 0.2.52).** `add_body` (and the `step` of `import_part`)+removes the footprint's existing 3D bodies from the canvas before adding the new one, the+way `build_symbol_modes` replaces a symbol of the same LibReference. Before 0.2.52 every call+added another body, and `normalize_bodies` then centred both on the pads: the AMPLH5030S+inductor reached a board (2026-10-16) with the old and the fixed model inside each other. The+reply says `replacedBodies` (old bodies removed) and `bodiesNow` (should be 1). The old body is+only removed once the new STEP has loaded, so a bad path changes nothing. `keepExisting:true`+is the opt-out for a deliberate second body. `sameNameFootprints` above 1 means the library+holds several footprints of that name and only the FIRST got the body: remove the stale copies.+Boards that already place the part keep the old body until `PCB:UpdateFootprints`.+ ## Variants: N footprints on one component `altium_add_models {symbolName, models:[{name,pcblib,description}...], params, comment,
skills/altium-part-import/SKILL.md+7@@ -94,6 +94,13 @@ altium_save '{"filePath":"<lib>.PcbLib"}' ``` `add_symbol_art` replaces earlier art by default (`clear:true`); details in `altium-libraries`. +**Importing a part again replaces it (from 0.2.52).** `import_part` with a `step`, or+`add_body` on a footprint that already has a body, REPLACES the footprint's 3D body instead of+stacking a second one (check the add_body step's `replacedBodies` and `bodiesNow: 1`), just as+`build_symbol_modes` replaces the symbol. `add_body {keepExisting:true}` is the only way to put+a deliberate second body on a footprint. A board that already places the part still shows the+old body until `PCB:UpdateFootprints`.+ ## Path C — APPEND into an existing library ```bash
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.51+0.2.52
src/agent/AdomAgent.pas.tmpl+61−2@@ -1384,15 +1384,22 @@ Function CmdAddBody(Lines : TStringList) : String; Var PLib : IPCB_Library; Fp : IPCB_LibComponent;+ Fp2 : IPCB_LibComponent; Body : IPCB_ComponentBody; Mdl : IPCB_Model; Cont : IPCB_Contour;- Name, StepPath, Note : String;+ BIt : IPCB_BoardIterator;+ Old : IPCB_Primitive;+ Name, StepPath, Note, KeepS : String; I, BW, BH, HG, SO : Integer;- Found : Boolean;+ NRep, NBefore, NSame : Integer;+ Found, Keep : Boolean; Begin Name := Param(Lines, 'name'); StepPath := Param(Lines, 'step');+ { keep=1 is the deliberate-second-body opt-out; anything else REPLACES the footprint's bodies }+ Keep := (Param(Lines, 'keep') = '1');+ If Keep Then KeepS := 'true' Else KeepS := 'false'; PLib := PCBServer.GetCurrentPCBLibrary; If PLib = Nil Then Begin@@ -1422,6 +1429,13 @@ Begin Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'footprint not found: ' + Name) + '}'; Exit; End;+ { footprints sharing this name: more than one means the body went to the FIRST of them only }+ NSame := 0;+ For I := 0 To PLib.ComponentCount - 1 Do+ Begin+ Fp2 := PLib.GetComponent(I);+ If UpperCase(Fp2.Name) = UpperCase(Fp.Name) Then NSame := NSame + 1;+ End; PCBServer.PreProcess; Body := PCBServer.PCBObjectFactory(eComponentBodyObject, eNoDimension, eCreate_Default); { A primitive without a layer is silently dropped on AddPCBObject (measured@@ -1470,6 +1484,49 @@ Begin FIRST, then add to the CANVAS (PLib.Board); the editor owns the canvas and syncs it into the stored component on save. } PLib.CurrentComponent := Fp;+ NBefore := GroupCount(Fp, eComponentBodyObject);+ { a part imported again REPLACES its 3D body, the way build_symbol_modes replaces its symbol:+ adding on top left the old model and the fixed one inside each other on the board+ (seen 2026-10-16 on AMPLH5030S). Only now that the new model has loaded, so a bad step+ path keeps the old body. }+ NRep := 0;+ If Not Keep Then+ Begin+ { delete them the way the editor does: RemovePCBObject on the canvas or on the footprint+ left them in place after save and reopen (measured 2026-10-20: the stored footprint kept+ every body). Select exactly the canvas's bodies, end this command, run Edit>Delete+ (PCB:Clear) on the selection, then start a new command for the add. Nothing else+ may be selected: Clear deletes the whole selection. }+ BIt := PLib.Board.BoardIterator_Create;+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Old := BIt.FirstPCBObject;+ While Old <> Nil Do+ Begin+ If Old.Selected Then Old.Selected := False;+ Old := BIt.NextPCBObject;+ End;+ PLib.Board.BoardIterator_Destroy(BIt);+ BIt := PLib.Board.BoardIterator_Create;+ BIt.AddFilter_ObjectSet(MkSet(eComponentBodyObject));+ BIt.AddFilter_LayerSet(AllLayers);+ BIt.AddFilter_Method(eProcessAll);+ Old := BIt.FirstPCBObject;+ While Old <> Nil Do+ Begin+ Old.Selected := True;+ NRep := NRep + 1;+ Old := BIt.NextPCBObject;+ End;+ PLib.Board.BoardIterator_Destroy(BIt);+ If NRep > 0 Then+ Begin+ PCBServer.PostProcess;+ ResetParameters;+ RunProcess('PCB:Clear');+ PCBServer.PreProcess;+ End;+ End; PLib.Board.AddPCBObject(Body); PCBServer.SendMessageToRobots(PLib.Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Body.I_ObjectAddress); Note := Note + ':canvas';@@ -1477,6 +1534,8 @@ Begin PLib.Board.ViewManager_FullUpdate; PLib.RefreshView; Result := '{' + JRaw('ok', 'true') + ',' + JStr('footprint', Fp.Name) + ',' + JStr('body', Note) + ',' ++ JRaw('replacedBodies', IntToStr(NRep)) + ',' + JRaw('keptExisting', KeepS) + ',' ++ JRaw('bodiesBefore', IntToStr(NBefore)) + ',' + JRaw('sameNameFootprints', IntToStr(NSame)) + ',' + JRaw('bodiesNow', IntToStr(GroupCount(Fp, eComponentBodyObject))) + ',' + JRaw('bodiesOnCanvas', IntToStr(BoardBodyCount(PLib.Board))) + '}'; End;
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.51",+ "version": "0.2.52", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",