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.
main
a577bb1
1mo ago
name: altium-bridge-plan
user-invocable: false
description: >-
THE plan of record for the Altium Designer bridge (keyword altium, verbs altium_*) —
every automation surface Altium exposes, measured on a real AD 26.9.1 install, ranked into
lanes: the ScriptingSystem:RunScriptText eval, the resident DelphiScript agent (the REPL lane),
the 1,109-command process-launcher catalog, X2.EXE -R command-line bootstrap, UIA over the
VCL/DevExpress shell (and CDP for its WebView2 pages), the Altium 365 GraphQL API + embeddable
viewer, file-format parsers, the native SDK extension, and the sign-in/license gates. Includes
the verb catalog, the dialog doctrine, the phase plan with acceptance criteria, and the Phase 0
experiments still owed. Refer back here before any altium-bridge work. Trigger words — altium
bridge, altium_ verbs, drive altium, control altium designer, altium automation, altium
delphiscript, RunScriptText, altium process launcher, altium 365 api, altium sign in license,
altium add-in, altium plan.
Parent skill: adom-bridge-sdk-new-app (the method) and adom-bridge-sdk (the contract). Written 2026-08-29 from live probes of Altium Designer 26.9.1 on a Windows test machine (driven from an Adom container) plus the vendor docs and the community MCP bridges.
altium-bridge — plan of record
0. What we measured on the box (evidence, not guesses)
| Fact | Evidence |
|---|---|
| Install | C:\Program Files\Altium\AD26\X2.EXE, 26.9.1 (Build 10). System\ holds 634 DLLs, 47 .rcs process tables, 71 .ins server descriptors. No Examples\Scripts shipped. |
| Process | X2.exe pid 54932, main window class TDocumentForm, title License Management - Altium Designer (26.9.1) - No License. |
| Script engine | ScriptingSystem.dll + ScriptingSystem.ins declaring commands RunScript, RunScriptFile, RunScriptText; EditScript.dll/.rcs (script editor); Altium.EditScript.Interfaces.dll; OUT_Script.dll. Prefs page Scripting System → Global Projects / Form Designer. |
| Command catalog | 3,205 process launchers → 1,109 distinct Server:Command strings in the .rcs files: CAMTASTIC 230, PCB 211, Sch 190, TextEdit 100, WorkspaceManager 87, Client 50, SimView 44, IntegratedLibrary 28, UnifiedComponent 26, VersionControl 23, PCB3D 13, … Format: PL <id> Command='PCB:Zoom' Params='Action=...|...' Shortcut1='...' End. |
| Listening ports | none while idle. No application COM automation server registered — only document ProgIDs (AltiumLibrary, AltiumDraftsmanDocument, …). |
| UIA | The VCL/DevExpress shell IS exposed: 45 nodes on the licence screen — MainMenu MenuItems (File/View/Project/Window/Help/Preferences…), TGUIDocBarButtons, TdxTabSheet panes, all with automationIds and invokable:true. The Home / License Management page content is a WebView2 (ProgramData\...\WebView2Cache exists) — its Start Free Trial / Get License / Sign Out buttons are not in the UIA tree. |
| State dirs | %APPDATA%\Altium\Altium Designer {ED62C102-…}\ (prefs), %APPDATA%\Altium\AD26\, C:\ProgramData\Altium\Altium Designer {ED62C102-…}\Extensions\ (Altimade, Draftsman, IPC Footprint Generator, EXPORT3D, IPC2581, SchLibGenerationTool, …), HKCU\Software\Altium\Altium Designer {ED62C102-…}. |
| Auth state | Signed in to an Altium account, no license; a free Altium Designer Viewer seat is offered and a 15-day trial button exists. Vendor docs: Viewer mode prohibits scripting → licence is a hard prerequisite for every in-app lane. |
1. The surfaces, ranked into lanes
L2 — the REPL lane: ScriptingSystem:RunScriptText + a resident DelphiScript agent ⭐
Altium has a literal eval: process ScriptingSystem:RunScriptText with parameter
Text=Begin … End; (vendor docs, and the string is in ScriptingSystem.dll). Plus
RunScript(ProjectName=…|ProcName=…) and RunScriptFile. DelphiScript reaches the full DXP API
(PCBServer, SchServer, GetWorkspace, Client, RunProcess), so this is Fusion's
run_modeling_script equivalent.
Design (the same shape the two best community bridges converged on — salitronic/eda-agent
~400 tools, coffeenmusic/altium-mcp):
- Ship
Altium_Agent.PrjScr(+json_utils.pas,dispatcher.pas) in the Release zip; install it as a Global Project (Scripting System prefs) so it survives restarts. - Bootstrap from the bridge with the CLI lane:
X2.EXE -RScriptingSystem:RunScript(ProjectName="…\Altium_Agent.PrjScr"^|ProcName="StartAgent"). StartAgentruns a polling loop on the UI thread withApplication.ProcessMessagesyields (eda-agent measured ~10 ms/call active, 100 ms idle polling; auto-stop after 10 min idle unless the bridge keep-alives every 30 s). Transport: file droprequest_<id>.json→response_<id>.jsonunder the bridge cache (works in any DelphiScript); Phase 0 experiment:CreateOleObject('MSXML2.ServerXMLHTTP')for a direct HTTP long-poll to the bridge.altium_eval {text}= the agent runsRunProcess('ScriptingSystem:RunScriptText', 'Text=…')(or writes a temp.pasandRunScriptFile) and returns stdout captured to a file. Everything else —get_state,place_component,run_drc,create_footprint— is a canned eval with rich hints.
Known engine traps (all from the community bridges, all to design around, not discover again):
- DelphiScript is untyped: no records/classes/pointers; single-threaded; a runtime error
("Undeclared identifier", "Could not convert variant of type (Dispatch)") pauses the debugger
and every later run silently no-ops until it is stopped. The bridge must detect this
(heartbeat file stops updating) and recover it (UIA-click Stop in the Script IDE, or
Client:CloseDocumentthe script; then re-bootstrap). - Characters above U+00FF become
?on the way in — ASCII-escape JSON. - While the loop is resident, Altium's own script-driven buttons are unresponsive; expose
altium_agent_detach. - No scripted ECO executor:
proj_sync_pcbruns the GUI ECO and blocks on its dialog → L4.
L3 — the process-launcher lane (1,109 commands, no UI, no focus)
RunProcess('PCB:DesignRuleCheck'), RunProcess('WorkspaceManager:Compile'),
Client:OpenDocument, Client:CloseDocument, PCB:SwitchTo3D, PCB:Zoom Action=All,
Sch:AddComponentPart, WorkspaceManager:GenerateReport ObjectKind=OutputBatch|Action=Run
(the OutJob runner from Altium's own CI article). altium_run_command {command, params} exposes
the whole table; altium_describe ships the catalog parsed from the .rcs files on the box
(coffeenmusic/altium-mcp keeps one as dev/altium_process_catalog.txt). Deterministic and
background — prefer it over any menu click.
L6 — the CLI lane (bootstrap + batch)
X2.EXE "<doc>" opens documents; -R<Server>:<Command>(Param="v"^|Param2="v") remote-executes
a process. Altium's CI recipe: run a script that opens Build.OutJob, runs the OutputBatch, then
TerminateWithExitCode(0). Phase 0 experiment: confirm -R… targets the running instance
(single-instance forward) vs spawning a second X2.exe, and whether -RScriptingSystem:RunScriptText(Text="…")
works straight from the shell (that would make bootstrap one line).
L4 — UIA over the VCL shell; CDP for the WebView2 pages
desktop_ui_tree sees menus, doc-bar buttons, panels, status bar → desktop_ui_click /
desktop_ui_set for Preferences (incl. Scripting System → Global Projects install), dialogs,
the Projects/Components panels. The Home/License pages are WebView2: launch X2 with
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=<n> (Phase 0 experiment) and drive
them with CDP (nbrowser/pup-style), else image-space clicks with coordMap.shotId.
Dialog handling per fusion-driving: scan every window owned by the X2 PID, classify by title,
screenshot each, pre-empt what can be pre-empted (save first; pass every parameter; Client:CloseDocument
with explicit params), and treat any sweep as a logged defect.
L5 — Win32 lane
PostMessage(WM_COMMAND) after a GetMenu walk for cold-start actions before the agent binds;
PostMessage keys to exact hwnds; never SendInput/SetCursorPos on a default path. Altium
does not run elevated, so ab's synthetic input reaches it — but it is still the last rung.
L1 — Altium 365 cloud lane (works with the app closed)
GraphQL at https://{workspace}.altium.com/api/graphql/ (Voyager at /api/voyager, Nitro IDE),
Authorization: Bearer <token>; tokens from Admin → Developer (1-year access tokens, or
refresh + client credentials). Bounded contexts: Design (projects, variants, releases,
manufacturing packages), Library (components, symbols, footprints, part requests),
Procurement (BOMs), Platform, Collaboration (comments, tasks), Customization
(workflows, scripts, script executions — cloud-run scripts). Reads and writes. Plus the
embeddable viewer (viewer.altium.com/client/static/js/embed.js, data-project-src=<zip url>
or data-project-token) renders schematic/PCB/3D/BOM from a design zip — a render lane that
needs no desktop. Verbs: altium_a365_search/browse/project/bom/component/release/run_script,
altium_web_preview. Unknowns to verify: plan gating for tokens on a trial workspace; rate limits.
L0 — file lane (no app, no licence)
.PcbDoc/.SchDoc/.PcbLib/.SchLib are OLE-CFB containers; ASCII PcbDoc exists. Parsers:
KiCad's Altium importer (best maintained; dev-docs.kicad.org/en/import-formats/altium/),
vadmium/python-altium (schematic), thesourcerer8/altium2kicad, AtoK, PyAltium (alpha),
embedded-society/altium-designer-mcp (writes PcbLib/SchLib primitives). Use for: altium_state
evidence when the app is closed, lint, library generation (IPC-7351 from the Adom molecule
pipeline), and KiCad-side previews.
L7 — the native SDK extension (Phase 5, optional)
Altium Developer extension → SDK at C:\ProgramData\Altium\Altium Designer {…}\Extensions\Altium Developer\SDK;
DLL + .ins + .rcs; Delphi XE2+/VS2010+ C++/C#. A C# server DLL hosting an in-process HTTP
listener would be the Fusion-grade add-in (no file polling, real threads marshalled to the UI).
Costs a licensed AD + a build toolchain; do it only after L2 proves insufficient.
2. Authentication & licensing — the gates, in order
- Altium account sign-in — system browser OAuth (email / Google / SSO). Drive with
nbrowser/pup from the bridge; the user's browser is usually already signed in to their identity provider. Human step: password/2FA only. - Licence — On-Demand / Standalone / Trial (15 days) / Viewer (no scripting). The
trial button lives in the WebView2 licence page → CDP or image click, one toast to the user
because it is a contractual click.
altium_readinessmust reportlicensed:falsewith the exact next verb, and refuse in-app lanes while unlicensed (they fail silently otherwise). - Altium 365 API token — Admin → Developer in the workspace web UI; drive it, store the token
under
%LOCALAPPDATA%\Adom Bridge\bridges-cache\altium\a365.json, never in the repo. - Scripting enablement — verify whether AD 26 gates DelphiScript behind a preference
(
Legacy.Scripts.SupportOldLanguagesexists for VBScript/JScript; check for a DelphiScript gate) and whether a Global Project can be registered by writing the prefs file directly.
3. Verb catalog (v1 target)
| Group | Verbs |
|---|---|
| Contract | altium_readiness altium_status altium_describe altium_state altium_progress altium_uninstall altium_diagnostics |
| Lifecycle | altium_launch altium_close altium_signin altium_license {trial|viewer|ondemand} altium_agent_start/stop/detach |
| REPL | altium_eval {text} altium_run_command {command, params} altium_run_script {prjscr, proc} |
| Documents | altium_open_project altium_open_document altium_close_document altium_save altium_compile |
| Eight surfaces | altium_show_symbol _footprint _3d_chip _library _schematic _2d_board _3d_board _project |
| Evidence | altium_screenshot_all altium_window_info altium_check_dialogs altium_dismiss_dialogs |
| Outputs | altium_run_outjob altium_export_gerber/odb/ipc2581/step/pdf/bom altium_run_drc altium_run_erc |
| Libraries | altium_create_symbol altium_create_footprint altium_import_library altium_list_library altium_install_library_bundle |
| Cloud | altium_a365_* altium_web_preview |
| Demo | altium_demo (symbol → footprint → 3D part → schematic → 2D → 3D board, one beat per call, same shape as kicad_demo/fusion_demo) |
Every verb: _hint/_next, stable errorCode (host_app_not_installed, not_licensed,
agent_not_running, agent_wedged, foreground_reason_required, …), windowEtiquette,
dialogsDetected/dialogs[], measured progress on anything > 3 s, caller identity forwarded on
every ab callback with X-Adom-Caller-Delegate: altium.
4. bridge.json essentials
{ "name":"altium", "displayName":"Altium Designer", "spawn":{"kind":"python","entrypoint":"server.py","port":0,"persistent":true},
"verbPrefixes":["altium_"], "statusVerb":"altium_status", "uninstall":"altium_uninstall",
"detect":{"hostApp":"Altium Designer","appPathsExe":"X2.EXE","paths":{"windows":["%ProgramFiles%\\Altium\\AD*\\X2.EXE"]}},
"timeouts":{"default":60,"verbs":{"launch":240,"run_outjob":600,"export_step":360,"demo":300}},
"docs":"https://wiki.adom.inc/adom/altium-bridge" }
Python binds ADOM_BIND_HOST; scratch under %LOCALAPPDATA%\Adom Bridge\bridges-cache\altium\ with
the kicad retention rule (48 h / 300 files / oldest-first / 10-min rate limit).
5. Dialog doctrine for Altium (pre-empt, never sweep)
Commands that prompt → the pre-emption: closing a dirty doc → WorkspaceManager:SaveObject first or
Client:CloseDocument with SaveMode; ECO → run compile first and drive the ECO dialog via UIA
with a classified catalog; OutJob overwrite → pre-delete outputs; "Confirm" style boxes from
DelphiScript (ShowMessage) → never call them in the agent; Script IDE debugger halt → detect by
heartbeat and recover. Keep handlers/dialog_classify.py with title → category → resolution.
6. Phases + acceptance
- Phase 0 experiments (owed, on a LICENSED AD; ~1 day): (a) trial licence via CDP/image click
with toast; (b)
-Rforwards to running instance?; (c)RunScriptTextfrom CLI?; (d) DelphiScriptCreateOleObject('MSXML2.ServerXMLHTTP')?; (e) ProcessMessages loop keeps UI responsive?; (f) WebView2--remote-debugging-porthonoured?; (g) Global Project registration by prefs-file write?; (h) runtime-error recovery path; (i) ASCII PcbDoc export viaPCB:Export; (j) A365 token on a trial workspace. - Skeleton (1–2 days): bridge.json, server, readiness/status/describe/state/uninstall/diagnostics,
screenshot array by PID, rcs catalog parser. Green chip on a fresh VM;
statereturns real shots. - Bootstrap + REPL (3–5 days): agent PrjScr, Global Project install, CLI bootstrap, file-drop
transport,
eval,run_command, heartbeat + wedge recovery. 100 sequential evals, p50 recorded. - Eight surfaces + demo (3–5 days): each
show_*background + frame;altium_demo. Web-control page renders all eight. - Outputs + libraries (5 days): OutJob runner, exports, DRC/ERC, symbol/footprint creation, library bundle install from the wiki, dialog catalog. Adom molecule round-trip: wiki part → Altium library → board → Gerber.
- Cloud + auth (3 days): sign-in + licence driven with one toast, A365 verbs, web preview.
- Publish: Release zip + manifest, skills pkg (
altium-bridgecore +altium-driving,altium-libraries,altium-a365,altium-signinsub-skills), 30–40 discovery triggers, hero,make release, audit checklist. Then (optional) Phase 5: C# SDK extension.
6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial) — the facts the code is built on
| Experiment | Result |
|---|---|
| Trial licence | Start Free Trial (WebView2, image click) → Edge opens altium.com/365-intro/activate-add-eval → workspace form (name, region, terms, Activate) → a workspace is provisioned at <name>.365.altium.com in ~60 s → back in AD: Refresh, then Use License on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to … - Trial. |
-R forwarding |
A second X2.EXE -R<Server>:<Cmd>(…) forwards to the running instance and exits in ~2–8 s. If the instance is busy (any modal, or a script running) the helper hangs and shows its own "There is another instance … busy" Information dialog — kill it. |
RunScriptText |
Works from the shell: -RScriptingSystem:RunScriptText(Text="Begin RunApplication('…'); End;"). Statements only — a Var block is "Unknown identifier: Var". |
RunScript |
-RScriptingSystem:RunScript(ProjectName="…\X.PrjScr"|ProcName="Main") works with a plain | when the arg is passed VERBATIM (PowerShell Start-Process, or Python Popen("<one string>")). Popen([exe, arg]) re-quotes (the path has a space) and the helper never forwards. ^| is only for cmd.exe. |
| Concurrency | One script at a time. A -R script while the agent loop is resident raises Error: Another script executing now on the MAIN window; every later -R is refused until it is dismissed. |
| Late binding | An unknown identifier (CreateOleObject, RunProcessWithParameters) is a runtime error: Error: Undeclared identifier … modal + the Script editor opens with the debugger halted on that line. The halted script counts as executing. |
| Recovery | Press OK on the Error (background BM_CLICK), then X2.EXE -REditScript:Stop (helper exits ≈1 s), then scripts run again. Implemented as altium_agent_recover. |
CreateOleObject |
Not available → no HTTP from inside. Transport is file drop. |
FindFirst('request_*.txt') |
Never matched from the engine. Use ONE fixed inbox request.txt (atomic tmp+rename, serialized by the bridge). |
RunProcess |
ResetParameters; AddStringParameter(k,v)…; RunProcess('Client:ArrangeAllWindows') works from the loop: 828 ms in-agent. |
| Agent loop | Application.ProcessMessages + Sleep(10) (100 ms idle) keeps AD responsive; heartbeat hb.txt every 500 ms; start → heartbeat 1.5 s; eval round trip 0.7 s; run_command 1.4 s. RunScript opens the .PrjScr as a document in the Projects panel (harmless). |
| ab spawn | Port 47311 is in a Hyper-V reserved range (WinError 10013) — always --port 0/ab-assigned. shell_execute does not return while a process it started (the server) is alive — start long-lived processes via ab's spawn/launch_process, never a foreground shell. |
The bridge source lives in ~/project/altium-bridge/src (server.py, handlers/agent.py, handlers/dialogs.py, handlers/rcs_catalog.py, handlers/altium_detect.py, agent/AdomAgent.pas.tmpl).
6c. Phase 1–3 RESULTS (2026-08-29 evening) — what is DONE
- ab adopted the bridge from
bridges-cache\altiumafterrefresh_bridges {name:"altium"}; it spawns on stable port 64070;adom-bridge altium_*works from the container.bridge_kill {name,confirm:true,reason}reloads code. - All eight surfaces return real frames (base64 PNG of the main window, background, foreground restored): show_project 6.7 s, show_schematic 6.8 s, show_2d_board 8.7 s, show_3d_board 14.5 s, show_library 21.6 s (39 components listed), show_symbol 18.4 s, show_footprint 5.1 s warm (a cold PcbLib open blew ab's 60 s default — declare
timeoutsand expect it), show_3d_chip 16.4 s.altium_demo {stage}chains them. - Agent lane: eval 0.7–1.1 s, run_command ~1.4 s,
lib_list/lib_selectviaSchServer.GetCurrentSchDocument+SchLibIterator_Create/PCBServer.GetCurrentPCBLibrary.GetComponent(i)+CurrentComponent. - Opening docs:
Client.OpenDocument('', path)returns Nil on AD 26 — useResetParameters; AddStringParameter('ObjectKind','Document'|'Project'); AddStringParameter('FileName',path); RunProcess('WorkspaceManager:OpenObject'), thenClient.GetDocumentByPath+Client.ShowDocument. - Three real-world races found and fixed: (1) a busy loop (long RunProcess) has no heartbeat → never auto-start a second loop (
alive= hb < 60 s,responsive= < 3 s); (2) a killed -R helper's request is still DELIVERED later by Altium → reap stray X2 helpers before every start; (3)tasklisttook 7 s under load → Toolhelp32 snapshot in-process. - Dialog text is PAINTED (no Static child, not in UIA) —
altium_check_dialogsattaches a base64 shot per dialog; OK buttons areTXPBitBtn(BM_CLICK) with abdesktop_ui_clickas fallback.
6d. Phase 4 RESULTS (2026-08-29 night) — outputs lane
altium_compile/altium_run_erc:WorkspaceManager:Compile Action=Compile|ObjectKind=Project, then read the Messages panel viaGetWorkspace.DM_MessagesManager→MessagesCount,Messages(i).MsgClass/Source/Document/Text(DM_MessageCountdoes NOT exist). 1.7 s. Declare the manager asIDMMessagesManager(that type name compiles);IMessagesManager/IMessageItemas Var types give "Error in declaration block".altium_run_outjob {outjobPath, medium}→WorkspaceManager:OpenObjectthe .OutJob, thenWorkspaceManager:GenerateReport Action=Run|ObjectKind=OutputBatch|OutputMedium=<name>|DisableDialog=True. Fabrication.OutJob → Gerber + NC Drill + ODB++ + DRC report in 23.6 s; Assembly.OutJob → BOM .xlsx + pick-and-place in 16.6 s. Output lands inProject Outputs for <project>next to the project.altium_export_gerber/altium_export_bomare those two mediums.altium_run_drc:PCB:DesignRuleCheckopens a MODAL "Design Rule Checker [mm]" (automationIdRun_Button= "Run Design Rule Check...") — the agent blocks inside RunProcess, the bridge clicks the button through ab UIA in the background, then reads violations off the board (BoardIteratorfilteredeViolationObject,V.Rule.Name,V.Description).- Script-engine gotchas that cost the most time: (1) never nest
{ }inside a brace comment — the inner}ends the comment and the rest becomes code ("Error in declaration block"); (2) Altium keeps the .PrjScr/.pas OPEN in its Script editor after RunScript and re-runs the STALE in-editor copy after you overwrite the file — the bridge now rendersAdomAgent_<digest>.pas/.PrjScrper source change; (3) a failed RunScript target opens the "Select Item To Run" picker (TForm_RunScriptParameters) and disables the main window — detect modals by "main window disabled + another enabled top-level window of the same PID", not by class. - Foreground: the -R bootstrap activates Altium; the bridge captures the user's foreground before and hands it back immediately and during the wait (John: "stop foregrounding on me"). Restart the agent as rarely as possible — every restart is one activation.
6e. Phase 4 COMPLETE (2026-08-29) — library authoring
altium_create_footprint {name, libraryPath, pads:"des,x,y,w,h,rect|round,hole;…"}— opens or CREATES the .PcbLib (CreateNewDocumentFromDocumentKind('PCBLIB')+Doc.DoSafeChangeFileNameAndSave(path,'PCBLIB')),PCBServer.CreatePCBLibComp, pads viaPCBObjectFactory(ePadObject…)+Comp.AddPCBObject,PLib.RegisterComponent,CurrentComponent,Board.ViewManager_FullUpdate. 3.1 s, returns a frame.altium_create_symbol {name, libraryPath, designator, bodyWidthMil, bodyHeightMil, pins:"des,name,left|right|top|bottom,offset;…"}—SchObjectFactory(eSchComponent),eRectanglebody,ePins withOrientation/PinLength,SLib.AddSchComponent+RobotManager.SendMessage(... SCHM_PrimitiveRegistration ...). 1.1 s.altium_save—WorkspaceManager:SaveObject ObjectKind=FocusedDocument. Both libraries verified on disk.- Release kit in the repo:
SKILL.md(user skill),README.md,package.json(39 discovery triggers),install.sh/uninstall.sh,CHANGELOG.md,scripts/release.py(runtime zip 52 KB + manifest). NOT yet published — the wiki already has Barrett'sbarrett-land/adom-desktop-altium-bridgeusing namealtium/prefixaltium_; resolve rename-vs-supersede first.
6f. Barrett merge + taskbar (2026-08-29/30)
Ported from barrett-land/adom-desktop-altium-bridge (v0.0.55) into the resident agent — all measured working: altium_set_params (parameters incl. LCSC Part #, comment, footprint link via ISch_Implementation.AddDataFileLink), altium_add_body (extruded contour or ModelFactory_FromFilename STEP), altium_install_library (IntegratedLibraryManager.InstallLibrary), altium_compile_intlib / altium_extract_intlib (CreateIntegratedLibrary / ExtractSources). His AdomBridge .NET extension (prebuilt DLL + .ins/.rcs + Install-AdomBridge.ps1 + REMOVE cmd) is vendored under src/extension/ and exposed as altium_install_extension (Altium CLOSED), altium_extension_ping, altium_export_step (headless STEP with Holes=All|Copper|Models), altium_import_kicad (no-wizard import). Dispatch is X2.EXE -RAdomBridge:<Process>(Req=<space-free request file>) — a server process, not a script, so it coexists with the resident agent. Facts from his page worth keeping: -R splits on whitespace (parameters go in a request file), OutJobs only generate when they are project members, STEP via OutJob no-ops headlessly, Client.ShowDocumentDontFocus exists (the agent's open now prefers it).
Taskbar: handlers/taskbar.py — overlay badge via ab desktop_taskbar {overlay:{icon}} on the first driving verb (cleared on idle), measured-estimate progress bar for verbs ≥3 s, no AUMID stamping. altium_state.drivingBadge reports {active, lastError}.
6g. Phase 5 RESULTS + the extension verdict (2026-08-30)
- AdomBridge .NET extension (Barrett's) does NOT load into the running AD 26.9.1 instance. Registered via his Install-AdomBridge.ps1 (registry node added cleanly, 20 items), Altium restarted → a
Could not start Vault Explorerwarning at startup;X2 -RAdomBridge:Pinghelpers waited ~65–90 s on the startup mutex and then hosted the server in themselves (their own DXP_Startup.log says "Starting server AdomBridge"), the main instance never did;RunProcess('AdomBridge:Ping')from the resident agent returned in 16 ms and wrote nothing (unknown process → silently ignored). Unregistered again (altium_install_extension {remove:true}). Verbs kept, fail withextension_timeout+ this note. Next step if wanted: rebuild against AD 26's in-box .NET assemblies (Barrett'sadom-altium-extensionproject) and verify with a clean-profile Altium. - Altium 365 API on the Develop trial: Admin shows Settings / Part Providers / Explorer / Usage and Billing / Extensions — no Developer page, every guessed token URL 404s.
altium_a365_*verbs are wired for a token from a Pro/Enterprise workspace. - Account/licence:
altium_signin_status,altium_license {action}(image-space clicks on the WebView2 page with measured coords: refresh (1288,178), use_license (1252,388), start_trial (788,607) on a 1400×840 capture — verify against the returned shot),altium_connect_workspace(opens theNot Connectedpicker,TWorkspacesForm, custom list without UIA children → returns a shot to click). - Saving example projects on close: Altium asks "Confirm Save Locally for (N) Modified Documents" (WPF grid: Save All / Save None / OK / Cancel, all UIA-invokable) —
altium_closereports it; answer Save None + OK for scratch work.
6h. First end-to-end demo — Ray's CH340G (2026-08-30) + two bugs found
Imported ray/ch340g (WCH USB-serial, SOP-16) from the wiki into a NATIVE Altium library entirely through the bridge, then walked the surfaces. WORKED, all of that chip: altium_create_footprint (16 SOIC pads parsed from the page's pads.json, mm→mil), altium_add_body (the page's real 217 KB KiCad STEP), altium_create_symbol (16 named pins GND/TXD/RXD/UD+/VCC/…), altium_set_params (MPN/mfr/pkg + footprint link), altium_show_symbol / _footprint / _library, altium_place_on_pcb (new place_on_pcb verb: IPCB_LibComponent.Replicate → Board.AddPCBObject → the footprint lands on a real PcbDoc, 16 pads), altium_show_2d_board.
TWO REAL BUGS surfaced — fix before publishing:
place_on_schloses the symbol graphics.ISch_Component.Replicateonto a fresh sheet placed the component (U1 designator + CH340G comment appear) but NOT its rectangle/pins — only the two text strings render. SettingCurrentPartID:=1/DisplayMode:=0on the copy did not fix it. The primitives inside the component group aren't being carried/registered onto the target sheet. Likely need to iterate the lib component's children andAddSchObjecteach onto the instance, or place viaIntegratedLibraryManagerrather thanReplicate.- STEP 3D body renders OFF-ORIGIN.
add_bodywithModelFactory_FromFilenameattaches (returns ok), but in both the library 3D view and on the board the body sits at the model's native origin (a small sliver at the board corner), not over the pads — so the 3D board shows bare gold pads. Reposition the body to the footprint centroid afterSetState_FromModel(setBody.ModelPosition/move), or bake the offset.
Also: PCB:SetCamera(To=RotateFit_*) is a no-op via RunProcess on AD 26.9.1 even with the window raised — the 3D view stays dead top-down. A real isometric needs either a focused numpad-key rotation (SendInput) or a different camera API; do not rely on SetCamera. Board substrate also renders white (no green plane) in the library/loose-PcbDoc 3D — a board shape + layer stack is needed for a proper 3D board.
Net for a demo today: symbol, footprint, library manager, schematic-with-part (text only), 2D board with the real footprint — all solid; 3D-with-body and schematic-graphics need the two fixes above.
6i. Demo fixes (2026-08-30) — schematic FIXED, one important bridge bug FIXED, 3D body still open
FIXED — place_on_sch now renders the full symbol. ISch_Component.Replicate does NOT re-parent a library part's pins/rectangle onto a normal sheet (only the designator/comment text placed). The working path: build a fresh SchServer.SchObjectFactory(eSchComponent), iterate the lib component's children (ePin/eRectangle/eLine/…), child.Replicate each, set OwnerPartId:=1/OwnerPartDisplayMode:=0, Inst.AddSchObject(child), then RegisterSchObjectInContainer + SendMessage. Proven on Ray's CH340G — 17 primitives (rect + 16 pins) render with all pin names on CH340G_Final.SchDoc. (Setting OwnerPartDisplayMode alone on the ORIGINAL symbol did NOT fix Replicate — the copy-primitives approach is required.)
FIXED — agent_start silently ran STALE code. The DelphiScript loop lives inside Altium; bridge_kill restarts the ab PROCESS but not the loop, and start() early-returned alreadyRunning when it saw the old loop's heartbeat — so redeployed agent code never loaded (every "fix" this session needed a manual agent_stop first). Fix: the agent stamps its generation (AGENT_GEN = template digest) into hb.txt; start() compares the resident gen to the current template digest and auto-stops+restarts a stale loop. ⚠ TWO traps hit while adding it: (1) _agent_base() must hash the RAW template, NOT rendered_agent_source() — the latter recursed and crashed the whole bridge; (2) editing agent.py/server.py needs a bridge_kill to reload the PROCESS, separate from restarting the in-Altium loop.
STILL OPEN — 3D component body does not render over the pads in the PCB Library 3D view. add_body attaches (STEP via ModelFactory_FromFilename, or an extruded contour) and returns ok, but neither shows as a solid over the pads in the footprint 3D editor — both appear as a small sliver in a corner. A post-add MoveByXY(BoundingRectangle…) centering attempt was WRONG (moved it further off) and has been reverted. Likely causes to chase next: STEP unit/scale on import; the PCB Library editor's "Display 3D Bodies" option; and bodies render reliably only on a real board (place the footprint on a PcbDoc WITH a layer stack + board shape, then view 3D) — the loose-PcbDoc/library 3D view is the weak path. Also PCB:SetCamera(To=RotateFit_*) is a no-op via RunProcess, so the view stays top-down regardless — a real isometric needs SendInput numpad keys to the focused 3D window. The Altium main window also self-MINIMIZED during the 3D switches; desktop_set_window_state {state:restore} fixes the sliver captures.
6j. 3D BODY FIXED end-to-end (2026-08-30) — five root causes, all measured
The chip now renders in 3D on a real board (black body + 16 gold pads on the green PCB, CH340G_Board14.PcbDoc). It took FIVE stacked fixes, each verified by data (altium_pcb_report — a new diagnostic verb that dumps per-component pads/bodies with layer + bbox in mils, plus a board object census by ObjectId):
- A primitive with no Layer is silently dropped.
add_bodynever setBody.Layer; pads always set theirs. Fix:Body.Layer := eMechanical1(+ enableLayerStack.LayerObject[eMechanical1].MechanicalLayerEnabledandLayerIsDisplayed) — andBody.BodyOpacity3D := 1.0. - The stored lib component is NOT the editing surface.
Fp.AddPCBObject(Body)counted 1 body immediately but the body was GONE after save+reopen — the editor canvas copy overwrites the stored component. Fix:PLib.CurrentComponent := FpFIRST, then add to the CANVAS (PLib.Board.AddPCBObject(Body)); the editor persists the canvas into the component on save. (bodiesNow=1AND survives reopen:libBodies=1.) - Group
Replicateof anIPCB_LibComponentDISSOLVES on a board — census showed 16 free pads, noeComponentObject, no body. Fix (same cure as the schematic): freshPCBObjectFactory(eComponentObject), copy each child with primitive-levelReplicate+Inst.AddPCBObject, and — critical — fill the group BEFOREBoard.AddPCBObject(Inst): children added to an already-registered component exist in data but never draw, whatever address they're robot-registered to. - The lib stores pads around the canvas origin (~50000,50000 mil) but an authored body contour around 0,0 — one group move can't land both (body hit the target, pads landed 50000 mil away). Fix: normalize per cluster — compute the copied PAD bbox center and
MoveByXYpads (and non-text children) bytarget−center; move each BODY to the target by its own bbox center. - Assigning
Inst.x/yAFTER moving children re-translates the whole group (everything landed at 8000,6000). AnchorInst.x/y := targetBEFORE the normalization. Also hideNameOn/CommentOn(the factory "Designator1" text is huge and hijacks Zoom All; hidden texts still bloat the bbox — frame withBoard.GraphicalView_ZoomOnRect(...)via eval instead of trusting Zoom All).
More traps measured on the way: GetCurrentPCBBoard answers with the LIBRARY's internal board when a PcbLib is focused (a board-reuse path placed into the lib canvas and renamed the lib doc — always create the PcbDoc and only then read GetCurrentPCBBoard); GetCurrentPCBLibrary answers even when a board is focused (pcb_report needs want:"board"); a nil BoardOutline.BoundingRectangle, SetFileName on an unassigned doc, and GroupIterator_Create on IPCB_Board all HALT the engine as "Undeclared identifier"; IPCB_Component.Name.Text := halts too (use NameOn/MoveToXY on Inst.Name). GroupIterators need AddFilter_LayerSet(AllLayers) or mech-layer children are invisible to them. WriteAtomic stage breadcrumbs into the workspace (agent/stage.txt) pinpoint hangs.
Still open (minor): the PCB Library editor 3D view does not render bodies (the board 3D does — use show_3d_chip for selection, the placed-board flow for true 3D); PCB:SetCamera remains a no-op so views are framed with GraphicalView_ZoomOnRect; the schematic place_on_sch fix (§6i) still stands. Cleanup owed: the fix session littered CH340G_v2..v8 libs and CH340G_Board..Board15 docs in Downloads and many open tabs.
Progress surface ruling (John, 2026-08-30)
Long-job progress goes on the Windows taskbar icon ONLY (desktop_taskbar progress on Altium's button — handlers/taskbar.py already paces it per verb). NOT ab progress toasts: "when i say progress bar i just mean in the windows taskbar icons." Toasts remain the nuclear only-a-human-can-act channel.
Ralph rule for generated CAD (John, 2026-08-30)
Frame-check EVERY generated look/variant, not a sample — the shipped symbol had colliding 4-side names, visible param texts (SetSchParam defaulted IsHidden False; now True), and std==2col, all invisible in a 3-of-10 spot check. symbol_modes.py now sizes bodies from measured name lengths (55 mil/char); ralph the full matrix before reporting.
6k. LAN7800 creative import (2026-08-30) — variants, display modes, footprint models, board proof
New verbs (all agent-lane, all measured): altium_build_footprint {libraryPath, spec} (spec from tools/kicad2altium.py: pads incl. duplicate-designator through-hole vias, roundrect %, tracks/arcs/filled discs on named mech layers), altium_build_symbol_modes {libraryPath, spec} (ONE component, N display modes from tools/symbol_modes.py), altium_symbol_mode {mode} (editor-driven: Sch:ToggleComponentDisplayMode Action=Mode0/Next — Comp.DisplayMode changes data only), altium_add_models {models[], params, comment, description} (N PCBLIB implementations = Altium footprint variants; the user picks the current footprint on the placed part), altium_normalize_bodies, altium_test_board {footprintName, wMm, hMm, assigns, stubs}, altium_board_shape {x0Mm,y0Mm,wMm,hMm} (process PCB:PlaceBoardOutline Mode=BOARDOUTLINE_FROM_SEL_PRIMS on 4 temp selected tracks — BoardOutline.PointCount/Segments is a silent no-op, .Clear halts), altium_swap_footprint {footprintName, libraryPath, assigns} (remove + per-primitive re-place at the same x/y, nets re-assigned by designator — tracks untouched, so wiring survives), altium_pcb_report gained padsBboxMil, nets, tracks.
Dialog pre-emption (John: a dismissed dialog is a defect). The DelphiScript debugger breaks on ANY raised exception even inside Try/Except — a raise IS an Error modal. ReadAll now probes with FileOpen (returns -1, never raises) before LoadFromFile. A halted command leaves PCBServer.PreProcess open = "a command is currently active" → save draws the save-a-copy Warning: TxClear (unbalanced PostProcess, harmless) runs at agent start, before every save, and after every recover. Undeclared identifiers hit this session: BottomXSize/Mid* pad sizes, IPCB_BoardOutline.Clear, eProcessFreePrimitives, IPCB_Board.GroupIterator_Create.
Library canvas offset. Bodies added through the PcbLib canvas land 50000 mil from the pads in the stored frame; the editor then paints an EMPTY canvas (Zoom All frames 50 inches; the CH340G "orange sliver" was this). normalize_bodies centres each body on its pad cluster; add_body now realigns automatically. GroupIterators need AddFilter_LayerSet(AllLayers) or mech-layer children are invisible.
Frames under a locked session. GDI editors (schematic, PcbLib 2D) still capture; the DirectX PCB canvas paints BLACK while the Windows session is locked (desktop_get_foreground = 0, desktop_list_windows = 0, full-screen capture white). Check the session before trusting a board frame.
7. Sources
Altium docs: scripting overview/setup, running scripts, DelphiScript, Altium SDK overview, Altium
365 API quick start + API page, embeddable viewer, licensing, sign-in. Vendor CI article (X2 -R
recipe). Community: salitronic/eda-agent, coffeenmusic/altium-mcp, ee-in-a-box/altium-copilot,
embedded-society/altium-designer-mcp, 10xgenomicsEng/altium-mcp-unified,
oharown/mcp-codex-altium-designer, Altium-Designer-addons/scripts-libraries, KiCad Altium
importer docs, vadmium/python-altium, thesourcerer8/altium2kicad.
---
name: altium-bridge-plan
user-invocable: false
description: >-
THE plan of record for the Altium Designer bridge (keyword `altium`, verbs `altium_*`) —
every automation surface Altium exposes, measured on a real AD 26.9.1 install, ranked into
lanes: the ScriptingSystem:RunScriptText eval, the resident DelphiScript agent (the REPL lane),
the 1,109-command process-launcher catalog, X2.EXE -R command-line bootstrap, UIA over the
VCL/DevExpress shell (and CDP for its WebView2 pages), the Altium 365 GraphQL API + embeddable
viewer, file-format parsers, the native SDK extension, and the sign-in/license gates. Includes
the verb catalog, the dialog doctrine, the phase plan with acceptance criteria, and the Phase 0
experiments still owed. Refer back here before any altium-bridge work. Trigger words — altium
bridge, altium_ verbs, drive altium, control altium designer, altium automation, altium
delphiscript, RunScriptText, altium process launcher, altium 365 api, altium sign in license,
altium add-in, altium plan.
---
Parent skill: **adom-bridge-sdk-new-app** (the method) and **adom-bridge-sdk** (the contract).
Written 2026-08-29 from live probes of Altium Designer 26.9.1 on a Windows test machine
(driven from an Adom container) plus the vendor docs and the community MCP bridges.
# altium-bridge — plan of record
## 0. What we measured on the box (evidence, not guesses)
| Fact | Evidence |
|---|---|
| Install | `C:\Program Files\Altium\AD26\X2.EXE`, 26.9.1 (Build 10). `System\` holds 634 DLLs, 47 `.rcs` process tables, 71 `.ins` server descriptors. No `Examples\Scripts` shipped. |
| Process | `X2.exe` pid 54932, main window class **`TDocumentForm`**, title `License Management - Altium Designer (26.9.1) - No License`. |
| Script engine | `ScriptingSystem.dll` + `ScriptingSystem.ins` declaring commands **`RunScript`, `RunScriptFile`, `RunScriptText`**; `EditScript.dll/.rcs` (script editor); `Altium.EditScript.Interfaces.dll`; `OUT_Script.dll`. Prefs page *Scripting System → Global Projects / Form Designer*. |
| Command catalog | **3,205 process launchers → 1,109 distinct `Server:Command` strings** in the `.rcs` files: CAMTASTIC 230, PCB 211, Sch 190, TextEdit 100, WorkspaceManager 87, Client 50, SimView 44, IntegratedLibrary 28, UnifiedComponent 26, VersionControl 23, PCB3D 13, … Format: `PL <id> Command='PCB:Zoom' Params='Action=...\|...' Shortcut1='...' End`. |
| Listening ports | **none** while idle. No application COM automation server registered — only document ProgIDs (`AltiumLibrary`, `AltiumDraftsmanDocument`, …). |
| UIA | The VCL/DevExpress shell **IS exposed**: 45 nodes on the licence screen — `MainMenu` MenuItems (File/View/Project/Window/Help/Preferences…), `TGUIDocBarButton`s, `TdxTabSheet` panes, all with `automationId`s and `invokable:true`. The **Home / License Management page content is a WebView2** (`ProgramData\...\WebView2Cache` exists) — its *Start Free Trial / Get License / Sign Out* buttons are **not** in the UIA tree. |
| State dirs | `%APPDATA%\Altium\Altium Designer {ED62C102-…}\` (prefs), `%APPDATA%\Altium\AD26\`, `C:\ProgramData\Altium\Altium Designer {ED62C102-…}\Extensions\` (Altimade, Draftsman, IPC Footprint Generator, EXPORT3D, IPC2581, SchLibGenerationTool, …), `HKCU\Software\Altium\Altium Designer {ED62C102-…}`. |
| Auth state | Signed in to an Altium account, **no license**; a free *Altium Designer Viewer* seat is offered and a **15-day trial** button exists. Vendor docs: **Viewer mode prohibits scripting** → licence is a hard prerequisite for every in-app lane. |
## 1. The surfaces, ranked into lanes
### L2 — the REPL lane: `ScriptingSystem:RunScriptText` + a resident DelphiScript agent ⭐
Altium has a literal eval: process **`ScriptingSystem:RunScriptText`** with parameter
**`Text=Begin … End;`** (vendor docs, and the string is in `ScriptingSystem.dll`). Plus
`RunScript(ProjectName=…|ProcName=…)` and `RunScriptFile`. DelphiScript reaches the full DXP API
(`PCBServer`, `SchServer`, `GetWorkspace`, `Client`, `RunProcess`), so this is Fusion's
`run_modeling_script` equivalent.
Design (the same shape the two best community bridges converged on — `salitronic/eda-agent`
~400 tools, `coffeenmusic/altium-mcp`):
1. Ship `Altium_Agent.PrjScr` (+ `json_utils.pas`, `dispatcher.pas`) in the Release zip; install
it as a **Global Project** (Scripting System prefs) so it survives restarts.
2. Bootstrap from the bridge with the CLI lane: `X2.EXE -RScriptingSystem:RunScript(ProjectName="…\Altium_Agent.PrjScr"^|ProcName="StartAgent")`.
3. `StartAgent` runs a **polling loop on the UI thread** with `Application.ProcessMessages`
yields (eda-agent measured ~10 ms/call active, 100 ms idle polling; auto-stop after 10 min idle
unless the bridge keep-alives every 30 s). Transport: **file drop** `request_<id>.json` →
`response_<id>.json` under the bridge cache (works in any DelphiScript); **Phase 0 experiment**:
`CreateOleObject('MSXML2.ServerXMLHTTP')` for a direct HTTP long-poll to the bridge.
4. `altium_eval {text}` = the agent runs `RunProcess('ScriptingSystem:RunScriptText', 'Text=…')`
(or writes a temp `.pas` and `RunScriptFile`) and returns stdout captured to a file.
Everything else — `get_state`, `place_component`, `run_drc`, `create_footprint` — is a canned
eval with rich hints.
Known engine traps (all from the community bridges, all to design around, not discover again):
- **DelphiScript is untyped**: no records/classes/pointers; single-threaded; a runtime error
("Undeclared identifier", "Could not convert variant of type (Dispatch)") **pauses the debugger
and every later run silently no-ops** until it is stopped. The bridge must detect this
(heartbeat file stops updating) and recover it (UIA-click *Stop* in the Script IDE, or
`Client:CloseDocument` the script; then re-bootstrap).
- Characters above **U+00FF become `?`** on the way in — ASCII-escape JSON.
- While the loop is resident, Altium's **own** script-driven buttons are unresponsive; expose
`altium_agent_detach`.
- No scripted ECO executor: `proj_sync_pcb` runs the GUI ECO and blocks on its dialog → L4.
### L3 — the process-launcher lane (1,109 commands, no UI, no focus)
`RunProcess('PCB:DesignRuleCheck')`, `RunProcess('WorkspaceManager:Compile')`,
`Client:OpenDocument`, `Client:CloseDocument`, `PCB:SwitchTo3D`, `PCB:Zoom Action=All`,
`Sch:AddComponentPart`, `WorkspaceManager:GenerateReport ObjectKind=OutputBatch|Action=Run`
(the OutJob runner from Altium's own CI article). `altium_run_command {command, params}` exposes
the whole table; `altium_describe` ships the catalog parsed from the `.rcs` files on the box
(`coffeenmusic/altium-mcp` keeps one as `dev/altium_process_catalog.txt`). Deterministic and
background — prefer it over any menu click.
### L6 — the CLI lane (bootstrap + batch)
`X2.EXE "<doc>"` opens documents; `-R<Server>:<Command>(Param="v"^|Param2="v")` remote-executes
a process. Altium's CI recipe: run a script that opens `Build.OutJob`, runs the OutputBatch, then
`TerminateWithExitCode(0)`. **Phase 0 experiment:** confirm `-R…` targets the *running* instance
(single-instance forward) vs spawning a second `X2.exe`, and whether `-RScriptingSystem:RunScriptText(Text="…")`
works straight from the shell (that would make bootstrap one line).
### L4 — UIA over the VCL shell; CDP for the WebView2 pages
`desktop_ui_tree` sees menus, doc-bar buttons, panels, status bar → `desktop_ui_click` /
`desktop_ui_set` for Preferences (incl. *Scripting System → Global Projects* install), dialogs,
the Projects/Components panels. The Home/License pages are **WebView2**: launch X2 with
`WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=<n>` (Phase 0 experiment) and drive
them with CDP (`nbrowser`/pup-style), else image-space clicks with `coordMap.shotId`.
Dialog handling per `fusion-driving`: scan every window owned by the X2 PID, classify by title,
screenshot each, pre-empt what can be pre-empted (save first; pass every parameter; `Client:CloseDocument`
with explicit params), and treat any sweep as a logged defect.
### L5 — Win32 lane
`PostMessage(WM_COMMAND)` after a `GetMenu` walk for cold-start actions before the agent binds;
`PostMessage` keys to exact hwnds; **never** `SendInput`/`SetCursorPos` on a default path. Altium
does not run elevated, so ab's synthetic input reaches it — but it is still the last rung.
### L1 — Altium 365 cloud lane (works with the app closed)
GraphQL at `https://{workspace}.altium.com/api/graphql/` (Voyager at `/api/voyager`, Nitro IDE),
`Authorization: Bearer <token>`; tokens from **Admin → Developer** (1-year access tokens, or
refresh + client credentials). Bounded contexts: **Design** (projects, variants, releases,
manufacturing packages), **Library** (components, symbols, footprints, part requests),
**Procurement** (BOMs), **Platform**, **Collaboration** (comments, tasks), **Customization**
(**workflows, scripts, script executions** — cloud-run scripts). Reads *and writes*. Plus the
**embeddable viewer** (`viewer.altium.com/client/static/js/embed.js`, `data-project-src=<zip url>`
or `data-project-token`) renders schematic/PCB/3D/BOM from a design zip — a render lane that
needs no desktop. Verbs: `altium_a365_search/browse/project/bom/component/release/run_script`,
`altium_web_preview`. Unknowns to verify: plan gating for tokens on a trial workspace; rate limits.
### L0 — file lane (no app, no licence)
`.PcbDoc/.SchDoc/.PcbLib/.SchLib` are OLE-CFB containers; ASCII PcbDoc exists. Parsers:
KiCad's Altium importer (best maintained; `dev-docs.kicad.org/en/import-formats/altium/`),
`vadmium/python-altium` (schematic), `thesourcerer8/altium2kicad`, `AtoK`, `PyAltium` (alpha),
`embedded-society/altium-designer-mcp` (writes PcbLib/SchLib primitives). Use for: `altium_state`
evidence when the app is closed, lint, library generation (IPC-7351 from the Adom molecule
pipeline), and KiCad-side previews.
### L7 — the native SDK extension (Phase 5, optional)
Altium Developer extension → SDK at `C:\ProgramData\Altium\Altium Designer {…}\Extensions\Altium Developer\SDK`;
DLL + `.ins` + `.rcs`; Delphi XE2+/VS2010+ C++/**C#**. A C# server DLL hosting an in-process HTTP
listener would be the Fusion-grade add-in (no file polling, real threads marshalled to the UI).
Costs a licensed AD + a build toolchain; do it only after L2 proves insufficient.
## 2. Authentication & licensing — the gates, in order
1. **Altium account sign-in** — system browser OAuth (email / Google / SSO). Drive with
`nbrowser`/pup from the bridge; the user's browser is usually already signed in to their identity provider. Human step: password/2FA only.
2. **Licence** — On-Demand / Standalone / **Trial (15 days)** / **Viewer (no scripting)**. The
trial button lives in the WebView2 licence page → CDP or image click, one toast to the user
because it is a contractual click. `altium_readiness` must report `licensed:false` with the
exact next verb, and refuse in-app lanes while unlicensed (they fail silently otherwise).
3. **Altium 365 API token** — Admin → Developer in the workspace web UI; drive it, store the token
under `%LOCALAPPDATA%\Adom Bridge\bridges-cache\altium\a365.json`, never in the repo.
4. **Scripting enablement** — verify whether AD 26 gates DelphiScript behind a preference
(`Legacy.Scripts.SupportOldLanguages` exists for VBScript/JScript; check for a DelphiScript
gate) and whether a Global Project can be registered by writing the prefs file directly.
## 3. Verb catalog (v1 target)
| Group | Verbs |
|---|---|
| Contract | `altium_readiness` `altium_status` `altium_describe` `altium_state` `altium_progress` `altium_uninstall` `altium_diagnostics` |
| Lifecycle | `altium_launch` `altium_close` `altium_signin` `altium_license {trial\|viewer\|ondemand}` `altium_agent_start/stop/detach` |
| REPL | `altium_eval {text}` `altium_run_command {command, params}` `altium_run_script {prjscr, proc}` |
| Documents | `altium_open_project` `altium_open_document` `altium_close_document` `altium_save` `altium_compile` |
| Eight surfaces | `altium_show_symbol` `_footprint` `_3d_chip` `_library` `_schematic` `_2d_board` `_3d_board` `_project` |
| Evidence | `altium_screenshot_all` `altium_window_info` `altium_check_dialogs` `altium_dismiss_dialogs` |
| Outputs | `altium_run_outjob` `altium_export_gerber/odb/ipc2581/step/pdf/bom` `altium_run_drc` `altium_run_erc` |
| Libraries | `altium_create_symbol` `altium_create_footprint` `altium_import_library` `altium_list_library` `altium_install_library_bundle` |
| Cloud | `altium_a365_*` `altium_web_preview` |
| Demo | `altium_demo` (symbol → footprint → 3D part → schematic → 2D → 3D board, one beat per call, same shape as `kicad_demo`/`fusion_demo`) |
Every verb: `_hint`/`_next`, stable `errorCode` (`host_app_not_installed`, `not_licensed`,
`agent_not_running`, `agent_wedged`, `foreground_reason_required`, …), `windowEtiquette`,
`dialogsDetected`/`dialogs[]`, measured `progress` on anything > 3 s, caller identity forwarded on
every ab callback with `X-Adom-Caller-Delegate: altium`.
## 4. bridge.json essentials
```jsonc
{ "name":"altium", "displayName":"Altium Designer", "spawn":{"kind":"python","entrypoint":"server.py","port":0,"persistent":true},
"verbPrefixes":["altium_"], "statusVerb":"altium_status", "uninstall":"altium_uninstall",
"detect":{"hostApp":"Altium Designer","appPathsExe":"X2.EXE","paths":{"windows":["%ProgramFiles%\\Altium\\AD*\\X2.EXE"]}},
"timeouts":{"default":60,"verbs":{"launch":240,"run_outjob":600,"export_step":360,"demo":300}},
"docs":"https://wiki.adom.inc/adom/altium-bridge" }
```
Python binds `ADOM_BIND_HOST`; scratch under `%LOCALAPPDATA%\Adom Bridge\bridges-cache\altium\` with
the kicad retention rule (48 h / 300 files / oldest-first / 10-min rate limit).
## 5. Dialog doctrine for Altium (pre-empt, never sweep)
Commands that prompt → the pre-emption: closing a dirty doc → `WorkspaceManager:SaveObject` first or
`Client:CloseDocument` with `SaveMode`; ECO → run compile first and drive the ECO dialog via UIA
with a classified catalog; OutJob overwrite → pre-delete outputs; "Confirm" style boxes from
DelphiScript (`ShowMessage`) → never call them in the agent; Script IDE debugger halt → detect by
heartbeat and recover. Keep `handlers/dialog_classify.py` with title → category → resolution.
## 6. Phases + acceptance
0. **Phase 0 experiments (owed, on a LICENSED AD; ~1 day):** (a) trial licence via CDP/image click
with toast; (b) `-R` forwards to running instance?; (c) `RunScriptText` from CLI?; (d) DelphiScript
`CreateOleObject('MSXML2.ServerXMLHTTP')`?; (e) ProcessMessages loop keeps UI responsive?; (f)
WebView2 `--remote-debugging-port` honoured?; (g) Global Project registration by prefs-file
write?; (h) runtime-error recovery path; (i) ASCII PcbDoc export via `PCB:Export`; (j) A365 token
on a trial workspace.
1. **Skeleton** (1–2 days): bridge.json, server, readiness/status/describe/state/uninstall/diagnostics,
screenshot array by PID, rcs catalog parser. *Green chip on a fresh VM; `state` returns real shots.*
2. **Bootstrap + REPL** (3–5 days): agent PrjScr, Global Project install, CLI bootstrap, file-drop
transport, `eval`, `run_command`, heartbeat + wedge recovery. *100 sequential evals, p50 recorded.*
3. **Eight surfaces + demo** (3–5 days): each `show_*` background + frame; `altium_demo`. *Web-control
page renders all eight.*
4. **Outputs + libraries** (5 days): OutJob runner, exports, DRC/ERC, symbol/footprint creation,
library bundle install from the wiki, dialog catalog. *Adom molecule round-trip: wiki part →
Altium library → board → Gerber.*
5. **Cloud + auth** (3 days): sign-in + licence driven with one toast, A365 verbs, web preview.
6. **Publish**: Release zip + manifest, skills pkg (`altium-bridge` core + `altium-driving`,
`altium-libraries`, `altium-a365`, `altium-signin` sub-skills), 30–40 discovery triggers, hero,
`make release`, audit checklist. Then (optional) Phase 5: C# SDK extension.
## 6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial) — the facts the code is built on
| Experiment | Result |
|---|---|
| Trial licence | `Start Free Trial` (WebView2, image click) → Edge opens `altium.com/365-intro/activate-add-eval` → workspace form (name, region, terms, **Activate**) → a workspace is provisioned at `<name>.365.altium.com` in ~60 s → back in AD: **Refresh**, then **Use License** on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to `… - Trial`. |
| `-R` forwarding | A second `X2.EXE -R<Server>:<Cmd>(…)` forwards to the running instance and exits in ~2–8 s. **If the instance is busy (any modal, or a script running) the helper hangs and shows its own "There is another instance … busy" Information dialog** — kill it. |
| `RunScriptText` | Works from the shell: `-RScriptingSystem:RunScriptText(Text="Begin RunApplication('…'); End;")`. **Statements only** — a `Var` block is "Unknown identifier: Var". |
| `RunScript` | `-RScriptingSystem:RunScript(ProjectName="…\X.PrjScr"\|ProcName="Main")` works with a plain `\|` when the arg is passed VERBATIM (PowerShell `Start-Process`, or Python `Popen("<one string>")`). `Popen([exe, arg])` re-quotes (the path has a space) and the helper never forwards. `^\|` is only for cmd.exe. |
| Concurrency | **One script at a time.** A -R script while the agent loop is resident raises `Error: Another script executing now` on the MAIN window; every later -R is refused until it is dismissed. |
| Late binding | An unknown identifier (`CreateOleObject`, `RunProcessWithParameters`) is a **runtime** error: `Error: Undeclared identifier …` modal + the Script editor opens with the **debugger halted** on that line. The halted script counts as executing. |
| Recovery | Press OK on the Error (background `BM_CLICK`), then `X2.EXE -REditScript:Stop` (helper exits ≈1 s), then scripts run again. Implemented as `altium_agent_recover`. |
| `CreateOleObject` | **Not available** → no HTTP from inside. Transport is file drop. |
| `FindFirst('request_*.txt')` | Never matched from the engine. Use ONE fixed inbox `request.txt` (atomic tmp+rename, serialized by the bridge). |
| `RunProcess` | `ResetParameters; AddStringParameter(k,v)…; RunProcess('Client:ArrangeAllWindows')` works from the loop: 828 ms in-agent. |
| Agent loop | `Application.ProcessMessages` + `Sleep(10)` (100 ms idle) keeps AD responsive; heartbeat `hb.txt` every 500 ms; **start → heartbeat 1.5 s**; `eval` round trip 0.7 s; `run_command` 1.4 s. RunScript opens the `.PrjScr` as a document in the Projects panel (harmless). |
| ab spawn | Port 47311 is in a Hyper-V reserved range (`WinError 10013`) — always `--port 0`/ab-assigned. `shell_execute` does not return while a process it started (the server) is alive — start long-lived processes via ab's spawn/`launch_process`, never a foreground shell. |
The bridge source lives in `~/project/altium-bridge/src` (server.py, handlers/agent.py, handlers/dialogs.py, handlers/rcs_catalog.py, handlers/altium_detect.py, agent/AdomAgent.pas.tmpl).
## 6c. Phase 1–3 RESULTS (2026-08-29 evening) — what is DONE
- ab adopted the bridge from `bridges-cache\altium` after `refresh_bridges {name:"altium"}`; it spawns on stable port 64070; `adom-bridge altium_*` works from the container. `bridge_kill {name,confirm:true,reason}` reloads code.
- **All eight surfaces return real frames** (base64 PNG of the main window, background, foreground restored): show_project 6.7 s, show_schematic 6.8 s, show_2d_board 8.7 s, show_3d_board 14.5 s, show_library 21.6 s (39 components listed), show_symbol 18.4 s, show_footprint 5.1 s warm (a cold PcbLib open blew ab's 60 s default — declare `timeouts` and expect it), show_3d_chip 16.4 s. `altium_demo {stage}` chains them.
- Agent lane: eval 0.7–1.1 s, run_command ~1.4 s, `lib_list` / `lib_select` via `SchServer.GetCurrentSchDocument` + `SchLibIterator_Create` / `PCBServer.GetCurrentPCBLibrary.GetComponent(i)` + `CurrentComponent`.
- Opening docs: `Client.OpenDocument('', path)` returns Nil on AD 26 — use `ResetParameters; AddStringParameter('ObjectKind','Document'|'Project'); AddStringParameter('FileName',path); RunProcess('WorkspaceManager:OpenObject')`, then `Client.GetDocumentByPath` + `Client.ShowDocument`.
- Three real-world races found and fixed: (1) a busy loop (long RunProcess) has no heartbeat → never auto-start a second loop (`alive` = hb < 60 s, `responsive` = < 3 s); (2) a killed -R helper's request is still DELIVERED later by Altium → reap stray X2 helpers before every start; (3) `tasklist` took 7 s under load → Toolhelp32 snapshot in-process.
- Dialog text is PAINTED (no Static child, not in UIA) — `altium_check_dialogs` attaches a base64 shot per dialog; OK buttons are `TXPBitBtn` (BM_CLICK) with ab `desktop_ui_click` as fallback.
## 6d. Phase 4 RESULTS (2026-08-29 night) — outputs lane
- `altium_compile` / `altium_run_erc`: `WorkspaceManager:Compile Action=Compile|ObjectKind=Project`, then read the Messages panel via `GetWorkspace.DM_MessagesManager` → **`MessagesCount`, `Messages(i).MsgClass/Source/Document/Text`** (`DM_MessageCount` does NOT exist). 1.7 s. Declare the manager as `IDMMessagesManager` (that type name compiles); `IMessagesManager`/`IMessageItem` as Var types give "Error in declaration block".
- `altium_run_outjob {outjobPath, medium}` → `WorkspaceManager:OpenObject` the .OutJob, then `WorkspaceManager:GenerateReport Action=Run|ObjectKind=OutputBatch|OutputMedium=<name>|DisableDialog=True`. Fabrication.OutJob → Gerber + NC Drill + ODB++ + DRC report in 23.6 s; Assembly.OutJob → BOM .xlsx + pick-and-place in 16.6 s. Output lands in `Project Outputs for <project>` next to the project. `altium_export_gerber` / `altium_export_bom` are those two mediums.
- `altium_run_drc`: `PCB:DesignRuleCheck` opens a MODAL "Design Rule Checker [mm]" (automationId `Run_Button` = "Run Design Rule Check...") — the agent blocks inside RunProcess, the bridge clicks the button through ab UIA in the background, then reads violations off the board (`BoardIterator` filtered `eViolationObject`, `V.Rule.Name`, `V.Description`).
- **Script-engine gotchas that cost the most time:** (1) never nest `{ }` inside a brace comment — the inner `}` ends the comment and the rest becomes code ("Error in declaration block"); (2) Altium keeps the .PrjScr/.pas OPEN in its Script editor after RunScript and re-runs the STALE in-editor copy after you overwrite the file — the bridge now renders `AdomAgent_<digest>.pas/.PrjScr` per source change; (3) a failed RunScript target opens the "Select Item To Run" picker (`TForm_RunScriptParameters`) and disables the main window — detect modals by "main window disabled + another enabled top-level window of the same PID", not by class.
- Foreground: the -R bootstrap activates Altium; the bridge captures the user's foreground before and hands it back immediately and during the wait (John: "stop foregrounding on me"). Restart the agent as rarely as possible — every restart is one activation.
## 6e. Phase 4 COMPLETE (2026-08-29) — library authoring
- `altium_create_footprint {name, libraryPath, pads:"des,x,y,w,h,rect|round,hole;…"}` — opens or CREATES the .PcbLib (`CreateNewDocumentFromDocumentKind('PCBLIB')` + `Doc.DoSafeChangeFileNameAndSave(path,'PCBLIB')`), `PCBServer.CreatePCBLibComp`, pads via `PCBObjectFactory(ePadObject…)` + `Comp.AddPCBObject`, `PLib.RegisterComponent`, `CurrentComponent`, `Board.ViewManager_FullUpdate`. 3.1 s, returns a frame.
- `altium_create_symbol {name, libraryPath, designator, bodyWidthMil, bodyHeightMil, pins:"des,name,left|right|top|bottom,offset;…"}` — `SchObjectFactory(eSchComponent)`, `eRectangle` body, `ePin`s with `Orientation`/`PinLength`, `SLib.AddSchComponent` + `RobotManager.SendMessage(... SCHM_PrimitiveRegistration ...)`. 1.1 s.
- `altium_save` — `WorkspaceManager:SaveObject ObjectKind=FocusedDocument`. Both libraries verified on disk.
- Release kit in the repo: `SKILL.md` (user skill), `README.md`, `package.json` (39 discovery triggers), `install.sh`/`uninstall.sh`, `CHANGELOG.md`, `scripts/release.py` (runtime zip 52 KB + manifest). NOT yet published — the wiki already has Barrett's `barrett-land/adom-desktop-altium-bridge` using name `altium`/prefix `altium_`; resolve rename-vs-supersede first.
## 6f. Barrett merge + taskbar (2026-08-29/30)
Ported from `barrett-land/adom-desktop-altium-bridge` (v0.0.55) into the resident agent — all measured working: `altium_set_params` (parameters incl. `LCSC Part #`, comment, footprint link via `ISch_Implementation.AddDataFileLink`), `altium_add_body` (extruded contour or `ModelFactory_FromFilename` STEP), `altium_install_library` (`IntegratedLibraryManager.InstallLibrary`), `altium_compile_intlib` / `altium_extract_intlib` (`CreateIntegratedLibrary` / `ExtractSources`). His **AdomBridge .NET extension** (prebuilt DLL + .ins/.rcs + Install-AdomBridge.ps1 + REMOVE cmd) is vendored under `src/extension/` and exposed as `altium_install_extension` (Altium CLOSED), `altium_extension_ping`, `altium_export_step` (headless STEP with `Holes=All|Copper|Models`), `altium_import_kicad` (no-wizard import). Dispatch is `X2.EXE -RAdomBridge:<Process>(Req=<space-free request file>)` — a server process, not a script, so it coexists with the resident agent. Facts from his page worth keeping: `-R` splits on whitespace (parameters go in a request file), OutJobs only generate when they are project members, STEP via OutJob no-ops headlessly, `Client.ShowDocumentDontFocus` exists (the agent's `open` now prefers it).
Taskbar: `handlers/taskbar.py` — overlay badge via ab `desktop_taskbar {overlay:{icon}}` on the first driving verb (cleared on idle), measured-estimate progress bar for verbs ≥3 s, no AUMID stamping. `altium_state.drivingBadge` reports `{active, lastError}`.
## 6g. Phase 5 RESULTS + the extension verdict (2026-08-30)
- **AdomBridge .NET extension (Barrett's) does NOT load into the running AD 26.9.1 instance.** Registered via his Install-AdomBridge.ps1 (registry node added cleanly, 20 items), Altium restarted → a `Could not start Vault Explorer` warning at startup; `X2 -RAdomBridge:Ping` helpers waited ~65–90 s on the *startup mutex* and then hosted the server in themselves (their own DXP_Startup.log says "Starting server AdomBridge"), the main instance never did; `RunProcess('AdomBridge:Ping')` from the resident agent returned in 16 ms and wrote nothing (unknown process → silently ignored). Unregistered again (`altium_install_extension {remove:true}`). Verbs kept, fail with `extension_timeout` + this note. Next step if wanted: rebuild against AD 26's in-box .NET assemblies (Barrett's `adom-altium-extension` project) and verify with a clean-profile Altium.
- **Altium 365 API on the Develop trial:** Admin shows Settings / Part Providers / Explorer / Usage and Billing / Extensions — **no Developer page**, every guessed token URL 404s. `altium_a365_*` verbs are wired for a token from a Pro/Enterprise workspace.
- Account/licence: `altium_signin_status`, `altium_license {action}` (image-space clicks on the WebView2 page with measured coords: refresh (1288,178), use_license (1252,388), start_trial (788,607) on a 1400×840 capture — verify against the returned shot), `altium_connect_workspace` (opens the `Not Connected` picker, `TWorkspacesForm`, custom list without UIA children → returns a shot to click).
- Saving example projects on close: Altium asks "Confirm Save Locally for (N) Modified Documents" (WPF grid: Save All / Save None / OK / Cancel, all UIA-invokable) — `altium_close` reports it; answer Save None + OK for scratch work.
## 6h. First end-to-end demo — Ray's CH340G (2026-08-30) + two bugs found
Imported `ray/ch340g` (WCH USB-serial, SOP-16) from the wiki into a NATIVE Altium library entirely through the bridge, then walked the surfaces. WORKED, all of that chip: `altium_create_footprint` (16 SOIC pads parsed from the page's pads.json, mm→mil), `altium_add_body` (the page's real 217 KB KiCad STEP), `altium_create_symbol` (16 named pins GND/TXD/RXD/UD+/VCC/…), `altium_set_params` (MPN/mfr/pkg + footprint link), `altium_show_symbol` / `_footprint` / `_library`, `altium_place_on_pcb` (new `place_on_pcb` verb: `IPCB_LibComponent.Replicate` → `Board.AddPCBObject` → the footprint lands on a real PcbDoc, 16 pads), `altium_show_2d_board`.
TWO REAL BUGS surfaced — fix before publishing:
1. **`place_on_sch` loses the symbol graphics.** `ISch_Component.Replicate` onto a fresh sheet placed the component (U1 designator + CH340G comment appear) but NOT its rectangle/pins — only the two text strings render. Setting `CurrentPartID:=1`/`DisplayMode:=0` on the copy did not fix it. The primitives inside the component group aren't being carried/registered onto the target sheet. Likely need to iterate the lib component's children and `AddSchObject` each onto the instance, or place via `IntegratedLibraryManager` rather than `Replicate`.
2. **STEP 3D body renders OFF-ORIGIN.** `add_body` with `ModelFactory_FromFilename` attaches (returns ok), but in both the library 3D view and on the board the body sits at the model's native origin (a small sliver at the board corner), not over the pads — so the 3D board shows bare gold pads. Reposition the body to the footprint centroid after `SetState_FromModel` (set `Body.ModelPosition`/move), or bake the offset.
Also: **`PCB:SetCamera(To=RotateFit_*)` is a no-op via RunProcess on AD 26.9.1** even with the window raised — the 3D view stays dead top-down. A real isometric needs either a focused numpad-key rotation (SendInput) or a different camera API; do not rely on SetCamera. Board substrate also renders white (no green plane) in the library/loose-PcbDoc 3D — a board shape + layer stack is needed for a proper 3D board.
Net for a demo today: symbol, footprint, library manager, schematic-with-part (text only), 2D board with the real footprint — all solid; 3D-with-body and schematic-graphics need the two fixes above.
## 6i. Demo fixes (2026-08-30) — schematic FIXED, one important bridge bug FIXED, 3D body still open
**FIXED — `place_on_sch` now renders the full symbol.** `ISch_Component.Replicate` does NOT re-parent a library part's pins/rectangle onto a normal sheet (only the designator/comment text placed). The working path: build a fresh `SchServer.SchObjectFactory(eSchComponent)`, iterate the lib component's children (`ePin/eRectangle/eLine/…`), `child.Replicate` each, set `OwnerPartId:=1`/`OwnerPartDisplayMode:=0`, `Inst.AddSchObject(child)`, then `RegisterSchObjectInContainer` + SendMessage. Proven on Ray's CH340G — 17 primitives (rect + 16 pins) render with all pin names on `CH340G_Final.SchDoc`. (Setting `OwnerPartDisplayMode` alone on the ORIGINAL symbol did NOT fix Replicate — the copy-primitives approach is required.)
**FIXED — `agent_start` silently ran STALE code.** The DelphiScript loop lives inside Altium; `bridge_kill` restarts the ab PROCESS but not the loop, and `start()` early-returned `alreadyRunning` when it saw the old loop's heartbeat — so redeployed agent code never loaded (every "fix" this session needed a manual `agent_stop` first). Fix: the agent stamps its generation (`AGENT_GEN` = template digest) into `hb.txt`; `start()` compares the resident `gen` to the current template digest and auto-stops+restarts a stale loop. ⚠ TWO traps hit while adding it: (1) `_agent_base()` must hash the RAW template, NOT `rendered_agent_source()` — the latter recursed and crashed the whole bridge; (2) editing `agent.py`/`server.py` needs a `bridge_kill` to reload the PROCESS, separate from restarting the in-Altium loop.
**STILL OPEN — 3D component body does not render over the pads in the PCB Library 3D view.** `add_body` attaches (STEP via `ModelFactory_FromFilename`, or an extruded contour) and returns ok, but neither shows as a solid over the pads in the footprint 3D editor — both appear as a small sliver in a corner. A post-add `MoveByXY(BoundingRectangle…)` centering attempt was WRONG (moved it further off) and has been reverted. Likely causes to chase next: STEP unit/scale on import; the PCB Library editor's "Display 3D Bodies" option; and bodies render reliably only on a real board (place the footprint on a PcbDoc WITH a layer stack + board shape, then view 3D) — the loose-PcbDoc/library 3D view is the weak path. Also `PCB:SetCamera(To=RotateFit_*)` is a no-op via RunProcess, so the view stays top-down regardless — a real isometric needs SendInput numpad keys to the focused 3D window. The Altium main window also self-MINIMIZED during the 3D switches; `desktop_set_window_state {state:restore}` fixes the sliver captures.
## 6j. 3D BODY FIXED end-to-end (2026-08-30) — five root causes, all measured
The chip now renders in 3D on a real board (black body + 16 gold pads on the green PCB, `CH340G_Board14.PcbDoc`). It took FIVE stacked fixes, each verified by data (`altium_pcb_report` — a new diagnostic verb that dumps per-component pads/bodies with layer + bbox in mils, plus a board object census by ObjectId):
1. **A primitive with no Layer is silently dropped.** `add_body` never set `Body.Layer`; pads always set theirs. Fix: `Body.Layer := eMechanical1` (+ enable `LayerStack.LayerObject[eMechanical1].MechanicalLayerEnabled` and `LayerIsDisplayed`) — and `Body.BodyOpacity3D := 1.0`.
2. **The stored lib component is NOT the editing surface.** `Fp.AddPCBObject(Body)` counted 1 body immediately but the body was GONE after save+reopen — the editor canvas copy overwrites the stored component. Fix: `PLib.CurrentComponent := Fp` FIRST, then add to the CANVAS (`PLib.Board.AddPCBObject(Body)`); the editor persists the canvas into the component on save. (`bodiesNow=1` AND survives reopen: `libBodies=1`.)
3. **Group `Replicate` of an `IPCB_LibComponent` DISSOLVES on a board** — census showed 16 free pads, no `eComponentObject`, no body. Fix (same cure as the schematic): fresh `PCBObjectFactory(eComponentObject)`, copy each child with primitive-level `Replicate` + `Inst.AddPCBObject`, and — critical — **fill the group BEFORE `Board.AddPCBObject(Inst)`**: children added to an already-registered component exist in data but never draw, whatever address they're robot-registered to.
4. **The lib stores pads around the canvas origin (~50000,50000 mil) but an authored body contour around 0,0** — one group move can't land both (body hit the target, pads landed 50000 mil away). Fix: normalize per cluster — compute the copied PAD bbox center and `MoveByXY` pads (and non-text children) by `target−center`; move each BODY to the target by its own bbox center.
5. **Assigning `Inst.x/y` AFTER moving children re-translates the whole group** (everything landed at 8000,6000). Anchor `Inst.x/y := target` BEFORE the normalization. Also hide `NameOn/CommentOn` (the factory "Designator1" text is huge and hijacks Zoom All; hidden texts still bloat the bbox — frame with `Board.GraphicalView_ZoomOnRect(...)` via eval instead of trusting Zoom All).
More traps measured on the way: `GetCurrentPCBBoard` answers with the LIBRARY's internal board when a PcbLib is focused (a board-reuse path placed into the lib canvas and renamed the lib doc — always create the PcbDoc and only then read GetCurrentPCBBoard); `GetCurrentPCBLibrary` answers even when a board is focused (pcb_report needs `want:"board"`); a nil `BoardOutline.BoundingRectangle`, `SetFileName` on an unassigned doc, and `GroupIterator_Create` on IPCB_Board all HALT the engine as "Undeclared identifier"; `IPCB_Component.Name.Text :=` halts too (use `NameOn`/`MoveToXY` on `Inst.Name`). GroupIterators need `AddFilter_LayerSet(AllLayers)` or mech-layer children are invisible to them. `WriteAtomic` stage breadcrumbs into the workspace (`agent/stage.txt`) pinpoint hangs.
Still open (minor): the PCB **Library** editor 3D view does not render bodies (the board 3D does — use `show_3d_chip` for selection, the placed-board flow for true 3D); `PCB:SetCamera` remains a no-op so views are framed with `GraphicalView_ZoomOnRect`; the schematic `place_on_sch` fix (§6i) still stands. Cleanup owed: the fix session littered `CH340G_v2..v8` libs and `CH340G_Board..Board15` docs in Downloads and many open tabs.
### Progress surface ruling (John, 2026-08-30)
Long-job progress goes on the Windows taskbar icon ONLY (`desktop_taskbar` progress on Altium's button — handlers/taskbar.py already paces it per verb). NOT ab progress toasts: "when i say progress bar i just mean in the windows taskbar icons." Toasts remain the nuclear only-a-human-can-act channel.
### Ralph rule for generated CAD (John, 2026-08-30)
Frame-check EVERY generated look/variant, not a sample — the shipped symbol had colliding 4-side names, visible param texts (SetSchParam defaulted IsHidden False; now True), and std==2col, all invisible in a 3-of-10 spot check. symbol_modes.py now sizes bodies from measured name lengths (55 mil/char); ralph the full matrix before reporting.
## 6k. LAN7800 creative import (2026-08-30) — variants, display modes, footprint models, board proof
New verbs (all agent-lane, all measured): `altium_build_footprint {libraryPath, spec}` (spec from `tools/kicad2altium.py`: pads incl. duplicate-designator through-hole vias, roundrect %, tracks/arcs/filled discs on named mech layers), `altium_build_symbol_modes {libraryPath, spec}` (ONE component, N display modes from `tools/symbol_modes.py`), `altium_symbol_mode {mode}` (editor-driven: `Sch:ToggleComponentDisplayMode Action=Mode0/Next` — `Comp.DisplayMode` changes data only), `altium_add_models {models[], params, comment, description}` (N PCBLIB implementations = Altium footprint variants; the user picks the current footprint on the placed part), `altium_normalize_bodies`, `altium_test_board {footprintName, wMm, hMm, assigns, stubs}`, `altium_board_shape {x0Mm,y0Mm,wMm,hMm}` (process `PCB:PlaceBoardOutline Mode=BOARDOUTLINE_FROM_SEL_PRIMS` on 4 temp selected tracks — `BoardOutline.PointCount/Segments` is a silent no-op, `.Clear` halts), `altium_swap_footprint {footprintName, libraryPath, assigns}` (remove + per-primitive re-place at the same x/y, nets re-assigned by designator — tracks untouched, so wiring survives), `altium_pcb_report` gained `padsBboxMil`, `nets`, `tracks`.
**Dialog pre-emption (John: a dismissed dialog is a defect).** The DelphiScript debugger breaks on ANY raised exception even inside Try/Except — a raise IS an Error modal. `ReadAll` now probes with `FileOpen` (returns -1, never raises) before `LoadFromFile`. A halted command leaves `PCBServer.PreProcess` open = "a command is currently active" → save draws the save-a-copy Warning: `TxClear` (unbalanced `PostProcess`, harmless) runs at agent start, before every save, and after every recover. Undeclared identifiers hit this session: `BottomXSize`/`Mid*` pad sizes, `IPCB_BoardOutline.Clear`, `eProcessFreePrimitives`, `IPCB_Board.GroupIterator_Create`.
**Library canvas offset.** Bodies added through the PcbLib canvas land 50000 mil from the pads in the stored frame; the editor then paints an EMPTY canvas (Zoom All frames 50 inches; the CH340G "orange sliver" was this). `normalize_bodies` centres each body on its pad cluster; `add_body` now realigns automatically. GroupIterators need `AddFilter_LayerSet(AllLayers)` or mech-layer children are invisible.
**Frames under a locked session.** GDI editors (schematic, PcbLib 2D) still capture; the DirectX PCB canvas paints BLACK while the Windows session is locked (`desktop_get_foreground` = 0, `desktop_list_windows` = 0, full-screen capture white). Check the session before trusting a board frame.
## 7. Sources
Altium docs: scripting overview/setup, running scripts, DelphiScript, Altium SDK overview, Altium
365 API quick start + API page, embeddable viewer, licensing, sign-in. Vendor CI article (X2 `-R`
recipe). Community: `salitronic/eda-agent`, `coffeenmusic/altium-mcp`, `ee-in-a-box/altium-copilot`,
`embedded-society/altium-designer-mcp`, `10xgenomicsEng/altium-mcp-unified`,
`oharown/mcp-codex-altium-designer`, `Altium-Designer-addons/scripts-libraries`, KiCad Altium
importer docs, `vadmium/python-altium`, `thesourcerer8/altium2kicad`.