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.
main
Colby Knox
0.2.58 source (not yet released, needs signing): autoroute, layer_stack, rule descriptors, sch_zoom
f82b5b5
10h ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894
{ AdomAgent.pas — the resident Adom agent that runs INSIDE Altium Designer's
DelphiScript engine. Generated from AdomAgent.pas.tmpl by the bridge: every
__WORKSPACE__ token is replaced with the absolute agent workspace directory
(DelphiScript has no reliable way to read env vars, so the path is baked in).
Transport: file drop. The bridge writes request_<id>.txt (one "key=value"
per line; a multi-line payload rides in request_<id>.payload) and the agent
answers with response_<id>.json (written to .tmp then renamed, so a reader
never sees a half file). hb.txt carries a heartbeat tick every ~500 ms so
the bridge can tell "still working" from "engine wedged".
Engine facts (measured on AD 26.9.1, 2026-08-29):
- only ONE script may run at a time; a -R RunScript/RunScriptText from the
shell while this loop is resident raises the modal "Another script
executing now" on the MAIN instance and makes it refuse every later -R
until dismissed. So while resident, EVERYTHING goes through this loop.
- RunScriptText accepts STATEMENTS only (Begin ... End;), no Var block.
- CreateOleObject is NOT available ("Undeclared identifier"), so no HTTP
from inside; file drop is the transport.
- DelphiScript is untyped: no records/classes; Try/Except works.
}
Const
AGENT_VERSION = '0.1.0';
WORKSPACE = '__WORKSPACE__';
AGENT_GEN = '__GEN__';
POLL_ACTIVE_MS = 10;
POLL_IDLE_MS = 100;
IDLE_AFTER_TICKS = 300; { ~3 s of no requests -> idle cadence }
HEARTBEAT_MS = 500;
AUTO_STOP_MS = 300000; { 5 min idle -> exit. Residency is the freeze exposure
window - a user X-press over a live loop can wedge
the shutdown, so the loop does not linger. }
Var
gRunning : Boolean;
gRequests : Integer;
gLastReqTick: Integer;
gLastHbTick : Integer;
gLastSleepMs : Integer;
gTicks : Integer;
gLastSaveMs, gLastRenameMs, gLastDeleteMs : Integer;
gLastSaveTries, gLastSaveExcept : Integer;
gDirectTries, gDirectExcept, gDirectMs : Integer;
gInputError : String; { first caller value that was not a number; see AdomFloat }
{ ------------------------------------------------------- safe numbers ---- }
{ StrToFloat / StrToInt on a CALLER value raise EConvertError on anything malformed, and in
Altium a raise is an Error box on the user's screen plus a script parked on a breakpoint
(measured 2026-09-20: create_footprint with pads in the wrong shape). These never raise: a bad
token is remembered in gInputError and the dispatcher answers errorCode bad_number. }
Function AdomIsNumber(T : String; AllowDot : Boolean) : Boolean;
Var
I, N, Dots, Digits : Integer;
C : String;
Begin
N := Length(T); Dots := 0; Digits := 0; Result := N > 0;
For I := 1 To N Do
Begin
C := Copy(T, I, 1);
If (C >= '0') And (C <= '9') Then Digits := Digits + 1
Else If (C = '.') And AllowDot Then Dots := Dots + 1
Else If ((C = '-') Or (C = '+')) And (I = 1) Then Dots := Dots
Else Result := False;
End;
If (Digits = 0) Or (Dots > 1) Then Result := False;
End;
Function AdomFloat(S : String) : Double;
Var
T : String;
Begin
Result := 0;
T := Trim(S);
If AdomIsNumber(T, True) Then Result := StrToFloat(T)
Else If gInputError = '' Then gInputError := T;
End;
Function AdomInt(S : String) : Integer;
Var
T : String;
Begin
Result := 0;
T := Trim(S);
If AdomIsNumber(T, False) Then Result := StrToInt(T)
Else If gInputError = '' Then gInputError := T;
End;
{ ---------------------------------------------------------------- files ---- }
Function ReadAll(Path : String) : String;
Var
L : TStringList;
K, H : Integer;
Begin
Result := '';
{ NEVER let LoadFromFile raise: the script debugger breaks on ANY exception
even inside Try/Except and draws an Error modal (EFOpenError "being used
by another process" when the bridge is mid-write, measured 2026-08-30).
Probe with FileOpen, which returns -1 instead of raising, until the file
is readable, then load. }
For K := 1 To 100 Do
Begin
H := FileOpen(Path, fmOpenRead Or fmShareDenyNone);
If H >= 0 Then
Begin
FileClose(H);
Break;
End;
Sleep(20);
End;
If H < 0 Then Exit;
L := TStringList.Create;
Try
L.LoadFromFile(Path);
Result := L.Text;
Finally
L.Free;
End;
End;
Procedure WriteAtomic(Path : String; Content : String);
Var
L : TStringList;
Tmp : String;
Done : Boolean;
K, TA, TB, TC : Integer;
Begin
Tmp := Path + '.tmp';
L := TStringList.Create;
Try
L.Text := Content;
TA := GetTickCount;
gLastSaveTries := 0;
gLastSaveExcept := 0;
Done := False;
K := 0;
{ NOTE: never `Break` out of a Try..Except here - this engine implements Break as an
exception, the Except swallows it, and the loop runs all 20 iterations (~220 ms). }
While (Not Done) And (K < 20) Do
Begin
K := K + 1;
gLastSaveTries := K;
Try
L.SaveToFile(Tmp);
Done := True;
Except
gLastSaveExcept := gLastSaveExcept + 1;
Sleep(10);
End;
End;
TB := GetTickCount;
Finally
L.Free;
End;
If FileExists(Path) Then DeleteFile(Path);
TC := GetTickCount;
RenameFile(Tmp, Path);
gLastSaveMs := TB - TA;
gLastDeleteMs := TC - TB;
gLastRenameMs := GetTickCount - TC;
End;
{ Direct write, no tmp+rename (A/B against WriteAtomic for the response path). Readers tolerate a
partially written file by retrying the JSON parse. }
Procedure WriteDirect(Path : String; Content : String);
Var
L : TStringList;
Done : Boolean;
K : Integer;
T0 : Integer;
Begin
L := TStringList.Create;
Try
L.Text := Content;
T0 := GetTickCount;
gDirectTries := 0;
gDirectExcept := 0;
Done := False;
K := 0;
While (Not Done) And (K < 20) Do
Begin
K := K + 1;
gDirectTries := K;
Try
L.SaveToFile(Path);
Done := True;
Except
gDirectExcept := gDirectExcept + 1;
Sleep(10);
End;
End;
gDirectMs := GetTickCount - T0;
Finally
L.Free;
End;
End;
{ Runs of plain characters are copied whole: appending one character at a time is quadratic in
DelphiScript, and one part's long parameter (a Mini PC example IC) took minutes to escape (2026-10-21). }
Function JsonEsc(S : String) : String;
Var
I, Start, N : Integer;
C : Char;
E : String;
Begin
Result := '';
N := Length(S);
Start := 1;
For I := 1 To N Do
Begin
C := S[I];
E := '';
If C = '"' Then E := '\"'
Else If C = '\' Then E := '\\'
Else If C = #13 Then E := '\r'
Else If C = #10 Then E := '\n'
Else If C = #9 Then E := '\t'
Else If Ord(C) < 32 Then E := ' ';
If E <> '' Then
Begin
If I > Start Then Result := Result + Copy(S, Start, I - Start);
Result := Result + E;
Start := I + 1;
End;
End;
If Start <= N Then Result := Result + Copy(S, Start, N - Start + 1);
End;
Function JStr(Key : String; Val : String) : String;
Begin
Result := '"' + Key + '":"' + JsonEsc(Val) + '"';
End;
Function JRaw(Key : String; Val : String) : String;
Begin
Result := '"' + Key + '":' + Val;
End;
Function JBool(B : Boolean) : String;
Begin
If B Then Result := 'true' Else Result := 'false';
End;
{ Close a dangling PCBServer transaction. A command that halted between
PreProcess and PostProcess leaves Altium in "a command is currently active",
and the next save draws the save-a-copy Warning. Measured 2026-08-30: an
unbalanced PostProcess is harmless and clears it. }
Procedure TxClear;
Begin
Try
PCBServer.PostProcess;
Except
End;
End;
{ hb.txt MUST be written tmp+rename: readers (the bridge's is_alive poll) hold the final path
open, so a direct SaveToFile onto it raises EFCreateError and halts the engine with a modal.
Response files use WriteDirect - a fresh unique name nobody has open yet. }
Procedure Heartbeat(Force : Boolean);
Var
T : Integer;
Begin
T := GetTickCount;
If Force Or ((T - gLastHbTick) >= HEARTBEAT_MS) Then
Begin
Try
WriteAtomic(WORKSPACE + 'hb.txt', IntToStr(T) + #13#10 + 'requests=' + IntToStr(gRequests) + #13#10 + 'ticks=' + IntToStr(gTicks) + #13#10 + 'version=' + AGENT_VERSION + #13#10 + 'gen=' + AGENT_GEN);
Except
End;
gLastHbTick := T;
End;
End;
{ --------------------------------------------------------- request parse --- }
{ Requests are "key=value" lines. Param(Lines, 'command') etc. }
Function Param(Lines : TStringList; Key : String) : String;
Var
I : Integer;
L : String;
Begin
Result := '';
For I := 0 To Lines.Count - 1 Do
Begin
L := Lines[I];
If Pos(Key + '=', L) = 1 Then
Begin
Result := Copy(L, Length(Key) + 2, Length(L));
Break;
End;
End;
End;
{ ------------------------------------------------------------- handlers --- }
Function CmdPing(Lines : TStringList) : String;
Begin
Result := '{' + JRaw('ok', 'true') + ',' + JStr('agent', AGENT_VERSION) + ',' + JRaw('requests', IntToStr(gRequests)) + ',' + JRaw('tick', IntToStr(GetTickCount)) + '}';
End;
{ Every open document in the workspace + which one has focus. }
Function CmdState(Lines : TStringList) : String;
Var
WS : IWorkspace;
Prj : IProject;
Doc : IDocument;
I, J : Integer;
Docs : String;
Focused : String;
PrjName : String;
Begin
Docs := '';
Focused := '';
PrjName := '';
WS := GetWorkspace;
If WS <> Nil Then
Begin
Try
Doc := WS.DM_FocusedDocument;
If Doc <> Nil Then Focused := Doc.DM_FullPath;
Except
End;
Try
Prj := WS.DM_FocusedProject;
If Prj <> Nil Then PrjName := Prj.DM_ProjectFullPath;
Except
End;
For I := 0 To WS.DM_ProjectCount - 1 Do
Begin
Prj := WS.DM_Projects(I);
For J := 0 To Prj.DM_LogicalDocumentCount - 1 Do
Begin
Doc := Prj.DM_LogicalDocuments(J);
If Docs <> '' Then Docs := Docs + ',';
Docs := Docs + '{' + JStr('path', Doc.DM_FullPath) + ',' + JStr('kind', Doc.DM_DocumentKind) + ',' + JStr('project', Prj.DM_ProjectFileName) + ',' + JRaw('open', JBool(Doc.DM_DocumentIsLoaded)) + '}';
End;
End;
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('focusedDocument', Focused) + ',' + JStr('focusedProject', PrjName) + ',' + JRaw('documents', '[' + Docs + ']') + '}';
End;
{ Run any server process from the 1,109-entry catalog: command=PCB:Zoom params=Action=All|... }
Function CmdRunProcess(Lines : TStringList) : String;
Var
Cmd, P, Rest, Pair : String;
K : Integer;
Begin
Cmd := Param(Lines, 'process');
P := Param(Lines, 'params');
If Cmd = '' Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'process is required, e.g. PCB:Zoom') + '}';
Exit;
End;
{ RunProcessWithParameters halted the debugger at this line on AD 26.9.1
(2026-08-29) — the supported form is ResetParameters + one
AddStringParameter per Name=Value pair, then RunProcess(Command). }
ResetParameters;
Rest := P;
While Rest <> '' Do
Begin
K := Pos('|', Rest);
If K > 0 Then
Begin
Pair := Copy(Rest, 1, K - 1);
Rest := Copy(Rest, K + 1, Length(Rest));
End
Else
Begin
Pair := Rest;
Rest := '';
End;
K := Pos('=', Pair);
If K > 0 Then AddStringParameter(Copy(Pair, 1, K - 1), Copy(Pair, K + 1, Length(Pair)));
End;
RunProcess(Cmd);
Result := '{' + JRaw('ok', 'true') + ',' + JStr('process', Cmd) + ',' + JStr('params', P) + '}';
End;
{ Open a document or project by path. Client.OpenDocument('', path) returns Nil on
AD 26.9.1 (measured), so go through the WorkspaceManager process launcher, then
look the document up and show it. .PrjPcb/.PrjScr/.PrjMbd open as a Project. }
Function CmdOpen(Lines : TStringList) : String;
Var
Path, Ext, Kind : String;
Doc : IServerDocument;
K : Integer;
Begin
Path := Param(Lines, 'path');
If Not FileExists(Path) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'file not found: ' + Path) + '}';
Exit;
End;
Ext := '';
K := Length(Path);
While (K > 0) And (Path[K] <> '.') Do K := K - 1;
If K > 0 Then Ext := UpperCase(Copy(Path, K + 1, Length(Path)));
{ Altium wants the DOCUMENT KIND, not a generic word. Passing ObjectKind='Document' makes it
answer "File type not recognised." with a modal on the user's screen (measured twice on
2026-09-01) even for a perfectly valid .PcbDoc. Map the extension to the kind Altium uses,
and open through Client.OpenDocument — the sibling of ShowDocument, which this unit already
relies on — falling back to the OpenObject process only if that raises. }
Kind := '';
If (Ext = 'PRJPCB') Or (Ext = 'PRJSCR') Or (Ext = 'PRJMBD') Or (Ext = 'PRJHAR') Or (Ext = 'LIBPKG') Then
Kind := 'Project'
Else If Ext = 'PCBDOC' Then Kind := 'PCB'
Else If Ext = 'SCHDOC' Then Kind := 'SCH'
Else If Ext = 'PCBLIB' Then Kind := 'PCBLIB'
Else If Ext = 'SCHLIB' Then Kind := 'SCHLIB'
Else If Ext = 'OUTJOB' Then Kind := 'OUTPUTJOB'
Else If Ext = 'PCB3DLIB' Then Kind := 'PCB3DLIB'
Else If (Ext = 'TXT') Or (Ext = 'PAS') Or (Ext = 'REP') Then Kind := 'TEXT';
If Kind = '' Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unsupported document type: .' + Ext) + ',' +
JStr('supported', 'PcbDoc, SchDoc, PcbLib, SchLib, OutJob, PrjPcb, PrjScr, txt/pas/rep') + '}';
Exit;
End;
If Kind <> 'Project' Then
Begin
Doc := Nil;
Try
Doc := Client.OpenDocument(Kind, Path);
Except
Doc := Nil;
End;
If Doc <> Nil Then
Begin
Try
Client.ShowDocumentDontFocus(Doc);
Except
Client.ShowDocument(Doc);
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('path', Path) + ',' + JStr('objectKind', Kind) + ',' +
JStr('kind', Doc.Kind) + ',' + JRaw('shown', 'true') + ',' + JStr('lane', 'OpenDocument') + '}';
Exit;
End;
End;
{ fallback: the process lane, with the mapped kind rather than a generic one }
ResetParameters;
AddStringParameter('ObjectKind', Kind);
AddStringParameter('FileName', Path);
RunProcess('WorkspaceManager:OpenObject');
Doc := Nil;
Try
Doc := Client.GetDocumentByPath(Path);
Except
End;
If Doc <> Nil Then
Begin
Try
Client.ShowDocumentDontFocus(Doc);
Except
Client.ShowDocument(Doc);
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('path', Path) + ',' + JStr('objectKind', Kind) + ',' + JStr('kind', Doc.Kind) + ',' + JRaw('shown', 'true') + '}';
End
Else
Result := '{' + JRaw('ok', 'true') + ',' + JStr('path', Path) + ',' + JStr('objectKind', Kind) + ',' + JRaw('shown', 'false') + ',' + JStr('note', 'OpenObject ran; document handle not resolved (projects have no document view)') + '}';
End;
{ Close ONE open document by path, never saving (a caller who wants a save saves first). Goes
through the document HANDLE: the menu processes are broken for this on ADDevelop - RunProcess
Client:CloseDocument no-ops, and Client:CloseAllOpenDocuments unregisters documents while
their views stay open (both measured 2026-09-02, adom/altium-bridge issue 7). }
Function CmdCloseDocument(Lines : TStringList) : String;
Var
Path : String;
Doc : IServerDocument;
Begin
Path := Param(Lines, 'path');
Doc := Nil;
Try
Doc := Client.GetDocumentByPath(Path);
Except
End;
If Doc = Nil Then
Begin
Result := '{' + JRaw('ok', 'true') + ',' + JRaw('closed', 'false') + ',' + JStr('note', 'not open: ' + Path) + '}';
Exit;
End;
Try
Client.CloseDocument(Doc);
Result := '{' + JRaw('ok', 'true') + ',' + JRaw('closed', 'true') + ',' + JStr('path', Path) + '}';
Except
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'CloseDocument raised for ' + Path) + '}';
End;
End;
{ Inline statements: RunScriptText from INSIDE the agent (measured separately;
if the engine refuses nested scripts this returns the error in-band). }
Function CmdEval(Lines : TStringList; Payload : String) : String;
Var
Text : String;
Begin
Text := Payload;
If Text = '' Then Text := Param(Lines, 'text');
If Text = '' Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'text (statements only, Begin ... End;) is required') + '}';
Exit;
End;
ResetParameters;
AddStringParameter('Text', Text);
RunProcess('ScriptingSystem:RunScriptText');
Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'RunScriptText dispatched; statements only, no Var block; capture output by writing a file') + '}';
End;
{ ------------------------------------------------------------ libraries --- }
{ List components of the CURRENT library document (open it first). kind=sch|pcb }
Function CmdLibList(Lines : TStringList) : String;
Var
Kind : String;
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
PLib : IPCB_Library;
I, N : Integer;
Names : String;
Limit : Integer;
Begin
Kind := Param(Lines, 'kind');
Limit := StrToIntDef(Param(Lines, 'limit'), 200);
Names := '';
N := 0;
If Kind = 'sch' Then
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If N < Limit Then
Begin
If Names <> '' Then Names := Names + ',';
Names := Names + '"' + JsonEsc(Comp.LibReference) + '"';
End;
N := N + 1;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
End
Else
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a PCB library') + '}';
Exit;
End;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
If N < Limit Then
Begin
If Names <> '' Then Names := Names + ',';
Names := Names + '"' + JsonEsc(PLib.GetComponent(I).Name) + '"';
End;
N := N + 1;
End;
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('kind', Kind) + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('names', '[' + Names + ']') + '}';
End;
{ Make one component current in the open library editor. kind=sch|pcb name=<LibReference|footprint>
(name '' = the first one), then zoom all. }
Function CmdLibSelect(Lines : TStringList) : String;
Var
Kind, Name, Found : String;
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
PLib : IPCB_Library;
PComp : IPCB_LibComponent;
I : Integer;
Begin
Kind := Param(Lines, 'kind');
Name := Param(Lines, 'name');
Found := '';
If Kind = 'sch' Then
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If (Name = '') Or (UpperCase(Comp.LibReference) = UpperCase(Name)) Then
Begin
SLib.CurrentSchComponent := Comp;
Found := Comp.LibReference;
Break;
End;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Found <> '' Then
Begin
SLib.GraphicallyInvalidate;
ResetParameters;
AddStringParameter('Action', 'All');
RunProcess('Sch:Zoom');
End;
End
Else
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a PCB library') + '}';
Exit;
End;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
PComp := PLib.GetComponent(I);
If (Name = '') Or (UpperCase(PComp.Name) = UpperCase(Name)) Then
Begin
PLib.CurrentComponent := PComp;
Found := PComp.Name;
Break;
End;
End;
If Found <> '' Then
Begin
PLib.RefreshView;
ResetParameters;
AddStringParameter('Action', 'All');
RunProcess('PCB:Zoom');
End;
End;
If Found = '' Then
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + ',' + JStr('hint', 'lib_list to see names') + '}'
Else
Result := '{' + JRaw('ok', 'true') + ',' + JStr('kind', Kind) + ',' + JStr('selected', Found) + '}';
End;
{ ------------------------------------------------------------- outputs --- }
{ Compile the focused project (= ERC for schematics) and return the Messages panel.
IMessagesManager (Workspace Manager API): MessagesCount, Messages(i) -> IMessageItem
with MsgClass, Text, Source, Document. DM_MessageCount does not exist (measured).
NEVER nest braces inside a brace comment - the inner one ends the comment early
and the rest becomes code ("Error in declaration block"). }
Function CmdCompile(Lines : TStringList) : String;
Var
WS : IWorkspace;
MM : IDMMessagesManager;
I, N : Integer;
Msgs : String;
Kind : String;
Limit: Integer;
Begin
Kind := Param(Lines, 'objectKind');
If Kind = '' Then Kind := 'Project';
Limit := StrToIntDef(Param(Lines, 'limit'), 200);
ResetParameters;
AddStringParameter('Action', 'Compile');
AddStringParameter('ObjectKind', Kind);
RunProcess('WorkspaceManager:Compile');
Msgs := '';
N := 0;
WS := GetWorkspace;
If WS <> Nil Then
Begin
MM := WS.DM_MessagesManager;
If MM <> Nil Then
Begin
N := MM.MessagesCount;
For I := 0 To N - 1 Do
Begin
If I >= Limit Then Break;
If Msgs <> '' Then Msgs := Msgs + ',';
Msgs := Msgs + '{' + JStr('class', MM.Messages(I).MsgClass) + ',' + JStr('source', MM.Messages(I).Source) + ',' + JStr('document', MM.Messages(I).Document) + ',' + JStr('text', MM.Messages(I).Text) + '}';
End;
End;
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('objectKind', Kind) + ',' + JRaw('messageCount', IntToStr(N)) + ',' + JRaw('messages', '[' + Msgs + ']') + '}';
End;
{ DRC violations currently on the focused board (run PCB:DesignRuleCheck first). }
Function CmdViolations(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Iter : IPCB_BoardIterator;
V : IPCB_Violation;
N : Integer;
Items : String;
Limit : Integer;
Begin
Limit := StrToIntDef(Param(Lines, 'limit'), 200);
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'no PCB document is current') + '}';
Exit;
End;
Items := '';
N := 0;
Iter := Board.BoardIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eViolationObject));
Iter.AddFilter_LayerSet(AllLayers);
Iter.AddFilter_Method(eProcessAll);
V := Iter.FirstPCBObject;
While V <> Nil Do
Begin
If N < Limit Then
Begin
If Items <> '' Then Items := Items + ',';
Items := Items + '{' + JStr('rule', V.Rule.Name) + ',' + JStr('description', V.Description) + '}';
End;
N := N + 1;
V := Iter.NextPCBObject;
End;
Board.BoardIterator_Destroy(Iter);
Result := '{' + JRaw('ok', 'true') + ',' + JStr('board', Board.FileName) + ',' + JRaw('violationCount', IntToStr(N)) + ',' + JRaw('violations', '[' + Items + ']') + '}';
End;
{ Run an OutJob medium. path=<.OutJob> medium=<OutputMedium name, e.g. Folder Structure> }
Function CmdOutJob(Lines : TStringList) : String;
Var
Path, Medium, Kind : String;
Doc : IServerDocument;
Begin
Path := Param(Lines, 'path');
Medium := Param(Lines, 'medium');
Kind := Param(Lines, 'objectKind');
If Kind = '' Then Kind := 'OutputBatch';
If Not FileExists(Path) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'OutJob not found: ' + Path) + '}';
Exit;
End;
{ the OutJob's own document kind: ObjectKind 'Document' is the generic word Altium answers with
"File type not recognised" (see CmdOpen) }
Doc := Nil;
Try
Doc := Client.OpenDocument('OUTPUTJOB', Path);
Except
Doc := Nil;
End;
If Doc = Nil Then
Begin
ResetParameters;
AddStringParameter('ObjectKind', 'OUTPUTJOB');
AddStringParameter('FileName', Path);
RunProcess('WorkspaceManager:OpenObject');
Doc := Client.GetDocumentByPath(Path);
End;
If Doc <> Nil Then Client.ShowDocument(Doc);
ResetParameters;
AddStringParameter('Action', 'Run');
AddStringParameter('ObjectKind', Kind);
If Medium <> '' Then AddStringParameter('OutputMedium', Medium);
AddStringParameter('DisableDialog', 'True');
RunProcess('WorkspaceManager:GenerateReport');
Result := '{' + JRaw('ok', 'true') + ',' + JStr('outjob', Path) + ',' + JStr('medium', Medium) + ',' + JStr('objectKind', Kind) + '}';
End;
{ ------------------------------------------------------- library authoring --- }
{ Split "a;b;c" into a TStringList (one item per ';'). }
Function SplitSemi(S : String) : TStringList;
Var
K : Integer;
Part : String;
Begin
Result := TStringList.Create;
While S <> '' Do
Begin
K := Pos(';', S);
If K > 0 Then
Begin
Part := Copy(S, 1, K - 1);
S := Copy(S, K + 1, Length(S));
End
Else
Begin
Part := S;
S := '';
End;
If Trim(Part) <> '' Then Result.Add(Trim(Part));
End;
End;
{ Field N (1-based) of a comma list. }
Function Field(S : String; N : Integer) : String;
Var
I, K : Integer;
Begin
Result := '';
For I := 1 To N Do
Begin
K := Pos(',', S);
If K > 0 Then
Begin
Result := Copy(S, 1, K - 1);
S := Copy(S, K + 1, Length(S));
End
Else
Begin
Result := S;
S := '';
End;
End;
Result := Trim(Result);
End;
{ Open an existing library or create + save a new one. kind=PCBLIB|SCHLIB path=<file> }
Function EnsureLibrary(Kind : String; Path : String) : String;
Var
Doc : IServerDocument;
Begin
If FileExists(Path) Then
Begin
ResetParameters;
AddStringParameter('ObjectKind', 'Document');
AddStringParameter('FileName', Path);
RunProcess('WorkspaceManager:OpenObject');
Doc := Client.GetDocumentByPath(Path);
If Doc <> Nil Then Client.ShowDocument(Doc);
Result := 'opened';
End
Else
Begin
Doc := CreateNewDocumentFromDocumentKind(Kind);
If Doc = Nil Then
Begin
Result := 'error: could not create ' + Kind;
Exit;
End;
Client.ShowDocument(Doc);
Doc.DoSafeChangeFileNameAndSave(Path, Kind);
Result := 'created';
End;
End;
{ create_footprint: name, libraryPath, description,
pads = des,x_mil,y_mil,w_mil,h_mil,shape(rect|round),hole_mil ; ... (hole 0 = SMD) }
Function CmdCreateFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Comp : IPCB_LibComponent;
Pad : IPCB_Pad;
Pads : TStringList;
I : Integer;
Name, LibPath, Desc, Spec, Shape, LibState : String;
Hole : Integer;
Begin
Name := Param(Lines, 'name');
LibPath := Param(Lines, 'libraryPath');
Desc := Param(Lines, 'description');
If (Name = '') Or (LibPath = '') Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'name and libraryPath are required') + '}';
Exit;
End;
LibState := EnsureLibrary('PCBLIB', LibPath);
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'no PCB library is current (' + LibState + ')') + '}';
Exit;
End;
PCBServer.PreProcess;
Comp := PCBServer.CreatePCBLibComp;
Comp.Name := Name;
Comp.Description := Desc;
Pads := SplitSemi(Param(Lines, 'pads'));
For I := 0 To Pads.Count - 1 Do
Begin
Spec := Pads[I];
Pad := PCBServer.PCBObjectFactory(ePadObject, eNoDimension, eCreate_Default);
Pad.Name := Field(Spec, 1);
Pad.X := MilsToCoord(AdomFloat(Field(Spec, 2)));
Pad.Y := MilsToCoord(AdomFloat(Field(Spec, 3)));
Pad.TopXSize := MilsToCoord(AdomFloat(Field(Spec, 4)));
Pad.TopYSize := MilsToCoord(AdomFloat(Field(Spec, 5)));
Shape := LowerCase(Field(Spec, 6));
If Shape = 'round' Then Pad.TopShape := eRounded Else Pad.TopShape := eRectangular;
Hole := 0;
If Field(Spec, 7) <> '' Then Hole := AdomInt(Field(Spec, 7));
If Hole > 0 Then
Begin
Pad.HoleSize := MilsToCoord(Hole);
Pad.Layer := eMultiLayer;
End
Else
Begin
Pad.HoleSize := 0;
Pad.Layer := eTopLayer;
End;
Comp.AddPCBObject(Pad);
End;
Pads.Free;
PLib.RegisterComponent(Comp);
PLib.CurrentComponent := Comp;
PCBServer.PostProcess;
PLib.Board.ViewManager_FullUpdate;
PLib.RefreshView;
ResetParameters;
AddStringParameter('Action', 'All');
RunProcess('PCB:Zoom');
Result := '{' + JRaw('ok', 'true') + ',' + JStr('footprint', Name) + ',' + JStr('library', LibPath) + ',' + JStr('libraryState', LibState) + ',' + JRaw('pads', IntToStr(Comp.GetPrimitiveCount(MkSet(ePadObject)))) + '}';
End;
{ create_symbol: name, libraryPath, designator(U), description, body_w_mil, body_h_mil,
pins = des,name,side(left|right|top|bottom),offset_mil ; ... }
Function CmdCreateSymbol(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Comp : ISch_Component;
Rect : ISch_Rectangle;
Pin : ISch_Pin;
Pins : TStringList;
I, W, H, Off : Integer;
Name, LibPath, Desc, Des, Spec, Side, LibState : String;
Begin
Name := Param(Lines, 'name');
LibPath := Param(Lines, 'libraryPath');
Desc := Param(Lines, 'description');
Des := Param(Lines, 'designator');
If Des = '' Then Des := 'U';
W := StrToIntDef(Param(Lines, 'body_w_mil'), 800);
H := StrToIntDef(Param(Lines, 'body_h_mil'), 600);
If (Name = '') Or (LibPath = '') Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'name and libraryPath are required') + '}';
Exit;
End;
LibState := EnsureLibrary('SCHLIB', LibPath);
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'no schematic library is current (' + LibState + ')') + '}';
Exit;
End;
Comp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Comp.CurrentPartID := 1;
Comp.DisplayMode := 0;
Comp.LibReference := Name;
Comp.ComponentDescription := Desc;
Comp.Designator.Text := Des;
Rect := SchServer.SchObjectFactory(eRectangle, eCreate_Default);
Rect.OwnerPartId := 1;
Rect.OwnerPartDisplayMode := 0;
Rect.Location := Point(MilsToCoord(0), MilsToCoord(0));
Rect.Corner := Point(MilsToCoord(W), MilsToCoord(H));
Rect.IsSolid := True;
Comp.AddSchObject(Rect);
Pins := SplitSemi(Param(Lines, 'pins'));
For I := 0 To Pins.Count - 1 Do
Begin
Spec := Pins[I];
Pin := SchServer.SchObjectFactory(ePin, eCreate_Default);
Pin.OwnerPartId := 1;
Pin.OwnerPartDisplayMode := 0;
Pin.Designator := Field(Spec, 1);
Pin.Name := Field(Spec, 2);
Side := LowerCase(Field(Spec, 3));
Off := StrToIntDef(Field(Spec, 4), 100);
Pin.PinLength := MilsToCoord(300);
If Side = 'right' Then
Begin
Pin.Orientation := eRotate0;
Pin.Location := Point(MilsToCoord(W), MilsToCoord(H - Off));
End
Else If Side = 'top' Then
Begin
Pin.Orientation := eRotate90;
Pin.Location := Point(MilsToCoord(Off), MilsToCoord(H));
End
Else If Side = 'bottom' Then
Begin
Pin.Orientation := eRotate270;
Pin.Location := Point(MilsToCoord(Off), MilsToCoord(0));
End
Else
Begin
Pin.Orientation := eRotate180;
Pin.Location := Point(MilsToCoord(0), MilsToCoord(H - Off));
End;
Comp.AddSchObject(Pin);
End;
I := Pins.Count;
Pins.Free;
SLib.AddSchComponent(Comp);
SchServer.RobotManager.SendMessage(SLib.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Comp.I_ObjectAddress);
SLib.CurrentSchComponent := Comp;
SLib.GraphicallyInvalidate;
ResetParameters;
AddStringParameter('Action', 'All');
RunProcess('Sch:Zoom');
Result := '{' + JRaw('ok', 'true') + ',' + JStr('symbol', Name) + ',' + JStr('library', LibPath) + ',' + JStr('libraryState', LibState) + ',' + JRaw('pins', IntToStr(I)) + '}';
End;
{ --------------------------------------------- library management (Barrett port) --- }
{ Register a library in Altium's installed-libraries list (IntLib, SchLib, PcbLib, DbLib). }
{ Every library in Altium's Installed Libraries list, as a JSON array of paths. }
Function CmdListInstalledLibs(Lines : TStringList) : String;
Var
I, N : Integer;
S : String;
Begin
N := IntegratedLibraryManager.InstalledLibraryCount;
S := '';
For I := 0 To N - 1 Do
Begin
If S <> '' Then S := S + ',';
S := S + '"' + JsonEsc(IntegratedLibraryManager.InstalledLibraryPath(I)) + '"';
End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('libraries', '[' + S + ']') + '}';
End;
{ Unregister installed libraries by exact path (paths=a;b). The files are never touched. A
library that is open as a document is closed first: an installed SchLib loads as a document
that comes up modified, and a close that discards it drops the unregistration (measured by
John on Altium Develop, 2026-09-23). Removal takes effect live. }
Function CmdUninstallLibrary(Lines : TStringList) : String;
Var
PL : TStringList;
K : Integer;
P, Removed, Closed : String;
Doc : IServerDocument;
Begin
PL := SplitSemi(Param(Lines, 'paths'));
Removed := ''; Closed := '';
For K := 0 To PL.Count - 1 Do
Begin
P := PL[K];
Doc := Nil;
Try
Doc := Client.GetDocumentByPath(P);
Except
Doc := Nil;
End;
If Doc <> Nil Then
Begin
Try
Client.CloseDocument(Doc);
If Closed <> '' Then Closed := Closed + ',';
Closed := Closed + '"' + JsonEsc(P) + '"';
Except
End;
End;
IntegratedLibraryManager.UninstallLibrary(P);
If Removed <> '' Then Removed := Removed + ',';
Removed := Removed + '"' + JsonEsc(P) + '"';
End;
PL.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('removed', '[' + Removed + ']') + ',' +
JRaw('closedDocuments', '[' + Closed + ']') + ',' +
JRaw('remaining', IntToStr(IntegratedLibraryManager.InstalledLibraryCount)) + '}';
End;
Function CmdInstallLibrary(Lines : TStringList) : String;
Var
Path : String;
Begin
Path := Param(Lines, 'path');
If Not FileExists(Path) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'library not found: ' + Path) + '}';
Exit;
End;
IntegratedLibraryManager.InstallLibrary(Path);
Result := '{' + JRaw('ok', 'true') + ',' + JStr('installed', Path) + '}';
End;
{ Compile a .LibPkg into a .IntLib: path=<LibPkg> output=<dir> }
Function CmdCompileIntLib(Lines : TStringList) : String;
Var
Path, OutDir : String;
Proj : IProject;
Begin
Path := Param(Lines, 'path');
OutDir := Param(Lines, 'output');
If Not FileExists(Path) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'LibPkg not found: ' + Path) + '}';
Exit;
End;
Proj := GetWorkspace.DM_OpenProject(Path, True);
If Proj = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'could not open LibPkg') + '}';
Exit;
End;
Proj.DM_Compile;
IntegratedLibraryManager.CreateIntegratedLibrary(Proj, OutDir, True);
Result := '{' + JRaw('ok', 'true') + ',' + JStr('libpkg', Path) + ',' + JStr('outputDir', OutDir) + '}';
End;
{ The folder part of a path, ending in '\'. Built from Copy/Length only: SysUtils names that
this engine does not expose raise an Error box and park the script on a breakpoint, so the agent
never calls one that is not already proven elsewhere in this file. }
Function AdomDirOf(P : String) : String;
Var
I : Integer;
Begin
Result := '';
For I := Length(P) DownTo 1 Do
Begin
If (Copy(P, I, 1) = '\') Or (Copy(P, I, 1) = '/') Then
Begin
Result := Copy(P, 1, I);
Break;
End;
End;
End;
{ Extract editable SchLib/PcbLib sources from a compiled .IntLib: path=<IntLib> }
Function CmdExtractIntLib(Lines : TStringList) : String;
Var
Path, P, Acc, Dir, UP : String;
WS : IWorkspace;
Prj : IProject;
Doc : IDocument;
I, J : Integer;
Begin
Path := Param(Lines, 'path');
If Not FileExists(Path) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'IntLib not found: ' + Path) + '}';
Exit;
End;
IntegratedLibraryManager.ExtractSources(Path);
{ ExtractSources writes the editable libraries BESIDE the .IntLib. This used to report every
.SchLib/.PcbLib open anywhere in the workspace, so a caller with other libraries open was
handed paths that had nothing to do with this IntLib. Scope it to the IntLib's own folder. }
Dir := UpperCase(AdomDirOf(Path));
Acc := '';
WS := GetWorkspace;
If WS <> Nil Then
For I := 0 To WS.DM_ProjectCount - 1 Do
Begin
Prj := WS.DM_Projects(I);
For J := 0 To Prj.DM_LogicalDocumentCount - 1 Do
Begin
Doc := Prj.DM_LogicalDocuments(J);
P := Doc.DM_FullPath;
UP := UpperCase(P);
If ((Pos('.SCHLIB', UP) > 0) Or (Pos('.PCBLIB', UP) > 0)) And
((Dir = '') Or (Pos(Dir, UP) = 1)) Then
Begin
If Acc <> '' Then Acc := Acc + ',';
Acc := Acc + '"' + JsonEsc(P) + '"';
End;
End;
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('intlib', Path) + ',' +
JStr('searchedUnder', AdomDirOf(Path)) + ',' + JRaw('sources', '[' + Acc + ']') + '}';
End;
{ Set/replace parameters on a component of the CURRENT SchLib and optionally link a footprint.
name=<LibReference> params=Key=Value;Key=Value footprint=<PcbLib footprint name> comment=<text> }
Procedure SetSchParam(Cmp : ISch_Component; PName : String; PVal : String);
Var
It : ISch_Iterator;
Pm, Existing : ISch_Parameter;
Begin
Existing := Nil;
It := Cmp.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eParameter));
Pm := It.FirstSchObject;
While Pm <> Nil Do
Begin
If Pm.Name = PName Then
Begin
Existing := Pm;
Break;
End;
Pm := It.NextSchObject;
End;
Cmp.SchIterator_Destroy(It);
If Existing <> Nil Then
Existing.Text := PVal
Else
Begin
Pm := SchServer.SchObjectFactory(eParameter, eCreate_GlobalCopy);
Pm.Name := PName;
Pm.Text := PVal;
{ library-part parameters are metadata; visible ones pile up over the symbol (measured 2026-08-30) }
Pm.IsHidden := True;
Cmp.AddSchObject(Pm);
End;
End;
Function CmdSetParams(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
Impl : ISch_Implementation;
C2 : ISch_Component;
Name, Fp, Cmt, Spec, K, V : String;
Items : TStringList;
I, P, N : Integer;
Begin
Name := Param(Lines, 'name');
Fp := Param(Lines, 'footprint');
Cmt := Param(Lines, 'comment');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
{ With no name this used to take whichever component the iterator yielded FIRST, so a
set_params against a multi-symbol library silently wrote the footprint and comment onto
an arbitrary part. An omitted name may only mean "the one component in here". }
Comp := Nil;
N := 0;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
C2 := Iter.FirstSchObject;
While C2 <> Nil Do
Begin
N := N + 1;
If Name = '' Then
Begin
If Comp = Nil Then Comp := C2;
End
Else If UpperCase(C2.LibReference) = UpperCase(Name) Then Comp := C2;
C2 := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If (Name = '') And (N > 1) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('errorCode', 'name_required') + ',' +
JStr('error', 'this library holds ' + IntToStr(N) + ' components: name the one to set') + '}';
Exit;
End;
If Comp = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('errorCode', 'name_not_found') + ',' +
JStr('error', 'component not found: ' + Name) + '}';
Exit;
End;
N := 0;
Items := SplitSemi(Param(Lines, 'params'));
For I := 0 To Items.Count - 1 Do
Begin
Spec := Items[I];
P := Pos('=', Spec);
If P > 0 Then
Begin
K := Trim(Copy(Spec, 1, P - 1));
V := Trim(Copy(Spec, P + 1, Length(Spec)));
SetSchParam(Comp, K, V);
N := N + 1;
End;
End;
Items.Free;
If Cmt <> '' Then Comp.Comment.Text := Cmt;
If Fp <> '' Then
Begin
Impl := Comp.AddSchImplementation;
Impl.ClearAllDatafileLinks;
Impl.ModelName := Fp;
Impl.ModelType := 'PCBLIB';
Impl.AddDataFileLink(Fp, '', 'PCBLIB');
Impl.IsCurrent := True;
End;
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('component', Comp.LibReference) + ',' + JRaw('paramsSet', IntToStr(N)) + ',' + JStr('footprint', Fp) + '}';
End;
{ Add a 3D body to a footprint of the CURRENT PcbLib: name=<footprint>
step=<path.step> OR extruded box: bw_mil,bh_mil,h_mil,so_mil }
{ ------------------------------------------------- diagnostics: pcb_report --- }
Function BodyJson(Prim : IPCB_Primitive) : String;
Var
R : TCoordRect;
HasModel : String;
Begin
R := Prim.BoundingRectangle;
HasModel := 'false';
Try
If Prim.Model <> Nil Then HasModel := 'true';
Except
End;
Result := '[' + IntToStr(Prim.Layer) + ',' +
IntToStr(Round(CoordToMils(R.Right - R.Left))) + ',' +
IntToStr(Round(CoordToMils(R.Top - R.Bottom))) + ',' +
IntToStr(Round(CoordToMils(R.Left))) + ',' +
IntToStr(Round(CoordToMils(R.Bottom))) + ',' +
IntToStr(Round(CoordToMils(Prim.StandoffHeight))) + ',' +
IntToStr(Round(CoordToMils(Prim.OverallHeight))) + ',' +
HasModel + ']';
End;
Function GroupBodies(Grp : IPCB_Group) : String;
Var
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
S : String;
Begin
S := '';
GIt := Grp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(eComponentBodyObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If S <> '' Then S := S + ',';
S := S + BodyJson(Prim);
Prim := GIt.NextPCBObject;
End;
Grp.GroupIterator_Destroy(GIt);
Result := '[' + S + ']';
End;
Function GroupCensus(Grp : IPCB_Group) : String;
Var
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
N : Integer;
S : String;
Begin
N := 0;
S := '';
GIt := Grp.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
N := N + 1;
If S <> '' Then S := S + ';';
S := S + IntToStr(Prim.ObjectId);
Prim := GIt.NextPCBObject;
End;
Grp.GroupIterator_Destroy(GIt);
Result := IntToStr(N) + ':' + S;
End;
Function GroupCount(Grp : IPCB_Group; Kind : Integer) : Integer;
Var
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
N : Integer;
Begin
N := 0;
GIt := Grp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(Kind));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
N := N + 1;
Prim := GIt.NextPCBObject;
End;
Grp.GroupIterator_Destroy(GIt);
Result := N;
End;
Function BoardBodyCount(Board : IPCB_Board) : Integer;
Var
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
N : Integer;
Begin
N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentBodyObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
N := N + 1;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := N;
End;
Function CmdAddBody(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Fp : IPCB_LibComponent;
Fp2 : IPCB_LibComponent;
Body : IPCB_ComponentBody;
Mdl : IPCB_Model;
Cont : IPCB_Contour;
BIt : IPCB_BoardIterator;
Old : IPCB_Primitive;
Name, StepPath, Note, KeepS : String;
I, BW, BH, HG, SO : Integer;
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
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a PCB library') + '}';
Exit;
End;
{ An omitted name may only mean "the one footprint in here": adding a 3D body to whichever
footprint happened to be first is a silent edit of the wrong part. }
If (Name = '') And (PLib.ComponentCount > 1) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('errorCode', 'name_required') + ',' +
JStr('error', 'this library holds ' + IntToStr(PLib.ComponentCount) + ' footprints: name the one to add the body to') + '}';
Exit;
End;
Found := False;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
Fp := PLib.GetComponent(I);
If (Name = '') Or (UpperCase(Fp.Name) = UpperCase(Name)) Then
Begin
Found := True;
Break;
End;
End;
If Not Found Then
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
2026-08-30: add returned ok, census showed 16 pads and 0 bodies). Component
bodies live on a mechanical layer. }
Body.Layer := eMechanical1;
{ An invisible body is indistinguishable from a missing one: opacity defaults
to 0 in this engine. Make sure the mech layer is enabled and the body opaque. }
PLib.Board.LayerStack.LayerObject[eMechanical1].MechanicalLayerEnabled := True;
PLib.Board.LayerIsDisplayed[eMechanical1] := True;
Body.BodyOpacity3D := 1.0;
If StepPath <> '' Then
Begin
Mdl := Body.ModelFactory_FromFilename(StepPath, False);
If Mdl = Nil Then
Begin
PCBServer.PostProcess;
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'could not load model: ' + StepPath) + '}';
Exit;
End;
Body.SetState_FromModel;
Body.Model := Mdl;
Note := 'step';
End
Else
Begin
BW := StrToIntDef(Param(Lines, 'bw_mil'), 100);
BH := StrToIntDef(Param(Lines, 'bh_mil'), 100);
HG := StrToIntDef(Param(Lines, 'h_mil'), 40);
SO := StrToIntDef(Param(Lines, 'so_mil'), 0);
Body.BodyProjection := eBoardSide_Top;
Body.StandoffHeight := MilsToCoord(SO);
Body.OverallHeight := MilsToCoord(SO + HG);
Body.BodyColor3D := $00202020;
Cont := PCBServer.PCBContourFactory;
Cont.AddPoint(MilsToCoord(-(BW Div 2)), MilsToCoord(-(BH Div 2)));
Cont.AddPoint(MilsToCoord((BW Div 2)), MilsToCoord(-(BH Div 2)));
Cont.AddPoint(MilsToCoord((BW Div 2)), MilsToCoord((BH Div 2)));
Cont.AddPoint(MilsToCoord(-(BW Div 2)), MilsToCoord((BH Div 2)));
Body.SetOutlineContour(Cont);
Note := 'extruded';
End;
{ The stored component is NOT the editing surface: a direct Fp.AddPCBObject
counted 1 body immediately but the editor's canvas copy overwrote it on
save/reopen - libBodies read back 0 (measured 2026-08-30). Make Fp current
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';
PCBServer.PostProcess;
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;
{ --------------------------------------------------- demo: place on sch + pcb --- }
{ Create a new schematic sheet and place the CH340G symbol (Replicated from the
currently-open SchLib) at the centre. sch=<save path> libref=<component> }
{ true/false for JSON }
Function BoolStr(B : Boolean) : String;
Begin
If B Then Result := 'true' Else Result := 'false';
End;
{ Set (or add) a DOCUMENT parameter on a sheet: Title / DocumentNumber / Revision / DrawnBy feed the
standard title block through its special strings. }
Procedure SetDocParam(Sheet : ISch_Document; PName : String; PVal : String);
Var
It : ISch_Iterator;
Pm, Existing : ISch_Parameter;
Begin
Existing := Nil;
It := Sheet.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eParameter));
Pm := It.FirstSchObject;
While Pm <> Nil Do
Begin
If Pm.Name = PName Then
Begin
Existing := Pm;
Break;
End;
Pm := It.NextSchObject;
End;
Sheet.SchIterator_Destroy(It);
If Existing <> Nil Then
Existing.Text := PVal
Else
Begin
Pm := SchServer.SchObjectFactory(eParameter, eCreate_Default);
Pm.Name := PName;
Pm.Text := PVal;
Pm.IsHidden := True;
Sheet.AddSchObject(Pm);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Pm.I_ObjectAddress);
End;
End;
{ Split on single spaces }
Function SplitSpace(S : String) : TStringList;
Var
K : Integer;
Part : String;
Begin
Result := TStringList.Create;
S := Trim(S);
While S <> '' Do
Begin
K := Pos(' ', S);
If K > 0 Then
Begin
Part := Copy(S, 1, K - 1);
S := Trim(Copy(S, K + 1, Length(S)));
End
Else
Begin
Part := S;
S := '';
End;
If Part <> '' Then Result.Add(Part);
End;
End;
{ Draw the chip-outline artwork (chip-thumbnailer polylines, as baked into the KiCad symbol) into a
SchLib component. Args: libref, kicadRect=x0,y0,x1,y1 (mils, the KiCad body rectangle). Payload:
one polyline per line, 'x,y x,y ...' in mils of the KiCad frame. The KiCad body rectangle is mapped
onto the component's own rectangle, so the outline lands between the pin-name columns exactly as
it does in KiCad and Fusion (John, 2026-09-19). }
Function CmdAddSymbolArt(Lines : TStringList; Payload : String) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
Rect : ISch_Rectangle;
Poly : ISch_Polyline;
Seg : ISch_Line;
Prim : ISch_GraphicalObject;
Victims : TInterfaceList;
PL, Pts : TStringList;
I, J, N, NPoly, NPts, NCleared : Integer;
Name, KR, Ln, Tok, Mode : String;
KX0, KY0, KX1, KY1, AX0, AY0, AX1, AY1, SX, SY, MX, MY, PX, PY, Tmp : Double;
UseLines : Boolean;
Begin
Name := Param(Lines, 'libref');
KR := Param(Lines, 'kicadRect');
Mode := Param(Lines, 'mode');
{ lines (default): every polyline becomes N-1 eLine segments, the primitive BuildSymbolModes has
always drawn with; polyline: ISch_Polyline objects (measured 2026-09-19: they render on one
part and stay invisible on another with identical geometry). }
UseLines := (Mode <> 'polyline');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
Comp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Comp = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';
Exit;
End;
Rect := Nil;
Iter := Comp.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eRectangle));
Rect := Iter.FirstSchObject;
If Rect <> Nil Then
Begin
AX0 := CoordToMils(Rect.Location.X); AY0 := CoordToMils(Rect.Location.Y);
AX1 := CoordToMils(Rect.Corner.X); AY1 := CoordToMils(Rect.Corner.Y);
End;
Comp.SchIterator_Destroy(Iter);
If Rect = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component has no body rectangle to fit the artwork into') + '}';
Exit;
End;
KX0 := AdomFloat(Field(KR, 1)); KY0 := AdomFloat(Field(KR, 2));
KX1 := AdomFloat(Field(KR, 3)); KY1 := AdomFloat(Field(KR, 4));
If KX1 < KX0 Then Begin Tmp := KX0; KX0 := KX1; KX1 := Tmp; End;
If KY1 < KY0 Then Begin Tmp := KY0; KY0 := KY1; KY1 := Tmp; End;
If AX1 < AX0 Then Begin Tmp := AX0; AX0 := AX1; AX1 := Tmp; End;
If AY1 < AY0 Then Begin Tmp := AY0; AY0 := AY1; AY1 := Tmp; End;
If (KX1 - KX0 < 1) Or (KY1 - KY0 < 1) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'kicadRect is degenerate: ' + KR) + '}';
Exit;
End;
SX := (AX1 - AX0) / (KX1 - KX0);
SY := (AY1 - AY0) / (KY1 - KY0);
SchServer.ProcessControl.PreProcess(SLib, '');
{ replace, never stack: every earlier polyline and every line that is not part of the pins
or the body outline is art from a previous run. Collect first, remove after the walk. }
NCleared := 0;
If Param(Lines, 'clear') <> '0' Then
Begin
Victims := TInterfaceList.Create;
Iter := Comp.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(ePolyline, eLine));
Prim := Iter.FirstSchObject;
While Prim <> Nil Do
Begin
Victims.Add(Prim);
Prim := Iter.NextSchObject;
End;
Comp.SchIterator_Destroy(Iter);
For I := 0 To Victims.Count - 1 Do
Begin
Prim := Victims[I];
Comp.RemoveSchObject(Prim);
SchServer.DestroySchObject(Prim);
NCleared := NCleared + 1;
End;
Victims.Free;
End;
PL := TStringList.Create;
PL.Text := Payload;
NPoly := 0; NPts := 0;
For I := 0 To PL.Count - 1 Do
Begin
Ln := Trim(PL[I]);
If Ln = '' Then Continue;
Pts := SplitSpace(Ln);
N := Pts.Count;
If N >= 2 Then
Begin
If UseLines Then
Begin
PX := 0; PY := 0;
For J := 1 To N Do
Begin
Tok := Pts[J - 1];
MX := AX0 + (AdomFloat(Field(Tok, 1)) - KX0) * SX;
MY := AY0 + (AdomFloat(Field(Tok, 2)) - KY0) * SY;
If J > 1 Then
Begin
Seg := SchServer.SchObjectFactory(eLine, eCreate_Default);
Seg.OwnerPartId := 1;
Seg.OwnerPartDisplayMode := 0;
Seg.LineWidth := eSmall;
Seg.Color := $8A8A8A;
Seg.Location := Point(MilsToCoord(PX), MilsToCoord(PY));
Seg.Corner := Point(MilsToCoord(MX), MilsToCoord(MY));
Comp.AddSchObject(Seg);
SchServer.RobotManager.SendMessage(Comp.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Seg.I_ObjectAddress);
End;
PX := MX; PY := MY;
End;
End
Else
Begin
Poly := SchServer.SchObjectFactory(ePolyline, eCreate_Default);
Poly.OwnerPartId := 1;
Poly.OwnerPartDisplayMode := 0;
Poly.LineWidth := eSmall;
Poly.Color := $8A8A8A;
Poly.VerticesCount := N;
For J := 1 To N Do
Begin
Tok := Pts[J - 1];
MX := AX0 + (AdomFloat(Field(Tok, 1)) - KX0) * SX;
MY := AY0 + (AdomFloat(Field(Tok, 2)) - KY0) * SY;
Poly.Vertex[J] := Point(MilsToCoord(MX), MilsToCoord(MY));
End;
Comp.AddSchObject(Poly);
SchServer.RobotManager.SendMessage(Comp.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Poly.I_ObjectAddress);
End;
NPoly := NPoly + 1;
NPts := NPts + N;
End;
Pts.Free;
End;
SchServer.ProcessControl.PostProcess(SLib, '');
PL.Free;
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' + JRaw('polylines', IntToStr(NPoly)) + ',' +
JRaw('points', IntToStr(NPts)) + ',' + JRaw('cleared', IntToStr(NCleared)) + ',' + JStr('mode', Mode) + ',' +
JRaw('bodyRectMil', '[' + IntToStr(Round(AX0)) + ',' + IntToStr(Round(AY0)) + ',' + IntToStr(Round(AX1)) + ',' + IntToStr(Round(AY1)) + ']') + '}';
End;
{ Read a SchLib component drawing: primitive counts by kind, the body rectangle, the polyline
bounding box and the footprint models. Args: libref }
{ Regression test for the script-error safety net: raise a REAL runtime error inside the agent's
own code, on purpose. The bridge must photograph the Error box, stop the halted script itself
and answer agent_script_error; a timeout here means the net is broken. Never call this except
through altium_selftest_script_error. }
Function CmdSelfTestHalt(Lines : TStringList) : String;
Begin
AdomDeliberateUndeclaredIdentifierForSelfTest(1);
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'the engine did not halt: this Altium build tolerated an undeclared identifier') + '}';
End;
Function CmdSymbolPrims(Lines : TStringList) : String;
Var
AnyR0 : Boolean;
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
Prim : ISch_GraphicalObject;
Impl : ISch_Implementation;
Name, Models : String;
NPin, NRect, NLine, NPoly, NOther, I : Integer;
R : TCoordRect;
PX0, PY0, PX1, PY1, RX0, RY0, RX1, RY1 : Integer;
AnyP, AnyR : Boolean;
Begin
Name := Param(Lines, 'libref');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
Comp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Comp = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';
Exit;
End;
NPin := 0; NRect := 0; NLine := 0; NPoly := 0; NOther := 0;
AnyP := False; AnyR := False; AnyR0 := False;
PX0 := 0; PY0 := 0; PX1 := 0; PY1 := 0; RX0 := 0; RY0 := 0; RX1 := 0; RY1 := 0;
Iter := Comp.SchIterator_Create;
Prim := Iter.FirstSchObject;
While Prim <> Nil Do
Begin
If Prim.ObjectID = ePin Then NPin := NPin + 1
Else If Prim.ObjectID = eRectangle Then
Begin
NRect := NRect + 1;
{ the body of the mode the symbol shows (the sheet draws mode 0): the union of that
mode's rectangles (a capacitor's two plates are two). The first rectangle found
could be any mode's: a 10-mode symbol answered a wider mode's body. }
R := Prim.BoundingRectangle;
If Prim.OwnerPartDisplayMode = Comp.DisplayMode Then
Begin
If Not AnyR0 Then
Begin
RX0 := Round(CoordToMils(R.Left)); RY0 := Round(CoordToMils(R.Bottom));
RX1 := Round(CoordToMils(R.Right)); RY1 := Round(CoordToMils(R.Top));
End
Else
Begin
If Round(CoordToMils(R.Left)) < RX0 Then RX0 := Round(CoordToMils(R.Left));
If Round(CoordToMils(R.Bottom)) < RY0 Then RY0 := Round(CoordToMils(R.Bottom));
If Round(CoordToMils(R.Right)) > RX1 Then RX1 := Round(CoordToMils(R.Right));
If Round(CoordToMils(R.Top)) > RY1 Then RY1 := Round(CoordToMils(R.Top));
End;
AnyR0 := True; AnyR := True;
End
Else If Not AnyR Then
Begin
RX0 := Round(CoordToMils(R.Left)); RY0 := Round(CoordToMils(R.Bottom));
RX1 := Round(CoordToMils(R.Right)); RY1 := Round(CoordToMils(R.Top));
AnyR := True;
End;
End
Else If Prim.ObjectID = eLine Then NLine := NLine + 1
Else If Prim.ObjectID = ePolyline Then
Begin
NPoly := NPoly + 1;
R := Prim.BoundingRectangle;
If Not AnyP Then
Begin
PX0 := Round(CoordToMils(R.Left)); PY0 := Round(CoordToMils(R.Bottom));
PX1 := Round(CoordToMils(R.Right)); PY1 := Round(CoordToMils(R.Top));
AnyP := True;
End
Else
Begin
If Round(CoordToMils(R.Left)) < PX0 Then PX0 := Round(CoordToMils(R.Left));
If Round(CoordToMils(R.Bottom)) < PY0 Then PY0 := Round(CoordToMils(R.Bottom));
If Round(CoordToMils(R.Right)) > PX1 Then PX1 := Round(CoordToMils(R.Right));
If Round(CoordToMils(R.Top)) > PY1 Then PY1 := Round(CoordToMils(R.Top));
End;
End
Else NOther := NOther + 1;
Prim := Iter.NextSchObject;
End;
Comp.SchIterator_Destroy(Iter);
Models := '';
If Param(Lines, 'models') = '1' Then
Begin
{ ISch_Component has no ImplementationCount/Implementation[] in DelphiScript on every AD
build (26.9.1 halts the script there); the eImplementation child iterator works on all. }
Iter := Comp.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eImplementation));
Impl := Iter.FirstSchObject;
While Impl <> Nil Do
Begin
If Models <> '' Then Models := Models + ',';
Models := Models + '"' + Impl.ModelType + ':' + Impl.ModelName + '"';
Impl := Iter.NextSchObject;
End;
Comp.SchIterator_Destroy(Iter);
End;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' +
JRaw('pins', IntToStr(NPin)) + ',' + JRaw('rectangles', IntToStr(NRect)) + ',' + JRaw('lines', IntToStr(NLine)) + ',' +
JRaw('polylines', IntToStr(NPoly)) + ',' + JRaw('other', IntToStr(NOther)) + ',' +
JRaw('displayMode', IntToStr(Comp.DisplayMode)) + ',' + JRaw('displayModeCount', IntToStr(Comp.DisplayModeCount)) + ',' +
JRaw('currentPartId', IntToStr(Comp.CurrentPartID)) + ',' +
JRaw('bodyRectMil', '[' + IntToStr(RX0) + ',' + IntToStr(RY0) + ',' + IntToStr(RX1) + ',' + IntToStr(RY1) + ']') + ',' +
JRaw('polyBboxMil', '[' + IntToStr(PX0) + ',' + IntToStr(PY0) + ',' + IntToStr(PX1) + ',' + IntToStr(PY1) + ']') + ',' +
JRaw('models', '[' + Models + ']') + '}';
End;
{ Replace every footprint model on a SchLib component with ONE PCBLIB model (set_params
footprint APPENDS an implementation per call, which is how a symbol ends up with two).
Args: libref, footprint }
Function CmdSetModels(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
Impl : ISch_Implementation;
Name, Fp : String;
Removed : Integer;
Begin
Name := Param(Lines, 'libref');
Fp := Param(Lines, 'footprint');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'current document is not a schematic library') + '}';
Exit;
End;
Comp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If UpperCase(Comp.LibReference) = UpperCase(Name) Then Break;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Comp = Nil Then
Begin
Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'component not found: ' + Name) + '}';
Exit;
End;
Removed := 0;
{ Same rule as CmdSymbolPrims: walk implementations with the eImplementation iterator. One
iterator per removal, because removing under a live iterator invalidates it. }
While Removed < 64 Do
Begin
Iter := Comp.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eImplementation));
Impl := Iter.FirstSchObject;
Comp.SchIterator_Destroy(Iter);
If Impl = Nil Then Break;
Comp.RemoveSchImplementation(Impl);
Removed := Removed + 1;
End;
If Fp <> '' Then
Begin
Impl := Comp.AddSchImplementation;
Impl.ClearAllDatafileLinks;
Impl.ModelName := Fp;
Impl.ModelType := 'PCBLIB';
Impl.AddDataFileLink(Fp, '', 'PCBLIB');
Impl.IsCurrent := True;
End;
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' + JRaw('removed', IntToStr(Removed)) + ',' + JStr('footprint', Fp) + '}';
End;
Function CmdPlaceOnSch(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
PIt : ISch_Iterator;
LibCmp : ISch_Component;
Inst : ISch_Component;
Prim : ISch_GraphicalObject;
Cpy : ISch_GraphicalObject;
SD : IServerDocument;
Sheet : ISch_Document;
SchPath, LibRef, Missing, Placed, Title, DocNum, Rev, Drawn, Desc : String;
NCopied, K, W, H, XK, YK, BCX, BCY, BH : Integer;
PWs, CXs, CYs, TPs, BTs : TStringList;
R : TCoordRect;
TotalW, MaxH, Gap, Margin, GapA, MarginA, Cur, DX, DY, BBL, BBB, BBR, BBT, BTop, BBot : Integer;
Append : Boolean;
EIt : ISch_Iterator;
ECmp : ISch_Component;
NExist, ExtX, SumY, GW : Integer;
Grown : Boolean;
RefList : TStringList;
RIt : ISch_Iterator;
LRect : ISch_Rectangle;
Pm : ISch_Parameter;
TF : ISch_TextFrame;
Begin
SchPath := Param(Lines, 'sch');
LibRef := Param(Lines, 'libref');
Title := Param(Lines, 'title');
DocNum := Param(Lines, 'number');
Rev := Param(Lines, 'revision');
Drawn := Param(Lines, 'drawnBy');
Desc := Param(Lines, 'description');
{ libref may be a ';' list: every symbol lands on ONE new sheet as U1..Un, 5000 mil apart
(John, 2026-09-19: three ToF sensors on one schematic for the demo). }
RefList := SplitSemi(LibRef);
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first (it must be the current document)') + '}';
Exit;
End;
Missing := '';
For K := 0 To RefList.Count - 1 Do
Begin
LibCmp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
LibCmp := Iter.FirstSchObject;
While LibCmp <> Nil Do
Begin
If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;
LibCmp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If LibCmp = Nil Then Missing := Missing + RefList[K] + ' ';
End;
If (RefList.Count = 0) Or (Missing <> '') Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found in SchLib: ' + Trim(Missing + LibRef)) + '}';
RefList.Free;
Exit;
End;
{ Append mode (issue 11): sch names an EXISTING .SchDoc -> open it and add the parts to
the right of what is there; the sheet keeps its size and title block. }
SD := Nil;
Append := (SchPath <> '') And FileExists(SchPath);
If (SchPath <> '') And (Not Append) Then
Begin
SD := Nil;
Try
SD := Client.GetDocumentByPath(SchPath);
Except
SD := Nil;
End;
If SD <> Nil Then Append := True;
End;
If Append Then
Begin
If SD = Nil Then SD := Client.OpenDocument('SCH', SchPath);
If SD = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the existing schematic: ' + SchPath) + '}';
RefList.Free;
Exit;
End;
Client.ShowDocument(SD);
End
Else
Begin
SD := CreateNewDocumentFromDocumentKind('SCH');
If SD = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not create a schematic sheet') + '}';
RefList.Free;
Exit;
End;
Client.ShowDocument(SD);
End;
Sheet := SchServer.GetCurrentSchDocument;
If Sheet = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic after open') + '}';
RefList.Free;
Exit;
End;
{ existing symbols: count (designators continue) and the rightmost anchor + height band }
NExist := 0; ExtX := 0; SumY := 0; Grown := False;
If Append Then
Begin
EIt := Sheet.SchIterator_Create;
EIt.AddFilter_ObjectSet(MkSet(eSchComponent));
ECmp := EIt.FirstSchObject;
While ECmp <> Nil Do
Begin
NExist := NExist + 1;
If (NExist = 1) Or (Round(CoordToMils(ECmp.Location.X)) > ExtX) Then ExtX := Round(CoordToMils(ECmp.Location.X));
SumY := SumY + Round(CoordToMils(ECmp.Location.Y));
ECmp := EIt.NextSchObject;
End;
Sheet.SchIterator_Destroy(EIt);
End;
{ Pass 1: MEASURE every part (union of its primitives' bounding rectangles, pins
included, +250 mil a side for the pin-number text that hangs outside the stub).
John, 2026-09-19, reviewing three 5000 mil bodies squeezed onto one sheet:
"you are smooshing these symbols together such that their pin names overlap; no
real EE would ever do that, they'd space them out". So the sheet is sized FROM the
parts, never the parts from the sheet. }
PWs := TStringList.Create; CXs := TStringList.Create; CYs := TStringList.Create;
TPs := TStringList.Create; BTs := TStringList.Create;
TotalW := 0; MaxH := 0;
For K := 0 To RefList.Count - 1 Do
Begin
LibCmp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
LibCmp := Iter.FirstSchObject;
While LibCmp <> Nil Do
Begin
If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;
LibCmp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
BBL := 0; BBB := 0; BBR := 0; BBT := 0; BTop := 0; BBot := 0; NCopied := 0;
PIt := LibCmp.SchIterator_Create;
Prim := PIt.FirstSchObject;
While Prim <> Nil Do
Begin
If (Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or
(Prim.ObjectID = eLine) Or (Prim.ObjectID = eArc) Or
(Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or
(Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel) Then
Begin
R := Prim.BoundingRectangle;
If (NCopied = 0) Or (Round(CoordToMils(R.Left)) < BBL) Then BBL := Round(CoordToMils(R.Left));
If (NCopied = 0) Or (Round(CoordToMils(R.Bottom)) < BBB) Then BBB := Round(CoordToMils(R.Bottom));
If (NCopied = 0) Or (Round(CoordToMils(R.Right)) > BBR) Then BBR := Round(CoordToMils(R.Right));
If (NCopied = 0) Or (Round(CoordToMils(R.Top)) > BBT) Then BBT := Round(CoordToMils(R.Top));
If Prim.ObjectID = eRectangle Then
Begin
BTop := Round(CoordToMils(R.Top)); BBot := Round(CoordToMils(R.Bottom));
End;
NCopied := NCopied + 1;
End;
Prim := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
If BTop = 0 Then Begin BTop := BBT; BBot := BBB; End;
PWs.Add(IntToStr((BBR - BBL) + 500));
CXs.Add(IntToStr((BBL + BBR) Div 2));
CYs.Add(IntToStr((BBB + BBT) Div 2));
TPs.Add(IntToStr(BTop)); BTs.Add(IntToStr(BBot));
TotalW := TotalW + (BBR - BBL) + 500;
If (BBT - BBB) > MaxH Then MaxH := BBT - BBB;
End;
{ Sheet from the parts: 1000 mil margins, at least 1500 mil of air between parts, and
room above (1700 mil description strip) and below (title block) the row. Sheet.SheetStyle
does NOT take on ADDevelop (measured 2026-09-19: the sheet stayed the 9500 x 7500 mil
default), so the sheet is a CUSTOM size, never smaller than A4 (11500 x 7600). }
{ John, 2026-09-19 (after the 1500 mil cut): "put those symbols a bit closer together so
we get a better screenshot" - 500 mil of air between parts, 600 mil margins, and the
sheet hugs the row so a zoom-all fills the frame. }
Gap := 500; Margin := 600;
W := TotalW + Gap * (RefList.Count - 1) + 2 * Margin;
If W < 11500 Then W := 11500;
H := MaxH + 3600;
If H < 7600 Then H := 7600;
If H * 2 < W Then H := W Div 2;
If Not Append Then
Begin
Sheet.UseCustomSheet := True;
Sheet.CustomX := MilsToCoord(W);
Sheet.CustomY := MilsToCoord(H);
Sheet.CustomXZones := (W + 2499) Div 2500;
Sheet.CustomYZones := (H + 2499) Div 2500;
Sheet.CustomMarginWidth := MilsToCoord(200);
End;
{ Do NOT read SheetSizeX/Y back here: on ADDevelop they still report the 9500 x 7500 mil
default after the custom size is set (measured 2026-09-19: the read-back shrank W to
9500 and squeezed three 5800 mil parts into it, pin numbers on top of each other,
while the sheet itself drew at the custom size). W and H ARE the sheet. }
{ spread the slack evenly: the row is centred and every gap grows by the same amount }
GapA := Gap; MarginA := Margin;
If W > TotalW + Gap * (RefList.Count - 1) + 2 * Margin Then
Begin
GapA := Gap + (W - (TotalW + Gap * (RefList.Count - 1) + 2 * Margin)) Div (RefList.Count + 1);
MarginA := (W - TotalW - GapA * (RefList.Count - 1)) Div 2;
End;
Placed := '';
NCopied := 0;
Cur := MarginA;
If Append Then Cur := ExtX + 4000;
For K := 0 To RefList.Count - 1 Do
Begin
XK := Cur + AdomInt(PWs[K]) Div 2;
Cur := Cur + AdomInt(PWs[K]) + GapA;
{ the row sits a little below centre: the description strip takes the top 1700 mil;
appending, it sits in line with the existing symbols instead }
YK := (H Div 2) - 300;
If Append Then
Begin
If NExist > 0 Then YK := SumY Div NExist Else YK := 3500;
End;
DX := XK - AdomInt(CXs[K]);
DY := YK - AdomInt(CYs[K]);
LibCmp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
LibCmp := Iter.FirstSchObject;
While LibCmp <> Nil Do
Begin
If UpperCase(LibCmp.LibReference) = UpperCase(RefList[K]) Then Break;
LibCmp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
{ Build a fresh component instance and copy the library part's primitives into it.
ISch_Component.Replicate does NOT re-parent the pins/rectangle onto a normal
sheet (measured 2026-08-30: only the designator/comment text placed), so copy
each child primitive with primitive-level Replicate + AddSchObject. }
Inst := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Inst.CurrentPartID := 1;
Inst.DisplayMode := 0;
Inst.LibReference := LibCmp.LibReference;
Inst.ComponentDescription := LibCmp.ComponentDescription;
Inst.Location := Point(MilsToCoord(XK), MilsToCoord(YK));
{ body rectangle centre (mil, library frame) for the designator / comment columns }
BCX := AdomInt(CXs[K]); BCY := AdomInt(CYs[K]);
RIt := LibCmp.SchIterator_Create;
RIt.AddFilter_ObjectSet(MkSet(eRectangle));
LRect := RIt.FirstSchObject;
If LRect <> Nil Then
BCX := Round(CoordToMils(LRect.Location.X + LRect.Corner.X) / 2);
LibCmp.SchIterator_Destroy(RIt);
PIt := LibCmp.SchIterator_Create;
Prim := PIt.FirstSchObject;
While Prim <> Nil Do
Begin
If (Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or
(Prim.ObjectID = eLine) Or (Prim.ObjectID = eArc) Or
(Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or
(Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel) Then
Begin
Cpy := Prim.Replicate;
Cpy.OwnerPartId := 1;
Cpy.OwnerPartDisplayMode := 0;
{ copied primitives keep the library coordinates, Inst.Location does not carry them
(measured 2026-09-19: three parts drew on top of each other), so shift each child }
Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));
Inst.AddSchObject(Cpy);
NCopied := NCopied + 1;
End;
Prim := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
{ Designator centred ABOVE the body and the comment centred BELOW it, whatever the
library part did with them (measured 2026-09-19: +200 mil put U1 on top of the chip
outline inside a 3700 mil body). }
Inst.Designator.Text := 'U' + IntToStr(NExist + K + 1);
Inst.Designator.IsHidden := False;
Inst.Designator.Justification := eJustify_BottomCenter;
Inst.Designator.Location := Point(MilsToCoord(BCX + DX), MilsToCoord(AdomInt(TPs[K]) + DY + 100));
{ never leave the star placeholder: the comment is the part number }
Inst.Comment.Text := LibCmp.LibReference;
Inst.Comment.IsHidden := False;
Inst.Comment.Justification := eJustify_TopCenter;
Inst.Comment.Location := Point(MilsToCoord(BCX + DX), MilsToCoord(AdomInt(BTs[K]) + DY - 100));
Sheet.RegisterSchObjectInContainer(Inst);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);
If Placed <> '' Then Placed := Placed + ';';
Placed := Placed + RefList[K];
End;
{ Appended parts extend the row to the right; grow the sheet so they stay inside the
drawing frame (a fixed sheet left the appended part out in the gray, measured
2026-10-15). Never shrinks a sheet. }
If Append Then
Begin
GW := Cur + 600;
If Sheet.UseCustomSheet Then
Begin
If GW > Round(CoordToMils(Sheet.CustomX)) Then
Begin
Sheet.CustomX := MilsToCoord(GW);
Sheet.CustomXZones := (GW + 2499) Div 2500;
Grown := True;
End;
End
Else
Begin
Sheet.UseCustomSheet := True;
Sheet.CustomX := MilsToCoord(GW);
Sheet.CustomY := MilsToCoord(H);
Sheet.CustomXZones := (GW + 2499) Div 2500;
Sheet.CustomYZones := (H + 2499) Div 2500;
Sheet.CustomMarginWidth := MilsToCoord(200);
Grown := True;
End;
If Grown Then W := GW;
End;
{ Title block: the standard sheet renders the document parameters Title, DocumentNumber,
Revision and DrawnBy through its special strings; an empty block is a demo defect. }
If Append Then
Begin
Title := ''; DocNum := ''; Rev := ''; Drawn := ''; Desc := '';
End;
If Title <> '' Then SetDocParam(Sheet, 'Title', Title);
If DocNum <> '' Then SetDocParam(Sheet, 'DocumentNumber', DocNum);
If Rev <> '' Then SetDocParam(Sheet, 'Revision', Rev);
If Drawn <> '' Then SetDocParam(Sheet, 'DrawnBy', Drawn);
If Desc <> '' Then
Begin
SetDocParam(Sheet, 'Description', Desc);
TF := SchServer.SchObjectFactory(eTextFrame, eCreate_Default);
{ inside the drawing frame: the border + zone strip take ~400 mil, so the frame
runs from 700 mil below the top edge down 1000 mil, and stops 700 mil short of
the right edge }
TF.Location := Point(MilsToCoord(1000), MilsToCoord(H - 1700));
TF.Corner := Point(MilsToCoord(W - 700), MilsToCoord(H - 700));
TF.Text := Desc;
TF.WordWrap := True;
TF.ShowBorder := False;
Sheet.RegisterSchObjectInContainer(TF);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, TF.I_ObjectAddress);
End;
Sheet.GraphicallyInvalidate;
ResetParameters; AddStringParameter('Action','All'); RunProcess('Sch:Zoom');
If SchPath <> '' Then
Begin
If Not Append Then SD.SetFileName(SchPath);
SD.DoFileSave('');
End;
PWs.Free; CXs.Free; CYs.Free; TPs.Free; BTs.Free;
Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JRaw('sheetMil', '[' + IntToStr(W) + ',' + IntToStr(H) + ']') + ',' +
JRaw('titleBlock', '{' + JRaw('title', BoolStr(Title <> '')) + ',' + JRaw('number', BoolStr(DocNum <> '')) + ',' + JRaw('revision', BoolStr(Rev <> '')) + ',' + JRaw('drawnBy', BoolStr(Drawn <> '')) + ',' + JRaw('description', BoolStr(Desc <> '')) + '}') + ',' +
JStr('placed', Placed) + ',' + JStr('designator','U' + IntToStr(NExist + 1)) + ',' + JRaw('appended', BoolStr(Append)) + ',' + JRaw('sheetGrown', BoolStr(Grown)) + ',' + JRaw('existingParts', IntToStr(NExist)) + ',' + JRaw('count', IntToStr(RefList.Count)) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';
RefList.Free;
End;
Function CmdPlaceOnPcb(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
LibFp : IPCB_LibComponent;
Inst : IPCB_Component;
Board : IPCB_Board;
SD : IServerDocument;
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
Cpy : IPCB_Primitive;
PcbPath, FpName : String;
I, NCopied : Integer;
Found : Boolean;
R : TCoordRect;
DX, DY, CL, CB, CR, CT : Integer;
AnyP : Boolean;
Begin
PcbPath := Param(Lines, 'pcb');
FpName := Param(Lines, 'footprint');
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first (it must be the current document)') + '}';
Exit;
End;
Found := False;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
LibFp := PLib.GetComponent(I);
If UpperCase(LibFp.Name) = UpperCase(FpName) Then Begin Found := True; Break; End;
End;
If Not Found Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found in PcbLib: ' + FpName) + '}';
Exit;
End;
{ ALWAYS create a fresh PcbDoc: with a PcbLib focused, GetCurrentPCBBoard
returns the LIBRARY internal board - a reuse path put the part into the lib
canvas and renamed the lib doc, measured 2026-08-30. }
SD := CreateNewDocumentFromDocumentKind('PCB');
If SD = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not create a PCB document') + '}';
Exit;
End;
Client.ShowDocument(SD);
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board') + '}';
Exit;
End;
Board.LayerStack.LayerObject[eMechanical1].MechanicalLayerEnabled := True;
Board.LayerIsDisplayed[eMechanical1] := True;
WriteAtomic(WORKSPACE + '\stage.txt', 'board_ready');
{ Group Replicate of an IPCB_LibComponent DISSOLVES on a board: census showed
16 free pads, no eComponentObject, no body - measured 2026-08-30. Build a
fresh board component and copy each child primitive, the same cure the
schematic placement needed. }
PCBServer.PreProcess;
Inst := PCBServer.PCBObjectFactory(eComponentObject, eNoDimension, eCreate_Default);
Inst.Layer := eTopLayer;
{ The factory component's auto Designator1/Comment text is enormous and
hijacks Zoom All - hide both. }
Inst.NameOn := False;
Inst.CommentOn := False;
{ ORDER MATTERS: fill the group BEFORE the board add. Children added to an
already-registered component exist in data but never draw, whatever address
they are robot-registered to (measured 2026-08-30). This mirrors
create_footprint, where pads go in before RegisterComponent. }
NCopied := 0;
GIt := LibFp.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Cpy := Prim.Replicate;
Inst.AddPCBObject(Cpy);
NCopied := NCopied + 1;
Prim := GIt.NextPCBObject;
End;
LibFp.GroupIterator_Destroy(GIt);
Board.AddPCBObject(Inst);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Inst.I_ObjectAddress);
WriteAtomic(WORKSPACE + '\stage.txt', 'component_registered');
WriteAtomic(WORKSPACE + '\stage.txt', 'children_copied');
{ Copied children keep their LIBRARY coordinates (lib origin ~50000,50000 mil);
anchor the fresh component onto them BEFORE the move so MoveToXY translates
the whole group to the target (bbox was 50146 mil wide without this). }
{ Anchor the component BEFORE normalizing children: assigning Inst.x/y
afterwards re-translates the whole group by the same delta (measured
2026-08-30: everything landed at 8000,6000 instead of 4000,3000). }
Inst.x := MilsToCoord(4000);
Inst.y := MilsToCoord(3000);
{ Normalize positions: the lib stores pads around the canvas origin
(~50000,50000 mil) but an authored body contour around 0,0 - one group move
cannot land both (measured 2026-08-30: body at 4000, pads at 54000). Move
the pad cluster and each body to the target independently. }
AnyP := False;
CL := 0; CB := 0; CR := 0; CT := 0;
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
If Not AnyP Then
Begin
CL := R.Left; CB := R.Bottom; CR := R.Right; CT := R.Top; AnyP := True;
End
Else
Begin
If R.Left < CL Then CL := R.Left;
If R.Bottom < CB Then CB := R.Bottom;
If R.Right > CR Then CR := R.Right;
If R.Top > CT Then CT := R.Top;
End;
Prim := GIt.NextPCBObject;
End;
Inst.GroupIterator_Destroy(GIt);
DX := 0; DY := 0;
If AnyP Then
Begin
DX := MilsToCoord(4000) - ((CL + CR) Div 2);
DY := MilsToCoord(3000) - ((CB + CT) Div 2);
End;
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If Prim.ObjectId = eComponentBodyObject Then
Begin
R := Prim.BoundingRectangle;
Prim.MoveByXY(MilsToCoord(4000) - ((R.Left + R.Right) Div 2),
MilsToCoord(3000) - ((R.Bottom + R.Top) Div 2));
End
Else If Prim.ObjectId <> eTextObject Then
Prim.MoveByXY(DX, DY);
Prim := GIt.NextPCBObject;
End;
Inst.GroupIterator_Destroy(GIt);
{ Hidden auto Name/Comment texts otherwise bloat the bbox by ~50000 mil. }
Inst.Name.MoveToXY(MilsToCoord(4000), MilsToCoord(3250));
Inst.Comment.MoveToXY(MilsToCoord(4000), MilsToCoord(2750));
PCBServer.PostProcess;
WriteAtomic(WORKSPACE + '\stage.txt', 'moved');
Board.ViewManager_FullUpdate;
ResetParameters; AddStringParameter('Action','All'); RunProcess('PCB:Zoom');
If PcbPath <> '' Then
Begin
SD.SetFileName(PcbPath);
SD.DoFileSave('');
End;
WriteAtomic(WORKSPACE + '\stage.txt', 'saved');
Result := '{' + JRaw('ok','true') + ',' + JStr('pcb', PcbPath) + ',' + JStr('placed', FpName) + ',' +
JStr('libFile', PLib.Board.FileName) + ',' +
JRaw('libBodies', IntToStr(GroupCount(LibFp, eComponentBodyObject))) + ',' +
JRaw('primitives', IntToStr(NCopied)) + ',' +
JRaw('padsOnComp', IntToStr(GroupCount(Inst, ePadObject))) + ',' +
JRaw('bodiesOnComp', IntToStr(GroupCount(Inst, eComponentBodyObject))) + '}';
End;
Function BoardCensus(Board : IPCB_Board) : String;
Var
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
N : Integer;
S : String;
Begin
N := 0;
S := '';
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(AllObjects);
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While (Prim <> Nil) And (N < 200) Do
Begin
N := N + 1;
If S <> '' Then S := S + ';';
S := S + IntToStr(Prim.ObjectId);
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := IntToStr(N) + ':' + S;
End;
Function GroupPadBbox(Grp : IPCB_Group) : String;
Var
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
R : TCoordRect;
L, B, Rt, T : Integer;
Any : Boolean;
Begin
Any := False;
L := 0; B := 0; Rt := 0; T := 0;
GIt := Grp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
If Not Any Then
Begin
L := R.Left; B := R.Bottom; Rt := R.Right; T := R.Top; Any := True;
End
Else
Begin
If R.Left < L Then L := R.Left;
If R.Bottom < B Then B := R.Bottom;
If R.Right > Rt Then Rt := R.Right;
If R.Top > T Then T := R.Top;
End;
Prim := GIt.NextPCBObject;
End;
Grp.GroupIterator_Destroy(GIt);
If Not Any Then
Begin
Result := '[]';
Exit;
End;
Result := '[' + IntToStr(Round(CoordToMils(L))) + ',' + IntToStr(Round(CoordToMils(B))) + ',' +
IntToStr(Round(CoordToMils(Rt))) + ',' + IntToStr(Round(CoordToMils(T))) + ']';
End;
{ Net names on the pads that matter for the swap proof: "des=net;..." for pads
1 and 49 (every pad named 49 listed), plus the track count and their nets. }
Function CompNets(Comp : IPCB_Component) : String;
Var
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
S, N : String;
Begin
S := '';
GIt := Comp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
While Pad <> Nil Do
Begin
If (Pad.Name = '1') Or (Pad.Name = '49') Then
Begin
N := '';
If Pad.Net <> Nil Then N := Pad.Net.Name;
If S <> '' Then S := S + ';';
S := S + Pad.Name + '=' + N;
End;
Pad := GIt.NextPCBObject;
End;
Comp.GroupIterator_Destroy(GIt);
Result := S;
End;
Function BoardTracks(Board : IPCB_Board) : String;
Var
BIt : IPCB_BoardIterator;
T : IPCB_Track;
S, N : String;
Begin
S := '';
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTrackObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
T := BIt.FirstPCBObject;
While T <> Nil Do
Begin
N := '';
If T.Net <> Nil Then N := T.Net.Name;
If S <> '' Then S := S + ';';
S := S + N + '@' + IntToStr(Round(CoordToMils(T.X1))) + ',' + IntToStr(Round(CoordToMils(T.Y1)));
T := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := S;
End;
{ The board outline's extent in mil as '[left,bottom,right,top]', read from the outline's own
vertices. BoardOutline.BoundingRectangle is a cached rectangle that does not follow a reshape
in the same session on AD 26.9.1 or Altium Develop (it kept the 6000 x 4000 mil default while
the view showed the new shape, measured 2026-10-15); the vertices are what the view draws. }
Function OutlineMilFromVertices(BO : IPCB_BoardOutline) : String;
Var
Seg : TPolySegment;
K, L, B, R, T : Integer;
Begin
L := 0; B := 0; R := 0; T := 0;
For K := 0 To BO.PointCount - 1 Do
Begin
Seg := BO.Segments[K];
If (K = 0) Or (Seg.vx < L) Then L := Seg.vx;
If (K = 0) Or (Seg.vy < B) Then B := Seg.vy;
If (K = 0) Or (Seg.vx > R) Then R := Seg.vx;
If (K = 0) Or (Seg.vy > T) Then T := Seg.vy;
End;
Result := '[' + IntToStr(Round(CoordToMils(L))) + ',' + IntToStr(Round(CoordToMils(B))) + ',' +
IntToStr(Round(CoordToMils(R))) + ',' + IntToStr(Round(CoordToMils(T))) + ']';
End;
Function CmdPcbReport(Lines : TStringList) : String;
Var
Board : IPCB_Board;
PLib : IPCB_Library;
Fp : IPCB_LibComponent;
Comp : IPCB_Component;
BIt : IPCB_BoardIterator;
R : TCoordRect;
Name, S, OM : String;
I : Integer;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
Name := Param(Lines, 'name');
{ GetCurrentPCBLibrary answers even when a BOARD is focused; want=board skips
the library branch so the report describes the focused board. }
If Param(Lines, 'want') = 'board' Then PLib := Nil;
If PLib <> Nil Then
Begin
S := '';
For I := 0 To PLib.ComponentCount - 1 Do
Begin
Fp := PLib.GetComponent(I);
If (Name = '') Or (UpperCase(Fp.Name) = UpperCase(Name)) Then
Begin
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', Fp.Name) + ',' +
JRaw('pads', IntToStr(GroupCount(Fp, ePadObject))) + ',' +
JRaw('padsBboxMil', GroupPadBbox(Fp)) + ',' +
JStr('census', GroupCensus(Fp)) + ',' +
JRaw('bodies', GroupBodies(Fp)) + '}';
End;
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('kind','library') + ',' +
JRaw('components', '[' + S + ']') + ',' +
JStr('bodyCols','layerId,wMil,hMil,leftMil,bottomMil,standoffMil,overallMil,hasModel') + '}';
Exit;
End;
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','current document is neither a PCB library nor a board') + '}';
Exit;
End;
S := '';
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While Comp <> Nil Do
Begin
If S <> '' Then S := S + ',';
R := Comp.BoundingRectangle;
S := S + '{' + JStr('pattern', Comp.Pattern) + ',' +
JRaw('xMil', IntToStr(Round(CoordToMils(Comp.x)))) + ',' +
JRaw('yMil', IntToStr(Round(CoordToMils(Comp.y)))) + ',' +
JRaw('wMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' +
JRaw('pads', IntToStr(GroupCount(Comp, ePadObject))) + ',' +
JRaw('padsBboxMil', GroupPadBbox(Comp)) + ',' +
JStr('nets', CompNets(Comp)) + ',' +
JRaw('bodies', GroupBodies(Comp)) + '}';
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
{ A library's internal board has no outline; touching BoundingRectangle on the
nil BoardOutline halts the engine (measured 2026-08-30). }
If Board.BoardOutline <> Nil Then
OM := OutlineMilFromVertices(Board.BoardOutline)
Else
Begin
R := Board.BoundingRectangle;
OM := '[' + IntToStr(Round(CoordToMils(R.Left))) + ',' + IntToStr(Round(CoordToMils(R.Bottom))) + ',' +
IntToStr(Round(CoordToMils(R.Right))) + ',' + IntToStr(Round(CoordToMils(R.Top))) + ']';
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('kind','board') + ',' +
JRaw('outlineMil', OM) + ',' +
JRaw('components', '[' + S + ']') + ',' +
JStr('census', BoardCensus(Board)) + ',' +
JStr('tracks', BoardTracks(Board)) + ',' +
JStr('bodyCols','layerId,wMil,hMil,leftMil,bottomMil,standoffMil,overallMil,hasModel') + '}';
End;
{ ------------------------------------------------ build_footprint (spec file) --- }
{ Pipe-separated field N (1-based) of S. }
Function FieldP(S : String; N : Integer) : String;
Var
I, Cnt, Start : Integer;
Begin
Cnt := 1; Start := 1; Result := '';
For I := 1 To Length(S) Do
Begin
If S[I] = '|' Then
Begin
If Cnt = N Then Begin Result := Copy(S, Start, I - Start); Exit; End;
Cnt := Cnt + 1; Start := I + 1;
End;
End;
If Cnt = N Then Result := Copy(S, Start, Length(S) - Start + 1);
End;
Function MmC(S : String) : Integer;
Begin
Result := MMsToCoord(AdomFloat(S));
End;
Function SpecLayer(Tag : String) : Integer;
Begin
If Tag = 'overlay' Then Result := eTopOverlay
Else If Tag = 'fab' Then Result := eMechanical13
Else If Tag = 'courtyard' Then Result := eMechanical15
Else If Tag = 'balls' Then Result := eMechanical9
Else If Tag = 'comment' Then Result := eMechanical1
Else If Tag = 'multi' Then Result := eMultiLayer
Else Result := eTopLayer;
End;
{ Build a footprint from a spec file (tools/kicad2altium.py output):
META|name|desc PAD|des|x|y|w|h|shape|hole|layer|crpct LINE|layer|x1|y1|x2|y2|w
ARC|layer|cx|cy|r|w|a0|a1 DISC|layer|cx|cy|r. Units mm, Y up. Args:
libraryPath, spec=<windows path>. Pads with a repeated designator are legal in
Altium and all map to that pin (the heatsink-via convention). }
Function CmdBuildFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Comp : IPCB_LibComponent;
Pad : IPCB_Pad;
Trk : IPCB_Track;
Arc : IPCB_Arc;
Spec : TStringList;
I, NPads, NTh, NPrims : Integer;
L, Kind, Name, Desc, LibState, LibPath, SpecPath, Shape : String;
R : Integer;
Begin
LibPath := Param(Lines, 'libraryPath');
SpecPath := Param(Lines, 'spec');
If (LibPath = '') Or (SpecPath = '') Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','libraryPath and spec are required') + '}';
Exit;
End;
LibState := EnsureLibrary('PCBLIB', LibPath);
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no PCB library is current (' + LibState + ')') + '}';
Exit;
End;
Spec := TStringList.Create;
Spec.LoadFromFile(SpecPath);
Name := ''; Desc := '';
NPads := 0; NTh := 0; NPrims := 0;
{ mech layers this spec may use: enable + name them once per library }
PLib.Board.LayerStack.LayerObject[eMechanical1].MechanicalLayerEnabled := True;
PLib.Board.LayerStack.LayerObject[eMechanical9].MechanicalLayerEnabled := True;
PLib.Board.LayerStack.LayerObject[eMechanical9].Name := 'Adom.SolderBalls';
PLib.Board.LayerStack.LayerObject[eMechanical13].MechanicalLayerEnabled := True;
PLib.Board.LayerStack.LayerObject[eMechanical13].Name := 'Fab';
PLib.Board.LayerStack.LayerObject[eMechanical15].MechanicalLayerEnabled := True;
PLib.Board.LayerStack.LayerObject[eMechanical15].Name := 'Courtyard';
PLib.Board.LayerIsDisplayed[eMechanical9] := False;
PLib.Board.LayerIsDisplayed[eMechanical13] := True;
PLib.Board.LayerIsDisplayed[eMechanical15] := True;
PCBServer.PreProcess;
Comp := PCBServer.CreatePCBLibComp;
For I := 0 To Spec.Count - 1 Do
Begin
L := Spec[I];
Kind := FieldP(L, 1);
If Kind = 'META' Then
Begin
Name := FieldP(L, 2); Desc := FieldP(L, 3);
Comp.Name := Name; Comp.Description := Desc;
End
Else If Kind = 'PAD' Then
Begin
Pad := PCBServer.PCBObjectFactory(ePadObject, eNoDimension, eCreate_Default);
Pad.Name := FieldP(L, 2);
Pad.X := MmC(FieldP(L, 3));
Pad.Y := MmC(FieldP(L, 4));
Pad.TopXSize := MmC(FieldP(L, 5));
Pad.TopYSize := MmC(FieldP(L, 6));
Shape := FieldP(L, 7);
If Shape = 'rect' Then Pad.TopShape := eRectangular
Else If Shape = 'roundrect' Then Pad.TopShape := eRoundedRectangular
Else Pad.TopShape := eRounded;
Pad.HoleSize := MmC(FieldP(L, 8));
If FieldP(L, 9) = 'multi' Then
Begin
Pad.Layer := eMultiLayer;
NTh := NTh + 1;
End
Else Pad.Layer := eTopLayer;
R := StrToIntDef(FieldP(L, 10), 0);
If (Shape = 'roundrect') And (R > 0) Then
Pad.SetState_StackCRPctOnLayer(eTopLayer, R);
Comp.AddPCBObject(Pad);
NPads := NPads + 1;
End
Else If Kind = 'LINE' Then
Begin
Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);
Trk.Layer := SpecLayer(FieldP(L, 2));
Trk.X1 := MmC(FieldP(L, 3)); Trk.Y1 := MmC(FieldP(L, 4));
Trk.X2 := MmC(FieldP(L, 5)); Trk.Y2 := MmC(FieldP(L, 6));
Trk.Width := MmC(FieldP(L, 7));
Comp.AddPCBObject(Trk);
NPrims := NPrims + 1;
End
Else If Kind = 'ARC' Then
Begin
Arc := PCBServer.PCBObjectFactory(eArcObject, eNoDimension, eCreate_Default);
Arc.Layer := SpecLayer(FieldP(L, 2));
Arc.XCenter := MmC(FieldP(L, 3)); Arc.YCenter := MmC(FieldP(L, 4));
Arc.Radius := MmC(FieldP(L, 5));
Arc.LineWidth := MmC(FieldP(L, 6));
Arc.StartAngle := AdomFloat(FieldP(L, 7)); Arc.EndAngle := AdomFloat(FieldP(L, 8));
Comp.AddPCBObject(Arc);
NPrims := NPrims + 1;
End
Else If Kind = 'DISC' Then
Begin
{ a filled disc of radius r = a full arc at r/2 with stroke r }
Arc := PCBServer.PCBObjectFactory(eArcObject, eNoDimension, eCreate_Default);
Arc.Layer := SpecLayer(FieldP(L, 2));
Arc.XCenter := MmC(FieldP(L, 3)); Arc.YCenter := MmC(FieldP(L, 4));
Arc.Radius := MmC(FieldP(L, 5)) Div 2;
Arc.LineWidth := MmC(FieldP(L, 5));
Arc.StartAngle := 0; Arc.EndAngle := 360;
Comp.AddPCBObject(Arc);
NPrims := NPrims + 1;
End;
End;
Spec.Free;
PLib.RegisterComponent(Comp);
PLib.CurrentComponent := Comp;
PCBServer.PostProcess;
PLib.Board.ViewManager_FullUpdate;
PLib.RefreshView;
{ no PCB:Zoom here: an interactive process left 'a command active' and the
next save drew Altium's save-a-copy Warning (measured 2026-08-30). }
Result := '{' + JRaw('ok', 'true') + ',' + JStr('footprint', Name) + ',' + JStr('description', Desc) + ',' +
JStr('library', LibPath) + ',' + JStr('libraryState', LibState) + ',' +
JRaw('pads', IntToStr(NPads)) + ',' + JRaw('throughHolePads', IntToStr(NTh)) + ',' +
JRaw('drawingPrimitives', IntToStr(NPrims)) + '}';
End;
{ ------------------------------------------- build_symbol_modes (spec file) --- }
{ ONE component with N display modes (the Altium alternate-symbol mechanism:
Normal + Alternate 1..N-1). Spec (tools/symbol_modes.py), mils, Y up:
META|libref|desc|designator MODE|i|name RECT|mode|x1|y1|x2|y2
PIN|mode|number|name|x|y|orient|len LINE|mode|x1|y1|x2|y2
Args: libraryPath, spec=<windows path>. Every primitive carries
OwnerPartId 1 and OwnerPartDisplayMode = its mode. }
Function CmdBuildSymbolModes(Lines : TStringList) : String;
Var
EType : String;
SLib : ISch_Lib;
Comp : ISch_Component;
Rect : ISch_Rectangle;
Pin : ISch_Pin;
Ln : ISch_Line;
Ar : ISch_Arc;
Spec : TStringList;
Old : ISch_Component;
OIt : ISch_Iterator;
NOld : Integer;
I, NModes, NPins, NLines, Md : Integer;
L, Kind, LibRef, Desc, Des, LibPath, SpecPath, LibState, Orient : String;
Begin
LibPath := Param(Lines, 'libraryPath');
SpecPath := Param(Lines, 'spec');
If (LibPath = '') Or (SpecPath = '') Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','libraryPath and spec are required') + '}';
Exit;
End;
LibState := EnsureLibrary('SCHLIB', LibPath);
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no schematic library is current (' + LibState + ')') + '}';
Exit;
End;
Spec := TStringList.Create;
Spec.LoadFromFile(SpecPath);
NModes := 0; NPins := 0; NLines := 0;
LibRef := ''; Desc := ''; Des := 'U';
Comp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Comp.CurrentPartID := 1;
Comp.DisplayMode := 0;
For I := 0 To Spec.Count - 1 Do
Begin
L := Spec[I];
Kind := FieldP(L, 1);
If Kind = 'META' Then
Begin
LibRef := FieldP(L, 2); Desc := FieldP(L, 3); Des := FieldP(L, 4);
Comp.LibReference := LibRef;
Comp.ComponentDescription := Desc;
Comp.Designator.Text := Des + '?';
Comp.Comment.Text := LibRef;
End
Else If Kind = 'MODE' Then
Begin
Md := AdomInt(FieldP(L, 2));
If Md + 1 > NModes Then NModes := Md + 1;
End
Else If Kind = 'RECT' Then
Begin
Md := AdomInt(FieldP(L, 2));
Rect := SchServer.SchObjectFactory(eRectangle, eCreate_Default);
Rect.OwnerPartId := 1;
Rect.OwnerPartDisplayMode := Md;
Rect.Location := Point(MilsToCoord(AdomInt(FieldP(L, 3))), MilsToCoord(AdomInt(FieldP(L, 4))));
Rect.Corner := Point(MilsToCoord(AdomInt(FieldP(L, 5))), MilsToCoord(AdomInt(FieldP(L, 6))));
Rect.IsSolid := True;
Comp.AddSchObject(Rect);
End
Else If Kind = 'PIN' Then
Begin
Md := AdomInt(FieldP(L, 2));
Pin := SchServer.SchObjectFactory(ePin, eCreate_Default);
Pin.OwnerPartId := 1;
Pin.OwnerPartDisplayMode := Md;
Pin.Designator := FieldP(L, 3);
Pin.Name := FieldP(L, 4);
Pin.PinLength := MilsToCoord(AdomInt(FieldP(L, 8)));
Orient := FieldP(L, 7);
{ orientation = the direction the pin points AWAY from the body }
If Orient = 'left' Then Pin.Orientation := eRotate180
Else If Orient = 'right' Then Pin.Orientation := eRotate0
Else If Orient = 'up' Then Pin.Orientation := eRotate90
Else Pin.Orientation := eRotate270;
Pin.Location := Point(MilsToCoord(AdomInt(FieldP(L, 5))), MilsToCoord(AdomInt(FieldP(L, 6))));
{ the KiCad pin type (field 10): until 0.2.52 every pin was Passive, so Altium's ERC
and AI Flow's pin-type check could not see an output, an input or a power pin }
EType := FieldP(L, 10);
If EType = 'input' Then Pin.Electrical := eElectricInput
Else If EType = 'output' Then Pin.Electrical := eElectricOutput
Else If EType = 'bidirectional' Then Pin.Electrical := eElectricIO
Else If EType = 'tri_state' Then Pin.Electrical := eElectricHiZ
Else If EType = 'open_collector' Then Pin.Electrical := eElectricOpenCollector
Else If EType = 'open_emitter' Then Pin.Electrical := eElectricOpenEmitter
Else If (EType = 'power_in') Or (EType = 'power_out') Then Pin.Electrical := eElectricPower
Else Pin.Electrical := eElectricPassive;
{ a small part's pins carry no visible name or number, as a library resistor's do }
If FieldP(L, 9) = 'hide' Then
Begin
Pin.ShowName := False;
Pin.ShowDesignator := False;
End;
Comp.AddSchObject(Pin);
NPins := NPins + 1;
End
Else If Kind = 'ARC' Then
Begin
{ ARC|mode|cx|cy|r|startDeg|endDeg, counterclockwise }
Md := AdomInt(FieldP(L, 2));
Ar := SchServer.SchObjectFactory(eArc, eCreate_Default);
Ar.OwnerPartId := 1;
Ar.OwnerPartDisplayMode := Md;
Ar.Location := Point(MilsToCoord(AdomInt(FieldP(L, 3))), MilsToCoord(AdomInt(FieldP(L, 4))));
Ar.Radius := MilsToCoord(AdomInt(FieldP(L, 5)));
Ar.StartAngle := AdomInt(FieldP(L, 6));
Ar.EndAngle := AdomInt(FieldP(L, 7));
Comp.AddSchObject(Ar);
NLines := NLines + 1;
End
Else If Kind = 'LINE' Then
Begin
Md := AdomInt(FieldP(L, 2));
Ln := SchServer.SchObjectFactory(eLine, eCreate_Default);
Ln.OwnerPartId := 1;
Ln.OwnerPartDisplayMode := Md;
Ln.Location := Point(MilsToCoord(AdomInt(FieldP(L, 3))), MilsToCoord(AdomInt(FieldP(L, 4))));
Ln.Corner := Point(MilsToCoord(AdomInt(FieldP(L, 5))), MilsToCoord(AdomInt(FieldP(L, 6))));
Comp.AddSchObject(Ln);
NLines := NLines + 1;
End;
End;
Spec.Free;
Comp.DisplayModeCount := NModes;
{ a part imported again REPLACES its symbol: a second component with the same LibReference
left the old one first in the library, so every reader kept drawing the old pins
(measured 2026-10-15: a fixed 2-pin capacitor still placed with 1 pin) }
NOld := 0;
Repeat
Old := Nil;
OIt := SLib.SchLibIterator_Create;
OIt.AddFilter_ObjectSet(MkSet(eSchComponent));
Old := OIt.FirstSchObject;
While Old <> Nil Do
Begin
If (Old <> Comp) And (UpperCase(Old.LibReference) = UpperCase(LibRef)) Then Break;
Old := OIt.NextSchObject;
End;
SLib.SchIterator_Destroy(OIt);
If Old <> Nil Then Begin SLib.RemoveSchComponent(Old); NOld := NOld + 1; End;
Until (Old = Nil) Or (NOld > 20);
SLib.AddSchComponent(Comp);
SchServer.RobotManager.SendMessage(SLib.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Comp.I_ObjectAddress);
SLib.CurrentSchComponent := Comp;
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', LibRef) + ',' + JStr('library', LibPath) + ',' + JRaw('replaced', IntToStr(NOld)) + ',' +
JStr('libraryState', LibState) + ',' + JRaw('modes', IntToStr(NModes)) + ',' +
JRaw('pins', IntToStr(NPins)) + ',' + JRaw('lines', IntToStr(NLines)) + ',' +
JRaw('displayModeCount', IntToStr(Comp.DisplayModeCount)) + '}';
End;
{ Switch the display mode of the current library component (for frames). mode=<int> }
Function CmdSymbolMode(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Comp : ISch_Component;
Md : Integer;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no schematic library is current') + '}';
Exit;
End;
Comp := SLib.CurrentSchComponent;
If Comp = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current component') + '}';
Exit;
End;
Md := StrToIntDef(Param(Lines, 'mode'), 0);
Comp.DisplayMode := Md;
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JRaw('mode', IntToStr(Comp.DisplayMode)) + ',' +
JRaw('displayModeCount', IntToStr(Comp.DisplayModeCount)) + ',' + JStr('libref', Comp.LibReference) + '}';
End;
{ ------------------------------------------------------------- add_models --- }
{ Attach PCB footprint models (name|pcblib|description;...) and parameters
(k=v;...) to a SchLib component; set comment + description. Altium keeps
several footprints on one component and the user picks the current one on
the placed part (Properties > Footprint) - the CHANGE PACKAGE equivalent. }
Function CmdAddModels(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
Cmp : ISch_Component;
Impl : ISch_Implementation;
Models, Params, Name, Item, K, V, Cmt, Desc : String;
ML, PL : TStringList;
I, NModels, NParams : Integer;
Begin
Name := Param(Lines, 'name');
Models := Param(Lines, 'models');
Params := Param(Lines, 'params');
Cmt := Param(Lines, 'comment');
Desc := Param(Lines, 'description');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
Cmp := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Cmp := Iter.FirstSchObject;
While Cmp <> Nil Do
Begin
If UpperCase(Cmp.LibReference) = UpperCase(Name) Then Break;
Cmp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Cmp = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Name) + '}';
Exit;
End;
SchServer.ProcessControl.PreProcess(SLib, '');
If Cmt <> '' Then Cmp.Comment.Text := Cmt;
If Desc <> '' Then Cmp.ComponentDescription := Desc;
NModels := 0;
ML := SplitSemi(Models);
For I := 0 To ML.Count - 1 Do
Begin
Item := ML[I];
If FieldP(Item, 1) = '' Then Continue;
Impl := Cmp.AddSchImplementation;
Impl.ModelName := FieldP(Item, 1);
Impl.ModelType := 'PCBLIB';
Impl.Description := FieldP(Item, 3);
Impl.IsCurrent := (NModels = 0);
If FieldP(Item, 2) <> '' Then Impl.AddDataFileLink(FieldP(Item, 1), FieldP(Item, 2), 'PCBLIB');
NModels := NModels + 1;
End;
ML.Free;
NParams := 0;
PL := SplitSemi(Params);
For I := 0 To PL.Count - 1 Do
Begin
Item := PL[I];
If Pos('=', Item) = 0 Then Continue;
K := Copy(Item, 1, Pos('=', Item) - 1);
V := Copy(Item, Pos('=', Item) + 1, Length(Item));
SetSchParam(Cmp, K, V);
NParams := NParams + 1;
End;
PL.Free;
SchServer.ProcessControl.PostProcess(SLib, '');
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JStr('libref', Cmp.LibReference) + ',' +
JRaw('models', IntToStr(NModels)) + ',' + JRaw('params', IntToStr(NParams)) + '}';
End;
{ ------------------------------------------------------ plate_vias --- }
{ Every drilled pad in the current PcbLib (all footprints, or name=<filter>) becomes a
plated MULTI-LAYER pad. adom-lbr writes heatsink vias as pads with a hole, and Altium
loaded them on the Top Layer (John, 2026-09-20: "did Altium support bringing in those
heatsink vias?" - the holes came, the layer did not). Saving the library afterwards
also yields a file whose bytes show the encoding Altium wants. }
Function CmdPlateVias(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Fp : IPCB_LibComponent;
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
I, NFixed, NHoles : Integer;
Name, S : String;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
Name := Param(Lines, 'name');
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no PCB library is current') + '}';
Exit;
End;
NFixed := 0; NHoles := 0; S := '';
PCBServer.PreProcess;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
Fp := PLib.GetComponent(I);
If (Name <> '') And (UpperCase(Fp.Name) <> UpperCase(Name)) Then Continue;
GIt := Fp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
While Pad <> Nil Do
Begin
If Pad.HoleSize > 0 Then
Begin
NHoles := NHoles + 1;
If Pad.Layer <> eMultiLayer Then
Begin
Pad.BeginModify;
Pad.Layer := eMultiLayer;
Pad.Plated := True;
Pad.EndModify;
NFixed := NFixed + 1;
If S <> '' Then S := S + ';';
S := S + Fp.Name + ':' + Pad.Name;
End;
End;
Pad := GIt.NextPCBObject;
End;
Fp.GroupIterator_Destroy(GIt);
End;
PCBServer.PostProcess;
PLib.Board.ViewManager_FullUpdate;
PLib.RefreshView;
Result := '{' + JRaw('ok','true') + ',' + JRaw('holes', IntToStr(NHoles)) + ',' + JRaw('plated', IntToStr(NFixed)) + ',' + JStr('fixed', S) + '}';
End;
{ ------------------------------------------------------ normalize_bodies --- }
{ The PcbLib canvas origin sits 50000 mil from the stored component origin, so a
body added through the canvas lands 50000 mil away from the pads in the stored
frame (measured: body left -50138 vs pads +/-157) - Zoom All then frames 50
inches of nothing and the editor paints an empty canvas. Align every body
centre onto the pad-cluster centre of its footprint. name=<optional filter> }
Function CmdNormalizeBodies(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Fp : IPCB_LibComponent;
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
R : TCoordRect;
I, NMoved : Integer;
PL, PB, PR, PT, DX, DY : Integer;
AnyP : Boolean;
Name, S : String;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
Name := Param(Lines, 'name');
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no PCB library is current') + '}';
Exit;
End;
NMoved := 0; S := '';
PCBServer.PreProcess;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
Fp := PLib.GetComponent(I);
If (Name <> '') And (UpperCase(Fp.Name) <> UpperCase(Name)) Then Continue;
AnyP := False; PL := 0; PB := 0; PR := 0; PT := 0;
GIt := Fp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
If Not AnyP Then Begin PL := R.Left; PB := R.Bottom; PR := R.Right; PT := R.Top; AnyP := True; End
Else
Begin
If R.Left < PL Then PL := R.Left;
If R.Bottom < PB Then PB := R.Bottom;
If R.Right > PR Then PR := R.Right;
If R.Top > PT Then PT := R.Top;
End;
Prim := GIt.NextPCBObject;
End;
Fp.GroupIterator_Destroy(GIt);
If Not AnyP Then Continue;
GIt := Fp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(eComponentBodyObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
DX := ((PL + PR) Div 2) - ((R.Left + R.Right) Div 2);
DY := ((PB + PT) Div 2) - ((R.Bottom + R.Top) Div 2);
If (Abs(DX) > MilsToCoord(5)) Or (Abs(DY) > MilsToCoord(5)) Then
Begin
Prim.MoveByXY(DX, DY);
NMoved := NMoved + 1;
If S <> '' Then S := S + ';';
S := S + Fp.Name + ':' + IntToStr(Round(CoordToMils(DX))) + ',' + IntToStr(Round(CoordToMils(DY)));
End;
Prim := GIt.NextPCBObject;
End;
Fp.GroupIterator_Destroy(GIt);
End;
PCBServer.PostProcess;
PLib.Board.ViewManager_FullUpdate;
PLib.RefreshView;
Result := '{' + JRaw('ok','true') + ',' + JRaw('moved', IntToStr(NMoved)) + ',' + JStr('shifts', S) + '}';
End;
{ ------------------------------------------ test_board / swap_footprint --- }
{ Copy a library footprint onto Board as a fresh component centred at (CX,CY)
coords, per-primitive (group Replicate dissolves), children BEFORE the board
add, pad cluster and bodies normalised to the target independently. }
Function PlaceCopy(Board : IPCB_Board; LibFp : IPCB_LibComponent; CX, CY : Integer) : IPCB_Component;
Var
Inst : IPCB_Component;
GIt : IPCB_GroupIterator;
Prim, Cpy : IPCB_Primitive;
R : TCoordRect;
DX, DY, CL, CB, CR, CT : Integer;
AnyP : Boolean;
Begin
Inst := PCBServer.PCBObjectFactory(eComponentObject, eNoDimension, eCreate_Default);
Inst.Layer := eTopLayer;
Inst.NameOn := False;
Inst.CommentOn := False;
GIt := LibFp.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Cpy := Prim.Replicate;
Inst.AddPCBObject(Cpy);
Prim := GIt.NextPCBObject;
End;
LibFp.GroupIterator_Destroy(GIt);
Board.AddPCBObject(Inst);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Inst.I_ObjectAddress);
Inst.x := CX; Inst.y := CY;
AnyP := False; CL := 0; CB := 0; CR := 0; CT := 0;
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
If Not AnyP Then Begin CL := R.Left; CB := R.Bottom; CR := R.Right; CT := R.Top; AnyP := True; End
Else
Begin
If R.Left < CL Then CL := R.Left;
If R.Bottom < CB Then CB := R.Bottom;
If R.Right > CR Then CR := R.Right;
If R.Top > CT Then CT := R.Top;
End;
Prim := GIt.NextPCBObject;
End;
Inst.GroupIterator_Destroy(GIt);
DX := 0; DY := 0;
If AnyP Then Begin DX := CX - ((CL + CR) Div 2); DY := CY - ((CB + CT) Div 2); End;
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If Prim.ObjectId = eComponentBodyObject Then
Begin
R := Prim.BoundingRectangle;
Prim.MoveByXY(CX - ((R.Left + R.Right) Div 2), CY - ((R.Bottom + R.Top) Div 2));
End
Else If Prim.ObjectId <> eTextObject Then
Prim.MoveByXY(DX, DY);
Prim := GIt.NextPCBObject;
End;
Inst.GroupIterator_Destroy(GIt);
Inst.Name.MoveToXY(CX, CY + MilsToCoord(250));
Inst.Comment.MoveToXY(CX, CY - MilsToCoord(250));
Result := Inst;
End;
Function FindNet(Board : IPCB_Board; NetName : String) : IPCB_Net;
Var
BIt : IPCB_BoardIterator;
N : IPCB_Net;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eNetObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
N := BIt.FirstPCBObject;
While N <> Nil Do
Begin
If N.Name = NetName Then Begin Result := N; Break; End;
N := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
{ Assign nets to pads by designator: assigns=<des>=<net>;... (all pads sharing a
designator get the net - the heatsink-via convention). }
Procedure AssignPadNets(Board : IPCB_Board; Inst : IPCB_Component; Assigns : String);
Var
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
AL : TStringList;
I : Integer;
Des, NetName : String;
Net : IPCB_Net;
Begin
AL := SplitSemi(Assigns);
For I := 0 To AL.Count - 1 Do
Begin
If Pos('=', AL[I]) = 0 Then Continue;
Des := Copy(AL[I], 1, Pos('=', AL[I]) - 1);
NetName := Copy(AL[I], Pos('=', AL[I]) + 1, Length(AL[I]));
Net := FindNet(Board, NetName);
If Net = Nil Then
Begin
{ a net that is only added (not registered) lives in memory: the save drops it and
Altium builds no ratsnest for it (measured 2026-10-15: reopened board, 0 nets) }
Net := PCBServer.PCBObjectFactory(eNetObject, eNoDimension, eCreate_Default);
Net.Name := NetName;
Board.AddPCBObject(Net);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Net.I_ObjectAddress);
End;
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
While Pad <> Nil Do
Begin
If Pad.Name = Des Then
Begin
Pad.BeginModify;
Pad.Net := Net;
Pad.EndModify;
Net.AddPCBObject(Pad);
End;
Pad := GIt.NextPCBObject;
End;
Inst.GroupIterator_Destroy(GIt);
End;
AL.Free;
End;
{ New PcbDoc: FR4 rectangle wMm x hMm (origin at 0,0), the footprint placed at
the centre, pad 1 and pad 49 given nets with a stub track each.
Args: footprint, wMm, hMm, pcb=<save path>, assigns=<des=net;...>, stubs=<des=x,y;...> (mm from centre) }
Function CmdTestBoard(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
LibFp : IPCB_LibComponent;
Board : IPCB_Board;
SD : IServerDocument;
Inst : IPCB_Component;
BO : IPCB_BoardOutline;
Seg : TPolySegment;
Trk : IPCB_Track;
Txt : IPCB_Text;
R : TCoordRect;
Net : IPCB_Net;
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
SL : TStringList;
FpList : TStringList;
BIt : IPCB_BoardIterator;
ECmp : IPCB_Component;
GIt2 : IPCB_GroupIterator;
MPrim : IPCB_Primitive;
I, K, WC, HC, CX, CY, PX, PY, Found : Integer;
NExist, ExtR, Cur, FW, FL, FR, AnyF : Integer;
FpName, PcbPath, Des, NetName, Item, Missing : String;
Ok, Append : Boolean;
Begin
FpName := Param(Lines, 'footprint');
PcbPath := Param(Lines, 'pcb');
{ footprint may be a ';' list: every part lands on ONE board, spread evenly along X
(John, 2026-09-19: three ToF sensors on one board for the demo). }
FpList := SplitSemi(FpName);
WC := MMsToCoord(AdomFloat(Param(Lines, 'wMm')));
HC := MMsToCoord(AdomFloat(Param(Lines, 'hMm')));
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first') + '}';
Exit;
End;
Missing := '';
For K := 0 To FpList.Count - 1 Do
Begin
Ok := False;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
LibFp := PLib.GetComponent(I);
If UpperCase(LibFp.Name) = UpperCase(FpList[K]) Then Begin Ok := True; Break; End;
End;
If Not Ok Then Missing := Missing + FpList[K] + ' ';
End;
If (FpList.Count = 0) Or (Missing <> '') Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Trim(Missing + FpName)) + '}';
FpList.Free;
Exit;
End;
{ Append mode (issue 11): pcb names an EXISTING .PcbDoc -> open it and add the parts,
instead of a new board per call. The board keeps its own outline and layers. }
SD := Nil;
Append := (PcbPath <> '') And FileExists(PcbPath);
If (PcbPath <> '') And (Not Append) Then
Begin
SD := Nil;
Try
SD := Client.GetDocumentByPath(PcbPath);
Except
SD := Nil;
End;
If SD <> Nil Then Append := True;
End;
If Append Then
Begin
If SD = Nil Then SD := Client.OpenDocument('PCB', PcbPath);
If SD = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the existing board: ' + PcbPath) + '}';
FpList.Free;
Exit;
End;
Client.ShowDocument(SD);
End
Else
Begin
SD := CreateNewDocumentFromDocumentKind('PCB');
If SD = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not create a PCB document') + '}';
FpList.Free;
Exit;
End;
Client.ShowDocument(SD);
End;
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board after open') + '}';
FpList.Free;
Exit;
End;
{ existing parts: how many (designators continue from there) and how far right they reach }
NExist := 0; ExtR := 0; CY := 0;
If Append Then
Begin
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
ECmp := BIt.FirstPCBObject;
While ECmp <> Nil Do
Begin
NExist := NExist + 1;
If (NExist = 1) Or (ECmp.x > ExtR) Then ExtR := ECmp.x;
CY := CY + ECmp.y;
ECmp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
If NExist > 0 Then CY := CY Div NExist Else CY := MMsToCoord(7);
End;
PCBServer.PreProcess;
If Not Append Then
Begin
Board.LayerStack.LayerObject[eMechanical9].MechanicalLayerEnabled := True;
Board.LayerStack.LayerObject[eMechanical9].Name := 'Adom.SolderBalls';
Board.LayerIsDisplayed[eMechanical9] := False;
Board.LayerStack.LayerObject[eMechanical13].MechanicalLayerEnabled := True;
Board.LayerStack.LayerObject[eMechanical13].Name := 'Fab';
Board.LayerStack.LayerObject[eMechanical15].MechanicalLayerEnabled := True;
Board.LayerStack.LayerObject[eMechanical15].Name := 'Courtyard';
{ board shape }
BO := Board.BoardOutline;
BO.Invalidate;
{ no Clear: IPCB_BoardOutline has no Clear in this engine (halt, measured);
PointCount + Segments[] + Rebuild is the working pattern }
BO.PointCount := 4;
Seg := BO.Segments[0]; Seg.vx := 0; Seg.vy := 0; Seg.Kind := ePolySegmentLine; BO.Segments[0] := Seg;
Seg := BO.Segments[1]; Seg.vx := WC; Seg.vy := 0; Seg.Kind := ePolySegmentLine; BO.Segments[1] := Seg;
Seg := BO.Segments[2]; Seg.vx := WC; Seg.vy := HC; Seg.Kind := ePolySegmentLine; BO.Segments[2] := Seg;
Seg := BO.Segments[3]; Seg.vx := 0; Seg.vy := HC; Seg.Kind := ePolySegmentLine; BO.Segments[3] := Seg;
BO.Rebuild;
BO.Validate;
End;
CX := WC Div 2;
If Append Then Cur := ExtR + MMsToCoord(6)
Else CY := HC Div 2;
{ one part: centred; N parts: at W*(k+1)/(N+1). Inst keeps the LAST placed part so the
single-part assigns/stubs behaviour is unchanged. }
For K := 0 To FpList.Count - 1 Do
Begin
For I := 0 To PLib.ComponentCount - 1 Do
Begin
LibFp := PLib.GetComponent(I);
If UpperCase(LibFp.Name) = UpperCase(FpList[K]) Then Break;
End;
If Append Then
Begin
{ pad-extent width of the library footprint, measured before placing }
AnyF := 0; FL := 0; FR := 0;
GIt2 := LibFp.GroupIterator_Create;
GIt2.AddFilter_ObjectSet(MkSet(ePadObject));
GIt2.AddFilter_LayerSet(AllLayers);
MPrim := GIt2.FirstPCBObject;
While MPrim <> Nil Do
Begin
R := MPrim.BoundingRectangle;
If (AnyF = 0) Or (R.Left < FL) Then FL := R.Left;
If (AnyF = 0) Or (R.Right > FR) Then FR := R.Right;
AnyF := 1;
MPrim := GIt2.NextPCBObject;
End;
LibFp.GroupIterator_Destroy(GIt2);
FW := FR - FL;
If FW < MMsToCoord(2) Then FW := MMsToCoord(2);
CX := Cur + ((FW * 3) Div 4) + MMsToCoord(1);
Cur := CX + ((FW * 3) Div 4) + MMsToCoord(1);
End
Else
CX := (WC * (K + 1)) Div (FpList.Count + 1);
Inst := PlaceCopy(Board, LibFp, CX, CY);
{ U1..Un so the read verbs (component_pins, footprint_primitives) can address each part;
the factory left every part named Designator1 (measured 2026-09-20). Name stays hidden. }
Inst.Name.Text := 'U' + IntToStr(NExist + K + 1);
{ the footprint name: the primitives were copied one by one, so the component carries
no Pattern unless it is set here, and BOM / STEP naming / footprint compare key on it }
Inst.Pattern := LibFp.Name;
{ The part name is the component's own COMMENT, shown on the top overlay under the
footprint, so it moves with the part (free overlay text stayed behind when parts were
moved and piled up in the silkscreen Gerber). 35 mil stroke, its top 2.5 mm below the
part centre, clear of a 3 mm part's courtyard; centred on its real extent. }
Inst.Comment.Text := LibFp.Name;
Inst.CommentOn := True;
Inst.Comment.Size := MilsToCoord(35);
Inst.Comment.Width := MilsToCoord(5);
Inst.Comment.MoveToXY(CX, CY - MMsToCoord(3.4));
R := Inst.Comment.BoundingRectangle;
Inst.Comment.MoveByXY(CX - ((R.Left + R.Right) Div 2), 0);
End;
AssignPadNets(Board, Inst, Param(Lines, 'assigns'));
{ stub tracks: des=x,y (mm from centre) -> a track from that pad to the point, on its net }
SL := SplitSemi(Param(Lines, 'stubs'));
For I := 0 To SL.Count - 1 Do
Begin
Item := SL[I];
If Pos('=', Item) = 0 Then Continue;
Des := Copy(Item, 1, Pos('=', Item) - 1);
PX := CX + MMsToCoord(AdomFloat(Field(Copy(Item, Pos('=', Item) + 1, Length(Item)), 1)));
PY := CY + MMsToCoord(AdomFloat(Field(Copy(Item, Pos('=', Item) + 1, Length(Item)), 2)));
GIt := Inst.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
Found := 0;
While (Pad <> Nil) And (Found = 0) Do
Begin
If (Pad.Name = Des) And (Pad.Layer <> eMultiLayer) Then Found := 1
Else If (Pad.Name = Des) And (Found = 0) And (Pad.Layer = eMultiLayer) Then Found := 0;
If Found = 1 Then Break;
Pad := GIt.NextPCBObject;
End;
If Pad <> Nil Then
Begin
Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);
Trk.Layer := eTopLayer;
Trk.X1 := Pad.x; Trk.Y1 := Pad.y; Trk.X2 := PX; Trk.Y2 := PY;
Trk.Width := MMsToCoord(0.2);
Trk.Net := Pad.Net;
Board.AddPCBObject(Trk);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Trk.I_ObjectAddress);
End;
Inst.GroupIterator_Destroy(GIt);
End;
SL.Free;
PCBServer.PostProcess;
Board.ViewManager_FullUpdate;
If PcbPath <> '' Then
Begin
If Not Append Then SD.SetFileName(PcbPath);
SD.DoFileSave('');
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('pcb', PcbPath) + ',' + JStr('placed', FpName) + ',' +
JRaw('appended', BoolStr(Append)) + ',' + JRaw('existingParts', IntToStr(NExist)) + ',' +
JRaw('count', IntToStr(FpList.Count)) + ',' +
JRaw('padsOnComp', IntToStr(GroupCount(Inst, ePadObject))) + ',' +
JRaw('bodiesOnComp', IntToStr(GroupCount(Inst, eComponentBodyObject))) + '}';
FpList.Free;
End;
{ Replace the first component on the current board with another library
footprint at the same position, keeping the nets by pad designator. Tracks are
untouched, so the wiring survives the swap. Args: footprint, assigns }
Function CmdSwapFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
LibFp : IPCB_LibComponent;
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Old, Inst : IPCB_Component;
I, CX, CY : Integer;
FpName, LibPath : String;
Ok : Boolean;
Begin
FpName := Param(Lines, 'footprint');
LibPath := Param(Lines, 'libraryPath');
Board := PCBServer.GetCurrentPCBBoard;
PLib := Nil;
If LibPath <> '' Then PLib := PCBServer.LoadPCBLibraryByPath(LibPath);
If PLib = Nil Then PLib := PCBServer.GetCurrentPCBLibrary;
If (Board = Nil) Or (PLib = Nil) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','need the board focused and the PcbLib path') + '}';
Exit;
End;
Ok := False;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
LibFp := PLib.GetComponent(I);
If UpperCase(LibFp.Name) = UpperCase(FpName) Then Begin Ok := True; Break; End;
End;
If Not Ok Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + FpName) + '}';
Exit;
End;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Old := BIt.FirstPCBObject;
Board.BoardIterator_Destroy(BIt);
If Old = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no component on the board') + '}';
Exit;
End;
CX := Old.x; CY := Old.y;
PCBServer.PreProcess;
Board.RemovePCBObject(Old);
Inst := PlaceCopy(Board, LibFp, CX, CY);
AssignPadNets(Board, Inst, Param(Lines, 'assigns'));
PCBServer.PostProcess;
Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('placed', FpName) + ',' +
JRaw('padsOnComp', IntToStr(GroupCount(Inst, ePadObject))) + ',' +
JRaw('bodiesOnComp', IntToStr(GroupCount(Inst, eComponentBodyObject))) + '}';
End;
{ ------------------------------------------------------------ board_shape --- }
{ Redefine the CURRENT board shape as a wMm x hMm rectangle at (x0Mm,y0Mm).
Until 0.2.31 this drew four temporary tracks and ran the editor process
PCB:PlaceBoardOutline (Mode=BOARDOUTLINE_FROM_SEL_PRIMS), then read
Board.BoardOutline.BoundingRectangle with NO nil guard. On AD 26.10.1 and on Altium Develop the
process left no outline, the read raised on a nil object, and the script parked on a breakpoint
with no error box (found by the verb battery on two builds, 2026-09-20). It now uses the pattern
CmdTestBoard has always used and that renders correctly on every build: PointCount + Segments[]
+ Rebuild on the board's own outline, every access nil-guarded. }
Function CmdBoardShape(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BO : IPCB_BoardOutline;
Seg : TPolySegment;
X0, Y0, X1, Y1, K, NTr : Integer;
Trk : IPCB_Track;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no board focused') + '}';
Exit;
End;
X0 := MMsToCoord(AdomFloat(Param(Lines, 'x0Mm')));
Y0 := MMsToCoord(AdomFloat(Param(Lines, 'y0Mm')));
X1 := X0 + MMsToCoord(AdomFloat(Param(Lines, 'wMm')));
Y1 := Y0 + MMsToCoord(AdomFloat(Param(Lines, 'hMm')));
If (X1 <= X0) Or (Y1 <= Y0) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','bad_size') + ',' + JStr('error','wMm and hMm must both be greater than 0') + '}';
Exit;
End;
BO := Board.BoardOutline;
If BO = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_board_outline') + ',' +
JStr('error','this document has no board outline object (a PcbLib internal board has none); open a .PcbDoc') + '}';
Exit;
End;
PCBServer.PreProcess;
BO.Invalidate;
BO.PointCount := 4;
Seg := BO.Segments[0]; Seg.vx := X0; Seg.vy := Y0; Seg.Kind := ePolySegmentLine; BO.Segments[0] := Seg;
Seg := BO.Segments[1]; Seg.vx := X1; Seg.vy := Y0; Seg.Kind := ePolySegmentLine; BO.Segments[1] := Seg;
Seg := BO.Segments[2]; Seg.vx := X1; Seg.vy := Y1; Seg.Kind := ePolySegmentLine; BO.Segments[2] := Seg;
Seg := BO.Segments[3]; Seg.vx := X0; Seg.vy := Y1; Seg.Kind := ePolySegmentLine; BO.Segments[3] := Seg;
BO.Rebuild;
BO.Validate;
{ The board shape is not plotted into any Gerber: a fab outline needs real tracks. On
request, draw the rectangle on Mechanical 1 (0.1 mm) so .GM1 carries it. Additive: a
second call draws a second set, so callers reshaping repeatedly pass it once at the end. }
NTr := 0;
If Param(Lines, 'mechOutline') = '1' Then
Begin
For K := 0 To 3 Do
Begin
Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);
Trk.Layer := eMechanical1;
Trk.Width := MMsToCoord(0.1);
If K = 0 Then Begin Trk.X1 := X0; Trk.Y1 := Y0; Trk.X2 := X1; Trk.Y2 := Y0; End;
If K = 1 Then Begin Trk.X1 := X1; Trk.Y1 := Y0; Trk.X2 := X1; Trk.Y2 := Y1; End;
If K = 2 Then Begin Trk.X1 := X1; Trk.Y1 := Y1; Trk.X2 := X0; Trk.Y2 := Y1; End;
If K = 3 Then Begin Trk.X1 := X0; Trk.Y1 := Y1; Trk.X2 := X0; Trk.Y2 := Y0; End;
Board.AddPCBObject(Trk);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Trk.I_ObjectAddress);
NTr := NTr + 1;
End;
End;
PCBServer.PostProcess;
Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JRaw('outlineMil', OutlineMilFromVertices(BO)) + ',' +
JStr('outlineSource', 'vertices') + ',' + JRaw('mechOutlineTracks', IntToStr(NTr)) + '}';
End;
{ ---------------------------------------------------------- remove_symbol --- }
Function CmdRemoveSymbol(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Iter : ISch_Iterator;
Comp : ISch_Component;
Name : String;
Found : Boolean;
Begin
Name := Param(Lines, 'name');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
Found := False;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If UpperCase(Comp.LibReference) = UpperCase(Name) Then Begin Found := True; Break; End;
Comp := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
If Not Found Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Name) + '}';
Exit;
End;
SLib.RemoveSchComponent(Comp);
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JStr('removed', Name) + '}';
End;
{ -------------------------------------- Barrett-parity: library manipulation --- }
Function FindSchComp(SLib : ISch_Lib; Name : String) : ISch_Component;
Var
Iter : ISch_Iterator;
C : ISch_Component;
Begin
Result := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
C := Iter.FirstSchObject;
While C <> Nil Do
Begin
If UpperCase(C.LibReference) = UpperCase(Name) Then Begin Result := C; Break; End;
C := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
End;
Function FindFp(PLib : IPCB_Library; Name : String) : IPCB_LibComponent;
Var
I : Integer;
F : IPCB_LibComponent;
Begin
Result := Nil;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
F := PLib.GetComponent(I);
If UpperCase(F.Name) = UpperCase(Name) Then Begin Result := F; Break; End;
End;
End;
{ Clone a symbol inside the current SchLib under a new LibReference, all display
modes included (per-primitive copy - group Replicate drops children). }
Function CmdCloneSymbol(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
Src, Dst : ISch_Component;
It : ISch_Iterator;
Prim, Cpy : ISch_GraphicalObject;
NCopied : Integer;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
Src := FindSchComp(SLib, Param(Lines, 'from'));
If Src = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Param(Lines, 'from')) + '}';
Exit;
End;
If FindSchComp(SLib, Param(Lines, 'to')) <> Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','target already exists: ' + Param(Lines, 'to')) + '}';
Exit;
End;
Dst := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Dst.LibReference := Param(Lines, 'to');
Dst.ComponentDescription := Src.ComponentDescription;
Dst.CurrentPartID := 1;
Dst.DisplayMode := 0;
Dst.Designator.Text := Src.Designator.Text;
Dst.Comment.Text := Src.Comment.Text;
NCopied := 0;
It := Src.SchIterator_Create;
Prim := It.FirstSchObject;
While Prim <> Nil Do
Begin
If Prim.ObjectID <> eParameter Then
Begin
Cpy := Prim.Replicate;
Dst.AddSchObject(Cpy);
NCopied := NCopied + 1;
End;
Prim := It.NextSchObject;
End;
Src.SchIterator_Destroy(It);
Dst.DisplayModeCount := Src.DisplayModeCount;
SLib.AddSchComponent(Dst);
SchServer.RobotManager.SendMessage(SLib.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Dst.I_ObjectAddress);
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JStr('from', Src.LibReference) + ',' + JStr('to', Dst.LibReference) + ',' +
JRaw('primitives', IntToStr(NCopied)) + ',' + JRaw('displayModeCount', IntToStr(Dst.DisplayModeCount)) + '}';
End;
Function CmdRenameSymbol(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
C : ISch_Component;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
C := FindSchComp(SLib, Param(Lines, 'from'));
If C = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Param(Lines, 'from')) + '}';
Exit;
End;
If Trim(Param(Lines, 'to')) = '' Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_required') + ',' + JStr('error','to (the new name) is required') + '}';
Exit;
End;
If FindSchComp(SLib, Param(Lines, 'to')) <> Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_taken') + ',' + JStr('error','a component named ' + Param(Lines, 'to') + ' already exists in this library') + '}';
Exit;
End;
C.LibReference := Param(Lines, 'to');
SLib.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JStr('renamed', Param(Lines, 'to')) + '}';
End;
{ Read a symbol back: pins (designator/name/mode), description, modes, params. }
Function CmdReadSymbol(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
C : ISch_Component;
It : ISch_Iterator;
Prim : ISch_GraphicalObject;
Pin : ISch_Pin;
Pm : ISch_Parameter;
Impl : ISch_Implementation;
Pins, Params, Models : String;
NPins : Integer;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
C := FindSchComp(SLib, Param(Lines, 'name'));
If C = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Param(Lines, 'name')) + '}';
Exit;
End;
Pins := ''; Params := ''; NPins := 0;
It := C.SchIterator_Create;
Prim := It.FirstSchObject;
While Prim <> Nil Do
Begin
If Prim.ObjectID = ePin Then
Begin
Pin := Prim;
If Pin.OwnerPartDisplayMode = 0 Then
Begin
If Pins <> '' Then Pins := Pins + ',';
Pins := Pins + '{' + JStr('des', Pin.Designator) + ',' + JStr('name', Pin.Name) + ',' + JRaw('electrical', IntToStr(Pin.Electrical)) + '}';
NPins := NPins + 1;
End;
End
Else If Prim.ObjectID = eParameter Then
Begin
Pm := Prim;
If Params <> '' Then Params := Params + ',';
Params := Params + '{' + JStr('name', Pm.Name) + ',' + JStr('value', Pm.Text) + ',' +
JRaw('hidden', JBool(Pm.IsHidden)) + '}';
End;
Prim := It.NextSchObject;
End;
C.SchIterator_Destroy(It);
{ the symbol's model links (footprints, sim, 3D): the eImplementation child iterator, as in
CmdSymbolPrims (ImplementationCount halts the script on some builds) (#24) }
Models := '';
It := C.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eImplementation));
Impl := It.FirstSchObject;
While Impl <> Nil Do
Begin
If Models <> '' Then Models := Models + ',';
Models := Models + '{' + JStr('type', Impl.ModelType) + ',' + JStr('name', Impl.ModelName) + ',' +
JRaw('isCurrent', JBool(Impl.IsCurrent)) + '}';
Impl := It.NextSchObject;
End;
C.SchIterator_Destroy(It);
Result := '{' + JRaw('ok','true') + ',' + JStr('libref', C.LibReference) + ',' +
JStr('description', C.ComponentDescription) + ',' +
JRaw('displayModeCount', IntToStr(C.DisplayModeCount)) + ',' +
JRaw('pinsMode0', IntToStr(NPins)) + ',' +
JRaw('pins', '[' + Pins + ']') + ',' + JRaw('params', '[' + Params + ']') + ',' + JRaw('models', '[' + Models + ']') + '}';
End;
{ Copy a footprint inside the current PcbLib under a new name (per-primitive). }
Function CmdCopyFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
Src, Dst : IPCB_LibComponent;
GIt : IPCB_GroupIterator;
Prim, Cpy : IPCB_Primitive;
NCopied : Integer;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first') + '}';
Exit;
End;
Src := FindFp(PLib, Param(Lines, 'from'));
If Src = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Param(Lines, 'from')) + '}';
Exit;
End;
If FindFp(PLib, Param(Lines, 'to')) <> Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','target already exists: ' + Param(Lines, 'to')) + '}';
Exit;
End;
PCBServer.PreProcess;
Dst := PCBServer.CreatePCBLibComp;
Dst.Name := Param(Lines, 'to');
Dst.Description := Src.Description;
NCopied := 0;
GIt := Src.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Cpy := Prim.Replicate;
Dst.AddPCBObject(Cpy);
NCopied := NCopied + 1;
Prim := GIt.NextPCBObject;
End;
Src.GroupIterator_Destroy(GIt);
PLib.RegisterComponent(Dst);
PCBServer.PostProcess;
PLib.Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('from', Src.Name) + ',' + JStr('to', Dst.Name) + ',' +
JRaw('primitives', IntToStr(NCopied)) + '}';
End;
Function CmdRenameFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
F : IPCB_LibComponent;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first') + '}';
Exit;
End;
F := FindFp(PLib, Param(Lines, 'from'));
If F = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Param(Lines, 'from')) + '}';
Exit;
End;
If Trim(Param(Lines, 'to')) = '' Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_required') + ',' + JStr('error','to (the new name) is required') + '}';
Exit;
End;
If FindFp(PLib, Param(Lines, 'to')) <> Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_taken') + ',' + JStr('error','a footprint named ' + Param(Lines, 'to') + ' already exists in this library') + '}';
Exit;
End;
F.Name := Param(Lines, 'to');
PLib.Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('renamed', Param(Lines, 'to')) + '}';
End;
Function CmdRemoveFootprint(Lines : TStringList) : String;
Var
PLib : IPCB_Library;
F : IPCB_LibComponent;
Begin
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib first') + '}';
Exit;
End;
F := FindFp(PLib, Param(Lines, 'name'));
If F = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Param(Lines, 'name')) + '}';
Exit;
End;
PLib.RemoveComponent(F);
PLib.Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('removed', Param(Lines, 'name')) + '}';
End;
{ ---------------------------------------- cleanup_libs / set_visibility / xcopy --- }
{ Remove factory-default empty components (Component_1 / PCBComponent_1) from the
current SchLib or PcbLib - the strays every fresh library carries. }
Function CmdCleanupLibs(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
PLib : IPCB_Library;
C : ISch_Component;
F : IPCB_LibComponent;
PIt : ISch_Iterator;
Pin : ISch_Pin;
NPin, NRemoved, NKept : Integer;
Begin
NRemoved := 0;
NKept := 0;
SLib := SchServer.GetCurrentSchDocument;
If (SLib <> Nil) And (SLib.ObjectID = eSchLib) Then
Begin
C := FindSchComp(SLib, 'Component_1');
If C <> Nil Then
Begin
{ Only the FACTORY stray, never a symbol someone drew into it. The PcbLib side has
always checked this (GroupCount = 0); the SchLib side deleted whatever was called
Component_1, pins and all. }
NPin := 0;
PIt := C.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(ePin));
Pin := PIt.FirstSchObject;
While Pin <> Nil Do
Begin
NPin := NPin + 1;
Pin := PIt.NextSchObject;
End;
C.SchIterator_Destroy(PIt);
If NPin = 0 Then
Begin
SLib.RemoveSchComponent(C);
NRemoved := NRemoved + 1;
End
Else NKept := NKept + 1;
End;
SLib.GraphicallyInvalidate;
End;
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib <> Nil Then
Begin
F := FindFp(PLib, 'PCBComponent_1');
If F <> Nil Then
Begin
If GroupCount(F, ePadObject) = 0 Then
Begin
PLib.RemoveComponent(F);
NRemoved := NRemoved + 1;
End
Else NKept := NKept + 1;
End;
PLib.Board.ViewManager_FullUpdate;
End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('removed', IntToStr(NRemoved)) +
',' + JRaw('keptNotEmpty', IntToStr(NKept)) + '}';
End;
{ Show or hide a named parameter on a SchLib component. name, param, visible=0|1 }
Function CmdSetVisibility(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
C : ISch_Component;
It : ISch_Iterator;
Pm : ISch_Parameter;
Target : String;
Found : Boolean;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first') + '}';
Exit;
End;
C := FindSchComp(SLib, Param(Lines, 'name'));
If C = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Param(Lines, 'name')) + '}';
Exit;
End;
Target := Param(Lines, 'param');
Found := False;
It := C.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eParameter));
Pm := It.FirstSchObject;
While Pm <> Nil Do
Begin
If UpperCase(Pm.Name) = UpperCase(Target) Then
Begin
Pm.IsHidden := Param(Lines, 'visible') <> '1';
Found := True;
End;
Pm := It.NextSchObject;
End;
C.SchIterator_Destroy(It);
SLib.GraphicallyInvalidate;
If Not Found Then
Result := '{' + JRaw('ok','false') + ',' + JStr('error','parameter not found: ' + Target) + '}'
Else
Result := '{' + JRaw('ok','true') + ',' + JStr('param', Target) + ',' + JStr('visible', Param(Lines, 'visible')) + '}';
End;
{ Copy a footprint ACROSS libraries: fromLib(path)+from -> the CURRENT PcbLib as to. }
Function CmdXCopyFootprint(Lines : TStringList) : String;
Var
SrcLib, DstLib : IPCB_Library;
Src, Dst : IPCB_LibComponent;
GIt : IPCB_GroupIterator;
Prim, Cpy : IPCB_Primitive;
NCopied : Integer;
Begin
DstLib := PCBServer.GetCurrentPCBLibrary;
If DstLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the destination PcbLib first') + '}';
Exit;
End;
SrcLib := PCBServer.LoadPCBLibraryByPath(Param(Lines, 'fromLib'));
If SrcLib = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','source library not loadable: ' + Param(Lines, 'fromLib')) + '}';
Exit;
End;
Src := FindFp(SrcLib, Param(Lines, 'from'));
If Src = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found in source: ' + Param(Lines, 'from')) + '}';
Exit;
End;
If FindFp(DstLib, Param(Lines, 'to')) <> Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','target already exists: ' + Param(Lines, 'to')) + '}';
Exit;
End;
PCBServer.PreProcess;
Dst := PCBServer.CreatePCBLibComp;
Dst.Name := Param(Lines, 'to');
Dst.Description := Src.Description;
NCopied := 0;
GIt := Src.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Cpy := Prim.Replicate;
Dst.AddPCBObject(Cpy);
NCopied := NCopied + 1;
Prim := GIt.NextPCBObject;
End;
Src.GroupIterator_Destroy(GIt);
DstLib.RegisterComponent(Dst);
PCBServer.PostProcess;
DstLib.Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('to', Dst.Name) + ',' + JRaw('primitives', IntToStr(NCopied)) + '}';
End;
{ ---- Phase 1: board introspection, read-only (altium-bridge parity plan) ---- }
{ Every pad of a component as name=net pairs, all pads, capped. }
Function AllPadNets(Comp : IPCB_Component) : String;
Var
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
S, N : String;
K : Integer;
Begin
S := ''; K := 0;
GIt := Comp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
While (Pad <> Nil) And (K < 400) Do
Begin
N := '';
If Pad.Net <> Nil Then N := Pad.Net.Name;
If S <> '' Then S := S + ';';
S := S + Pad.Name + '=' + N;
K := K + 1;
Pad := GIt.NextPCBObject;
End;
Comp.GroupIterator_Destroy(GIt);
Result := S;
End;
Function FindCompByDes(Board : IPCB_Board; Des : String) : IPCB_Component;
Var
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While (Comp <> Nil) And (Result = Nil) Do
Begin
If UpperCase(Comp.Name.Text) = UpperCase(Des) Then Result := Comp;
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Function CompJson(Comp : IPCB_Component) : String;
Var
R : TCoordRect;
Begin
R := Comp.BoundingRectangle;
Result := '{' + JStr('designator', Comp.Name.Text) + ',' +
JStr('pattern', Comp.Pattern) + ',' +
JStr('comment', Comp.Comment.Text) + ',' +
JRaw('layerId', IntToStr(Comp.Layer)) + ',' +
JRaw('bottom', JBool(Comp.Layer = eBottomLayer)) + ',' +
JRaw('xMil', IntToStr(Round(CoordToMils(Comp.x)))) + ',' +
JRaw('yMil', IntToStr(Round(CoordToMils(Comp.y)))) + ',' +
JRaw('rotation', IntToStr(Round(Comp.Rotation))) + ',' +
JRaw('wMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' +
JRaw('hMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + ',' +
JRaw('pads', IntToStr(GroupCount(Comp, ePadObject))) + ',' +
JStr('nets', AllPadNets(Comp)) + '}';
End;
{ bom_placement: every component's footprint, side and reference point relative to the board origin, for a
pick-and-place file. Lean on purpose (no pads, no nets): a large board's full read takes minutes.
x/y are RAW coords from the origin (10000 per mil), rotation in thousandths of a degree (locale-free). }
Function CmdBomPlacement(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
S : String;
N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := ''; N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While Comp <> Nil Do
Begin
If S <> '' Then S := S + ',';
S := S + '[' + '"' + JsonEsc(Comp.Name.Text) + '","' + JsonEsc(Comp.Pattern) + '",' +
IntToStr(Comp.x - Board.XOrigin) + ',' + IntToStr(Comp.y - Board.YOrigin) + ',' +
IntToStr(Round(Comp.Rotation * 1000)) + ',' + JBool(Comp.Layer = eBottomLayer) + ']';
N := N + 1;
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('comps', '[' + S + ']') + '}';
End;
{ list_designators: every component designator on the current board }
Function CmdListDesignators(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
S : String;
N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := ''; N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While Comp <> Nil Do
Begin
If S <> '' Then S := S + ',';
S := S + '"' + JsonEsc(Comp.Name.Text) + '"';
N := N + 1;
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('designators', '[' + S + ']') + '}';
End;
{ component_data: designator=<one> or all (limit=N, default 500) }
Function CmdComponentData(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
Des, S : String;
N, Limit : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Des := Param(Lines, 'designator');
Limit := StrToIntDef(Param(Lines, 'limit'), 500);
If Des <> '' Then
Begin
Comp := FindCompByDes(Board, Des);
If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('component', CompJson(Comp)) + '}';
Exit;
End;
S := ''; N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While (Comp <> Nil) And (N < Limit) Do
Begin
If S <> '' Then S := S + ',';
S := S + CompJson(Comp);
N := N + 1;
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';
End;
{ component_pins: designator=<one> -> every pad with net, position, size, hole }
Function CmdComponentPins(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Comp : IPCB_Component;
GIt : IPCB_GroupIterator;
Pad : IPCB_Pad;
Des, S, N : String;
K : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Des := Param(Lines, 'designator');
If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;
Comp := FindCompByDes(Board, Des);
If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;
S := ''; K := 0;
GIt := Comp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Pad := GIt.FirstPCBObject;
While (Pad <> Nil) And (K < 1000) Do
Begin
N := '';
If Pad.Net <> Nil Then N := Pad.Net.Name;
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', Pad.Name) + ',' + JStr('net', N) + ',' +
JRaw('xMil', IntToStr(Round(CoordToMils(Pad.x)))) + ',' +
JRaw('yMil', IntToStr(Round(CoordToMils(Pad.y)))) + ',' +
JRaw('layerId', IntToStr(Pad.Layer)) + ',' +
JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' +
JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' +
JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';
K := K + 1;
Pad := GIt.NextPCBObject;
End;
Comp.GroupIterator_Destroy(GIt);
Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pins', '[' + S + ']') + '}';
End;
{ list_nets: every net on the board with its pad count (tallied over all pads) }
Function CmdListNets(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Pad : IPCB_Pad;
SL : TStringList;
S, N : String;
I, C : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
SL := TStringList.Create;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePadObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Pad := BIt.FirstPCBObject;
While Pad <> Nil Do
Begin
If Pad.Net <> Nil Then
Begin
N := Pad.Net.Name;
C := StrToIntDef(SL.Values[N], 0);
SL.Values[N] := IntToStr(C + 1);
End;
Pad := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
S := '';
For I := 0 To SL.Count - 1 Do
Begin
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', SL.Names[I]) + ',' + JRaw('pads', SL.Values[SL.Names[I]]) + '}';
End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(SL.Count)) + ',' + JRaw('nets', '[' + S + ']') + '}';
SL.Free;
End;
Function LayerJson(Board : IPCB_Board; L : Integer; Kind : String) : String;
Begin
Result := '{' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' +
JRaw('id', IntToStr(L)) + ',' + JStr('kind', Kind) + ',' +
JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';
End;
{ layer_stack: the copper layers in stack order with their copper thickness and the dielectric under each
(height, constant x1000, material). Classic stack API (FirstLayer / NextLayer / Dielectric). }
Function CmdLayerStack(Lines : TStringList) : String;
Var
Board : IPCB_Board;
LS : IPCB_LayerStack_V7;
L : IPCB_LayerObject_V7;
S : String;
N, Tot : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := ''; N := 0; Tot := 0;
LS := Board.LayerStack_V7;
L := LS.FirstLayer;
While (L <> Nil) And (N < 64) Do
Begin
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', L.Name) + ',' + JRaw('copperUm', IntToStr(Round(CoordToMils(L.CopperThickness) * 25.4)));
Tot := Tot + L.CopperThickness;
{ a layer's dielectric is the one below it: the last copper layer has none in the board }
If (L.Dielectric <> Nil) And (LS.NextLayer(L) <> Nil) Then
Begin
S := S + ',' + JRaw('dielectricUm', IntToStr(Round(CoordToMils(L.Dielectric.DielectricHeight) * 25.4))) + ',' +
JRaw('dk1000', IntToStr(Round(L.Dielectric.DielectricConstant * 1000))) + ',' + JStr('material', L.Dielectric.DielectricMaterial);
Tot := Tot + L.Dielectric.DielectricHeight;
End;
S := S + '}';
N := N + 1;
L := LS.NextLayer(L);
End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('copperLayers', IntToStr(N)) + ',' + JRaw('totalUm', IntToStr(Round(CoordToMils(Tot) * 25.4))) + ',' + JRaw('layers', '[' + S + ']') + '}';
End;
{ layers: the fixed layer set with names and visibility (stackup detail is a later verb) }
Function CmdLayers(Lines : TStringList) : String;
Var
Board : IPCB_Board;
S : String;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := LayerJson(Board, eTopLayer, 'signal') + ',' + LayerJson(Board, eBottomLayer, 'signal') + ',' +
LayerJson(Board, eTopOverlay, 'overlay') + ',' + LayerJson(Board, eBottomOverlay, 'overlay') + ',' +
LayerJson(Board, eTopPaste, 'paste') + ',' + LayerJson(Board, eBottomPaste, 'paste') + ',' +
LayerJson(Board, eTopSolder, 'solder') + ',' + LayerJson(Board, eBottomSolder, 'solder') + ',' +
LayerJson(Board, eKeepOutLayer, 'keepout') + ',' + LayerJson(Board, eMultiLayer, 'multi') + ',' +
LayerJson(Board, eDrillGuide, 'drill') + ',' + LayerJson(Board, eDrillDrawing, 'drill') + ',' +
LayerJson(Board, eMechanical1, 'mechanical') + ',' + LayerJson(Board, eMechanical2, 'mechanical') + ',' +
LayerJson(Board, eMechanical3, 'mechanical') + ',' + LayerJson(Board, eMechanical4, 'mechanical') + ',' +
LayerJson(Board, eMechanical5, 'mechanical') + ',' + LayerJson(Board, eMechanical6, 'mechanical') + ',' +
LayerJson(Board, eMechanical7, 'mechanical') + ',' + LayerJson(Board, eMechanical8, 'mechanical') + ',' +
LayerJson(Board, eMechanical9, 'mechanical') + ',' + LayerJson(Board, eMechanical10, 'mechanical') + ',' +
LayerJson(Board, eMechanical11, 'mechanical') + ',' + LayerJson(Board, eMechanical12, 'mechanical') + ',' +
LayerJson(Board, eMechanical13, 'mechanical') + ',' + LayerJson(Board, eMechanical14, 'mechanical') + ',' +
LayerJson(Board, eMechanical15, 'mechanical') + ',' + LayerJson(Board, eMechanical16, 'mechanical');
Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';
End;
{ ---- Phase 1 batch 2 ---------------------------------------------------- }
{ net_connections: every pad on the board with its net and owner, plus per-net span.
net=<name> filters to one net. limit caps pads (default 4000). }
Function CmdNetConnections(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Pad : IPCB_Pad;
Want, S, N, Own, Spans, V : String;
K, Limit, X, Y, I : Integer;
SL : TStringList;
P : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Want := Param(Lines, 'net');
Limit := StrToIntDef(Param(Lines, 'limit'), 4000);
SL := TStringList.Create;
S := ''; K := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePadObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Pad := BIt.FirstPCBObject;
While (Pad <> Nil) And (K < Limit) Do
Begin
If Pad.Net <> Nil Then
Begin
N := Pad.Net.Name;
If (Want = '') Or (UpperCase(N) = UpperCase(Want)) Then
Begin
Own := '';
If Pad.Component <> Nil Then Own := Pad.Component.Name.Text;
X := Round(CoordToMils(Pad.x)); Y := Round(CoordToMils(Pad.y));
If S <> '' Then S := S + ',';
S := S + '{' + JStr('net', N) + ',' + JStr('pad', Own + '.' + Pad.Name) + ',' +
JRaw('xMil', IntToStr(X)) + ',' + JRaw('yMil', IntToStr(Y)) + '}';
K := K + 1;
V := SL.Values[N];
If V = '' Then SL.Values[N] := IntToStr(X) + '|' + IntToStr(Y) + '|' + IntToStr(X) + '|' + IntToStr(Y) + '|1'
Else
Begin
P := Pos('|', V); Spans := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));
I := StrToIntDef(Spans, X); If X < I Then I := X; Spans := IntToStr(I);
P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));
I := StrToIntDef(N, Y); If Y < I Then I := Y; Spans := Spans + '|' + IntToStr(I);
P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));
I := StrToIntDef(N, X); If X > I Then I := X; Spans := Spans + '|' + IntToStr(I);
P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));
I := StrToIntDef(N, Y); If Y > I Then I := Y; Spans := Spans + '|' + IntToStr(I);
I := StrToIntDef(V, 0) + 1; Spans := Spans + '|' + IntToStr(I);
SL.Values[Pad.Net.Name] := Spans;
End;
End;
End;
Pad := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Spans := '';
For I := 0 To SL.Count - 1 Do
Begin
If Spans <> '' Then Spans := Spans + ',';
Spans := Spans + '{' + JStr('net', SL.Names[I]) + ',' + JStr('minXminYmaxXmaxYpads', SL.Values[SL.Names[I]]) + '}';
End;
SL.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pads', '[' + S + ']') + ',' + JRaw('nets', '[' + Spans + ']') + '}';
End;
{ schematic_data: components on the CURRENT schematic sheet with pins.
designator=<one> filters. pins=0 skips pin detail. limit caps components. }
Function CmdSchematicData(Lines : TStringList) : String;
Var
Doc : ISch_Document;
Iter : ISch_Iterator;
PIt : ISch_Iterator;
Comp : ISch_Component;
Pin : ISch_Pin;
Prm : ISch_Parameter;
QIt : ISch_Iterator;
Want, S, PS, MS, PF, PV, Desc : String;
N, Limit, PN : Integer;
WantPins, WantParams : Boolean;
Begin
Doc := SchServer.GetCurrentSchDocument;
If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;
{ a schematic LIBRARY is an ISch_Document too: read as a sheet it answered ok with 0 parts (#21) }
If Doc.ObjectID = eSchLib Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','the current document is a schematic library, not a sheet: ' + Doc.DocumentName) + '}'; Exit; End;
Want := Param(Lines, 'designator');
Limit := StrToIntDef(Param(Lines, 'limit'), 500);
WantPins := Param(Lines, 'pins') <> '0';
WantParams := Param(Lines, 'params') = '1';
PF := UpperCase(Param(Lines, 'paramFilter'));
S := ''; N := 0;
Iter := Doc.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While (Comp <> Nil) And (N < Limit) Do
Begin
If (Want = '') Or (UpperCase(Comp.Designator.Text) = UpperCase(Want)) Then
Begin
PS := '';
If WantPins Then
Begin
PIt := Comp.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(ePin));
Pin := PIt.FirstSchObject;
While Pin <> Nil Do
Begin
If PS <> '' Then PS := PS + ',';
PS := PS + '{' + JStr('number', Pin.Designator) + ',' + JStr('name', Pin.Name) + ',' +
JRaw('electrical', IntToStr(Pin.Electrical)) + ',' +
JRaw('xMil', IntToStr(Round(CoordToMils(Pin.Location.X)))) + ',' + JRaw('yMil', IntToStr(Round(CoordToMils(Pin.Location.Y)))) + ',' +
JRaw('orientation', IntToStr(Pin.Orientation)) + ',' + JRaw('lengthMil', IntToStr(Round(CoordToMils(Pin.PinLength)))) + '}';
Pin := PIt.NextSchObject;
End;
Comp.SchIterator_Destroy(PIt);
End;
MS := '';
If WantParams Then
Begin
PN := 0;
QIt := Comp.SchIterator_Create;
QIt.AddFilter_ObjectSet(MkSet(eParameter));
Prm := QIt.FirstSchObject;
While (Prm <> Nil) And (PN < 40) Do
Begin
If MS <> '' Then MS := MS + ',';
{ paramFilter=|NAME|NAME| reads only those values: an example IC's three 'Available Preview Images'
parameters held so much that reading them took minutes (2026-10-21). Long values are cut. }
If (Param(Lines, 'paramNamesOnly') = '1') Or ((PF <> '') And (Pos('|' + UpperCase(Prm.Name) + '|', PF) = 0)) Then
MS := MS + '{' + JStr('name', Prm.Name) + '}'
Else
Begin
PV := Prm.Text;
If Length(PV) > 2000 Then
MS := MS + '{' + JStr('name', Prm.Name) + ',' + JStr('value', Copy(PV, 1, 2000)) + ',' + JRaw('truncatedFrom', IntToStr(Length(PV))) + '}'
Else
MS := MS + '{' + JStr('name', Prm.Name) + ',' + JStr('value', PV) + '}';
End;
PN := PN + 1;
Prm := QIt.NextSchObject;
End;
Comp.SchIterator_Destroy(QIt);
End;
{ a workspace-managed part's description is fetched from its vault: on the Bluetooth Sentinel
example's Altium Content Vault parts that call never returned (2026-10-21). Their item GUID says
where they live; the description of an unmanaged part is read as before. }
If Comp.ItemGUID = '' Then Desc := Comp.ComponentDescription Else Desc := '';
If S <> '' Then S := S + ',';
S := S + '{' + JRaw('params', '[' + MS + ']') + ',' + JStr('designator', Comp.Designator.Text) + ',' +
JRaw('partId', IntToStr(Comp.CurrentPartID)) + ',' +
JStr('libReference', Comp.LibReference) + ',' +
JStr('comment', Comp.Comment.Text) + ',' +
JStr('description', Desc) + ',' +
JStr('uniqueId', Comp.UniqueId) + ',' +
JStr('itemGuid', Comp.ItemGUID) + ',' +
JStr('vaultGuid', Comp.VaultGUID) + ',' +
JStr('sourceLibrary', Comp.SourceLibraryName) + ',' +
JRaw('kind', IntToStr(Comp.ComponentKind)) + ',' +
JRaw('xMil', IntToStr(Round(CoordToMils(Comp.Location.X)))) + ',' +
JRaw('yMil', IntToStr(Round(CoordToMils(Comp.Location.Y)))) + ',' +
JRaw('orientation', IntToStr(Comp.Orientation)) + ',' +
JRaw('pins', '[' + PS + ']') + '}';
N := N + 1;
End;
Comp := Iter.NextSchObject;
End;
Doc.SchIterator_Destroy(Iter);
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';
End;
{ sheet_params: set parameters on PLACED components of the CURRENT schematic sheet (the BOM write-back).
designators=R1,R2 (every part of a multi-part component) params=Key=Value;Key=Value
A missing parameter is added hidden; an existing one gets the value (PreProcess/PostProcess around the edit;
c_BroadcastBroadcast robot messages are not declared in AD 26's script engine). Save with altium_save. }
Function CmdSheetParams(Lines : TStringList) : String;
Var
Doc : ISch_Document;
Iter : ISch_Iterator;
Comp : ISch_Component;
Want, Items : TStringList;
Spec, K, V, Found, Missing : String;
I, P, NComp, NSet : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
Want := TStringList.Create;
Want.Delimiter := ',';
Want.StrictDelimiter := True;
Want.DelimitedText := UpperCase(Param(Lines, 'designators'));
Items := SplitSemi(Param(Lines, 'params'));
If (Want.Count = 0) Or (Items.Count = 0) Then
Begin
Want.Free; Items.Free;
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','bad_args') + ',' + JStr('error','designators and params are required') + '}';
Exit;
End;
Found := ''; NComp := 0; NSet := 0;
SchServer.ProcessControl.PreProcess(Doc, '');
Iter := Doc.SchIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
Comp := Iter.FirstSchObject;
While Comp <> Nil Do
Begin
If Want.IndexOf(UpperCase(Comp.Designator.Text)) >= 0 Then
Begin
For I := 0 To Items.Count - 1 Do
Begin
Spec := Items[I];
P := Pos('=', Spec);
If P > 0 Then
Begin
K := Trim(Copy(Spec, 1, P - 1));
V := Trim(Copy(Spec, P + 1, Length(Spec)));
If K <> '' Then Begin SetSchParam(Comp, K, V); NSet := NSet + 1; End;
End;
End;
If Pos('"' + Comp.Designator.Text + '"', Found) = 0 Then
Begin
If Found <> '' Then Found := Found + ',';
Found := Found + '"' + JsonEsc(Comp.Designator.Text) + '"';
End;
NComp := NComp + 1;
End;
Comp := Iter.NextSchObject;
End;
Doc.SchIterator_Destroy(Iter);
SchServer.ProcessControl.PostProcess(Doc, '');
Doc.GraphicallyInvalidate;
Missing := '';
For I := 0 To Want.Count - 1 Do
If Pos('"' + UpperCase(Want[I]) + '"', UpperCase(Found)) = 0 Then
Begin
If Missing <> '' Then Missing := Missing + ',';
Missing := Missing + '"' + JsonEsc(Want[I]) + '"';
End;
Want.Free; Items.Free;
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('parts', IntToStr(NComp)) + ',' +
JRaw('paramsSet', IntToStr(NSet)) + ',' + JRaw('designators', '[' + Found + ']') + ',' + JRaw('notOnSheet', '[' + Missing + ']') + '}';
End;
{ copper_layers: the full copper set (top, mid 1-30, planes 1-16, bottom) with names and visibility }
Function CmdCopperLayers(Lines : TStringList) : String;
Var
Board : IPCB_Board;
S : String;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := LayerJson(Board, eTopLayer, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer1, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer2, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer3, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer4, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer5, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer6, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer7, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer8, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer9, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer10, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer11, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer12, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer13, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer14, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer15, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer16, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer17, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer18, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer19, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer20, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer21, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer22, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer23, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer24, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer25, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer26, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer27, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer28, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer29, 'signal');
S := S + ',' + LayerJson(Board, eMidLayer30, 'signal');
S := S + ',' + LayerJson(Board, eInternalPlane1, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane2, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane3, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane4, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane5, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane6, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane7, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane8, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane9, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane10, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane11, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane12, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane13, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane14, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane15, 'plane');
S := S + ',' + LayerJson(Board, eInternalPlane16, 'plane');
S := S + ',' + LayerJson(Board, eBottomLayer, 'signal');
Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';
End;
{ ---- Phase 1 batch 3 ---------------------------------------------------- }
{ read_rules: every design rule on the board }
Function CmdReadRules(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Rule : IPCB_Rule;
S : String;
N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
S := ''; N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eRuleObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Rule := BIt.FirstPCBObject;
While Rule <> Nil Do
Begin
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', Rule.Name) + ',' + JRaw('kind', IntToStr(Rule.RuleKind)) + ',' +
JRaw('enabled', JBool(Rule.Enabled)) + ',' + JRaw('priority', IntToStr(Rule.Priority)) + ',' +
JStr('scope1', Rule.Scope1Expression) + ',' + JStr('scope2', Rule.Scope2Expression) + ',' +
JStr('comment', Rule.Comment) + ',' + JStr('descriptor', Rule.Descriptor) + '}';
N := N + 1;
Rule := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('rules', '[' + S + ']') + '}';
End;
{ set_layer_visibility: layerId=<int> displayed=1|0 }
Function CmdSetLayerVisibility(Lines : TStringList) : String;
Var
Board : IPCB_Board;
L : Integer;
Show : Boolean;
LName : String;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
L := StrToIntDef(Param(Lines, 'layerId'), -1);
If L < 0 Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','layerId (from altium_layers) is required') + '}'; Exit; End;
Show := Param(Lines, 'displayed') <> '0';
{ an id that is not in THIS board's stack has a nil LayerObject: reading .Name off it halts the
engine. Refuse by name instead (found by review, 2026-09-21). }
If Board.LayerStack.LayerObject[L] = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','unknown_layer') + ',' + JStr('error','layerId ' + IntToStr(L) + ' is not in this board; altium_layers lists the valid ids') + '}'; Exit; End;
LName := Board.LayerStack.LayerObject[L].Name;
Board.LayerIsDisplayed[L] := Show;
Board.ViewManager_UpdateLayerTabs;
Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JRaw('layerId', IntToStr(L)) + ',' + JStr('name', LName) + ',' + JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';
End;
{ create_net_class: name=<class> nets=<A;B;C> }
Function CmdCreateNetClass(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Cls : IPCB_ObjectClass;
Name, Nets, N : String;
P, K : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Name := Param(Lines, 'name');
Nets := Param(Lines, 'nets');
If (Name = '') Or (Nets = '') Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','name and nets (semicolon list) are required') + '}'; Exit; End;
PCBServer.PreProcess;
Cls := PCBServer.PCBClassFactory(eClassMemberKind_Net);
Cls.Name := Name;
K := 0;
While Nets <> '' Do
Begin
P := Pos(';', Nets);
If P = 0 Then Begin N := Nets; Nets := ''; End
Else Begin N := Copy(Nets, 1, P - 1); Nets := Copy(Nets, P + 1, Length(Nets)); End;
If N <> '' Then Begin Cls.AddMemberByName(N); K := K + 1; End;
End;
Board.AddPCBObject(Cls);
PCBServer.PostProcess;
Result := '{' + JRaw('ok','true') + ',' + JStr('class', Name) + ',' + JRaw('members', IntToStr(K)) + '}';
End;
{ footprint_primitives: designator=<board component> -> geometry dump (tracks, arcs, pads, texts, others counted) }
Function CmdFootprintPrimitives(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Comp : IPCB_Component;
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
Trk : IPCB_Track;
Arc : IPCB_Arc;
Pad : IPCB_Pad;
Txt : IPCB_Text;
Des, S, Item : String;
K, Other : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Des := Param(Lines, 'designator');
If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;
Comp := FindCompByDes(Board, Des);
If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;
S := ''; K := 0; Other := 0;
GIt := Comp.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While (Prim <> Nil) And (K < 2000) Do
Begin
Item := '';
If Prim.ObjectId = eTrackObject Then
Begin
Trk := Prim;
Item := '{' + JStr('kind','track') + ',' + JRaw('layerId', IntToStr(Trk.Layer)) + ',' +
JRaw('x1', IntToStr(Round(CoordToMils(Trk.x1)))) + ',' + JRaw('y1', IntToStr(Round(CoordToMils(Trk.y1)))) + ',' +
JRaw('x2', IntToStr(Round(CoordToMils(Trk.x2)))) + ',' + JRaw('y2', IntToStr(Round(CoordToMils(Trk.y2)))) + ',' +
JRaw('wMil', IntToStr(Round(CoordToMils(Trk.Width)))) + '}';
End
Else If Prim.ObjectId = eArcObject Then
Begin
Arc := Prim;
Item := '{' + JStr('kind','arc') + ',' + JRaw('layerId', IntToStr(Arc.Layer)) + ',' +
JRaw('cx', IntToStr(Round(CoordToMils(Arc.XCenter)))) + ',' + JRaw('cy', IntToStr(Round(CoordToMils(Arc.YCenter)))) + ',' +
JRaw('rMil', IntToStr(Round(CoordToMils(Arc.Radius)))) + ',' + JRaw('start', IntToStr(Round(Arc.StartAngle))) + ',' +
JRaw('end', IntToStr(Round(Arc.EndAngle))) + ',' + JRaw('wMil', IntToStr(Round(CoordToMils(Arc.LineWidth)))) + '}';
End
Else If Prim.ObjectId = ePadObject Then
Begin
Pad := Prim;
Item := '{' + JStr('kind','pad') + ',' + JStr('name', Pad.Name) + ',' + JRaw('layerId', IntToStr(Pad.Layer)) + ',' +
JRaw('x', IntToStr(Round(CoordToMils(Pad.x)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Pad.y)))) + ',' +
JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' + JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' +
JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';
End
Else If Prim.ObjectId = eTextObject Then
Begin
Txt := Prim;
Item := '{' + JStr('kind','text') + ',' + JRaw('layerId', IntToStr(Txt.Layer)) + ',' + JStr('text', Txt.Text) + ',' +
JRaw('x', IntToStr(Round(CoordToMils(Txt.XLocation)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Txt.YLocation)))) + '}';
End
Else Other := Other + 1;
If Item <> '' Then
Begin
If S <> '' Then S := S + ',';
S := S + Item;
K := K + 1;
End;
Prim := GIt.NextPCBObject;
End;
Comp.GroupIterator_Destroy(GIt);
Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JStr('pattern', Comp.Pattern) + ',' +
JRaw('count', IntToStr(K)) + ',' + JRaw('otherPrimitives', IntToStr(Other)) + ',' + JRaw('primitives', '[' + S + ']') + '}';
End;
{ I/O bench: where does the ~300 ms per file write go? Times TStringList.SaveToFile against
several paths (workspace / %TEMP% / Documents), overwrite vs new, and rename/delete. Uses only
routines the agent already relies on, so it cannot introduce a new undeclared identifier. }
Function BenchOne(Path : String; Content : String) : String;
Var
L : TStringList;
T0, TNew, TOver, TDel : Integer;
Begin
L := TStringList.Create;
Try
L.Text := Content;
If FileExists(Path) Then DeleteFile(Path);
T0 := GetTickCount; L.SaveToFile(Path); TNew := GetTickCount - T0;
T0 := GetTickCount; L.SaveToFile(Path); TOver := GetTickCount - T0;
T0 := GetTickCount; DeleteFile(Path); TDel := GetTickCount - T0;
Finally
L.Free;
End;
Result := '{' + JStr('path', Path) + ',' + JRaw('newMs', IntToStr(TNew)) + ',' + JRaw('overwriteMs', IntToStr(TOver)) + ',' + JRaw('deleteMs', IntToStr(TDel)) + '}';
End;
Function CmdIoBench(Lines : TStringList) : String;
Var
Big, Small, TmpDir, Docs : String;
K, T0, TRen, TSleep, TLoop, X : Integer;
L : TStringList;
Begin
Small := 'payload 0123456789 abcdefghij';
Big := '';
For K := 1 To 200 Do Big := Big + 'line ' + IntToStr(K) + ' 0123456789 abcdefghij 0123456789 abcdefghij' + #13#10;
{ derive %TEMP% and Documents from WORKSPACE (…\AppData\Local\Adom Bridge\…) - no new identifiers }
TmpDir := Copy(WORKSPACE, 1, Pos('\Adom Bridge\', WORKSPACE)) + 'Temp';
Docs := Copy(WORKSPACE, 1, Pos('\AppData\', WORKSPACE)) + 'Documents';
{ rename cost on an existing small file }
L := TStringList.Create;
Try
L.Text := Small;
L.SaveToFile(WORKSPACE + 'bench_ren.tmp');
Finally
L.Free;
End;
T0 := GetTickCount; RenameFile(WORKSPACE + 'bench_ren.tmp', WORKSPACE + 'bench_ren.txt'); TRen := GetTickCount - T0;
DeleteFile(WORKSPACE + 'bench_ren.txt');
T0 := GetTickCount; Sleep(1); TSleep := GetTickCount - T0;
T0 := GetTickCount; For K := 1 To 1000 Do X := K * 2; TLoop := GetTickCount - T0;
Result := '{' + JRaw('ok', 'true')
+ ',' + JRaw('wsSmall', BenchOne(WORKSPACE + 'bench_a.txt', Small))
+ ',' + JRaw('wsSmallTmpExt', BenchOne(WORKSPACE + 'bench_b.tmp', Small))
+ ',' + JRaw('wsBig', BenchOne(WORKSPACE + 'bench_c.json', Big))
+ ',' + JRaw('tempSmall', BenchOne(TmpDir + '\adom_bench_d.txt', Small))
+ ',' + JRaw('docsSmall', BenchOne(Docs + '\adom_bench_e.txt', Small))
+ ',' + JRaw('renameMs', IntToStr(TRen))
+ ',' + JRaw('sleep1Ms', IntToStr(TSleep))
+ ',' + JRaw('loop1000Ms', IntToStr(TLoop))
+ '}';
End;
{ Which copper layers actually CARRY copper. The layer stack's "is this layer in the stack" flag has
no identifier we have proven on this Altium build, and guessing one halts the engine behind a modal
on the user's screen — so instead of asking the stack, count primitives. ONE pass over pads, tracks
and arcs across all layers, tallied by Prim.Layer; every identifier here is already used elsewhere
in this unit. An empty inner plane reports zero, which is why the caller must read this as
"layers with copper on them", never as "the board's layer count". }
Function CmdLayerOccupancy(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
SL : TStringList;
S, K : String;
I, C, N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';
Exit;
End;
SL := TStringList.Create;
N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePadObject, eTrackObject, eArcObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
K := IntToStr(Prim.Layer);
C := StrToIntDef(SL.Values[K], 0);
SL.Values[K] := IntToStr(C + 1);
N := N + 1;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
S := '';
For I := 0 To SL.Count - 1 Do
Begin
If S <> '' Then S := S + ',';
K := SL.Names[I];
S := S + '{' + JRaw('layerId', K) + ',' +
JStr('name', Board.LayerStack.LayerObject[StrToIntDef(K, 1)].Name) + ',' +
JRaw('primitives', SL.Values[K]) + '}';
End;
SL.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('primitives', IntToStr(N)) + ',' + JRaw('occupancy', '[' + S + ']') + '}';
End;
{ The schematic-side companion to read_board: ONE round trip for the current sheet. Composed from
the existing reader (same rule — no new Altium API surface, so it cannot introduce an
engine-halting identifier). A hierarchical PARENT sheet holds sheet symbols, not parts, so this
reports what the CURRENT sheet contains; the caller activates a leaf sheet first. }
Function CmdReadSchematic(Lines : TStringList) : String;
Var
Doc : ISch_Document;
Sects : String;
S : String;
Begin
Doc := SchServer.GetCurrentSchDocument;
If Doc = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}';
Exit;
End;
Sects := Param(Lines, 'sections');
If Sects = '' Then Sects := 'components';
S := '';
If Pos('components', Sects) > 0 Then S := S + ',' + JRaw('components', CmdSchematicData(Lines));
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JStr('sections', Sects) + S + '}';
End;
{ The board's extent, for the checks that need to know where the edge is. Uses the SAME guarded
pattern the library reader already relies on: a library's internal board has a NIL BoardOutline and
touching BoundingRectangle on it halts the engine (measured 2026-08-30). Reports which source it
used, because the outline's bounding box and the board's bounding box are not the same claim. }
Function CmdReadOutline(Lines : TStringList) : String;
Var
Board : IPCB_Board;
R : TCoordRect;
Src : String;
Seg : TPolySegment;
K, L, B, RR, T : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';
Exit;
End;
If Board.BoardOutline <> Nil Then
Begin
{ vertices, not the cached BoundingRectangle (see OutlineMilFromVertices) }
L := 0; B := 0; RR := 0; T := 0;
For K := 0 To Board.BoardOutline.PointCount - 1 Do
Begin
Seg := Board.BoardOutline.Segments[K];
If (K = 0) Or (Seg.vx < L) Then L := Seg.vx;
If (K = 0) Or (Seg.vy < B) Then B := Seg.vy;
If (K = 0) Or (Seg.vx > RR) Then RR := Seg.vx;
If (K = 0) Or (Seg.vy > T) Then T := Seg.vy;
End;
Src := 'boardOutline';
End
Else
Begin
R := Board.BoundingRectangle;
L := R.Left; B := R.Bottom; RR := R.Right; T := R.Top;
Src := 'boardBounds';
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('source', Src) + ',' +
JRaw('leftMil', IntToStr(Round(CoordToMils(L)))) + ',' +
JRaw('bottomMil', IntToStr(Round(CoordToMils(B)))) + ',' +
JRaw('rightMil', IntToStr(Round(CoordToMils(RR)))) + ',' +
JRaw('topMil', IntToStr(Round(CoordToMils(T)))) + ',' +
JRaw('widthMil', IntToStr(Round(CoordToMils(RR - L)))) + ',' +
JRaw('heightMil', IntToStr(Round(CoordToMils(T - B)))) + '}';
End;
{ ONE round trip to READ the whole board. Composed from the EXISTING readers on purpose: no new Altium
API surface, so it cannot introduce an undeclared identifier (which halts the engine behind a
modal). The bridge shapes these into typed views; recorded dumps are the unit-test fixtures. }
Function CmdReadBoard(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Sects, S : String;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';
Exit;
End;
Sects := Param(Lines, 'sections');
If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy,outline';
S := '';
If Pos('components', Sects) > 0 Then S := S + ',' + JRaw('components', CmdComponentData(Lines));
If Pos('nets', Sects) > 0 Then S := S + ',' + JRaw('nets', CmdListNets(Lines));
If Pos('layers', Sects) > 0 Then S := S + ',' + JRaw('layers', CmdLayers(Lines));
If Pos('copper', Sects) > 0 Then S := S + ',' + JRaw('copper', CmdCopperLayers(Lines));
If Pos('rules', Sects) > 0 Then S := S + ',' + JRaw('rules', CmdReadRules(Lines));
If Pos('occupancy', Sects) > 0 Then S := S + ',' + JRaw('occupancy', CmdLayerOccupancy(Lines));
If Pos('outline', Sects) > 0 Then S := S + ',' + JRaw('outline', CmdReadOutline(Lines));
Result := '{' + JRaw('ok','true') + ',' + JStr('board', Board.FileName) + ',' + JStr('sections', Sects) + S + '}';
End;
{ routing_state: the whole live board as data for AI Flow's Altium lane. Every coordinate is the
RAW internal unit (an integer; 10000 per mil), so nothing is rounded and no decimal is
formatted in the user's locale. Rotations and angles are in thousandths of a degree. The
bridge converts to mm and hashes the dump into a revision. Arrays, not objects, keep a board
with thousands of tracks small:
tracks [x1,y1,x2,y2,width,layer,"net"]
arcs [cx,cy,radius,startMilliDeg,endMilliDeg,width,layer,"net"]
vias [x,y,size,hole,lowLayer,highLayer,"net"]
pads ["comp","name",x,y,xSize,ySize,rotMilliDeg,layer,hole,"net",shape]
comps ["designator",x,y,rotMilliDeg,layer,"pattern",bodies]
polys ["net",layer,[[x,y],...]]
outline [[x,y,kind,cx,cy,radius,angle1MilliDeg,angle2MilliDeg],...] kind 1 = an arc to the next vertex
unrouted the ratsnest connection count (connection objects on the board) }
Function JoinList(L : TStringList) : String;
Var
I : Integer;
S : String;
Begin
S := '';
For I := 0 To L.Count - 1 Do
Begin
If I > 0 Then S := S + ',';
S := S + L[I];
End;
Result := '[' + S + ']';
End;
Function NetOf(Prim : IPCB_Primitive) : String;
Begin
Result := '""';
If Prim.Net <> Nil Then Result := '"' + JsonEsc(Prim.Net.Name) + '"';
End;
Function CmdRoutingState(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
Trk : IPCB_Track;
Arc : IPCB_Arc;
Via : IPCB_Via;
Pad : IPCB_Pad;
Comp : IPCB_Component;
Poly : IPCB_Polygon;
BO : IPCB_BoardOutline;
Seg : TPolySegment;
LT, LA, LV, LP, LC, LG, LO : TStringList;
Own, Pts : String;
K, Unrouted : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
LT := TStringList.Create; LA := TStringList.Create; LV := TStringList.Create; LP := TStringList.Create;
LC := TStringList.Create; LG := TStringList.Create; LO := TStringList.Create;
Unrouted := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTrackObject, eArcObject, eViaObject, ePadObject, eComponentObject, ePolyObject, eConnectionObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If Prim.ObjectId = eTrackObject Then
Begin
Trk := Prim;
If Not Trk.InComponent Then
LT.Add('[' + IntToStr(Trk.x1) + ',' + IntToStr(Trk.y1) + ',' + IntToStr(Trk.x2) + ',' + IntToStr(Trk.y2) + ',' +
IntToStr(Trk.Width) + ',' + IntToStr(Trk.Layer) + ',' + NetOf(Trk) + ']');
End
Else If Prim.ObjectId = eArcObject Then
Begin
Arc := Prim;
If Not Arc.InComponent Then
LA.Add('[' + IntToStr(Arc.XCenter) + ',' + IntToStr(Arc.YCenter) + ',' + IntToStr(Arc.Radius) + ',' +
IntToStr(Round(Arc.StartAngle * 1000)) + ',' + IntToStr(Round(Arc.EndAngle * 1000)) + ',' +
IntToStr(Arc.LineWidth) + ',' + IntToStr(Arc.Layer) + ',' + NetOf(Arc) + ']');
End
Else If Prim.ObjectId = eViaObject Then
Begin
Via := Prim;
LV.Add('[' + IntToStr(Via.x) + ',' + IntToStr(Via.y) + ',' + IntToStr(Via.Size) + ',' + IntToStr(Via.HoleSize) + ',' +
IntToStr(Via.LowLayer) + ',' + IntToStr(Via.HighLayer) + ',' + NetOf(Via) + ']');
End
Else If Prim.ObjectId = ePadObject Then
Begin
Pad := Prim;
Own := '';
If Pad.Component <> Nil Then Own := Pad.Component.Name.Text;
LP.Add('["' + JsonEsc(Own) + '","' + JsonEsc(Pad.Name) + '",' + IntToStr(Pad.x) + ',' + IntToStr(Pad.y) + ',' +
IntToStr(Pad.TopXSize) + ',' + IntToStr(Pad.TopYSize) + ',' + IntToStr(Round(Pad.Rotation * 1000)) + ',' +
IntToStr(Pad.Layer) + ',' + IntToStr(Pad.HoleSize) + ',' + NetOf(Pad) + ',' + IntToStr(Pad.TopShape) + ']');
End
Else If Prim.ObjectId = eComponentObject Then
Begin
Comp := Prim;
LC.Add('["' + JsonEsc(Comp.Name.Text) + '",' + IntToStr(Comp.x) + ',' + IntToStr(Comp.y) + ',' +
IntToStr(Round(Comp.Rotation * 1000)) + ',' + IntToStr(Comp.Layer) + ',"' + JsonEsc(Comp.Pattern) + '",' +
IntToStr(GroupCount(Comp, eComponentBodyObject)) + ']');
End
Else If Prim.ObjectId = ePolyObject Then
Begin
Poly := Prim;
Pts := '';
For K := 0 To Poly.PointCount - 1 Do
Begin
Seg := Poly.Segments[K];
If K > 0 Then Pts := Pts + ',';
Pts := Pts + '[' + IntToStr(Seg.vx) + ',' + IntToStr(Seg.vy) + ']';
End;
LG.Add('[' + NetOf(Poly) + ',' + IntToStr(Poly.Layer) + ',[' + Pts + ']]');
End
Else If Prim.ObjectId = eConnectionObject Then
Unrouted := Unrouted + 1;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
BO := Board.BoardOutline;
If BO <> Nil Then
For K := 0 To BO.PointCount - 1 Do
Begin
Seg := BO.Segments[K];
If Seg.Kind = ePolySegmentArc Then
LO.Add('[' + IntToStr(Seg.vx) + ',' + IntToStr(Seg.vy) + ',1,' + IntToStr(Seg.cx) + ',' + IntToStr(Seg.cy) + ',' +
IntToStr(Seg.Radius) + ',' + IntToStr(Round(Seg.Angle1 * 1000)) + ',' + IntToStr(Round(Seg.Angle2 * 1000)) + ']')
Else
LO.Add('[' + IntToStr(Seg.vx) + ',' + IntToStr(Seg.vy) + ',0]');
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('file', Board.FileName) + ',' + JRaw('coordPerMil','10000') + ',' +
JRaw('tracks', JoinList(LT)) + ',' + JRaw('arcs', JoinList(LA)) + ',' + JRaw('vias', JoinList(LV)) + ',' +
JRaw('pads', JoinList(LP)) + ',' + JRaw('comps', JoinList(LC)) + ',' + JRaw('polys', JoinList(LG)) + ',' +
JRaw('outline', JoinList(LO)) + ',' + JRaw('unrouted', IntToStr(Unrouted)) + '}';
LT.Free; LA.Free; LV.Free; LP.Free; LC.Free; LG.Free; LO.Free;
End;
{ edit_board: a batch of copper and placement edits in ONE PreProcess/PostProcess (one undo
step), for AI Flow's Altium lane. The payload is one op per line, '|'-separated, coordinates
in RAW internal units (10000 per mil), rotation in thousandths of a degree:
T|x1|y1|x2|y2|width|layer|net add a track
V|x|y|size|hole|lowLayer|highLayer|net add a via
M|ref|x|y|rotMilliDeg move + rotate a placed component
D|T|x1|y1|x2|y2|layer delete the free track with these ends (either direction)
D|V|x|y delete the via at x,y
N|net rip up: delete every free track, arc and via on the net
Each op answers in results[] in order: "ok", "ok:<n>" (deleted count) or "err:<why>". A
failed op does not stop the batch; the bridge reads the board back and reports what landed. }
Function FindTrack(Board : IPCB_Board; X1, Y1, X2, Y2, Lay : Integer) : IPCB_Track;
Var
BIt : IPCB_BoardIterator;
Trk : IPCB_Track;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTrackObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Trk := BIt.FirstPCBObject;
While Trk <> Nil Do
Begin
If (Not Trk.InComponent) And (Trk.Layer = Lay) And
(((Trk.x1 = X1) And (Trk.y1 = Y1) And (Trk.x2 = X2) And (Trk.y2 = Y2)) Or
((Trk.x1 = X2) And (Trk.y1 = Y2) And (Trk.x2 = X1) And (Trk.y2 = Y1))) Then
Begin
Result := Trk; Break;
End;
Trk := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Function FindViaAt(Board : IPCB_Board; X, Y : Integer) : IPCB_Via;
Var
BIt : IPCB_BoardIterator;
Via : IPCB_Via;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eViaObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Via := BIt.FirstPCBObject;
While Via <> Nil Do
Begin
If (Via.x = X) And (Via.y = Y) Then Begin Result := Via; Break; End;
Via := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
{ The first free track, arc or via on the net, or Nil. }
Function FirstOnNet(Board : IPCB_Board; NetName : String) : IPCB_Primitive;
Var
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTrackObject, eArcObject, eViaObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If (Not Prim.InComponent) And (Prim.Net <> Nil) Then
If Prim.Net.Name = NetName Then Begin Result := Prim; Break; End;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Procedure RegisterPrim(Board : IPCB_Board; Prim : IPCB_Primitive);
Begin
Board.AddPCBObject(Prim);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Prim.I_ObjectAddress);
End;
Function CmdEditBoard(Lines : TStringList; Payload : String) : String;
Var
Board : IPCB_Board;
Ops : TStringList;
Op, Kind, Res, Results : String;
Trk : IPCB_Track;
Via : IPCB_Via;
Comp : IPCB_Component;
Prim : IPCB_Primitive;
Net : IPCB_Net;
I, N, Okay, Failed : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Ops := TStringList.Create;
Ops.Text := Payload;
Results := ''; Okay := 0; Failed := 0;
PCBServer.PreProcess;
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If Op = '' Then Continue;
Kind := FieldP(Op, 1);
Res := 'ok';
gInputError := '';
If Kind = 'T' Then
Begin
Net := FindNet(Board, FieldP(Op, 8));
If (Net = Nil) And (FieldP(Op, 8) <> '') Then Res := 'err:no net ' + FieldP(Op, 8)
Else
Begin
Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);
Trk.Layer := AdomInt(FieldP(Op, 7));
Trk.X1 := AdomInt(FieldP(Op, 2)); Trk.Y1 := AdomInt(FieldP(Op, 3));
Trk.X2 := AdomInt(FieldP(Op, 4)); Trk.Y2 := AdomInt(FieldP(Op, 5));
Trk.Width := AdomInt(FieldP(Op, 6));
If Net <> Nil Then Trk.Net := Net;
If gInputError <> '' Then Res := 'err:bad number ' + gInputError Else RegisterPrim(Board, Trk);
End;
End
Else If Kind = 'V' Then
Begin
Net := FindNet(Board, FieldP(Op, 8));
If (Net = Nil) And (FieldP(Op, 8) <> '') Then Res := 'err:no net ' + FieldP(Op, 8)
Else
Begin
Via := PCBServer.PCBObjectFactory(eViaObject, eNoDimension, eCreate_Default);
Via.x := AdomInt(FieldP(Op, 2)); Via.y := AdomInt(FieldP(Op, 3));
Via.Size := AdomInt(FieldP(Op, 4)); Via.HoleSize := AdomInt(FieldP(Op, 5));
Via.LowLayer := AdomInt(FieldP(Op, 6)); Via.HighLayer := AdomInt(FieldP(Op, 7));
If Net <> Nil Then Via.Net := Net;
If gInputError <> '' Then Res := 'err:bad number ' + gInputError Else RegisterPrim(Board, Via);
End;
End
Else If Kind = 'M' Then
Begin
Comp := FindCompByDes(Board, FieldP(Op, 2));
If Comp = Nil Then Res := 'err:no component ' + FieldP(Op, 2)
Else
Begin
Comp.BeginModify;
Comp.Rotation := AdomInt(FieldP(Op, 5)) / 1000;
Comp.MoveToXY(AdomInt(FieldP(Op, 3)), AdomInt(FieldP(Op, 4)));
Comp.EndModify;
If gInputError <> '' Then Res := 'err:bad number ' + gInputError;
End;
End
Else If Kind = 'D' Then
Begin
Prim := Nil;
If FieldP(Op, 2) = 'T' Then
Prim := FindTrack(Board, AdomInt(FieldP(Op, 3)), AdomInt(FieldP(Op, 4)), AdomInt(FieldP(Op, 5)), AdomInt(FieldP(Op, 6)), AdomInt(FieldP(Op, 7)))
Else If FieldP(Op, 2) = 'V' Then
Prim := FindViaAt(Board, AdomInt(FieldP(Op, 3)), AdomInt(FieldP(Op, 4)));
If Prim = Nil Then Res := 'err:not found'
Else Board.RemovePCBObject(Prim);
End
Else If Kind = 'N' Then
Begin
N := 0;
Prim := FirstOnNet(Board, FieldP(Op, 2));
While (Prim <> Nil) And (N < 20000) Do
Begin
Board.RemovePCBObject(Prim);
N := N + 1;
Prim := FirstOnNet(Board, FieldP(Op, 2));
End;
Res := 'ok:' + IntToStr(N);
End
Else Res := 'err:unknown op ' + Kind;
If Copy(Res, 1, 3) = 'err' Then Failed := Failed + 1 Else Okay := Okay + 1;
If Results <> '' Then Results := Results + ',';
Results := Results + '"' + JsonEsc(Res) + '"';
End;
PCBServer.PostProcess;
gInputError := '';
Board.ViewManager_FullUpdate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('applied', IntToStr(Okay)) + ',' + JRaw('failed', IntToStr(Failed)) + ',' +
JRaw('results', '[' + Results + ']') + '}';
End;
{ apply_pours: solid polygon pours, one op per payload line, RAW internal units:
P|net|layer|pourIndex|x,y;x,y;...|removeDead a closed outline (the last point need not repeat the
first); removeDead 0 keeps islands Altium calls dead
All in one PreProcess/PostProcess; each pour is rebuilt (poured) after it is added. Answers
results[] per line: "ok" or "err:<why>". }
{ a pour's connect style: Altium keeps no connect style on a polygon; a PolygonConnect rule
scoped IsNamedPolygon('<name>') sets it for that one pour. Altium gives a newly added rule
priority 1 and moves the rest down (measured AD 26.11.1, 2026-10-15), so it outranks the board's
own. Writing Rule.Priority from a script halted the engine with an empty Error box (same
date): do not reorder rules here. }
Function FindRuleNamed(Board : IPCB_Board; Kind : Integer; Name : String) : IPCB_Rule;
Var
BIt : IPCB_BoardIterator;
Rule : IPCB_Rule;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eRuleObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Rule := BIt.FirstPCBObject;
While Rule <> Nil Do
Begin
If (Rule.RuleKind = Kind) And (Rule.Name = Name) Then Begin Result := Rule; Break; End;
Rule := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Function PolyNamed(Board : IPCB_Board; Name : String) : Boolean;
Var
BIt : IPCB_BoardIterator;
Poly : IPCB_Polygon;
Begin
Result := False;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePolyObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Poly := BIt.FirstPCBObject;
While Poly <> Nil Do
Begin
If Poly.Name = Name Then Begin Result := True; Break; End;
Poly := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
{ Style: D direct (solid), R relief, N no connect. W/G raw units, E spokes (2 or 4). }
Procedure SetPourConnect(Board : IPCB_Board; PolyName, Style : String; W, G, E : Integer);
Var
Rule : IPCB_Rule;
Begin
Rule := FindRuleNamed(Board, eRule_PolygonConnectStyle, 'AdomPC ' + PolyName);
If Rule <> Nil Then Board.RemovePCBObject(Rule);
Rule := PCBServer.PCBRuleFactory(eRule_PolygonConnectStyle);
Rule.Name := 'AdomPC ' + PolyName;
Rule.Comment := PolyName;
{ IsNamedPolygon is the polygon itself; InNamedPolygon is 'belongs to' (its pour primitives) and
never matched the polygon, so the catch-all relief stayed in force (measured 2026-10-15). The
same OR on both sides: which of the pair (polygon, pad) Altium tests against Scope1 is not ours
to know. }
Rule.Scope1Expression := 'IsNamedPolygon(''' + PolyName + ''') Or IsPad Or IsVia';
Rule.Scope2Expression := Rule.Scope1Expression;
If Style = 'D' Then Rule.ConnectStyle := eDirectConnectToPlane
Else If Style = 'N' Then Rule.ConnectStyle := eNoConnect
Else
Begin
Rule.ConnectStyle := eReliefConnectToPlane;
If W > 0 Then Rule.ReliefConductorWidth := W;
{ no air gap here: AD 26.11.1 keeps it per pad type (SetAirGapWidth(primitiveType, v));
a plain AirGapWidth is 'Undeclared identifier' (measured 2026-10-15). Altium's default
gap is 10 mil. }
If E > 0 Then Rule.ReliefEntries := E;
End;
Board.AddPCBObject(Rule);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Rule.I_ObjectAddress);
End;
{ solid joints inside a pour: pads completely inside each rect (the bridge widens a joint box to
the pads it touches) connect to this pour full width
(KiCad's small solid zone over a pad, which Altium cannot pour as a separate overlapping
polygon: a same-net polygon inside another pours nothing, measured 2026-10-15). One direct rule
per rect; both scopes are the same OR so the pair matches in either order. Rects: 'l,b,r,t'
RAW, ';'-joined. The rule's Comment holds the polygon name for cleanup. }
Procedure SetPourJoints(Board : IPCB_Board; PolyName, Rects : String);
Var
RL : TStringList;
Rule : IPCB_Rule;
K : Integer;
Q, R : String;
Begin
RL := SplitSemi(Rects);
For K := 0 To RL.Count - 1 Do
Begin
R := RL[K];
{ whole mils, widened outward: the region test wants the pad completely inside }
Q := 'IsNamedPolygon(''' + PolyName + ''') Or InRegionAbsolute(' +
IntToStr(Trunc(CoordToMils(AdomInt(Field(R, 1)))) - 1) + ',' + IntToStr(Trunc(CoordToMils(AdomInt(Field(R, 2)))) - 1) + ',' +
IntToStr(Trunc(CoordToMils(AdomInt(Field(R, 3)))) + 1) + ',' + IntToStr(Trunc(CoordToMils(AdomInt(Field(R, 4)))) + 1) + ')';
Rule := FindRuleNamed(Board, eRule_PolygonConnectStyle, 'AdomPJ ' + PolyName + ' #' + IntToStr(K + 1));
If Rule <> Nil Then Board.RemovePCBObject(Rule);
Rule := PCBServer.PCBRuleFactory(eRule_PolygonConnectStyle);
Rule.Name := 'AdomPJ ' + PolyName + ' #' + IntToStr(K + 1);
Rule.Comment := PolyName;
Rule.Scope1Expression := Q;
Rule.Scope2Expression := Q;
Rule.ConnectStyle := eDirectConnectToPlane;
Board.AddPCBObject(Rule);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Rule.I_ObjectAddress);
End;
RL.Free;
End;
{ a pour keepout: a polygon cutout region on one layer (pours stay out, tracks do not) }
Function AddCutout(Board : IPCB_Board; Lay : Integer; Pts : TStringList; Name : String) : String;
Var
Reg : IPCB_Region;
Cont : IPCB_Contour;
K : Integer;
Begin
Result := 'ok';
Cont := PCBServer.PCBContourFactory;
For K := 0 To Pts.Count - 1 Do Cont.AddPoint(AdomInt(Field(Pts[K], 1)), AdomInt(Field(Pts[K], 2)));
If gInputError <> '' Then Begin Result := 'err:bad number ' + gInputError; Exit; End;
Reg := PCBServer.PCBObjectFactory(eRegionObject, eNoDimension, eCreate_Default);
Reg.BeginModify;
Reg.Layer := Lay;
Reg.Kind := eRegionKind_Cutout;
Reg.Name := Name;
Reg.SetOutlineContour(Cont);
Reg.EndModify;
RegisterPrim(Board, Reg);
End;
Function CmdApplyPours(Lines : TStringList; Payload : String) : String;
Var
Board : IPCB_Board;
Ops, Pts : TStringList;
Op, Res, Results, P, PName : String;
Poly : IPCB_Polygon;
Seg : TPolySegment;
Net : IPCB_Net;
I, K, C, Okay, Failed : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Ops := TStringList.Create;
Ops.Text := Payload;
Results := ''; Okay := 0; Failed := 0;
PCBServer.PreProcess;
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If Op = '' Then Continue;
Res := 'ok';
gInputError := '';
If FieldP(Op, 1) = 'C' Then
Begin
Pts := SplitSemi(FieldP(Op, 3));
If Pts.Count < 3 Then Res := 'err:a cutout needs 3 or more points'
Else Res := AddCutout(Board, AdomInt(FieldP(Op, 2)), Pts, FieldP(Op, 4));
Pts.Free;
End
Else If FieldP(Op, 1) <> 'P' Then Res := 'err:unknown op'
Else
Begin
Net := FindNet(Board, FieldP(Op, 2));
Pts := SplitSemi(FieldP(Op, 5));
If (Net = Nil) And (FieldP(Op, 2) <> '') Then Res := 'err:no net ' + FieldP(Op, 2)
Else If Pts.Count < 3 Then Res := 'err:an outline needs 3 or more points'
Else
Begin
Poly := PCBServer.PCBObjectFactory(ePolyObject, eNoDimension, eCreate_Default);
Poly.BeginModify;
Poly.PolygonType := eSignalLayerPolygon;
Poly.Layer := AdomInt(FieldP(Op, 3));
Poly.PolyHatchStyle := ePolySolid;
PName := FieldP(Op, 7);
If PName = '' Then PName := 'Adom ' + FieldP(Op, 2) + ' L' + FieldP(Op, 3);
If PolyNamed(Board, PName) Then
Begin
K := 2;
While PolyNamed(Board, PName + ' ' + IntToStr(K)) Do K := K + 1;
PName := PName + ' ' + IntToStr(K);
End;
Poly.Name := PName;
Poly.RemoveDead := FieldP(Op, 6) <> '0';
{ connected copper of any size stays, as in a KiCad zone: Altium's own default drops
islands under 2500 sq mil, which emptied every small joint pour (measured
2026-10-15: a 1.8 mm box around a pad poured 0 mm2). Dead copper still goes. }
Poly.RemoveIslandsByArea := FieldP(Op, 12) = '1';
If AdomInt(FieldP(Op, 13)) > 0 Then
Begin
Poly.RemoveNarrowNecks := True;
Poly.NeckWidthThreshold := AdomInt(FieldP(Op, 13));
End;
{ pour over every same-net object, as a KiCad zone does: by default Altium pours AROUND
same-net tracks, which leaves slivers that touch no pad and are removed as dead copper
(measured 2026-10-15: a buck's SW patch poured 0 mm2 beside its own SW track) }
Poly.PourOver := ePolygonPourOver_SameNet;
If Net <> Nil Then Poly.Net := Net;
Poly.PointCount := Pts.Count;
For K := 0 To Pts.Count - 1 Do
Begin
P := Pts[K];
Seg := Poly.Segments[K];
Seg.Kind := ePolySegmentLine;
Seg.vx := AdomInt(Field(P, 1));
Seg.vy := AdomInt(Field(P, 2));
Poly.Segments[K] := Seg;
End;
C := AdomInt(FieldP(Op, 4));
If C > 0 Then Poly.PourIndex := C;
Poly.EndModify;
If gInputError <> '' Then Res := 'err:bad number ' + gInputError
Else
Begin
RegisterPrim(Board, Poly);
If FieldP(Op, 8) <> '' Then
SetPourConnect(Board, PName, FieldP(Op, 8), AdomInt(FieldP(Op, 9)), AdomInt(FieldP(Op, 10)), AdomInt(FieldP(Op, 11)));
If FieldP(Op, 14) <> '' Then SetPourJoints(Board, PName, FieldP(Op, 14));
Res := 'ok:' + PName;
Poly.Rebuild;
Poly.GraphicallyInvalidate;
End;
End;
Pts.Free;
End;
If Copy(Res, 1, 3) = 'err' Then Failed := Failed + 1 Else Okay := Okay + 1;
If Results <> '' Then Results := Results + ',';
Results := Results + '"' + JsonEsc(Res) + '"';
End;
PCBServer.PostProcess;
gInputError := '';
Board.ViewManager_FullUpdate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('applied', IntToStr(Okay)) + ',' + JRaw('failed', IntToStr(Failed)) + ',' +
JRaw('results', '[' + Results + ']') + '}';
End;
{ pour_state: every polygon with its outline AND the copper it actually poured: the region
primitives inside it, each as a main contour plus holes, in RAW units. The bridge computes
the areas (shoelace), so nothing here does floating point.
polys [["net",layer,pourIndex,[[x,y]...outline],[ [[x,y]...main], [[[x,y]...hole]...] ]...]] }
Function ContourPts(Ct : IPCB_Contour) : String;
Var
K : Integer;
S : String;
Begin
S := '';
For K := 0 To Ct.Count - 1 Do
Begin
If K > 0 Then S := S + ',';
S := S + '[' + IntToStr(Ct.x[K]) + ',' + IntToStr(Ct.y[K]) + ']';
End;
Result := '[' + S + ']';
End;
Function CmdPourState(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
GIt : IPCB_GroupIterator;
Poly : IPCB_Polygon;
Prim : IPCB_Primitive;
Reg : IPCB_Region;
Seg : TPolySegment;
LG : TStringList;
Outl, Fills, Holes : String;
K, H : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
LG := TStringList.Create;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePolyObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Poly := BIt.FirstPCBObject;
While Poly <> Nil Do
Begin
Outl := '';
For K := 0 To Poly.PointCount - 1 Do
Begin
Seg := Poly.Segments[K];
If K > 0 Then Outl := Outl + ',';
Outl := Outl + '[' + IntToStr(Seg.vx) + ',' + IntToStr(Seg.vy) + ']';
End;
Fills := '';
GIt := Poly.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(eRegionObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Reg := Prim;
Holes := '';
For H := 0 To Reg.HoleCount - 1 Do
Begin
If H > 0 Then Holes := Holes + ',';
Holes := Holes + ContourPts(Reg.Holes[H]);
End;
If Fills <> '' Then Fills := Fills + ',';
Fills := Fills + '[' + ContourPts(Reg.MainContour) + ',[' + Holes + ']]';
Prim := GIt.NextPCBObject;
End;
Poly.GroupIterator_Destroy(GIt);
LG.Add('[' + NetOf(Poly) + ',' + IntToStr(Poly.Layer) + ',' + IntToStr(Poly.PourIndex) + ',[' + Outl + '],[' + Fills + '],"' + JsonEsc(Poly.Name) + '"]');
Poly := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JStr('file', Board.FileName) + ',' + JRaw('polys', JoinList(LG)) + '}';
LG.Free;
End;
{ remove_pours: delete polygons. Payload lines: "net|layer" (layer 0 = any layer), or "*" for all. }
Function FirstPoly(Board : IPCB_Board; NetName : String; Lay : Integer; All : Boolean) : IPCB_Polygon;
Var
BIt : IPCB_BoardIterator;
Poly : IPCB_Polygon;
N : String;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(ePolyObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Poly := BIt.FirstPCBObject;
While Poly <> Nil Do
Begin
N := '';
If Poly.Net <> Nil Then N := Poly.Net.Name;
{ NetName '=<name>' picks a polygon by its name }
If All Or ((N = NetName) And ((Lay = 0) Or (Poly.Layer = Lay))) Or ((Copy(NetName, 1, 1) = '=') And (Poly.Name = Copy(NetName, 2, Length(NetName)))) Then Begin Result := Poly; Break; End;
Poly := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Function FirstAdomCutout(Board : IPCB_Board; Lay : Integer) : IPCB_Region;
Var
BIt : IPCB_BoardIterator;
Reg : IPCB_Region;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eRegionObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Reg := BIt.FirstPCBObject;
While Reg <> Nil Do
Begin
If (Reg.Kind = eRegionKind_Cutout) And (Copy(Reg.Name, 1, 4) = 'Adom') And ((Lay = 0) Or (Reg.Layer = Lay)) Then Begin Result := Reg; Break; End;
Reg := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
{ the AdomPC/AdomPJ connect rule whose polygon (named in its Comment) is gone }
Function OrphanPourRule(Board : IPCB_Board) : IPCB_Rule;
Var
BIt : IPCB_BoardIterator;
Rule : IPCB_Rule;
PN : String;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eRuleObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Rule := BIt.FirstPCBObject;
While Rule <> Nil Do
Begin
If (Rule.RuleKind = eRule_PolygonConnectStyle) And ((Copy(Rule.Name, 1, 7) = 'AdomPC ') Or (Copy(Rule.Name, 1, 7) = 'AdomPJ ')) Then
Begin
PN := Rule.Comment;
If PN = '' Then PN := Copy(Rule.Name, 8, Length(Rule.Name));
If Not PolyNamed(Board, PN) Then Begin Result := Rule; Break; End;
End;
Rule := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
Function CmdRemovePours(Lines : TStringList; Payload : String) : String;
Var
Board : IPCB_Board;
Ops : TStringList;
Op : String;
Poly : IPCB_Polygon;
Reg : IPCB_Region;
Rule : IPCB_Rule;
I, N, NC, NR : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Ops := TStringList.Create;
Ops.Text := Payload;
N := 0; NC := 0; NR := 0;
PCBServer.PreProcess;
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If Op = '' Then Continue;
If FieldP(Op, 1) = 'C' Then
Begin
Reg := FirstAdomCutout(Board, AdomInt(FieldP(Op, 2)));
While (Reg <> Nil) And (NC < 5000) Do
Begin
Board.RemovePCBObject(Reg);
NC := NC + 1;
Reg := FirstAdomCutout(Board, AdomInt(FieldP(Op, 2)));
End;
Continue;
End;
Poly := FirstPoly(Board, FieldP(Op, 1), AdomInt(FieldP(Op, 2)), Op = '*');
While (Poly <> Nil) And (N < 5000) Do
Begin
Board.RemovePCBObject(Poly);
N := N + 1;
Poly := FirstPoly(Board, FieldP(Op, 1), AdomInt(FieldP(Op, 2)), Op = '*');
End;
End;
Rule := OrphanPourRule(Board);
While (Rule <> Nil) And (NR < 5000) Do
Begin
Board.RemovePCBObject(Rule);
NR := NR + 1;
Rule := OrphanPourRule(Board);
End;
PCBServer.PostProcess;
gInputError := '';
Board.ViewManager_FullUpdate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('removed', IntToStr(N)) + ',' + JRaw('cutoutsRemoved', IntToStr(NC)) + ',' + JRaw('rulesRemoved', IntToStr(NR)) + '}';
End;
{ routing_drc: the violations on the board right now (online DRC keeps them current as edits
land, a batch DRC refreshes them all) with each one's location, plus the ratsnest count.
violations [["rule","description",left,bottom,right,top]] RAW units }
Function CmdRoutingDrc(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
V : IPCB_Violation;
R : TCoordRect;
LV : TStringList;
Rule : String;
Unrouted, N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
LV := TStringList.Create;
Unrouted := 0; N := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eViolationObject, eConnectionObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If Prim.ObjectId = eConnectionObject Then Unrouted := Unrouted + 1
Else
Begin
V := Prim;
N := N + 1;
If N <= 500 Then
Begin
Rule := '';
If V.Rule <> Nil Then Rule := V.Rule.Name;
R := V.BoundingRectangle;
LV.Add('["' + JsonEsc(Rule) + '","' + JsonEsc(V.Description) + '",' + IntToStr(R.Left) + ',' + IntToStr(R.Bottom) + ',' +
IntToStr(R.Right) + ',' + IntToStr(R.Top) + ']');
End;
End;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JStr('file', Board.FileName) + ',' + JRaw('violationCount', IntToStr(N)) + ',' +
JRaw('unrouted', IntToStr(Unrouted)) + ',' + JRaw('violations', JoinList(LV)) + '}';
LV.Free;
End;
{ Outline vertices from the payload's O line into the board outline, RAW units:
O|x,y,kind,cx,cy,radius,angle1MilliDeg,angle2MilliDeg;... kind 1 = an arc from this vertex to the next }
Function SetOutlineFrom(Board : IPCB_Board; Spec : String) : Integer;
Var
BO : IPCB_BoardOutline;
Seg : TPolySegment;
Pts : TStringList;
K : Integer;
P : String;
Begin
Result := 0;
BO := Board.BoardOutline;
If BO = Nil Then Exit;
Pts := SplitSemi(Spec);
If Pts.Count < 3 Then Begin Pts.Free; Exit; End;
BO.Invalidate;
BO.PointCount := Pts.Count;
For K := 0 To Pts.Count - 1 Do
Begin
P := Pts[K];
Seg := BO.Segments[K];
Seg.vx := AdomInt(Field(P, 1));
Seg.vy := AdomInt(Field(P, 2));
If Field(P, 3) = '1' Then
Begin
Seg.Kind := ePolySegmentArc;
Seg.cx := AdomInt(Field(P, 4));
Seg.cy := AdomInt(Field(P, 5));
Seg.Radius := AdomInt(Field(P, 6));
Seg.Angle1 := AdomInt(Field(P, 7)) / 1000;
Seg.Angle2 := AdomInt(Field(P, 8)) / 1000;
End
Else Seg.Kind := ePolySegmentLine;
BO.Segments[K] := Seg;
End;
BO.Rebuild;
BO.Validate;
Result := Pts.Count;
Pts.Free;
End;
{ A library footprint placed by its OWN origin (the library's 0,0), then moved and rotated as a
whole: the coordinates a layout tool or a reference design gives mean the same here. }
{ A library stores a footprint either around the library's canvas origin (Altium's own editor: about
50000 mil out) or around 0,0 (bridge-built footprints once a body was aligned). Measured 2026-10-15:
one PcbLib held both. The footprint's frame is whichever its pad cluster sits nearer to; the offset
returned is what to take off every primitive. }
Function FpOffset(PLib : IPCB_Library; LibFp : IPCB_LibComponent; Var OX : Integer; Var OY : Integer) : Boolean;
Var
GIt : IPCB_GroupIterator;
Prim : IPCB_Primitive;
R : TCoordRect;
CL, CB, CR, CT, CX, CY, LX, LY : Integer;
AnyP : Boolean;
Begin
OX := 0; OY := 0; AnyP := False; CL := 0; CB := 0; CR := 0; CT := 0;
GIt := LibFp.GroupIterator_Create;
GIt.AddFilter_ObjectSet(MkSet(ePadObject));
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
R := Prim.BoundingRectangle;
If Not AnyP Then Begin CL := R.Left; CB := R.Bottom; CR := R.Right; CT := R.Top; AnyP := True; End
Else
Begin
If R.Left < CL Then CL := R.Left;
If R.Bottom < CB Then CB := R.Bottom;
If R.Right > CR Then CR := R.Right;
If R.Top > CT Then CT := R.Top;
End;
Prim := GIt.NextPCBObject;
End;
LibFp.GroupIterator_Destroy(GIt);
Result := False;
If Not AnyP Then Exit;
CX := (CL + CR) Div 2; CY := (CB + CT) Div 2;
LX := PLib.Board.XOrigin; LY := PLib.Board.YOrigin;
If (Abs(CX - LX) + Abs(CY - LY)) < (Abs(CX) + Abs(CY)) Then
Begin
OX := LX; OY := LY; Result := True;
End;
End;
Function PlaceByOrigin(Board : IPCB_Board; PLib : IPCB_Library; LibFp : IPCB_LibComponent; X, Y, RotMilli : Integer) : IPCB_Component;
Var
Inst : IPCB_Component;
GIt : IPCB_GroupIterator;
Prim, Cpy : IPCB_Primitive;
OX, OY : Integer;
Begin
FpOffset(PLib, LibFp, OX, OY);
Inst := PCBServer.PCBObjectFactory(eComponentObject, eNoDimension, eCreate_Default);
Inst.Layer := eTopLayer;
Inst.NameOn := False;
Inst.CommentOn := False;
GIt := LibFp.GroupIterator_Create;
GIt.AddFilter_LayerSet(AllLayers);
Prim := GIt.FirstPCBObject;
While Prim <> Nil Do
Begin
Cpy := Prim.Replicate;
If (OX <> 0) Or (OY <> 0) Then Cpy.MoveByXY(-OX, -OY);
Inst.AddPCBObject(Cpy);
Prim := GIt.NextPCBObject;
End;
LibFp.GroupIterator_Destroy(GIt);
Board.AddPCBObject(Inst);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Inst.I_ObjectAddress);
Inst.BeginModify;
Inst.MoveToXY(X, Y);
Inst.Rotation := RotMilli / 1000;
Inst.EndModify;
Result := Inst;
End;
Function FindLibFp(PLib : IPCB_Library; Name : String) : IPCB_LibComponent;
Var
I : Integer;
F : IPCB_LibComponent;
Begin
Result := Nil;
For I := 0 To PLib.ComponentCount - 1 Do
Begin
F := PLib.GetComponent(I);
If UpperCase(F.Name) = UpperCase(Name) Then Begin Result := F; Break; End;
End;
End;
Function JoinSemi(L : TStringList) : String;
Var
I : Integer;
Begin
Result := '';
For I := 0 To L.Count - 1 Do
Begin
If I > 0 Then Result := Result + ';';
Result := Result + L[I];
End;
End;
{ build_board: a NEW .PcbDoc from a parts list and a netlist, saved to pcb=<path>. The PcbLib with
every footprint must be the CURRENT document. Payload lines, RAW units:
O|<outline vertices, see SetOutlineFrom>
C|ref|footprint|x|y|rotMilliDeg|comment
N|ref|pad|net
Answers placed, missing footprints, the nets made and pads wired. }
Function CmdBuildBoard(Lines : TStringList; Payload : String) : String;
Var
PLib : IPCB_Library;
Board : IPCB_Board;
SD : IServerDocument;
Ops, Fps, Assign : TStringList;
Inst : IPCB_Component;
LibFp : IPCB_LibComponent;
Op, PcbPath, Missing, Outline, Refs : String;
I, K, Placed, Wired, OutN : Integer;
Begin
PcbPath := Param(Lines, 'pcb');
PLib := PCBServer.GetCurrentPCBLibrary;
If PLib = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the PcbLib with every footprint first (it must be the current document)') + '}'; Exit; End;
If PcbPath = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','pcb (the .PcbDoc path to save) is required') + '}'; Exit; End;
Ops := TStringList.Create;
Ops.Text := Payload;
{ every footprint must be in the library before anything is made }
Fps := TStringList.Create;
Missing := ''; Outline := '';
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If FieldP(Op, 1) = 'O' Then Outline := FieldP(Op, 2);
If FieldP(Op, 1) = 'C' Then
Begin
If FindLibFp(PLib, FieldP(Op, 3)) = Nil Then Missing := Missing + FieldP(Op, 3) + ' ';
End;
End;
If Missing <> '' Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','footprint_not_found') + ',' + JStr('error','not in the current PcbLib: ' + Trim(Missing)) + '}';
Ops.Free; Fps.Free; Exit;
End;
SD := CreateNewDocumentFromDocumentKind('PCB');
If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not create a PCB document') + '}'; Ops.Free; Fps.Free; Exit; End;
Client.ShowDocument(SD);
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board after create') + '}'; Ops.Free; Fps.Free; Exit; End;
Placed := 0; Wired := 0; OutN := 0; Refs := '';
gInputError := '';
PCBServer.PreProcess;
If Outline <> '' Then OutN := SetOutlineFrom(Board, Outline);
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If FieldP(Op, 1) <> 'C' Then Continue;
LibFp := FindLibFp(PLib, FieldP(Op, 3));
Inst := PlaceByOrigin(Board, PLib, LibFp, AdomInt(FieldP(Op, 4)), AdomInt(FieldP(Op, 5)), AdomInt(FieldP(Op, 6)));
Inst.Name.Text := FieldP(Op, 2);
Inst.Pattern := LibFp.Name;
Inst.Comment.Text := FieldP(Op, 7);
{ the pad assignments for this part, as AssignPadNets takes them }
Assign := TStringList.Create;
For K := 0 To Ops.Count - 1 Do
If (FieldP(Trim(Ops[K]), 1) = 'N') And (FieldP(Trim(Ops[K]), 2) = FieldP(Op, 2)) Then
Begin
Assign.Add(FieldP(Trim(Ops[K]), 3) + '=' + FieldP(Trim(Ops[K]), 4));
Wired := Wired + 1;
End;
If Assign.Count > 0 Then
Begin
AssignPadNets(Board, Inst, JoinSemi(Assign));
End;
Assign.Free;
Placed := Placed + 1;
If Refs <> '' Then Refs := Refs + ',';
Refs := Refs + '"' + JsonEsc(FieldP(Op, 2)) + '"';
End;
PCBServer.PostProcess;
Board.ViewManager_FullUpdate;
SD.SetFileName(PcbPath);
SD.DoFileSave('');
Ops.Free; Fps.Free;
Result := '{' + JRaw('ok','true') + ',' + JStr('pcb', PcbPath) + ',' + JRaw('placed', IntToStr(Placed)) + ',' + JRaw('padsWired', IntToStr(Wired)) + ',' +
JRaw('libOriginMil', '[' + IntToStr(Round(CoordToMils(PLib.Board.XOrigin))) + ',' + IntToStr(Round(CoordToMils(PLib.Board.YOrigin))) + ']') + ',' +
JRaw('outlineVertices', IntToStr(OutN)) + ',' + JRaw('refs', '[' + Refs + ']') + ',' + JRaw('badNumber', JBool(gInputError <> '')) + '}';
gInputError := '';
End;
{ set_outline: reshape the current board's outline (arcs allowed: rounded corners), one undo step.
Payload: O|<vertices, see SetOutlineFrom> }
Function CmdSetOutline(Lines : TStringList; Payload : String) : String;
Var
Board : IPCB_Board;
N : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
PCBServer.PreProcess;
N := SetOutlineFrom(Board, FieldP(Trim(Payload), 2));
PCBServer.PostProcess;
gInputError := '';
Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok', JBool(N >= 3)) + ',' + JRaw('vertices', IntToStr(N)) + '}';
End;
{ set_rule: write ONE design rule kind on the current board, every rule of that kind (each scope
keeps its own scope; the values are the fab's floor). One kind per call, so a property this
Altium build does not know fails alone. kind = clearance | width | via | hole | ring |
holetohole | edge. Values are RAW units: a, b, c (min / preferred / max where they apply),
d, e, f (via drill min / preferred / max). }
Function CmdSetRule(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Rule : IPCB_Rule;
Kind : String;
A, B, C, D, E, F, N, Want : Integer;
Created : Boolean;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Created := False;
Kind := Param(Lines, 'kind');
A := StrToIntDef(Param(Lines, 'a'), 0); B := StrToIntDef(Param(Lines, 'b'), 0); C := StrToIntDef(Param(Lines, 'c'), 0);
D := StrToIntDef(Param(Lines, 'd'), 0); E := StrToIntDef(Param(Lines, 'e'), 0); F := StrToIntDef(Param(Lines, 'f'), 0);
If Kind = 'clearance' Then Want := eRule_Clearance
Else If Kind = 'width' Then Want := eRule_MaxMinWidth
Else If Kind = 'via' Then Want := eRule_RoutingViaStyle
Else If Kind = 'hole' Then Want := eRule_MaxMinHoleSize
Else If Kind = 'ring' Then Want := eRule_MinimumAnnularRing
Else If Kind = 'holetohole' Then Want := eRule_HoleToHoleClearance
Else If Kind = 'edge' Then Want := eRule_BoardOutlineClearance
Else If Kind = 'polyconnect' Then Want := eRule_PolygonConnectStyle
Else Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','unknown rule kind: ' + Kind) + '}'; Exit; End;
N := 0;
PCBServer.PreProcess;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eRuleObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Rule := BIt.FirstPCBObject;
While Rule <> Nil Do
Begin
{ polyconnect sets the board's catch-all (All / All) rule only: per-pour rules keep theirs }
If (Rule.RuleKind = Want) And ((Kind <> 'polyconnect') Or ((Rule.Scope1Expression = 'All') And (Rule.Scope2Expression = 'All'))) Then
Begin
Rule.BeginModify;
If Kind = 'clearance' Then Rule.Gap := A
Else If Kind = 'width' Then
Begin
Rule.MinWidth[eTopLayer] := A; Rule.FavoredWidth[eTopLayer] := B; Rule.MaxWidth[eTopLayer] := C;
Rule.MinWidth[eBottomLayer] := A; Rule.FavoredWidth[eBottomLayer] := B; Rule.MaxWidth[eBottomLayer] := C;
End
Else If Kind = 'via' Then
Begin
Rule.MinWidth := A; Rule.PreferedWidth := B; Rule.MaxWidth := C;
Rule.MinHoleWidth := D; Rule.PreferedHoleWidth := E; Rule.MaxHoleWidth := F;
End
Else If Kind = 'hole' Then Begin Rule.MinLimit := A; Rule.MaxLimit := C; End
Else If Kind = 'ring' Then Rule.Minimum := A
Else If Kind = 'holetohole' Then Rule.Gap := A
Else If Kind = 'edge' Then
Begin
Rule.Gap := A;
{ the fab's edge clearance is copper to edge (KiCad's edge_clearance): an Adom edge rule an
earlier bridge scoped to All also caught silkscreen (adom/altium-bridge#26) }
If (Rule.Name = 'Adom edge') And (Rule.Scope1Expression = 'All') Then Rule.Scope1Expression := 'OnCopper';
End
Else If Kind = 'polyconnect' Then
Begin
{ A: 1 direct, 2 none, else relief with B spoke width (raw) and C spokes }
If A = 1 Then Rule.ConnectStyle := eDirectConnectToPlane
Else If A = 2 Then Rule.ConnectStyle := eNoConnect
Else
Begin
Rule.ConnectStyle := eReliefConnectToPlane;
If B > 0 Then Rule.ReliefConductorWidth := B;
If C > 0 Then Rule.ReliefEntries := C;
End;
End;
Rule.EndModify;
N := N + 1;
End;
Rule := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
{ a board with no rule of this kind (a new board has no annular-ring or edge rule) gets one,
scoped to everything }
If N = 0 Then
Begin
Rule := PCBServer.PCBRuleFactory(Want);
Rule.Name := 'Adom ' + Kind;
Rule.Scope1Expression := 'All';
{ edge: copper to the board edge, as the fab pack means it; silkscreen keeps its own rules }
If Kind = 'edge' Then Rule.Scope1Expression := 'OnCopper';
Rule.Scope2Expression := 'All';
If Kind = 'ring' Then Rule.Minimum := A
Else If (Kind = 'edge') Or (Kind = 'clearance') Or (Kind = 'holetohole') Then Rule.Gap := A
Else If Kind = 'hole' Then Begin Rule.MinLimit := A; Rule.MaxLimit := C; End;
Board.AddPCBObject(Rule);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Rule.I_ObjectAddress);
N := 1;
Created := True;
End;
PCBServer.PostProcess;
Result := '{' + JRaw('ok','true') + ',' + JStr('kind', Kind) + ',' + JRaw('rulesWritten', IntToStr(N)) + ',' + JRaw('created', JBool(Created)) + '}';
End;
{ zoom_rect: frame the current board's view on a rectangle, RAW units (l b r t). }
Function CmdZoomRect(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Board.GraphicalView_ZoomOnRect(StrToIntDef(Param(Lines, 'l'), 0), StrToIntDef(Param(Lines, 'b'), 0), StrToIntDef(Param(Lines, 'r'), 0), StrToIntDef(Param(Lines, 't'), 0));
Result := '{' + JRaw('ok','true') + '}';
End;
{ add_rule: a SCOPED rule (net, net class, any query) the fab-wide set_rule cannot express.
kind clearance (a = gap) | width (a min, b preferred, c max), raw units; name is 'Adom <name>'
and a second add with the same name replaces it. Lands at priority 1 (Altium puts a new rule
first), so it outranks the board's catch-all of its kind. }
Function CmdAddRule(Lines : TStringList) : String;
Var
Board : IPCB_Board;
Rule : IPCB_Rule;
Kind, Name : String;
A, B, C, Want : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Kind := Param(Lines, 'kind');
Name := 'Adom ' + Param(Lines, 'name');
A := StrToIntDef(Param(Lines, 'a'), 0); B := StrToIntDef(Param(Lines, 'b'), 0); C := StrToIntDef(Param(Lines, 'c'), 0);
If Kind = 'clearance' Then Want := eRule_Clearance
Else If Kind = 'width' Then Want := eRule_MaxMinWidth
Else Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','unknown rule kind: ' + Kind) + '}'; Exit; End;
PCBServer.PreProcess;
Rule := FindRuleNamed(Board, Want, Name);
If Rule <> Nil Then Board.RemovePCBObject(Rule);
If Param(Lines, 'remove') = '1' Then
Begin
PCBServer.PostProcess;
Result := '{' + JRaw('ok','true') + ',' + JStr('name', Name) + ',' + JRaw('removed', JBool(Rule <> Nil)) + '}';
Exit;
End;
Rule := PCBServer.PCBRuleFactory(Want);
Rule.Name := Name;
Rule.Comment := Param(Lines, 'comment');
Rule.Scope1Expression := Param(Lines, 'scope1');
Rule.Scope2Expression := Param(Lines, 'scope2');
If Kind = 'clearance' Then Rule.Gap := A
Else
Begin
Rule.MinWidth[eTopLayer] := A; Rule.FavoredWidth[eTopLayer] := B; Rule.MaxWidth[eTopLayer] := C;
Rule.MinWidth[eBottomLayer] := A; Rule.FavoredWidth[eBottomLayer] := B; Rule.MaxWidth[eBottomLayer] := C;
End;
Board.AddPCBObject(Rule);
PCBServer.SendMessageToRobots(Board.I_ObjectAddress, c_Broadcast, PCBM_BoardRegisteration, Rule.I_ObjectAddress);
PCBServer.PostProcess;
Result := '{' + JRaw('ok','true') + ',' + JStr('name', Name) + ',' + JRaw('priority', IntToStr(Rule.Priority)) + '}';
End;
{ build_schematic: one sheet drawn from a netlist with the project library's symbols. The SchLib
must be the current document. Each part is a fresh component with its symbol's display-mode-0
primitives copied in (place_on_sch's measured lane: ISch_Component.Replicate does not carry the
pins). Parts stand in one column per block, sized from the symbols; every pin on a net gets a
short wire and a net label at its end, so the sheet compiles to the netlist.
payload C|ref|libref|block|comment|footprint P|ref|pin|net (param pcblib: the footprints' .PcbLib)
Coordinates in mil. }
Function LibCmpOf(SLib : ISch_Lib; LibRef : String) : ISch_Component;
Var
Iter : ISch_Iterator;
C : ISch_Component;
Begin
Result := Nil;
Iter := SLib.SchLibIterator_Create;
Iter.AddFilter_ObjectSet(MkSet(eSchComponent));
C := Iter.FirstSchObject;
While C <> Nil Do
Begin
If UpperCase(C.LibReference) = UpperCase(LibRef) Then Begin Result := C; Break; End;
C := Iter.NextSchObject;
End;
SLib.SchIterator_Destroy(Iter);
End;
Function IsDrawn(Prim : ISch_GraphicalObject) : Boolean;
Begin
Result := ((Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or (Prim.ObjectID = eLine) Or
(Prim.ObjectID = eArc) Or (Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or
(Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel)) And (Prim.OwnerPartDisplayMode = 0);
End;
{ ------------------------------------------------------------ schematic editing (0.2.57) --- }
{ the placed parts of the current sheet with this designator (every part of a multi-part component) }
Function SheetCompsOf(Doc : ISch_Document; Des : String) : TInterfaceList;
Var
It : ISch_Iterator;
C : ISch_Component;
Begin
Result := TInterfaceList.Create;
It := Doc.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eSchComponent));
C := It.FirstSchObject;
While C <> Nil Do
Begin
If UpperCase(C.Designator.Text) = UpperCase(Des) Then Result.Add(C);
C := It.NextSchObject;
End;
Doc.SchIterator_Destroy(It);
End;
{ a GND-like net gets a ground symbol, anything else a bar }
Function PowerStyleOf(Net : String) : Integer;
Var
U : String;
Begin
U := UpperCase(Net);
If (U = 'GND') Or (U = 'AGND') Or (U = 'DGND') Or (U = 'PGND') Or (U = 'GNDA') Or (U = 'GNDD') Or (U = 'VSS') Or (U = '0V') Then
Result := ePowerGndPower
Else If (U = 'EARTH') Or (U = 'PE') Or (U = 'CHASSIS') Then
Result := ePowerGndEarth
Else
Result := ePowerBar;
End;
{ sch_connect: a wire stub and a net label (kind=label), power port (kind=power) or nothing (kind=wire)
at the outer end of pins of placed parts on the current sheet.
payload lines: designator|pin (number or name)|net|kind|stubMil
One undo step. Answers per line: ok, or why not. }
Function CmdSchConnect(Lines : TStringList; Payload : String) : String;
Var
Doc : ISch_Document;
Ops, Comps : TStringList;
CL : TInterfaceList;
C : ISch_Component;
PIt : ISch_Iterator;
Pin, Hit : ISch_Pin;
W : ISch_Wire;
NL : ISch_NetLabel;
PO : ISch_PowerObject;
Op, Des, PinKey, Net, Kind, Res, S : String;
I, J, Dir, HX, HY, EX, EY, Stub, NOk : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
Ops := TStringList.Create; Ops.Text := Payload;
S := ''; NOk := 0;
SchServer.ProcessControl.PreProcess(Doc, '');
For I := 0 To Ops.Count - 1 Do
Begin
Op := Ops[I];
If Trim(Op) = '' Then Continue;
Des := FieldP(Op, 1); PinKey := FieldP(Op, 2); Net := FieldP(Op, 3); Kind := LowerCase(FieldP(Op, 4));
Stub := StrToIntDef(FieldP(Op, 5), 200);
If Kind = '' Then Kind := 'label';
Res := '';
Hit := Nil;
CL := SheetCompsOf(Doc, Des);
If CL.Count = 0 Then Res := 'designator not on this sheet'
Else
Begin
For J := 0 To CL.Count - 1 Do
Begin
C := CL[J];
PIt := C.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(ePin));
Pin := PIt.FirstSchObject;
While Pin <> Nil Do
Begin
If (Pin.OwnerPartId = C.CurrentPartID) Or (Pin.OwnerPartId = 0) Then
If (UpperCase(Pin.Designator) = UpperCase(PinKey)) Or ((Hit = Nil) And (UpperCase(Pin.Name) = UpperCase(PinKey))) Then
If (Hit = Nil) Or (UpperCase(Pin.Designator) = UpperCase(PinKey)) Then Hit := Pin;
Pin := PIt.NextSchObject;
End;
C.SchIterator_Destroy(PIt);
End;
If Hit = Nil Then Res := 'pin not found on ' + Des;
End;
CL.Free;
If (Res = '') And (Net = '') And (Kind <> 'wire') Then Res := 'no net given';
If Res = '' Then
Begin
Dir := Hit.Orientation;
HX := Hit.Location.X; HY := Hit.Location.Y;
If Dir = 0 Then HX := HX + Hit.PinLength
Else If Dir = 1 Then HY := HY + Hit.PinLength
Else If Dir = 2 Then HX := HX - Hit.PinLength
Else HY := HY - Hit.PinLength;
EX := HX; EY := HY;
If Dir = 0 Then EX := HX + MilsToCoord(Stub)
Else If Dir = 1 Then EY := HY + MilsToCoord(Stub)
Else If Dir = 2 Then EX := HX - MilsToCoord(Stub)
Else EY := HY - MilsToCoord(Stub);
W := SchServer.SchObjectFactory(eWire, eCreate_Default);
W.Location := Point(HX, HY);
W.InsertVertex := 1;
W.SetState_Vertex(1, Point(HX, HY));
W.InsertVertex := 2;
W.SetState_Vertex(2, Point(EX, EY));
W.LineWidth := eSmall;
Doc.RegisterSchObjectInContainer(W);
SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, W.I_ObjectAddress);
If Kind = 'power' Then
Begin
PO := SchServer.SchObjectFactory(ePowerObject, eCreate_Default);
PO.Text := Net;
PO.Location := Point(EX, EY);
PO.Style := PowerStyleOf(Net);
If Dir = 0 Then PO.Orientation := eRotate0
Else If Dir = 1 Then PO.Orientation := eRotate90
Else If Dir = 2 Then PO.Orientation := eRotate180
Else PO.Orientation := eRotate270;
PO.ShowNetName := True;
Doc.RegisterSchObjectInContainer(PO);
SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, PO.I_ObjectAddress);
End
Else If Kind = 'label' Then
Begin
NL := SchServer.SchObjectFactory(eNetLabel, eCreate_Default);
NL.Text := Net;
NL.Location := Point(EX, EY);
If (Dir = 1) Or (Dir = 3) Then NL.Orientation := eRotate90;
If (Dir = 2) Or (Dir = 3) Then NL.Justification := eJustify_BottomRight;
Doc.RegisterSchObjectInContainer(NL);
SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, NL.I_ObjectAddress);
End;
NOk := NOk + 1;
Res := 'ok';
End;
If S <> '' Then S := S + ',';
S := S + '{' + JStr('designator', Des) + ',' + JStr('pin', PinKey) + ',' + JStr('net', Net) + ',' + JStr('kind', Kind) + ',' + JStr('result', Res);
If Res = 'ok' Then S := S + ',' + JRaw('endMil', '[' + IntToStr(Round(CoordToMils(EX))) + ',' + IntToStr(Round(CoordToMils(EY))) + ']');
S := S + '}';
End;
SchServer.ProcessControl.PostProcess(Doc, '');
Doc.GraphicallyInvalidate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('connected', IntToStr(NOk)) + ',' + JRaw('results', '[' + S + ']') + '}';
End;
{ sch_edit: change placed parts on the current sheet.
payload lines: designator|action|a|b actions: move x y (mil, the part's origin) | moveby dx dy |
rotate 0/90/180/270 | designator new | comment text | delete
move, moveby and rotate act on every part of a multi-part component (they are drawn apart; say which
with a single-part designator). One undo step. }
Function CmdSchEdit(Lines : TStringList; Payload : String) : String;
Var
Doc : ISch_Document;
Ops : TStringList;
CL, Dead : TInterfaceList;
C : ISch_Component;
Op, Des, Act, A, B, Res, S : String;
I, J, K, Want, Have, Steps, NOk : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
Ops := TStringList.Create; Ops.Text := Payload;
Dead := TInterfaceList.Create;
S := ''; NOk := 0;
SchServer.ProcessControl.PreProcess(Doc, '');
For I := 0 To Ops.Count - 1 Do
Begin
Op := Ops[I];
If Trim(Op) = '' Then Continue;
Des := FieldP(Op, 1); Act := LowerCase(FieldP(Op, 2)); A := FieldP(Op, 3); B := FieldP(Op, 4);
Res := 'ok';
CL := SheetCompsOf(Doc, Des);
If CL.Count = 0 Then Res := 'designator not on this sheet'
Else
For J := 0 To CL.Count - 1 Do
Begin
C := CL[J];
If Act = 'move' Then C.MoveToXY(MilsToCoord(StrToIntDef(A, 0)), MilsToCoord(StrToIntDef(B, 0)))
Else If Act = 'moveby' Then C.MoveByXY(MilsToCoord(StrToIntDef(A, 0)), MilsToCoord(StrToIntDef(B, 0)))
Else If Act = 'rotate' Then
Begin
Want := (StrToIntDef(A, 0) Div 90) Mod 4;
Have := C.Orientation;
Steps := (Want - Have + 4) Mod 4;
For K := 1 To Steps Do C.RotateBy90(C.Location, eRotate90);
End
Else If Act = 'designator' Then C.Designator.Text := A
Else If Act = 'comment' Then C.Comment.Text := A
Else If Act = 'delete' Then Dead.Add(C)
Else Res := 'unknown action ' + Act;
C.GraphicallyInvalidate;
End;
CL.Free;
If Res = 'ok' Then NOk := NOk + 1;
If S <> '' Then S := S + ',';
S := S + '{' + JStr('designator', Des) + ',' + JStr('action', Act) + ',' + JStr('result', Res) + '}';
End;
For I := 0 To Dead.Count - 1 Do
Begin
C := Dead[I];
Doc.RemoveSchObject(C);
SchServer.RobotManager.SendMessage(Doc.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, C.I_ObjectAddress);
End;
SchServer.ProcessControl.PostProcess(Doc, '');
Doc.GraphicallyInvalidate;
Ops.Free; Dead.Free;
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('applied', IntToStr(NOk)) + ',' + JRaw('results', '[' + S + ']') + '}';
End;
{ sch_place_part: place one component of the CURRENT SchLib on a sheet (opened and shown here).
libref, sch (the sheet path), designator, x, y (mil, snapped to 100), rotation, comment, partId,
footprint + pcblib (else the library part's own footprint models are copied by name). }
Function CmdSchPlacePart(Lines : TStringList) : String;
Var
SLib : ISch_Lib;
LibCmp, Inst : ISch_Component;
Prim, Cpy : ISch_GraphicalObject;
PIt : ISch_Iterator;
Impl, NImpl : ISch_Implementation;
Prm : ISch_Parameter;
SD : IServerDocument;
Sheet : ISch_Document;
SchPath, Fp, Models : String;
DX, DY, Part, K, Rot, NPins : Integer;
Begin
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first (it must be the current document)') + '}'; Exit; End;
LibCmp := LibCmpOf(SLib, Param(Lines, 'libref'));
If LibCmp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','libref_not_found') + ',' + JStr('error','no component ' + Param(Lines, 'libref') + ' in ' + SLib.DocumentName) + '}'; Exit; End;
SchPath := Param(Lines, 'sch');
SD := Nil;
Try
SD := Client.OpenDocument('SCH', SchPath);
Except
SD := Nil;
End;
If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the sheet ' + SchPath) + '}'; Exit; End;
Client.ShowDocument(SD);
Sheet := SchServer.GetCurrentSchDocument;
If (Sheet = Nil) Or (Sheet.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','the sheet did not become the current document') + '}'; Exit; End;
Part := StrToIntDef(Param(Lines, 'partId'), 1);
DX := (StrToIntDef(Param(Lines, 'x'), 1000) Div 100) * 100;
DY := (StrToIntDef(Param(Lines, 'y'), 1000) Div 100) * 100;
SchServer.ProcessControl.PreProcess(Sheet, '');
Inst := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Inst.CurrentPartID := Part;
Inst.DisplayMode := 0;
Inst.LibReference := LibCmp.LibReference;
Inst.ComponentDescription := LibCmp.ComponentDescription;
Inst.SourceLibraryName := SLib.DocumentName;
Inst.Location := Point(MilsToCoord(DX), MilsToCoord(DY));
NPins := 0;
PIt := LibCmp.SchIterator_Create;
Prim := PIt.FirstSchObject;
While Prim <> Nil Do
Begin
If ((Prim.ObjectID = ePin) Or (Prim.ObjectID = eRectangle) Or (Prim.ObjectID = eLine) Or (Prim.ObjectID = eArc) Or
(Prim.ObjectID = eEllipse) Or (Prim.ObjectID = ePolygon) Or (Prim.ObjectID = ePolyline) Or (Prim.ObjectID = eLabel)) And
(Prim.OwnerPartDisplayMode = 0) And ((Prim.OwnerPartId = Part) Or (Prim.OwnerPartId = 0)) Then
Begin
Cpy := Prim.Replicate;
Cpy.OwnerPartId := Part;
Cpy.OwnerPartDisplayMode := 0;
Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));
Inst.AddSchObject(Cpy);
If Prim.ObjectID = ePin Then NPins := NPins + 1;
End;
Prim := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
PIt := LibCmp.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(eParameter));
Prm := PIt.FirstSchObject;
While Prm <> Nil Do
Begin
If (UpperCase(Prm.Name) <> 'COMMENT') And (UpperCase(Prm.Name) <> 'DESIGNATOR') Then
Begin
Cpy := Prm.Replicate;
Cpy.OwnerPartId := Part;
Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));
Inst.AddSchObject(Cpy);
End;
Prm := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
{ the footprint: the one named, else the library part's own PCBLIB models by name }
Fp := Param(Lines, 'footprint');
Models := '';
If Fp <> '' Then
Begin
NImpl := Inst.AddSchImplementation;
NImpl.ClearAllDatafileLinks;
NImpl.Description := 'Footprint';
NImpl.ModelName := Fp;
NImpl.ModelType := 'PCBLIB';
NImpl.AddDataFileLink(Fp, Param(Lines, 'pcblib'), 'PCBLIB');
NImpl.IsCurrent := True;
Models := '"' + Fp + '"';
End
Else
Begin
PIt := LibCmp.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(eImplementation));
Impl := PIt.FirstSchObject;
While Impl <> Nil Do
Begin
If UpperCase(Impl.ModelType) = 'PCBLIB' Then
Begin
NImpl := Inst.AddSchImplementation;
NImpl.ClearAllDatafileLinks;
NImpl.Description := Impl.Description;
NImpl.ModelName := Impl.ModelName;
NImpl.ModelType := 'PCBLIB';
NImpl.AddDataFileLink(Impl.ModelName, Param(Lines, 'pcblib'), 'PCBLIB');
NImpl.IsCurrent := Impl.IsCurrent;
If Models <> '' Then Models := Models + ',';
Models := Models + '"' + JsonEsc(Impl.ModelName) + '"';
End;
Impl := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
End;
If Param(Lines, 'designator') <> '' Then Inst.Designator.Text := Param(Lines, 'designator')
Else Inst.Designator.Text := LibCmp.Designator.Text;
Inst.Designator.IsHidden := False;
Inst.Designator.Location := Point(MilsToCoord(DX), MilsToCoord(DY + 200));
If Param(Lines, 'comment') <> '' Then Inst.Comment.Text := Param(Lines, 'comment')
Else Inst.Comment.Text := LibCmp.Comment.Text;
Inst.Comment.IsHidden := False;
Inst.Comment.Location := Point(MilsToCoord(DX), MilsToCoord(DY - 200));
Sheet.RegisterSchObjectInContainer(Inst);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);
Rot := (StrToIntDef(Param(Lines, 'rotation'), 0) Div 90) Mod 4;
For K := 1 To Rot Do Inst.RotateBy90(Inst.Location, eRotate90);
SchServer.ProcessControl.PostProcess(Sheet, '');
Sheet.GraphicallyInvalidate;
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Sheet.DocumentName) + ',' + JStr('designator', Inst.Designator.Text) + ',' +
JStr('libref', LibCmp.LibReference) + ',' + JRaw('pins', IntToStr(NPins)) + ',' + JRaw('footprints', '[' + Models + ']') + ',' +
JRaw('atMil', '[' + IntToStr(DX) + ',' + IntToStr(DY) + ']') + '}';
End;
{ doc_params: the current sheet's document parameters (title block: Title, DocumentNumber, Revision,
DrawnBy, Author, CompanyName, ...). params=K=V;K=V sets them (added hidden when missing); always
answers every document parameter. }
Function CmdDocParams(Lines : TStringList) : String;
Var
Doc : ISch_Document;
It : ISch_Iterator;
Pm : ISch_Parameter;
Items : TStringList;
Spec, K, V, S : String;
I, P, NSet : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
NSet := 0;
If Param(Lines, 'params') <> '' Then
Begin
SchServer.ProcessControl.PreProcess(Doc, '');
Items := SplitSemi(Param(Lines, 'params'));
For I := 0 To Items.Count - 1 Do
Begin
Spec := Items[I];
P := Pos('=', Spec);
If P > 0 Then
Begin
K := Trim(Copy(Spec, 1, P - 1));
V := Trim(Copy(Spec, P + 1, Length(Spec)));
If K <> '' Then Begin SetDocParam(Doc, K, V); NSet := NSet + 1; End;
End;
End;
Items.Free;
SchServer.ProcessControl.PostProcess(Doc, '');
Doc.GraphicallyInvalidate;
End;
S := '';
It := Doc.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eParameter));
Pm := It.FirstSchObject;
While Pm <> Nil Do
Begin
{ the sheet's own parameters, not its parts' (the iterator walks into components) }
If (Pm.Container <> Nil) And (Pm.Container.ObjectID <> eSchComponent) Then
Begin
If S <> '' Then S := S + ',';
S := S + '{' + JStr('name', Pm.Name) + ',' + JStr('value', Pm.Text) + '}';
End;
Pm := It.NextSchObject;
End;
Doc.SchIterator_Destroy(It);
Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('set', IntToStr(NSet)) + ',' + JRaw('params', '[' + S + ']') + '}';
End;
{ sch_rename_at: rename placed parts named by designator AND position (several parts on a sheet can all
read 'R?'). payload lines: from|to|xMil|yMil (the part's Location). One undo step. }
Function CmdSchRenameAt(Lines : TStringList; Payload : String) : String;
Var
Doc : ISch_Document;
Ops : TStringList;
CL : TInterfaceList;
C, Hit : ISch_Component;
Op, Res, S : String;
I, J, X, Y : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
Ops := TStringList.Create; Ops.Text := Payload;
S := '';
SchServer.ProcessControl.PreProcess(Doc, '');
For I := 0 To Ops.Count - 1 Do
Begin
Op := Ops[I];
If Trim(Op) = '' Then Continue;
X := StrToIntDef(FieldP(Op, 3), 0); Y := StrToIntDef(FieldP(Op, 4), 0);
Hit := Nil;
CL := SheetCompsOf(Doc, FieldP(Op, 1));
For J := 0 To CL.Count - 1 Do
Begin
C := CL[J];
If (Abs(Round(CoordToMils(C.Location.X)) - X) <= 1) And (Abs(Round(CoordToMils(C.Location.Y)) - Y) <= 1) Then Hit := C;
End;
CL.Free;
If Hit = Nil Then Res := 'no part ' + FieldP(Op, 1) + ' at that position'
Else Begin Hit.Designator.Text := FieldP(Op, 2); Hit.GraphicallyInvalidate; Res := 'ok'; End;
If S <> '' Then S := S + ',';
S := S + '{' + JStr('from', FieldP(Op, 1)) + ',' + JStr('to', FieldP(Op, 2)) + ',' + JStr('result', Res) + '}';
End;
SchServer.ProcessControl.PostProcess(Doc, '');
Doc.GraphicallyInvalidate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('results', '[' + S + ']') + '}';
End;
{ sch_zoom: select the named parts on the current sheet (everything else deselected) and zoom the view
to them. designators=R1,U2 }
Function CmdSchZoom(Lines : TStringList) : String;
Var
Doc : ISch_Document;
It : ISch_Iterator;
C : ISch_Component;
Want : TStringList;
Found, S : String;
N : Integer;
Begin
Doc := SchServer.GetCurrentSchDocument;
If (Doc = Nil) Or (Doc.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','no_current_schematic') + ',' + JStr('error','no current schematic sheet (open a .SchDoc first)') + '}'; Exit; End;
Want := TStringList.Create;
Want.Delimiter := ',';
Want.StrictDelimiter := True;
Want.DelimitedText := UpperCase(Param(Lines, 'designators'));
N := 0; S := '';
It := Doc.SchIterator_Create;
It.AddFilter_ObjectSet(MkSet(eSchComponent));
C := It.FirstSchObject;
While C <> Nil Do
Begin
If Want.IndexOf(UpperCase(C.Designator.Text)) >= 0 Then
Begin
C.Selection := True;
N := N + 1;
If S <> '' Then S := S + ',';
S := S + '{' + JStr('designator', C.Designator.Text) + ',' + JRaw('xMil', IntToStr(Round(CoordToMils(C.Location.X)))) + ',' + JRaw('yMil', IntToStr(Round(CoordToMils(C.Location.Y)))) + '}';
End
Else C.Selection := False;
C := It.NextSchObject;
End;
Doc.SchIterator_Destroy(It);
Want.Free;
Doc.GraphicallyInvalidate;
If N > 0 Then Begin ResetParameters; AddStringParameter('Object', 'Selected'); RunProcess('Sch:Zoom'); End;
Result := '{' + JRaw('ok','true') + ',' + JRaw('selected', IntToStr(N)) + ',' + JRaw('parts', '[' + S + ']') + '}';
End;
Function CmdBuildSchematic(Lines : TStringList; Payload : String) : String;
Var
SLib : ISch_Lib;
LibCmp : ISch_Component;
Inst : ISch_Component;
Prim, Cpy : ISch_GraphicalObject;
PIt : ISch_Iterator;
Pin : ISch_Pin;
W : ISch_Wire;
Impl : ISch_Implementation;
Prm : ISch_Parameter;
NL : ISch_NetLabel;
Lb : ISch_Label;
SD : IServerDocument;
Sheet : ISch_Document;
R : TCoordRect;
Ops, Refs, LibRefs, Blocks, Comments, Fps, PinNet, BlockOrder, BL, BB, BR, BT, ColOf : TStringList;
Op, SchPath, Missing, Net, NoNet, Blk : String;
Cols, Idx : TStringList;
I, K, N, NWires, NLabels, NPlaced, Col, NCols : Integer;
BandW : Integer;
L0, B0, R0, T0, ColW, ColH, ColX, YCur, SheetW, SheetH, DX, DY, HX, HY, EX, EY, Dir, PW, MaxH : Integer;
Begin
SchPath := Param(Lines, 'sch');
SLib := SchServer.GetCurrentSchDocument;
If (SLib = Nil) Or (SLib.ObjectID <> eSchLib) Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','open the SchLib first (it must be the current document)') + '}';
Exit;
End;
Ops := TStringList.Create; Ops.Text := Payload;
Refs := TStringList.Create; LibRefs := TStringList.Create; Blocks := TStringList.Create; Comments := TStringList.Create; Fps := TStringList.Create;
PinNet := TStringList.Create; BlockOrder := TStringList.Create;
BL := TStringList.Create; BB := TStringList.Create; BR := TStringList.Create; BT := TStringList.Create; ColOf := TStringList.Create;
Missing := '';
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If FieldP(Op, 1) = 'C' Then
Begin
Refs.Add(FieldP(Op, 2)); LibRefs.Add(FieldP(Op, 3)); Blocks.Add(FieldP(Op, 4)); Comments.Add(FieldP(Op, 5)); Fps.Add(FieldP(Op, 6));
If BlockOrder.IndexOf(FieldP(Op, 4)) < 0 Then BlockOrder.Add(FieldP(Op, 4));
If LibCmpOf(SLib, FieldP(Op, 3)) = Nil Then Missing := Missing + FieldP(Op, 3) + ' ';
End
Else If FieldP(Op, 1) = 'P' Then PinNet.Add(FieldP(Op, 2) + '.' + FieldP(Op, 3) + '=' + FieldP(Op, 4));
End;
If Missing <> '' Then
Begin
Result := '{' + JRaw('ok','false') + ',' + JStr('error','symbols not in the SchLib: ' + Trim(Missing)) + '}';
Exit;
End;
{ measure every part (mil, library frame) }
For K := 0 To Refs.Count - 1 Do
Begin
LibCmp := LibCmpOf(SLib, LibRefs[K]);
N := 0; L0 := 0; B0 := 0; R0 := 0; T0 := 0;
PIt := LibCmp.SchIterator_Create;
Prim := PIt.FirstSchObject;
While Prim <> Nil Do
Begin
If IsDrawn(Prim) Then
Begin
R := Prim.BoundingRectangle;
If (N = 0) Or (Round(CoordToMils(R.Left)) < L0) Then L0 := Round(CoordToMils(R.Left));
If (N = 0) Or (Round(CoordToMils(R.Bottom)) < B0) Then B0 := Round(CoordToMils(R.Bottom));
If (N = 0) Or (Round(CoordToMils(R.Right)) > R0) Then R0 := Round(CoordToMils(R.Right));
If (N = 0) Or (Round(CoordToMils(R.Top)) > T0) Then T0 := Round(CoordToMils(R.Top));
N := N + 1;
End;
Prim := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
BL.Add(IntToStr(L0)); BB.Add(IntToStr(B0)); BR.Add(IntToStr(R0)); BT.Add(IntToStr(T0));
End;
{ columns: one per block, at most 6 parts tall (a taller block wraps into the next column),
each wide enough for its widest part plus a wire stub and a net label on each side; the
columns pack left to right into bands at most 22000 mil wide, so the sheet stays landscape
and a zoom-all keeps the parts readable. ColOf: 'x,width,bandTop,block|i;i;...' }
Cols := TStringList.Create;
For Col := 0 To BlockOrder.Count - 1 Do
Begin
N := 0; Op := '';
For K := 0 To Refs.Count - 1 Do
If Blocks[K] = BlockOrder[Col] Then
Begin
If N = 6 Then Begin Cols.Add(BlockOrder[Col] + '|' + Op); Op := ''; N := 0; End;
If Op <> '' Then Op := Op + ';';
Op := Op + IntToStr(K);
N := N + 1;
End;
If Op <> '' Then Cols.Add(BlockOrder[Col] + '|' + Op);
End;
NCols := Cols.Count;
{ the bands wrap inside the template's own width (its frame and title block stay true) }
BandW := AdomInt(Param(Lines, 'tplW')) - 600;
If BandW < 11000 Then BandW := 22600;
SheetW := 0; MaxH := 0; ColX := 600; YCur := 0; ColH := 0; Blk := '';
For Col := 0 To NCols - 1 Do
Begin
Idx := SplitSemi(FieldP(Cols[Col], 2));
PW := 0; DY := 0;
For I := 0 To Idx.Count - 1 Do
Begin
K := AdomInt(Idx[I]);
If AdomInt(BR[K]) - AdomInt(BL[K]) > PW Then PW := AdomInt(BR[K]) - AdomInt(BL[K]);
DY := DY + (AdomInt(BT[K]) - AdomInt(BB[K])) + 900;
End;
Idx.Free;
ColW := PW + 2000;
{ a new band when this column would run past 22000 mil }
If (ColX > 600) And (ColX + ColW > BandW) Then
Begin
YCur := YCur + ColH + 900;
ColH := 0;
ColX := 600;
End;
{ the block's name heads its first column only }
If FieldP(Cols[Col], 1) <> Blk Then Op := FieldP(Cols[Col], 1) Else Op := '';
Blk := FieldP(Cols[Col], 1);
ColOf.Add(IntToStr(ColX) + ',' + IntToStr(ColW) + ',' + IntToStr(YCur) + ',' + Op + '|' + FieldP(Cols[Col], 2));
ColX := ColX + ColW;
If ColX > SheetW Then SheetW := ColX;
If DY > ColH Then ColH := DY;
End;
MaxH := YCur + ColH;
SheetW := SheetW + 600;
SheetH := MaxH + 3600;
If SheetW < 11500 Then SheetW := 11500;
If SheetH < 7600 Then SheetH := 7600;
{ the sheet is a FILE the bridge put in place (an Altium sheet template copied to sch) and opened
inside its project: a new document from CreateNewDocumentFromDocumentKind joins whatever project
is focused (another design's, or the agent's own), measured 2026-10-15 }
SD := Nil;
Try
SD := Client.OpenDocument('SCH', SchPath);
Except
SD := Nil;
End;
If SD = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','could not open the sheet ' + SchPath) + '}'; Exit; End;
Client.ShowDocument(SD);
Sheet := SchServer.GetCurrentSchDocument;
If (Sheet = Nil) Or (Sheet.ObjectID = eSchLib) Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','the sheet did not become the current document') + '}'; Exit; End;
{ the template's own size (and title block) when the parts fit it, else a custom size }
If (AdomInt(Param(Lines, 'tplW')) >= SheetW) And (AdomInt(Param(Lines, 'tplH')) >= SheetH) Then
Begin
SheetW := AdomInt(Param(Lines, 'tplW')); SheetH := AdomInt(Param(Lines, 'tplH'));
End
Else
Begin
Sheet.UseCustomSheet := True;
Sheet.CustomX := MilsToCoord(SheetW);
Sheet.CustomY := MilsToCoord(SheetH);
Sheet.CustomXZones := (SheetW + 2499) Div 2500;
Sheet.CustomYZones := (SheetH + 2499) Div 2500;
Sheet.CustomMarginWidth := MilsToCoord(200);
End;
NWires := 0; NLabels := 0; NPlaced := 0; NoNet := '';
For Col := 0 To NCols - 1 Do
Begin
ColX := AdomInt(Field(FieldP(ColOf[Col], 1), 1)); ColW := AdomInt(Field(FieldP(ColOf[Col], 1), 2));
Blk := Field(FieldP(ColOf[Col], 1), 4);
If Blk <> '' Then
Begin
Lb := SchServer.SchObjectFactory(eLabel, eCreate_Default);
Lb.Location := Point(MilsToCoord(ColX + 300), MilsToCoord(SheetH - 1500 - AdomInt(Field(FieldP(ColOf[Col], 1), 3))));
Lb.Text := UpperCase(Blk);
Sheet.RegisterSchObjectInContainer(Lb);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Lb.I_ObjectAddress);
End;
{ below the template's header strip (its notes and revision block) }
YCur := SheetH - 2100 - AdomInt(Field(FieldP(ColOf[Col], 1), 3));
Idx := SplitSemi(FieldP(ColOf[Col], 2));
For I := 0 To Idx.Count - 1 Do
Begin
K := AdomInt(Idx[I]);
LibCmp := LibCmpOf(SLib, LibRefs[K]);
L0 := AdomInt(BL[K]); B0 := AdomInt(BB[K]); R0 := AdomInt(BR[K]); T0 := AdomInt(BT[K]);
{ snap to the 100 mil grid so pins land on the grid wires snap to }
DX := ((ColX + ColW Div 2 - (L0 + R0) Div 2) Div 100) * 100;
DY := ((YCur - 250 - T0) Div 100) * 100;
Inst := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);
Inst.CurrentPartID := 1;
Inst.DisplayMode := 0;
Inst.LibReference := LibCmp.LibReference;
Inst.ComponentDescription := LibCmp.ComponentDescription;
Inst.Location := Point(MilsToCoord(DX), MilsToCoord(DY));
PIt := LibCmp.SchIterator_Create;
Prim := PIt.FirstSchObject;
While Prim <> Nil Do
Begin
If IsDrawn(Prim) Then
Begin
Cpy := Prim.Replicate;
Cpy.OwnerPartId := 1;
Cpy.OwnerPartDisplayMode := 0;
Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));
Inst.AddSchObject(Cpy);
End;
Prim := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
{ the part's parameters (MPN, Manufacturer, distributor numbers): the BOM reads them from
here, and copying only the drawing left every number column empty (measured 2026-10-15).
Designator and Comment are the instance's own. }
PIt := LibCmp.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(eParameter));
Prm := PIt.FirstSchObject;
While Prm <> Nil Do
Begin
If (UpperCase(Prm.Name) <> 'COMMENT') And (UpperCase(Prm.Name) <> 'DESIGNATOR') Then
Begin
Cpy := Prm.Replicate;
Cpy.OwnerPartId := 1;
Cpy.MoveByXY(MilsToCoord(DX), MilsToCoord(DY));
Inst.AddSchObject(Cpy);
End;
Prm := PIt.NextSchObject;
End;
LibCmp.SchIterator_Destroy(PIt);
{ the footprint link the copied primitives do not carry: without it every part compiles
with 'Footprint ... cannot be found' }
If Fps[K] <> '' Then
Begin
Impl := Inst.AddSchImplementation;
Impl.ClearAllDatafileLinks;
Impl.Description := 'Footprint';
Impl.ModelName := Fps[K];
Impl.ModelType := 'PCBLIB';
Impl.AddDataFileLink(Fps[K], Param(Lines, 'pcblib'), 'PCBLIB');
Impl.IsCurrent := True;
End;
Inst.Designator.Text := Refs[K];
Inst.Designator.IsHidden := False;
Inst.Designator.Justification := eJustify_BottomCenter;
Inst.Designator.Location := Point(MilsToCoord(DX + (L0 + R0) Div 2), MilsToCoord(DY + T0 + 80));
Inst.Comment.Text := Comments[K];
Inst.Comment.IsHidden := False;
Inst.Comment.Justification := eJustify_TopCenter;
Inst.Comment.Location := Point(MilsToCoord(DX + (L0 + R0) Div 2), MilsToCoord(DY + B0 - 80));
Sheet.RegisterSchObjectInContainer(Inst);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, Inst.I_ObjectAddress);
NPlaced := NPlaced + 1;
{ a wire stub and a net label at every pin's hot spot (Location is the body end) }
PIt := Inst.SchIterator_Create;
PIt.AddFilter_ObjectSet(MkSet(ePin));
Pin := PIt.FirstSchObject;
While Pin <> Nil Do
Begin
Net := PinNet.Values[Refs[K] + '.' + Pin.Designator];
Dir := Pin.Orientation;
HX := Pin.Location.X; HY := Pin.Location.Y;
If Dir = 0 Then HX := HX + Pin.PinLength
Else If Dir = 1 Then HY := HY + Pin.PinLength
Else If Dir = 2 Then HX := HX - Pin.PinLength
Else HY := HY - Pin.PinLength;
EX := HX; EY := HY;
If Dir = 0 Then EX := HX + MilsToCoord(200)
Else If Dir = 1 Then EY := HY + MilsToCoord(200)
Else If Dir = 2 Then EX := HX - MilsToCoord(200)
Else EY := HY - MilsToCoord(200);
If Net = '' Then
Begin
If NoNet <> '' Then NoNet := NoNet + ';';
NoNet := NoNet + Refs[K] + '.' + Pin.Designator;
End
Else
Begin
W := SchServer.SchObjectFactory(eWire, eCreate_Default);
W.Location := Point(HX, HY);
W.InsertVertex := 1;
W.SetState_Vertex(1, Point(HX, HY));
W.InsertVertex := 2;
W.SetState_Vertex(2, Point(EX, EY));
W.LineWidth := eSmall;
Sheet.RegisterSchObjectInContainer(W);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, W.I_ObjectAddress);
NWires := NWires + 1;
NL := SchServer.SchObjectFactory(eNetLabel, eCreate_Default);
NL.Text := Net;
NL.Location := Point(EX, EY);
{ the label reads away from the part on every side }
If (Dir = 1) Or (Dir = 3) Then NL.Orientation := eRotate90;
If (Dir = 2) Or (Dir = 3) Then NL.Justification := eJustify_BottomRight;
Sheet.RegisterSchObjectInContainer(NL);
SchServer.RobotManager.SendMessage(Sheet.I_ObjectAddress, c_BroadCast, SCHM_PrimitiveRegistration, NL.I_ObjectAddress);
NLabels := NLabels + 1;
End;
Pin := PIt.NextSchObject;
End;
Inst.SchIterator_Destroy(PIt);
YCur := DY + B0 - 650;
End;
Idx.Free;
End;
If Param(Lines, 'title') <> '' Then SetDocParam(Sheet, 'Title', Param(Lines, 'title'));
If Param(Lines, 'number') <> '' Then SetDocParam(Sheet, 'DocumentNumber', Param(Lines, 'number'));
If Param(Lines, 'revision') <> '' Then SetDocParam(Sheet, 'Revision', Param(Lines, 'revision'));
If Param(Lines, 'drawnBy') <> '' Then SetDocParam(Sheet, 'DrawnBy', Param(Lines, 'drawnBy'));
Sheet.GraphicallyInvalidate;
ResetParameters; AddStringParameter('Action','All'); RunProcess('Sch:Zoom');
SD.DoFileSave('');
Result := '{' + JRaw('ok','true') + ',' + JStr('sch', SchPath) + ',' + JRaw('placed', IntToStr(NPlaced)) + ',' +
JRaw('wires', IntToStr(NWires)) + ',' + JRaw('labels', IntToStr(NLabels)) + ',' + JStr('pinsWithoutNet', NoNet) + ',' +
JRaw('sheetMil', '[' + IntToStr(SheetW) + ',' + IntToStr(SheetH) + ']') + ',' + JRaw('columns', IntToStr(NCols)) + '}';
Ops.Free; Refs.Free; LibRefs.Free; Blocks.Free; Comments.Free; Fps.Free; PinNet.Free; BlockOrder.Free;
BL.Free; BB.Free; BR.Free; BT.Free; ColOf.Free; Cols.Free;
End;
{ project_nets: the FOCUSED project's compiled netlist, flattened: every net with its pins as
designator.pin. Compile first (compile verb). }
Function CmdProjectNets(Lines : TStringList) : String;
Var
WS : IWorkspace;
Prj : IProject;
Doc : IDocument;
Net : INet;
Pin : IPin;
I, J : Integer;
S, P : String;
Begin
WS := GetWorkspace;
Prj := Nil;
If WS <> Nil Then Prj := WS.DM_FocusedProject;
If Prj = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no focused project') + '}'; Exit; End;
Doc := Prj.DM_DocumentFlattened;
If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','the project has no compiled document: compile it first') + ',' + JStr('project', Prj.DM_ProjectFullPath) + '}'; Exit; End;
S := '';
For I := 0 To Doc.DM_NetCount - 1 Do
Begin
Net := Doc.DM_Nets(I);
P := '';
For J := 0 To Net.DM_PinCount - 1 Do
Begin
Pin := Net.DM_Pins(J);
If P <> '' Then P := P + ',';
P := P + '"' + JsonEsc(Pin.DM_LogicalPartDesignator + '.' + Pin.DM_PinNumber) + '"';
End;
If S <> '' Then S := S + ',';
S := S + '"' + JsonEsc(Net.DM_NetName) + '":[' + P + ']';
End;
Result := '{' + JRaw('ok','true') + ',' + JStr('project', Prj.DM_ProjectFullPath) + ',' + JRaw('netCount', IntToStr(Doc.DM_NetCount)) + ',' + JRaw('nets', '{' + S + '}') + '}';
End;
{ select_net: select every primitive of one net (tracks, arcs, vias, pads, fills, regions, pours) and
deselect everything else; net empty = clear the selection. Answers the counts by kind. }
Function CmdSelectNet(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
NetName, N : String;
NT, NA, NV, NP, NPoly, NO : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
NetName := Param(Lines, 'net');
NT := 0; NA := 0; NV := 0; NP := 0; NPoly := 0; NO := 0;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTrackObject, eArcObject, eViaObject, ePadObject, eFillObject, eRegionObject, ePolyObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
N := '';
If Prim.Net <> Nil Then N := Prim.Net.Name;
If (NetName <> '') And (N = NetName) And (Not Prim.InPolygon) Then
Begin
Prim.Selected := True;
If Prim.ObjectId = eTrackObject Then NT := NT + 1
Else If Prim.ObjectId = eArcObject Then NA := NA + 1
Else If Prim.ObjectId = eViaObject Then NV := NV + 1
Else If Prim.ObjectId = ePadObject Then NP := NP + 1
Else If Prim.ObjectId = ePolyObject Then NPoly := NPoly + 1
Else NO := NO + 1;
End
Else If Prim.Selected Then Prim.Selected := False;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Board.ViewManager_FullUpdate;
Result := '{' + JRaw('ok','true') + ',' + JStr('net', NetName) + ',' + JRaw('selected', IntToStr(NT + NA + NV + NP + NPoly + NO)) + ',' +
JRaw('byKind', '{' + JRaw('tracks', IntToStr(NT)) + ',' + JRaw('arcs', IntToStr(NA)) + ',' + JRaw('vias', IntToStr(NV)) + ',' +
JRaw('pads', IntToStr(NP)) + ',' + JRaw('pours', IntToStr(NPoly)) + ',' + JRaw('other', IntToStr(NO)) + '}') + '}';
End;
{ ------------------------------------------------------------- silkscreen --- }
Function TextJson(T : IPCB_Text; Shown : Boolean) : String;
Var
R : TCoordRect;
Begin
R := T.BoundingRectangle;
Result := '[' + '"' + JsonEsc(T.Text) + '",' + IntToStr(T.XLocation) + ',' + IntToStr(T.YLocation) + ',' + IntToStr(T.Size) + ',' +
IntToStr(Round(T.Rotation)) + ',' + JBool(Shown) + ',[' + IntToStr(R.Left) + ',' + IntToStr(R.Bottom) + ',' + IntToStr(R.Right) + ',' + IntToStr(R.Top) + ']]';
End;
{ silk_state: what the silkscreen planner needs that routing_state does not carry, RAW units.
comps [["ref","pattern",x,y,rot,bottom,[body l,b,r,t],name,comment]] name/comment: [text,x,y,size,rot,shown,[box]]
texts [[layer,text,x,y,size,rot,mirror,[box]]] free overlay text (not a component's)
lines [[layer,x1,y1,x2,y2,width]] free overlay tracks }
Function CmdSilkState(Lines : TStringList) : String;
Var
Board : IPCB_Board;
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
Prim : IPCB_Primitive;
Txt : IPCB_Text;
Trk : IPCB_Track;
R : TCoordRect;
LC, LT, LL : TStringList;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
LC := TStringList.Create; LT := TStringList.Create; LL := TStringList.Create;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While Comp <> Nil Do
Begin
R := Comp.BoundingRectangleNoNameComment;
LC.Add('["' + JsonEsc(Comp.Name.Text) + '","' + JsonEsc(Comp.Pattern) + '",' + IntToStr(Comp.x) + ',' + IntToStr(Comp.y) + ',' +
IntToStr(Round(Comp.Rotation)) + ',' + JBool(Comp.Layer = eBottomLayer) + ',[' + IntToStr(R.Left) + ',' + IntToStr(R.Bottom) + ',' +
IntToStr(R.Right) + ',' + IntToStr(R.Top) + '],' + TextJson(Comp.Name, Comp.NameOn) + ',' + TextJson(Comp.Comment, Comp.CommentOn) + ']');
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eTextObject, eTrackObject));
BIt.AddFilter_LayerSet(MkSet(eTopOverlay, eBottomOverlay));
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If (Not Prim.InComponent) And ((Prim.Layer = eTopOverlay) Or (Prim.Layer = eBottomOverlay)) Then
Begin
If Prim.ObjectId = eTextObject Then
Begin
Txt := Prim; R := Txt.BoundingRectangle;
LT.Add('[' + IntToStr(Txt.Layer) + ',"' + JsonEsc(Txt.Text) + '",' + IntToStr(Txt.XLocation) + ',' + IntToStr(Txt.YLocation) + ',' +
IntToStr(Txt.Size) + ',' + IntToStr(Round(Txt.Rotation)) + ',' + JBool(Txt.MirrorFlag) + ',[' + IntToStr(R.Left) + ',' +
IntToStr(R.Bottom) + ',' + IntToStr(R.Right) + ',' + IntToStr(R.Top) + ']]');
End
Else
Begin
Trk := Prim;
LL.Add('[' + IntToStr(Trk.Layer) + ',' + IntToStr(Trk.x1) + ',' + IntToStr(Trk.y1) + ',' + IntToStr(Trk.x2) + ',' + IntToStr(Trk.y2) + ',' + IntToStr(Trk.Width) + ']');
End;
End;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
Result := '{' + JRaw('ok','true') + ',' + JRaw('top', IntToStr(eTopOverlay)) + ',' + JRaw('bottom', IntToStr(eBottomOverlay)) + ',' +
JRaw('comps', JoinList(LC)) + ',' + JRaw('texts', JoinList(LT)) + ',' + JRaw('lines', JoinList(LL)) + '}';
LC.Free; LT.Free; LL.Free;
End;
Function CompByRef(Board : IPCB_Board; Ref : String) : IPCB_Component;
Var
BIt : IPCB_BoardIterator;
Comp : IPCB_Component;
Begin
Result := Nil;
BIt := Board.BoardIterator_Create;
BIt.AddFilter_ObjectSet(MkSet(eComponentObject));
BIt.AddFilter_LayerSet(AllLayers);
BIt.AddFilter_Method(eProcessAll);
Comp := BIt.FirstPCBObject;
While Comp <> Nil Do
Begin
If Comp.Name.Text = Ref Then Begin Result := Comp; Break; End;
Comp := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
End;
{ apply_silk: payload lines, RAW units, one undo step.
N|ref|x|y|size|rot|show the designator (x,y = the text's own location)
V|ref|x|y|size|rot|show the comment (value)
T|T or B|text|x|y|size|rot a free overlay text (B = bottom overlay, mirrored)
L|T or B|x1|y1|x2|y2|width a free overlay line
DT|layer|text|x|y delete the free overlay text at exactly that spot
DL|layer|x1|y1|x2|y2 delete the free overlay line with exactly those ends }
Procedure SetText(T : IPCB_Text; X, Y, Sz, Rot : Integer);
Begin
T.BeginModify;
T.UseTTFonts := False;
T.Size := Sz;
T.Width := Sz Div 7;
T.Rotation := Rot;
T.XLocation := X;
T.YLocation := Y;
T.EndModify;
T.GraphicallyInvalidate;
End;
Function CmdApplySilk(Lines : TStringList; Payload : String) : String;
Var
Board : IPCB_Board;
Ops : TStringList;
Op, Kind, Res, Results : String;
Comp : IPCB_Component;
Txt : IPCB_Text;
Trk : IPCB_Track;
BIt : IPCB_BoardIterator;
Prim : IPCB_Primitive;
Lay, I, Okay, Failed : Integer;
Begin
Board := PCBServer.GetCurrentPCBBoard;
If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;
Ops := TStringList.Create; Ops.Text := Payload;
Results := ''; Okay := 0; Failed := 0;
PCBServer.PreProcess;
For I := 0 To Ops.Count - 1 Do
Begin
Op := Trim(Ops[I]);
If Op = '' Then Continue;
Kind := FieldP(Op, 1); Res := 'ok'; gInputError := '';
If (Kind = 'N') Or (Kind = 'V') Then
Begin
Comp := CompByRef(Board, FieldP(Op, 2));
If Comp = Nil Then Res := 'err:no part ' + FieldP(Op, 2)
Else
Begin
Comp.BeginModify;
If Kind = 'N' Then
Begin
Comp.NameAutoPosition := eAutoPos_Manual;
Comp.NameOn := FieldP(Op, 7) <> '0';
SetText(Comp.Name, AdomInt(FieldP(Op, 3)), AdomInt(FieldP(Op, 4)), AdomInt(FieldP(Op, 5)), AdomInt(FieldP(Op, 6)));
End
Else
Begin
Comp.CommentAutoPosition := eAutoPos_Manual;
Comp.CommentOn := FieldP(Op, 7) <> '0';
SetText(Comp.Comment, AdomInt(FieldP(Op, 3)), AdomInt(FieldP(Op, 4)), AdomInt(FieldP(Op, 5)), AdomInt(FieldP(Op, 6)));
End;
Comp.EndModify;
Comp.GraphicallyInvalidate;
End;
End
Else If (Kind = 'T') Or (Kind = 'L') Then
Begin
If FieldP(Op, 2) = 'B' Then Lay := eBottomOverlay Else Lay := eTopOverlay;
If Kind = 'T' Then
Begin
Txt := PCBServer.PCBObjectFactory(eTextObject, eNoDimension, eCreate_Default);
Txt.Layer := Lay;
Txt.Text := FieldP(Op, 3);
Txt.MirrorFlag := Lay = eBottomOverlay;
SetText(Txt, AdomInt(FieldP(Op, 4)), AdomInt(FieldP(Op, 5)), AdomInt(FieldP(Op, 6)), AdomInt(FieldP(Op, 7)));
If gInputError = '' Then RegisterPrim(Board, Txt);
End
Else
Begin
Trk := PCBServer.PCBObjectFactory(eTrackObject, eNoDimension, eCreate_Default);
Trk.Layer := Lay;
Trk.x1 := AdomInt(FieldP(Op, 3)); Trk.y1 := AdomInt(FieldP(Op, 4));
Trk.x2 := AdomInt(FieldP(Op, 5)); Trk.y2 := AdomInt(FieldP(Op, 6));
Trk.Width := AdomInt(FieldP(Op, 7));
If gInputError = '' Then RegisterPrim(Board, Trk);
End;
End
Else If (Kind = 'DT') Or (Kind = 'DL') Then
Begin
Res := 'err:not found';
BIt := Board.BoardIterator_Create;
If Kind = 'DT' Then BIt.AddFilter_ObjectSet(MkSet(eTextObject)) Else BIt.AddFilter_ObjectSet(MkSet(eTrackObject));
BIt.AddFilter_LayerSet(MkSet(eTopOverlay, eBottomOverlay));
BIt.AddFilter_Method(eProcessAll);
Prim := BIt.FirstPCBObject;
While Prim <> Nil Do
Begin
If (Not Prim.InComponent) And (IntToStr(Prim.Layer) = FieldP(Op, 2)) Then
Begin
If Kind = 'DT' Then
Begin
Txt := Prim;
If (Txt.Text = FieldP(Op, 3)) And (IntToStr(Txt.XLocation) = FieldP(Op, 4)) And (IntToStr(Txt.YLocation) = FieldP(Op, 5)) Then Begin Res := 'ok'; Break; End;
End
Else
Begin
Trk := Prim;
If (IntToStr(Trk.x1) = FieldP(Op, 3)) And (IntToStr(Trk.y1) = FieldP(Op, 4)) And (IntToStr(Trk.x2) = FieldP(Op, 5)) And (IntToStr(Trk.y2) = FieldP(Op, 6)) Then Begin Res := 'ok'; Break; End;
End;
End;
Prim := BIt.NextPCBObject;
End;
Board.BoardIterator_Destroy(BIt);
If Res = 'ok' Then Board.RemovePCBObject(Prim);
End
Else Res := 'err:unknown op ' + Kind;
If gInputError <> '' Then Res := 'err:bad number ' + gInputError;
If Copy(Res, 1, 3) = 'err' Then Failed := Failed + 1 Else Okay := Okay + 1;
If Results <> '' Then Results := Results + ',';
Results := Results + '"' + JsonEsc(Res) + '"';
End;
PCBServer.PostProcess;
gInputError := '';
Board.ViewManager_FullUpdate;
Ops.Free;
Result := '{' + JRaw('ok','true') + ',' + JRaw('applied', IntToStr(Okay)) + ',' + JRaw('failed', IntToStr(Failed)) + ',' + JRaw('results', '[' + Results + ']') + '}';
End;
{ close_old_agents: every earlier generation of this agent's script project still open in Altium's
Projects panel (each start opens a new AdomAgent_<gen>.PrjScr; the old ones piled up, ten deep by
2026-10-15). The running generation stays. Answers how many it closed. }
Function CloseOldAgents : Integer;
Var
WS : IWorkspace;
Prj : IProject;
Doc : IServerDocument;
I : Integer;
N : String;
Begin
Result := 0;
WS := GetWorkspace;
If WS = Nil Then Exit;
For I := WS.DM_ProjectCount - 1 DownTo 0 Do
Begin
Prj := WS.DM_Projects(I);
If Prj = Nil Then Continue;
N := Prj.DM_ProjectFullPath;
If (Pos('\AdomAgent_', N) > 0) And (Pos('.PRJSCR', UpperCase(N)) > 0) And (Pos(AGENT_GEN, N) = 0) Then
Begin
Doc := Nil;
Try
Doc := Client.GetDocumentByPath(Prj.DM_ProjectFullPath);
Except
Doc := Nil;
End;
If Doc <> Nil Then
Begin
Try
Client.CloseDocument(Doc);
Result := Result + 1;
Except
End;
End;
End;
End;
End;
Function CmdCloseOldAgents(Lines : TStringList) : String;
Begin
Result := '{' + JRaw('ok','true') + ',' + JRaw('closed', IntToStr(CloseOldAgents)) + '}';
End;
Function Dispatch(Cmd : String; Lines : TStringList; Payload : String) : String;
Begin
If Cmd = 'ping' Then Result := CmdPing(Lines)
Else If Cmd = 'state' Then Result := CmdState(Lines)
Else If Cmd = 'run_process' Then Result := CmdRunProcess(Lines)
Else If Cmd = 'open' Then Result := CmdOpen(Lines)
Else If Cmd = 'close_document' Then Result := CmdCloseDocument(Lines)
Else If Cmd = 'eval' Then Result := CmdEval(Lines, Payload)
Else If Cmd = 'io_bench' Then Result := CmdIoBench(Lines)
Else If Cmd = 'read_board' Then Result := CmdReadBoard(Lines)
Else If Cmd = 'layer_occupancy' Then Result := CmdLayerOccupancy(Lines)
Else If Cmd = 'read_schematic' Then Result := CmdReadSchematic(Lines)
Else If Cmd = 'read_outline' Then Result := CmdReadOutline(Lines)
Else If Cmd = 'lib_list' Then Result := CmdLibList(Lines)
Else If Cmd = 'lib_select' Then Result := CmdLibSelect(Lines)
Else If Cmd = 'compile' Then Result := CmdCompile(Lines)
Else If Cmd = 'violations' Then Result := CmdViolations(Lines)
Else If Cmd = 'outjob' Then Result := CmdOutJob(Lines)
Else If Cmd = 'create_footprint' Then Result := CmdCreateFootprint(Lines)
Else If Cmd = 'create_symbol' Then Result := CmdCreateSymbol(Lines)
Else If Cmd = 'install_library' Then Result := CmdInstallLibrary(Lines)
Else If Cmd = 'list_installed_libraries' Then Result := CmdListInstalledLibs(Lines)
Else If Cmd = 'uninstall_library' Then Result := CmdUninstallLibrary(Lines)
Else If Cmd = 'compile_intlib' Then Result := CmdCompileIntLib(Lines)
Else If Cmd = 'extract_intlib' Then Result := CmdExtractIntLib(Lines)
Else If Cmd = 'set_params' Then Result := CmdSetParams(Lines)
Else If Cmd = 'add_body' Then Result := CmdAddBody(Lines)
Else If Cmd = 'place_on_sch' Then Result := CmdPlaceOnSch(Lines)
Else If Cmd = 'add_symbol_art' Then Result := CmdAddSymbolArt(Lines, Payload)
Else If Cmd = 'selftest_halt' Then Result := CmdSelfTestHalt(Lines)
Else If Cmd = 'symbol_prims' Then Result := CmdSymbolPrims(Lines)
Else If Cmd = 'set_models' Then Result := CmdSetModels(Lines)
Else If Cmd = 'place_on_pcb' Then Result := CmdPlaceOnPcb(Lines)
Else If Cmd = 'pcb_report' Then Result := CmdPcbReport(Lines)
Else If Cmd = 'list_designators' Then Result := CmdListDesignators(Lines)
Else If Cmd = 'component_data' Then Result := CmdComponentData(Lines)
Else If Cmd = 'layer_stack' Then Result := CmdLayerStack(Lines)
Else If Cmd = 'bom_placement' Then Result := CmdBomPlacement(Lines)
Else If Cmd = 'component_pins' Then Result := CmdComponentPins(Lines)
Else If Cmd = 'list_nets' Then Result := CmdListNets(Lines)
Else If Cmd = 'layers' Then Result := CmdLayers(Lines)
Else If Cmd = 'net_connections' Then Result := CmdNetConnections(Lines)
Else If Cmd = 'schematic_data' Then Result := CmdSchematicData(Lines)
Else If Cmd = 'sheet_params' Then Result := CmdSheetParams(Lines)
Else If Cmd = 'sch_connect' Then Result := CmdSchConnect(Lines, Payload)
Else If Cmd = 'sch_edit' Then Result := CmdSchEdit(Lines, Payload)
Else If Cmd = 'sch_place_part' Then Result := CmdSchPlacePart(Lines)
Else If Cmd = 'doc_params' Then Result := CmdDocParams(Lines)
Else If Cmd = 'sch_rename_at' Then Result := CmdSchRenameAt(Lines, Payload)
Else If Cmd = 'sch_zoom' Then Result := CmdSchZoom(Lines)
Else If Cmd = 'copper_layers' Then Result := CmdCopperLayers(Lines)
Else If Cmd = 'read_rules' Then Result := CmdReadRules(Lines)
Else If Cmd = 'set_layer_visibility' Then Result := CmdSetLayerVisibility(Lines)
Else If Cmd = 'footprint_primitives' Then Result := CmdFootprintPrimitives(Lines)
Else If Cmd = 'routing_state' Then Result := CmdRoutingState(Lines)
Else If Cmd = 'edit_board' Then Result := CmdEditBoard(Lines, Payload)
Else If Cmd = 'apply_pours' Then Result := CmdApplyPours(Lines, Payload)
Else If Cmd = 'pour_state' Then Result := CmdPourState(Lines)
Else If Cmd = 'remove_pours' Then Result := CmdRemovePours(Lines, Payload)
Else If Cmd = 'routing_drc' Then Result := CmdRoutingDrc(Lines)
Else If Cmd = 'build_board' Then Result := CmdBuildBoard(Lines, Payload)
Else If Cmd = 'set_outline' Then Result := CmdSetOutline(Lines, Payload)
Else If Cmd = 'set_rule' Then Result := CmdSetRule(Lines)
Else If Cmd = 'zoom_rect' Then Result := CmdZoomRect(Lines)
Else If Cmd = 'add_rule' Then Result := CmdAddRule(Lines)
Else If Cmd = 'build_schematic' Then Result := CmdBuildSchematic(Lines, Payload)
Else If Cmd = 'project_nets' Then Result := CmdProjectNets(Lines)
Else If Cmd = 'select_net' Then Result := CmdSelectNet(Lines)
Else If Cmd = 'close_old_agents' Then Result := CmdCloseOldAgents(Lines)
Else If Cmd = 'silk_state' Then Result := CmdSilkState(Lines)
Else If Cmd = 'apply_silk' Then Result := CmdApplySilk(Lines, Payload)
Else If Cmd = 'build_footprint' Then Result := CmdBuildFootprint(Lines)
Else If Cmd = 'build_symbol_modes' Then Result := CmdBuildSymbolModes(Lines)
Else If Cmd = 'symbol_mode' Then Result := CmdSymbolMode(Lines)
Else If Cmd = 'add_models' Then Result := CmdAddModels(Lines)
Else If Cmd = 'remove_symbol' Then Result := CmdRemoveSymbol(Lines)
Else If Cmd = 'clone_symbol' Then Result := CmdCloneSymbol(Lines)
Else If Cmd = 'rename_symbol' Then Result := CmdRenameSymbol(Lines)
Else If Cmd = 'read_symbol' Then Result := CmdReadSymbol(Lines)
Else If Cmd = 'copy_footprint' Then Result := CmdCopyFootprint(Lines)
Else If Cmd = 'rename_footprint' Then Result := CmdRenameFootprint(Lines)
Else If Cmd = 'remove_footprint' Then Result := CmdRemoveFootprint(Lines)
Else If Cmd = 'cleanup_libs' Then Result := CmdCleanupLibs(Lines)
Else If Cmd = 'set_visibility' Then Result := CmdSetVisibility(Lines)
Else If Cmd = 'xcopy_footprint' Then Result := CmdXCopyFootprint(Lines)
Else If Cmd = 'normalize_bodies' Then Result := CmdNormalizeBodies(Lines)
Else If Cmd = 'plate_vias' Then Result := CmdPlateVias(Lines)
Else If Cmd = 'test_board' Then Result := CmdTestBoard(Lines)
Else If Cmd = 'board_shape' Then Result := CmdBoardShape(Lines)
Else If Cmd = 'swap_footprint' Then Result := CmdSwapFootprint(Lines)
Else If Cmd = 'tx_clear' Then Begin TxClear; Result := '{' + JRaw('ok','true') + ',' + JStr('note','PCBServer transaction cleared') + '}'; End
Else If Cmd = 'stop' Then
Begin
gRunning := False;
Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}';
End
Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy,read_schematic,read_outline') + '}';
End;
{ ------------------------------------------------------------------ loop --- }
{ ONE inbox file, request.txt, written atomically by the bridge (tmp + rename) and
serialized on the bridge side. A wildcard FindFirst('request_*.txt') never matched
in this engine (measured 2026-08-29: heartbeat fine, requests stuck on disk), so
the inbox is a fixed name and the id rides inside. }
Function ServeOne : Boolean;
Var
Id, ReqPath, PayPath, Cmd, Body, Payload : String;
Lines : TStringList;
T0, TDetect, TRead, TDone : Integer;
Begin
Result := False;
ReqPath := WORKSPACE + 'request.txt';
If Not FileExists(ReqPath) Then Exit;
PayPath := WORKSPACE + 'request.payload';
Lines := TStringList.Create;
Try
TDetect := GetTickCount;
Lines.Text := ReadAll(ReqPath);
DeleteFile(ReqPath);
TRead := GetTickCount;
Payload := '';
If FileExists(PayPath) Then
Begin
Payload := ReadAll(PayPath);
DeleteFile(PayPath);
End;
Id := Param(Lines, 'id');
Cmd := Param(Lines, 'command');
gRequests := gRequests + 1;
gLastReqTick := GetTickCount;
T0 := GetTickCount;
gInputError := '';
Try
Body := Dispatch(Cmd, Lines, Payload);
If gInputError <> '' Then
Body := '{' + JRaw('ok', 'false') + ',' + JStr('errorCode', 'bad_number') + ',' +
JStr('error', 'a value that must be a number was not: ' + gInputError) + ',' + JStr('command', Cmd) + '}';
Except
Body := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'exception in agent while handling ' + Cmd) + '}';
End;
TDone := GetTickCount;
{ wrap: id + timing + the handler body as "data"; agentPhases = read/dispatch ms + the loop tick that saw the request }
WriteDirect(WORKSPACE + 'response_' + Id + '.json',
'{' + JStr('id', Id) + ',' + JStr('command', Cmd) + ',' + JRaw('durationMs', IntToStr(TDone - T0)) + ',' +
JRaw('agentPhases', '{' + JRaw('readMs', IntToStr(TRead - TDetect)) + ',' + JRaw('dispatchMs', IntToStr(TDone - T0)) + ',' + JRaw('detectTick', IntToStr(TDetect)) + ',' + JRaw('sleepMs', IntToStr(gLastSleepMs)) + ',' + JRaw('hbSaveMs', IntToStr(gLastSaveMs)) + ',' + JRaw('hbSaveTries', IntToStr(gLastSaveTries)) + ',' + JRaw('hbSaveExcept', IntToStr(gLastSaveExcept)) + ',' + JRaw('hbDeleteMs', IntToStr(gLastDeleteMs)) + ',' + JRaw('hbRenameMs', IntToStr(gLastRenameMs)) + ',' + JRaw('directMs', IntToStr(gDirectMs)) + ',' + JRaw('directTries', IntToStr(gDirectTries)) + ',' + JRaw('directExcept', IntToStr(gDirectExcept)) + '}') + ',' +
JRaw('data', Body) + '}');
Finally
Lines.Free;
End;
Result := True;
End;
{ Register the PCB and SCH scripting APIs before the first PCBServer/SchServer
reference. On a fresh session where no PCB document was ever opened, the PCB
API unit is not registered and the first PCBServer call (TxClear, line ~140)
halts the engine with an "Undeclared identifier: PostProcess" modal on the
user's screen (altium-bridge#3, ADDevelop fresh session). Client.StartServer
loads the editor server, which registers the API — background, no UI. }
Procedure ServersUp;
Begin
Try
Client.StartServer('PCB');
Except
End;
Try
Client.StartServer('SCH');
Except
End;
End;
Procedure StartAgent;
Var
IdleTicks : Integer;
Had : Boolean;
SleepMs : Integer;
Begin
ServersUp;
TxClear;
gRunning := True;
gRequests := 0;
gLastReqTick := GetTickCount;
gLastHbTick := 0;
IdleTicks := 0;
gTicks := 0;
{ a stale stop file must not kill a fresh loop }
If FileExists(WORKSPACE + 'stop') Then DeleteFile(WORKSPACE + 'stop');
WriteAtomic(WORKSPACE + 'agent.json', '{' + JStr('version', AGENT_VERSION) + ',' + JRaw('startedTick', IntToStr(GetTickCount)) + ',' + JStr('workspace', WORKSPACE) + '}');
Heartbeat(True);
While gRunning Do
Begin
Try
{ two shutdown signals, both cheap reads: IsQuitting demonstrably never fired
in time during the 2026-09-02 freezes, so Terminated rides beside it }
If Client.IsQuitting Then gRunning := False;
If Application.Terminated Then gRunning := False;
Except
End;
If FileExists(WORKSPACE + 'stop') Then
Begin
DeleteFile(WORKSPACE + 'stop');
gRunning := False;
End;
If Not gRunning Then Break;
Had := False;
Try
Had := ServeOne;
Except
End;
If Had Then
Begin
IdleTicks := 0;
SleepMs := POLL_ACTIVE_MS;
End
Else
Begin
IdleTicks := IdleTicks + 1;
If IdleTicks > IDLE_AFTER_TICKS Then SleepMs := POLL_IDLE_MS Else SleepMs := POLL_ACTIVE_MS;
End;
If (GetTickCount - gLastReqTick) > AUTO_STOP_MS Then gRunning := False;
Heartbeat(False);
Application.ProcessMessages;
gLastSleepMs := SleepMs;
gTicks := gTicks + 1;
Sleep(SleepMs);
End;
Try
WriteAtomic(WORKSPACE + 'agent.json', '{' + JStr('version', AGENT_VERSION) + ',' + JRaw('stoppedTick', IntToStr(GetTickCount)) + ',' + JRaw('requests', IntToStr(gRequests)) + '}');
If FileExists(WORKSPACE + 'hb.txt') Then DeleteFile(WORKSPACE + 'hb.txt');
Except
End;
End;
{ Convenience entry points for File > Run Script users. }
Procedure StopAgent;
Begin
WriteAtomic(WORKSPACE + 'stop', '1');
End;