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;