Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
226 kB · 6155 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156//! Shared renderer scene boundary for the WASM worker and native preview.
#[cfg(not(target_arch = "wasm32"))]use polymodel_ldraw_core::Materialization;use polymodel_ldraw_core::{ ModelSummary, NormalizedPath, OwnedParseResult, RootId, SemanticEvent, SemanticSink, TraversalContext,};use polymodel_renderer_protocol::scene::{ GeometryId, MaterialId, NodeId, OccurrenceId, ProvenanceId, Scene, SceneBounds, SceneGeometry, SceneId, SceneMaterial, SceneOccurrence, ScenePrimitiveRange, SceneStats, SceneTexmapAssociation, SceneTexmapProjectionMode, SceneTexture, TextureId,};use polymodel_renderer_protocol::{LengthUnit, ModelResource, Units};use smol_str::SmolStr;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;use std::sync::Arc;#[cfg(not(target_arch = "wasm32"))]use std::sync::Mutex;#[cfg(not(target_arch = "wasm32"))]use three_d::{AmbientLight, Camera, DirectionalLight, OrbitControl};use three_d::{ AxisAlignedBoundingBox, Blend, BlendEquationType, BlendMultiplierType, CpuMaterial, CpuMesh, Cull, Effect, EffectMaterialId, Geometry, GeometryId as ThreeGeometryId, Gm, Indices, InstanceBuffer, InstancedMesh, Instances, Light, Mat4, Material, MaterialType, Matrix, PhysicalMaterial, Positions, Program, RenderStates, Srgba, VertexBuffer, Viewer, WriteMask,};#[cfg(not(target_arch = "wasm32"))]use three_d::{InnerSpace, Object};use three_d_asset::{Texture2D, TextureData};use web_time::Instant;
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum NativeStage { ProcessStarted, WindowCreated, PreparationStarted, PreparationComplete, PreparationFailed, NativeModelBuildStarted, NativeModelBuildComplete, PickIndexBuildStarted, PickIndexBuildComplete, SceneInstalled, FirstRedrawRequested, BaseRenderStarted, BaseRenderComplete, FirstSwapComplete, Rendered, Started, LibraryResolved, OmrParsed, SceneAdapted, ContextCreated, GpuResourcesCreated, FrameCallbackEntered, DrawCompleted, ReadbackCompleted, OutputWritten,}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Debug, Eq, PartialEq)]pub enum NativeTerminal { Success, Failure(String), Timeout(String),}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Debug)]pub struct NativeStageRecord { pub stage: NativeStage, pub elapsed_ms: u128,}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Debug, Default)]pub struct NativeTraceSnapshot { pub stages: Vec<NativeStage>, pub records: Vec<NativeStageRecord>, pub terminal: Option<NativeTerminal>,}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone)]pub struct NativeTrace { state: Arc<Mutex<NativeTraceSnapshot>>, started: Arc<Instant>,}
#[cfg(not(target_arch = "wasm32"))]impl Default for NativeTrace { fn default() -> Self { Self { state: Arc::new(Mutex::new(NativeTraceSnapshot::default())), started: Arc::new(Instant::now()), } }}
#[cfg(not(target_arch = "wasm32"))]impl NativeTrace { pub fn record(&self, stage: NativeStage) { let elapsed_ms = self.started.elapsed().as_millis(); let mut state = self.state.lock().expect("native trace poisoned"); state.stages.push(stage); state.records.push(NativeStageRecord { stage, elapsed_ms }); tracing::debug!("[ldraw-preview] stage={stage:?} elapsed_ms={elapsed_ms}"); }
pub fn finish(&self, terminal: NativeTerminal) { self.state.lock().expect("native trace poisoned").terminal = Some(terminal); }
pub fn snapshot(&self) -> NativeTraceSnapshot { self.state.lock().expect("native trace poisoned").clone() }
pub fn timeout(&self, detail: impl Into<String>) { let snapshot = self.snapshot(); let last = snapshot .stages .last() .map(|stage| format!("{stage:?}")) .unwrap_or_else(|| "<none>".into()); let display = std::env::var("DISPLAY").unwrap_or_else(|_| "<unset>".into()); let vendor = std::env::var("POLYMODEL_GL_VENDOR").unwrap_or_else(|_| "<unknown>".into()); let renderer = std::env::var("POLYMODEL_GL_RENDERER").unwrap_or_else(|_| "<unknown>".into()); let version = std::env::var("POLYMODEL_GL_VERSION").unwrap_or_else(|_| "<unknown>".into()); self.finish(NativeTerminal::Timeout(format!( "{}; last_stage={last}; DISPLAY={display}; gl_vendor={vendor}; gl_renderer={renderer}; gl_version={version}", detail.into() ))); }}
pub const LDRAW_TO_WORLD: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0]];
pub fn ldraw_to_world(point: [f64; 3]) -> [f64; 3] { [point[0], point[2], -point[1]]}
pub fn basis_determinant() -> f64 { 1.0}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]pub enum RendererError { #[error("missing geometry {0:?}")] MissingGeometry(GeometryId), #[error("missing material {0:?}")] MissingMaterial(MaterialId), #[error("missing texture {0:?}")] MissingTexture(TextureId), #[error("missing node {0:?}")] MissingNode(NodeId), #[error("duplicate geometry {0:?}")] DuplicateGeometry(GeometryId), #[error("index overflow: {context}")] IndexOverflow { context: &'static str }, #[error("invalid range: {context} (index {index}, length {len})")] InvalidRange { context: &'static str, index: usize, len: usize, }, #[error("non-finite transform or coordinate")] NonFiniteTransform, #[error("GPU/resource failure: {0}")] GpuResource(String), #[error("texture decode failure: {0}")] TextureDecode(String), #[error("parse/adaptation failure: {0}")] ParseAdaptation(&'static str), #[error("parse/adaptation failure: {0}")] ParseMessage(String), #[error("I/O failure: {0}")] Io(String), #[error("renderer generation exhausted; restart the worker")] GenerationExhausted, #[error("renderer request cancelled")] Cancelled, #[error("external runtime failure: {0}")] External(String),}
impl RendererError { fn parse(message: &'static str) -> Self { Self::ParseAdaptation(message) }
pub fn external_message(&self) -> Option<&str> { match self { Self::External(message) => Some(message), _ => None, } }}
fn finite_f32(value: f64) -> Result<f32, RendererError> { if !value.is_finite() || value < f32::MIN as f64 || value > f32::MAX as f64 { return Err(RendererError::NonFiniteTransform); } Ok(value as f32)}
#[derive(Debug)]pub enum ParseAndAdaptError { Parse(polymodel_ldraw_core::ParseError), Adapt(RendererError),}
pub fn parse_and_adapt_ldraw( request: polymodel_ldraw_core::AdapterRequest<'_>, limits: polymodel_ldraw_core::LdrawLimits, resources: Option<&[ModelResource]>,) -> Result<Scene, ParseAndAdaptError> { let mut sink = AdapterSink::new(resources); let result = polymodel_ldraw_core::InProcessRustAdapter::parse_with_limits_and_sink( request, limits, &mut sink, ) .map_err(ParseAndAdaptError::Parse)?; sink.take_error().map_err(ParseAndAdaptError::Adapt)?; let models = result.models.clone(); adapt_ldraw_assembly_with_sink( &models, Some(&result.result.scene), Some(&result), resources, Some(sink), ) .map_err(ParseAndAdaptError::Adapt)}
#[cfg(not(target_arch = "wasm32"))]fn texture_resource_root(root: RootId) -> &'static str { match root { RootId::CurrentMpd => "mpd", RootId::UploadedManifest => "manifest", RootId::UploadedLdraw => "models", RootId::OfficialLibrary => "official-parts", }}
fn texture_resource_for<'a>( resources: &'a [ModelResource], root: RootId, path: &NormalizedPath,) -> Option<&'a ModelResource> { let root_matches = |label: &str| match root { RootId::CurrentMpd => label == "mpd", RootId::UploadedManifest => label == "manifest", RootId::UploadedLdraw => matches!(label, "models" | "parts" | "p" | "external"), RootId::OfficialLibrary => matches!(label, "official-parts" | "official-p"), }; let authored = path.as_str(); let stripped = authored .split_once('/') .filter(|(prefix, _)| prefix.eq_ignore_ascii_case("textures")) .map(|(_, rest)| rest) .unwrap_or(authored); let prefixed = format!("textures/{stripped}"); for candidate in [authored, prefixed.as_str(), stripped] { if let Some(resource) = resources .iter() .find(|resource| root_matches(&resource.root) && resource.path == candidate) { return Some(resource); } } None}
fn adapt_ldraw_parse_result( result: &OwnedParseResult, resources: Option<&[polymodel_renderer_protocol::ModelResource]>, sink: AdapterSink<'_>,) -> Result<Scene, RendererError> { let started = Instant::now(); let models = result.models.as_slice(); if models.is_empty() { return Err(RendererError::parse("parse emitted no root model")); } let scene = adapt_ldraw_assembly_with_sink( models, Some(&result.result.scene), Some(result), resources, Some(sink), )?; if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { eprintln!( "[ldraw-metrics] phase=adapt-ldraw elapsed_ms={} geometries={} materials={} textures={} occurrences={} points={} triangles={} lines={} conditional_lines={}", started.elapsed().as_millis(), scene.geometries.len(), scene.materials.len(), scene.textures.len(), scene.stats.occurrences, scene.stats.points, scene.stats.triangles, scene.stats.lines, scene.stats.conditional_lines, ); } Ok(scene)}
#[derive(Clone, Hash, PartialEq, Eq)]struct PrimitiveBatchKey { colour: u32, bfc_state: polymodel_ldraw_core::BfcState, bfc_cull: bool, bfc_ccw: bool, reverse_winding: bool, texmap: Option<SceneTexmapAssociation>,}
fn append_items<T>(slot: &mut Vec<T>, items: impl IntoIterator<Item = T>) { slot.extend(items);}
struct SceneGeometryBuilder { id: GeometryId, provenance: ProvenanceId, points: Vec<[f64; 3]>, uvs: Vec<[f32; 2]>, triangles: Vec<[u32; 3]>, quads: Vec<[u32; 4]>, lines: Vec<[u32; 2]>, conditional_lines: Vec<[u32; 4]>, primitives: Vec<ScenePrimitiveRange>,}
impl SceneGeometryBuilder { fn into_scene_geometry(self) -> SceneGeometry { SceneGeometry { id: self.id, provenance: self.provenance, points: self.points.into_boxed_slice(), uvs: self.uvs.into_boxed_slice(), triangles: self.triangles.into_boxed_slice(), quads: self.quads.into_boxed_slice(), lines: self.lines.into_boxed_slice(), conditional_lines: self.conditional_lines.into_boxed_slice(), primitives: self.primitives.into_boxed_slice(), } }}
struct AdapterSink<'a> { geometries: Vec<SceneGeometryBuilder>, provenance: Vec<polymodel_renderer_protocol::scene::SceneProvenance>, nodes_by_model: HashMap<polymodel_ldraw_core::CacheKey, Vec<(NodeId, PrimitiveBatchKey)>>, batch_indices: HashMap<(ProvenanceId, u64), Vec<usize>>, resources: Option<&'a [ModelResource]>, current_model: Option<polymodel_ldraw_core::CacheKey>, current_provenance: Option<ProvenanceId>, error: Option<RendererError>,}
impl<'a> AdapterSink<'a> { pub fn new(resources: Option<&'a [ModelResource]>) -> Self { Self { geometries: Vec::new(), provenance: Vec::new(), nodes_by_model: HashMap::new(), batch_indices: HashMap::new(), resources, current_model: None, current_provenance: None, error: None, } }
fn take_error(&mut self) -> Result<(), RendererError> { self.error.take().map_or(Ok(()), Err) }
fn texmap_available(&self, root: RootId, texmap: &SceneTexmapAssociation) -> bool { let Some(resources) = self.resources else { return false; }; let valid = |reference: &str| { let path = NormalizedPath::new(reference).ok()?; let resource = texture_resource_for(resources, root, &path)?; resource.validate().ok().map(|_| ()) }; valid(texmap.pngfile.as_str()).is_some() }
fn emit_geometry( &mut self, colour: polymodel_ldraw_core::ColourCode, vertices: &[polymodel_ldraw_core::Point3], line_type: polymodel_ldraw_core::LineType, bfc: polymodel_ldraw_core::BfcFrame, texmap_source: Option<&polymodel_ldraw_core::TexmapAssociation>, inverted: bool, ) -> Result<(), RendererError> { use polymodel_ldraw_core::LineType; use std::hash::{Hash, Hasher}; let model = self .current_model .as_ref() .ok_or_else(|| RendererError::parse("geometry emitted before model boundary"))?; let provenance = self .current_provenance .ok_or_else(|| RendererError::parse("geometry emitted without provenance"))?; let texmap = texmap_source.and_then(|texmap| { let mode = match texmap.mode { polymodel_ldraw_core::TexmapProjectionMode::Planar => { SceneTexmapProjectionMode::Planar } polymodel_ldraw_core::TexmapProjectionMode::Cylindrical => { SceneTexmapProjectionMode::Cylindrical } polymodel_ldraw_core::TexmapProjectionMode::Spherical => { SceneTexmapProjectionMode::Spherical } }; let association = SceneTexmapAssociation { mode, parameters: texmap.parameters.iter().map(Into::into).collect(), pngfile: texmap.pngfile.as_str().into(), glossmap: texmap.glossmap.as_deref().map(Into::into), fallback: texmap.fallback, }; self.texmap_available(model.resolved_root, &association) .then_some(association) }); if texmap_source.is_some_and(|source| source.fallback && texmap.is_some()) { return Ok(()); } for point in vertices { finite_f32(point.x)?; finite_f32(point.y)?; finite_f32(point.z)?; } let key = PrimitiveBatchKey { colour: u32::from(colour), bfc_state: bfc.state, bfc_cull: bfc.clipping, bfc_ccw: matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), reverse_winding: inverted ^ !matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), texmap, }; let mut hasher = std::collections::hash_map::DefaultHasher::new(); key.hash(&mut hasher); let key_hash = hasher.finish(); let batch_key = (provenance, key_hash); let batch_index = self.batch_indices.get(&batch_key).and_then(|indices| { indices.iter().copied().find(|index| { self.geometries[*index].provenance == provenance && self.nodes_by_model.get(model).is_some_and(|nodes| { nodes.iter().any(|(node, candidate)| { *node == NodeId(self.geometries[*index].id.0) && candidate == &key }) }) }) }); let batch_index = if let Some(index) = batch_index { index } else { let id = GeometryId((self.geometries.len() + 1) as u64); let index = self.geometries.len(); self.geometries.push(SceneGeometryBuilder { id, provenance, points: Vec::new(), uvs: Vec::new(), triangles: Vec::new(), quads: Vec::new(), lines: Vec::new(), conditional_lines: Vec::new(), primitives: Vec::new(), }); self.nodes_by_model .entry(model.clone()) .or_default() .push((NodeId(id.0), key.clone())); self.batch_indices.entry(batch_key).or_default().push(index); index }; let batch = &mut self.geometries[batch_index]; let base = u32::try_from(batch.points.len()).map_err(|_| RendererError::IndexOverflow { context: "geometry index", })?; let point_count = u32::try_from(vertices.len()).map_err(|_| RendererError::IndexOverflow { context: "primitive point count", })?; append_items( &mut batch.primitives, [ScenePrimitiveRange { point_start: base, point_count, topology: smol_str::format_smolstr!("{:?}", line_type), bfc_state: smol_str::format_smolstr!("{:?}", bfc.state), bfc_cull: bfc.clipping, bfc_ccw: matches!(bfc.winding, polymodel_ldraw_core::Winding::Ccw), bfc_inverted: inverted, texmap: self.nodes_by_model[model] .iter() .find(|(node, _)| *node == NodeId(batch.id.0)) .map(|(_, key)| key.texmap.clone()) .flatten(), }], ); append_items( &mut batch.points, vertices .iter() .map(|point| ldraw_to_world([point.x, point.y, point.z])), ); if let Some(texmap) = self.nodes_by_model[model] .iter() .find(|(node, _)| *node == NodeId(batch.id.0)) .and_then(|(_, key)| key.texmap.as_ref()) { let mut parameters = texmap .parameters .iter() .map(|value| { value .parse::<f64>() .map_err(|_| RendererError::parse("invalid TEXMAP parameter")) }) .collect::<Result<Vec<_>, _>>()?; let expected = match texmap.mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 10, SceneTexmapProjectionMode::Spherical => 11, }; if parameters.len() != expected { return Err(RendererError::parse("invalid TEXMAP parameter count")); } for offset in (0..9).step_by(3) { let point = [ parameters[offset], parameters[offset + 1], parameters[offset + 2], ]; parameters[offset..offset + 3].copy_from_slice(&ldraw_to_world(point)); } let generated = texmap_uvs( texmap.mode, ¶meters, &batch.points[batch.points.len() - vertices.len()..], ) .map_err(|_| RendererError::parse("invalid TEXMAP UV projection"))?; append_items(&mut batch.uvs, generated); } match line_type { LineType::Two if vertices.len() == 2 => { append_items(&mut batch.lines, [[base, base + 1]]) } LineType::Three if vertices.len() == 3 => { append_items(&mut batch.triangles, [[base, base + 1, base + 2]]) } LineType::Four if vertices.len() == 4 => { append_items(&mut batch.quads, [[base, base + 1, base + 2, base + 3]]) } LineType::Five if vertices.len() == 4 => append_items( &mut batch.conditional_lines, [[base, base + 1, base + 2, base + 3]], ), _ => {} } Ok(()) }}
impl SemanticSink for AdapterSink<'_> { fn on_event(&mut self, event: &SemanticEvent<'_>, _context: &TraversalContext) { let result = match event { SemanticEvent::ModelBoundary { name, root } => { let provenance = ProvenanceId((self.provenance.len() + 1) as u64); self.provenance .push(polymodel_renderer_protocol::scene::SceneProvenance { id: provenance, source: name.clone(), span: None, }); self.current_model = Some(root.clone()); self.current_provenance = Some(provenance); Ok(()) } SemanticEvent::GeometryEmitted { colour, vertices, line_type, bfc, texmap, inverted, .. } => self.emit_geometry(*colour, vertices, *line_type, *bfc, *texmap, *inverted), _ => Ok(()), }; if let Err(error) = result { self.error.get_or_insert(error); } }}
fn adapt_ldraw_assembly_with_sink( models: &[ModelSummary], parsed_scene: Option<&polymodel_ldraw_core::SceneRecord>, parsed: Option<&OwnedParseResult>, resources: Option<&[polymodel_renderer_protocol::ModelResource]>, sink: Option<AdapterSink<'_>>,) -> Result<Scene, RendererError> { use std::collections::HashMap;
let event_models = models; let mut adapter_sink = sink.unwrap_or_else(|| AdapterSink::new(resources)); adapter_sink.take_error()?; let diagnostics_enabled = std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() || std::env::var_os("POLYMODEL_LDRAW_SCENE_DIAGNOSTICS").is_some(); let mut node_by_key = diagnostics_enabled.then(HashMap::new); let nodes_by_model = event_models .iter() .map(|model| { ( model.model, adapter_sink .nodes_by_model .get(&model.cache_key) .cloned() .unwrap_or_default(), ) }) .collect::<HashMap<_, _>>(); let path_by_model = event_models .iter() .map(|model| (model.model, SmolStr::from(model.path.as_str()))) .collect::<HashMap<_, _>>(); let definition_id_by_model = event_models .iter() .map(|model| { ( model.model, SmolStr::from(model.cache_key.content_hash.to_string()), ) }) .collect::<HashMap<_, _>>(); let canonical_path_by_model = event_models .iter() .map(|model| { ( model.model, SmolStr::from(model.cache_key.canonical_path.as_str()), ) }) .collect::<HashMap<_, _>>(); let source_class_by_model = event_models .iter() .map(|model| { ( model.model, SmolStr::from(format!("{:?}", model.cache_key.resolved_root)), ) }) .collect::<HashMap<_, _>>(); let source_by_path = event_models .iter() .map(|model| (model.path.clone(), path_by_model[&model.model].clone())) .collect::<HashMap<_, _>>(); if std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() { let definitions_with_semantic_triangles = nodes_by_model .values() .filter(|nodes| !nodes.is_empty()) .count(); let definitions_with_geometry_triangles = definitions_with_semantic_triangles; tracing::debug!( "[ldraw-adapter] definitions={} semantic_triangle_defs={} geometry_triangle_defs={} root_key={:?}", event_models.len(), definitions_with_semantic_triangles, definitions_with_geometry_triangles, models.first().map(|model| &model.cache_key), ); for (index, model) in models.iter().take(8).enumerate() { tracing::debug!( "[ldraw-adapter] def[{index}] path={} key={:?} semantic_triangles={} geometry_records={} geometry_triangles={}", model.path, model.cache_key, nodes_by_model .get(&model.model) .map(|nodes| nodes.len()) .unwrap_or(0), nodes_by_model .get(&model.model) .map(|nodes| nodes.len()) .unwrap_or(0), nodes_by_model .get(&model.model) .map(|nodes| nodes.len()) .unwrap_or(0), ); } } let geometries = adapter_sink .geometries .into_iter() .map(SceneGeometryBuilder::into_scene_geometry) .collect::<Vec<_>>(); let provenance = adapter_sink.provenance; let mut uv_cache = HashMap::<(GeometryId, u64, u64), Arc<[[f32; 2]]>>::new(); let mut uv_cache_hits = 0_u64; let mut uv_cache_misses = 0_u64; let mut texture_ids = HashMap::<(RootId, String, String), TextureId>::new(); let mut textures = Vec::<SceneTexture>::new(); let mut texture_for = |root: RootId, reference: &str, role: &str| -> Result<TextureId, RendererError> { let path = NormalizedPath::new(reference).map_err(|_| { RendererError::ParseAdaptation("TEXMAP texture reference is not a valid canonical path") })?; let key = (root, path.as_str().to_owned(), role.to_owned()); if let Some(id) = texture_ids.get(&key) { return Ok(*id); } let resource = resources .and_then(|resources| texture_resource_for(resources, root, &path)) .ok_or_else(|| RendererError::MissingTexture(TextureId((textures.len() + 1) as u64)))?; resource.validate().map_err(|_| { RendererError::TextureDecode("TEXMAP texture resource failed verification".into()) })?; let id = TextureId((textures.len() + 1) as u64); textures.push(SceneTexture { id, provenance: ProvenanceId(1), uri: reference.into(), resource_key: Some(resource.key.as_str().into()), sha256: Some(resource.sha256.clone()), byte_length: Some(resource.byte_length), role: Some(role.into()), }); texture_ids.insert(key, id); Ok(id) }; let texmap_available = |root: RootId, texmap: &SceneTexmapAssociation| { let Some(resources) = resources else { return false; }; let valid = |reference: &str| { let path = NormalizedPath::new(reference).ok()?; let resource = texture_resource_for(resources, root, &path)?; resource.validate().ok().map(|_| ()) }; valid(texmap.pngfile.as_str()).is_some() }; let mut material_ids = HashMap::< ( u32, Option<[u8; 3]>, u8, Option<TextureId>, Option<TextureId>, ), MaterialId, >::new(); let mut materials = Vec::new(); for model in event_models { if let Some(node_by_key) = node_by_key.as_mut() { node_by_key.insert( model.cache_key.clone(), nodes_by_model .get(&model.model) .cloned() .unwrap_or_default(), ); } } let edge_colour_for = |colour: u32| { parsed .and_then(|parsed| { parsed .result .colours .get(polymodel_ldraw_core::ColourCode(colour)) }) .and_then(|colour| match &colour.edge { polymodel_ldraw_core::ColourEdge::Code(code) => Some(u32::from(*code)), polymodel_ldraw_core::ColourEdge::Data(_) => None, }) .unwrap_or(0) }; let colour_data = |colour: u32| { parsed.and_then(|parsed| { parsed .result .colours .get(polymodel_ldraw_core::ColourCode(colour)) .map(|colour| colour.data) }) }; let mut material_for = |root: RootId, colour: u32, texmap: Option<&SceneTexmapAssociation>| { let textures = &mut texture_for; let colour_data = colour_data(colour); let colour_rgb = colour_data.map(|data| [data.r, data.g, data.b]); let alpha = colour_data.and_then(|data| data.alpha).unwrap_or(u8::MAX); let albedo = resources .is_some() .then_some(texmap) .flatten() .filter(|texmap| texmap_available(root, texmap)) .map(|texmap| textures(root, texmap.pngfile.as_str(), "albedo")) .transpose()?; let roughness = resources .is_some() .then_some(texmap) .flatten() .and_then(|texmap| texmap.glossmap.as_deref()) .and_then(|reference| textures(root, reference, "metallic_roughness").ok()); let key = (colour, colour_rgb, alpha, albedo, roughness); Ok::<MaterialId, RendererError>(*material_ids.entry(key).or_insert_with(|| { let id = MaterialId((materials.len() + 1) as u64); materials.push(SceneMaterial { id, colour, colour_rgb, alpha, edge_colour: None, finish: None, local_mapping: None, albedo_texture: albedo, metallic_roughness_texture: roughness, }); id })) }; let instances = parsed_scene .map(|scene| scene.instances.as_slice()) .unwrap_or(&[]); if let Some(node_by_key) = node_by_key.as_ref() && std::env::var_os("POLYMODEL_LDRAW_ADAPTER_DIAGNOSTICS").is_some() { let mut exact_hits = 0usize; let mut exact_misses = 0usize; for (index, occurrence) in instances.iter().enumerate().take(1024) { let hit = occurrence .target_key .as_ref() .and_then(|key| node_by_key.get(key)) .is_some(); if hit { exact_hits += 1; } else { exact_misses += 1; } if index < 12 { tracing::debug!( "[ldraw-adapter] edge[{index}] source={} target={} target_key={:?} exact_hit={hit}", occurrence.source_model, occurrence.target_model, occurrence.target_key, ); } } tracing::debug!( "[ldraw-adapter] edges_sampled={} exact_target_hits={} exact_target_misses={}", instances.len().min(1024), exact_hits, exact_misses, ); } let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; let geometry_indices = geometries .iter() .enumerate() .map(|(index, geometry)| (geometry.id, index)) .collect::<HashMap<_, _>>(); let root_batches = nodes_by_model .get(&event_models[0].model) .ok_or_else(|| RendererError::ParseAdaptation("root model has no geometry batches"))?; let mut occurrences = Vec::new(); let mut stats = SceneStats::default(); for (node, primitive) in root_batches { let inherited_colour = u32::from(polymodel_ldraw_core::DEFAULT_COLOUR); let effective_colour = match primitive.colour { 16 => inherited_colour, 24 => edge_colour_for(inherited_colour), colour => colour, }; let geometry = geometries .get( *geometry_indices .get(&GeometryId(node.0)) .ok_or(RendererError::MissingGeometry(GeometryId(node.0)))?, ) .ok_or(RendererError::MissingGeometry(GeometryId(node.0)))?; accumulate_scene_stats(&mut stats, geometry)?; occurrences.push(SceneOccurrence { id: OccurrenceId((occurrences.len() + 1) as u64), canonical_occurrence_id: "root".into(), parent_occurrence_id: None, logical_part_occurrence_id: None, logical_part_path: None, node: *node, uvs: None, definition_id: definition_id_by_model[&event_models[0].model].clone(), canonical_path: canonical_path_by_model[&event_models[0].model].clone(), source_class: source_class_by_model[&event_models[0].model].clone(), source: path_by_model[&event_models[0].model].clone(), target: path_by_model[&event_models[0].model].clone(), source_span: None, direct_colour: u32::from(polymodel_ldraw_core::DEFAULT_COLOUR), effective_colour, primitive_colour: primitive.colour, material: Some(material_for( event_models[0].cache_key.resolved_root, effective_colour, primitive.texmap.as_ref(), )?), bfc_state: smol_str::format_smolstr!("{:?}", primitive.bfc_state), bfc_cull: primitive.bfc_cull, bfc_ccw: primitive.bfc_ccw, bfc_inverted: primitive.reverse_winding, transform: identity, }); } let meaningful_part = |path: &str| path.starts_with("parts/") && !path.starts_with("parts/s/"); let mut logical_parts = Vec::<Option<(smol_str::SmolStr, smol_str::SmolStr)>>::with_capacity(instances.len()); for (_instance_index, occurrence) in instances.iter().enumerate() { let inherited_logical_part = occurrence .parent .and_then(|parent| logical_parts.get(parent as usize)) .cloned() .flatten(); let target_key = occurrence.target_key.as_ref().ok_or_else(|| { RendererError::ParseAdaptation("renderable occurrence missing target identity") })?; let batches = nodes_by_model.get(&occurrence.model).ok_or_else(|| { RendererError::ParseAdaptation("occurrence model was not materialized") })?; let logical_part = inherited_logical_part.or_else(|| { meaningful_part(target_key.canonical_path.as_str()).then(|| { ( occurrence.instance_id.clone(), target_key.canonical_path.as_str().into(), ) }) }); logical_parts.push(logical_part.clone()); let parent_occurrence_id = occurrence .parent .and_then(|parent| instances.get(parent as usize)) .map(|parent| parent.instance_id.clone()); let inherited_colour = occurrence.colour.map(u32::from).unwrap_or(16); let occurrence_transform = occurrence.transform; let transform = world_transform(occurrence_transform)?; for (node, primitive) in batches { let (render_node, occurrence_uvs) = if let Some(texmap) = primitive.texmap.as_ref() { let source_index = geometry_indices .get(&GeometryId(node.0)) .copied() .ok_or_else(|| RendererError::MissingGeometry(GeometryId(node.0)))?; use std::hash::{Hash, Hasher}; let mut transform_hasher = std::collections::hash_map::DefaultHasher::new(); for value in transform.into_iter().flatten() { value.to_bits().hash(&mut transform_hasher); } let transform_hash = transform_hasher.finish(); let mut texmap_hasher = std::collections::hash_map::DefaultHasher::new(); match texmap.mode { SceneTexmapProjectionMode::Planar => 0_u8.hash(&mut texmap_hasher), SceneTexmapProjectionMode::Cylindrical => 1_u8.hash(&mut texmap_hasher), SceneTexmapProjectionMode::Spherical => 2_u8.hash(&mut texmap_hasher), } texmap.parameters.hash(&mut texmap_hasher); texmap.pngfile.hash(&mut texmap_hasher); texmap.glossmap.hash(&mut texmap_hasher); texmap.fallback.hash(&mut texmap_hasher); let texmap_hash = texmap_hasher.finish(); let cache_key = (GeometryId(node.0), transform_hash, texmap_hash); let occurrence_uvs = if let Some(uvs) = uv_cache.get(&cache_key) { uv_cache_hits += 1; uvs.clone() } else { uv_cache_misses += 1; let uvs: Arc<[[f32; 2]]> = Arc::from( occurrence_texmap_uvs(texmap, transform, &geometries[source_index].points)? .into_boxed_slice(), ); uv_cache.insert(cache_key, uvs.clone()); uvs }; (*node, Some(occurrence_uvs)) } else { (*node, None) }; let effective_colour = match primitive.colour { 16 => inherited_colour, 24 => edge_colour_for(inherited_colour), colour => colour, }; let geometry = geometries .get( *geometry_indices .get(&GeometryId(render_node.0)) .ok_or(RendererError::MissingGeometry(GeometryId(render_node.0)))?, ) .ok_or(RendererError::MissingGeometry(GeometryId(render_node.0)))?; accumulate_scene_stats(&mut stats, geometry)?; occurrences.push(SceneOccurrence { id: OccurrenceId((occurrences.len() + 1) as u64), canonical_occurrence_id: occurrence.instance_id.clone(), parent_occurrence_id: parent_occurrence_id.clone(), logical_part_occurrence_id: logical_part.as_ref().map(|(id, _)| id.clone()), logical_part_path: logical_part.as_ref().map(|(_, path)| path.clone()), node: render_node, uvs: occurrence_uvs, definition_id: definition_id_by_model[&occurrence.model].clone(), canonical_path: canonical_path_by_model[&occurrence.model].clone(), source_class: source_class_by_model[&occurrence.model].clone(), source: source_by_path .get(&occurrence.source_model) .cloned() .ok_or_else(|| { RendererError::ParseAdaptation( "occurrence source path was not materialized", ) })?, target: path_by_model .get(&occurrence.model) .cloned() .ok_or_else(|| { RendererError::ParseAdaptation( "occurrence target model was not materialized", ) })?, source_span: occurrence .source_span .map(|span| [span.start, span.end, span.line, span.column]), direct_colour: occurrence.direct_colour.map(u32::from).unwrap_or(16), effective_colour, primitive_colour: primitive.colour, material: Some(material_for( target_key.resolved_root, effective_colour, primitive.texmap.as_ref(), )?), bfc_state: smol_str::format_smolstr!("{:?}", primitive.bfc_state), bfc_cull: primitive.bfc_cull, bfc_ccw: primitive.bfc_ccw, bfc_inverted: occurrence.inverted ^ primitive.reverse_winding, transform, }); } } let bounds = compute_scene_bounds(parsed_scene, &occurrences, &geometries, &geometry_indices); if let Some(node_by_key) = node_by_key.as_ref() && std::env::var_os("POLYMODEL_LDRAW_SCENE_DIAGNOSTICS").is_some() { #[derive(Debug)] struct OccurrenceBoundsDiagnostic<'a> { max_extent: f64, center_distance: f64, index: usize, source: &'a str, target: &'a str, target_key: &'a polymodel_ldraw_core::CacheKey, min: [f64; 3], max: [f64; 3], }
let aggregate_center = bounds.map(|value| { [ (value.min[0] + value.max[0]) * 0.5, (value.min[1] + value.max[1]) * 0.5, (value.min[2] + value.max[2]) * 0.5, ] }); if let Some(value) = bounds { tracing::debug!( "[ldraw-scene] aggregate_bounds min={:?} max={:?} center={:?} extents={:?}", value.min, value.max, aggregate_center, [ value.max[0] - value.min[0], value.max[1] - value.min[1], value.max[2] - value.min[2], ], ); } let mut distribution = Vec::new(); for (index, occurrence) in instances.iter().enumerate() { let Some(target_key) = occurrence.target_key.as_ref() else { continue; }; let Some(batches) = node_by_key.get(target_key) else { continue; }; let mut min = [f64::INFINITY; 3]; let mut max = [f64::NEG_INFINITY; 3]; let mut point_count = 0usize; let Ok(transform) = world_transform(occurrence.transform) else { continue; }; for &(node, _) in batches { let Some(geometry) = geometry_indices .get(&GeometryId(node.0)) .and_then(|index| geometries.get(*index)) else { continue; }; for point in &geometry.points { let Ok(point) = apply_scene_transform(transform, *point) else { continue; }; point_count += 1; for axis in 0..3 { min[axis] = min[axis].min(point[axis]); max[axis] = max[axis].max(point[axis]); } } } if point_count == 0 { continue; } let center = [ (min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5, (min[2] + max[2]) * 0.5, ]; let max_extent = (0..3) .map(|axis| max[axis] - min[axis]) .fold(0.0_f64, f64::max); let center_distance = aggregate_center .map(|aggregate| { (0..3) .map(|axis| (center[axis] - aggregate[axis]).powi(2)) .sum::<f64>() .sqrt() }) .unwrap_or(0.0); distribution.push(OccurrenceBoundsDiagnostic { max_extent, center_distance, index, source: &occurrence.source_model, target: &occurrence.target_model, target_key, min, max, }); } distribution.sort_by(|left, right| { right .center_distance .partial_cmp(&left.center_distance) .unwrap_or(std::cmp::Ordering::Equal) }); tracing::debug!( "[ldraw-scene] occurrence_bounds count={} ranked_by=center_distance", distribution.len() ); for diagnostic in distribution.iter().take(8) { tracing::debug!( "[ldraw-scene] outlier index={} center_distance={:.3} max_extent={:.3} min={:?} max={:?} source={} target={} target_key={:?}", diagnostic.index, diagnostic.center_distance, diagnostic.max_extent, diagnostic.min, diagnostic.max, diagnostic.source, diagnostic.target, diagnostic.target_key, ); } distribution.sort_by(|left, right| { right .max_extent .partial_cmp(&left.max_extent) .unwrap_or(std::cmp::Ordering::Equal) }); for diagnostic in distribution.iter().take(8) { tracing::debug!( "[ldraw-scene] largest index={} center_distance={:.3} max_extent={:.3} min={:?} max={:?} source={} target={} target_key={:?}", diagnostic.index, diagnostic.center_distance, diagnostic.max_extent, diagnostic.min, diagnostic.max, diagnostic.source, diagnostic.target, diagnostic.target_key, ); } } if std::env::var_os("POLYMODEL_LDRAW_TIMING").is_some() { let total = uv_cache_hits + uv_cache_misses; let hit_rate = if total == 0 { 1.0 } else { uv_cache_hits as f64 / total as f64 }; eprintln!( "[ldraw-metrics] phase=uv-cache uv_cache_hits={} uv_cache_misses={} hits={} misses={} hit_rate={hit_rate:.6} entries={}", uv_cache_hits, uv_cache_misses, uv_cache_hits, uv_cache_misses, uv_cache.len(), ); } let root = models .first() .ok_or_else(|| RendererError::parse("parse emitted no root model"))?; let source = root.path.clone(); let occurrence_count = u64::try_from(occurrences.len()) .map_err(|_| RendererError::ParseAdaptation("scene occurrence count overflow"))?; let scene = Scene { id: SceneId(1), units: Units::declared(LengthUnit::LdrawUnit), source, provenance: provenance.into_boxed_slice(), diagnostics: parsed .map(|result| { result .result .diagnostics .iter() .enumerate() .map(|(index, diagnostic)| { polymodel_renderer_protocol::scene::SceneDiagnostic { id: polymodel_renderer_protocol::scene::DiagnosticId( (index + 1) as u64, ), message: diagnostic.message.as_str().into(), } }) .collect::<Vec<_>>() }) .unwrap_or_default() .into_boxed_slice(), root: NodeId(1), geometries: geometries.into_boxed_slice(), materials: materials.into_boxed_slice(), textures: textures.into_boxed_slice(), occurrences: occurrences.into_boxed_slice(), bounds, stats: SceneStats { occurrences: occurrence_count, ..stats }, }; Ok(scene)}
fn compute_scene_bounds( parsed_scene: Option<&polymodel_ldraw_core::SceneRecord>, occurrences: &[SceneOccurrence], geometries: &[SceneGeometry], geometry_indices: &HashMap<GeometryId, usize>,) -> Option<SceneBounds> { parsed_scene .and_then(|scene| scene.bounds) .map(|bounds| SceneBounds { min: [bounds.min.x, bounds.min.z, -bounds.max.y], max: [bounds.max.x, bounds.max.z, -bounds.min.y], }) .or_else(|| { let mut bounds_points = Vec::new(); for occurrence in occurrences { let geometry = geometry_indices .get(&GeometryId(occurrence.node.0)) .and_then(|index| geometries.get(*index))?; for point in &geometry.points { bounds_points.push(apply_scene_transform(occurrence.transform, *point).ok()?); } } SceneBounds::from_points(bounds_points) })}
fn world_transform( transform: polymodel_ldraw_core::Transform,) -> Result<[[f64; 4]; 4], RendererError> { let origin = ldraw_to_world([0.0, 0.0, 0.0]); let apply = |point: [f64; 3]| -> Result<[f64; 3], RendererError> { let source = [point[0], -point[2], point[1]]; let transformed = transform .apply(polymodel_ldraw_core::Point3::new( source[0], source[1], source[2], )) .ok_or(RendererError::NonFiniteTransform)?; Ok(ldraw_to_world([ transformed.x, transformed.y, transformed.z, ])) }; let translation = apply(origin)?; let axes = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; let columns = axes.map(apply).into_iter().collect::<Result<Vec<_>, _>>()?; Ok([ [ columns[0][0] - translation[0], columns[0][1] - translation[1], columns[0][2] - translation[2], 0.0, ], [ columns[1][0] - translation[0], columns[1][1] - translation[1], columns[1][2] - translation[2], 0.0, ], [ columns[2][0] - translation[0], columns[2][1] - translation[1], columns[2][2] - translation[2], 0.0, ], [translation[0], translation[1], translation[2], 1.0], ])}
fn occurrence_texmap_uvs( texmap: &SceneTexmapAssociation, transform: [[f64; 4]; 4], points: &[[f64; 3]],) -> Result<Vec<[f32; 2]>, RendererError> { let mut parameters = texmap .parameters .iter() .map(|value| { value .parse::<f64>() .map_err(|_| RendererError::parse("invalid TEXMAP parameter")) }) .collect::<Result<Vec<_>, _>>()?; let control_points = 9; let expected = match texmap.mode { SceneTexmapProjectionMode::Planar => 9, SceneTexmapProjectionMode::Cylindrical => 10, SceneTexmapProjectionMode::Spherical => 11, }; if parameters.len() != expected { return Err(RendererError::parse("invalid TEXMAP parameter count")); } for offset in (0..control_points).step_by(3) { let source = [ parameters[offset], parameters[offset + 1], parameters[offset + 2], ]; let transformed = apply_scene_transform(transform, ldraw_to_world(source))?; parameters[offset..offset + 3].copy_from_slice(&transformed); } let transformed_points = points .iter() .copied() .map(|point| apply_scene_transform(transform, point)) .collect::<Result<Vec<_>, _>>()?; texmap_uvs(texmap.mode, ¶meters, &transformed_points) .map_err(|_| RendererError::parse("invalid TEXMAP UV projection"))}
fn scene_transform_reflection(transform: [[f64; 4]; 4]) -> Option<bool> { let a = transform[0][0]; let b = transform[1][0]; let c = transform[2][0]; let d = transform[0][1]; let e = transform[1][1]; let f = transform[2][1]; let g = transform[0][2]; let h = transform[1][2]; let i = transform[2][2]; let determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); (determinant.is_finite() && determinant.abs() > f64::EPSILON).then_some(determinant < 0.0)}
fn apply_scene_transform( transform: [[f64; 4]; 4], point: [f64; 3],) -> Result<[f64; 3], RendererError> { let values = [point[0], point[1], point[2], 1.0]; let output = (0..3) .map(|row| { (0..4) .map(|column| transform[column][row] * values[column]) .sum::<f64>() }) .collect::<Vec<_>>(); if output.iter().all(|value| value.is_finite()) { Ok([output[0], output[1], output[2]]) } else { Err(RendererError::NonFiniteTransform) }}
#[derive(Debug, Clone, Copy, PartialEq)]struct InverseSceneTransform { linear: [[f64; 3]; 3], translation: [f64; 3],}
impl InverseSceneTransform { fn new(transform: [[f64; 4]; 4]) -> Option<Self> { let a = transform[0][0]; let b = transform[1][0]; let c = transform[2][0]; let d = transform[0][1]; let e = transform[1][1]; let f = transform[2][1]; let g = transform[0][2]; let h = transform[1][2]; let i = transform[2][2]; let determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); if !determinant.is_finite() || determinant.abs() <= f64::EPSILON { return None; } let reciprocal = determinant.recip(); let linear = [ [ (e * i - f * h) * reciprocal, (c * h - b * i) * reciprocal, (b * f - c * e) * reciprocal, ], [ (f * g - d * i) * reciprocal, (a * i - c * g) * reciprocal, (c * d - a * f) * reciprocal, ], [ (d * h - e * g) * reciprocal, (b * g - a * h) * reciprocal, (a * e - b * d) * reciprocal, ], ]; let world_translation = [transform[3][0], transform[3][1], transform[3][2]]; let translation = linear.map(|row| { -(row[0] * world_translation[0] + row[1] * world_translation[1] + row[2] * world_translation[2]) }); linear .iter() .flatten() .chain(translation.iter()) .all(|value| value.is_finite()) .then_some(Self { linear, translation, }) }
fn ray(self, ray: PickRay) -> PickRay { let apply = |value: [f64; 3]| { self.linear .map(|row| row[0] * value[0] + row[1] * value[1] + row[2] * value[2]) }; let mut origin = apply(ray.origin); for (value, translation) in origin.iter_mut().zip(self.translation) { *value += translation; } PickRay { origin, direction: apply(ray.direction), } }}
/// Convert a world-space camera direction to the local space of one occurrence./// Conditional-line classification is defined in the part's projected plane, so/// translation is intentionally ignored and only the affine linear component is inverted.pub fn conditional_line_view_for_transform( transform: [[f64; 4]; 4], world_view: [f64; 3],) -> Option<[f64; 3]> { let a = transform[0][0]; let b = transform[1][0]; let c = transform[2][0]; let d = transform[0][1]; let e = transform[1][1]; let f = transform[2][1]; let g = transform[0][2]; let h = transform[1][2]; let i = transform[2][2]; let determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); if !determinant.is_finite() || determinant.abs() <= f64::EPSILON { return None; } let inverse = [ [e * i - f * h, c * h - b * i, b * f - c * e], [f * g - d * i, a * i - c * g, c * d - a * f], [d * h - e * g, b * g - a * h, a * e - b * d], ]; let result = [ (inverse[0][0] * world_view[0] + inverse[0][1] * world_view[1] + inverse[0][2] * world_view[2]) / determinant, (inverse[1][0] * world_view[0] + inverse[1][1] * world_view[1] + inverse[1][2] * world_view[2]) / determinant, (inverse[2][0] * world_view[0] + inverse[2][1] * world_view[1] + inverse[2][2] * world_view[2]) / determinant, ]; result .iter() .all(|value| value.is_finite()) .then_some(result)}
fn scene_transform_to_mat4(matrix: [[f64; 4]; 4]) -> Result<Mat4, RendererError> { Ok(Mat4::new( finite_f32(matrix[0][0])?, finite_f32(matrix[0][1])?, finite_f32(matrix[0][2])?, finite_f32(matrix[0][3])?, finite_f32(matrix[1][0])?, finite_f32(matrix[1][1])?, finite_f32(matrix[1][2])?, finite_f32(matrix[1][3])?, finite_f32(matrix[2][0])?, finite_f32(matrix[2][1])?, finite_f32(matrix[2][2])?, finite_f32(matrix[2][3])?, finite_f32(matrix[3][0])?, finite_f32(matrix[3][1])?, finite_f32(matrix[3][2])?, finite_f32(matrix[3][3])?, ))}
#[derive(Clone, Copy, Debug, PartialEq)]pub struct NativeCameraFit { pub center: three_d::Vec3, pub position: three_d::Vec3, pub near: f32, pub far: f32, pub control_min: f32, pub control_max: f32,}
pub const CAMERA_CASE_SCHEMA: &str = "pm-camera-case-v1";
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]pub struct CameraCase { pub case_id: String, pub schema_version: String, pub yaw_degrees: f64, pub pitch_degrees: f64, pub roll_degrees: f64, pub target: [f64; 3], pub distance: Option<f64>, pub fov_degrees: f64, pub width: u32, pub height: u32, pub fit_bounds: bool,}
#[derive(Clone, Debug, Eq, PartialEq)]pub enum CameraCaseError { UnknownSchema(String), EmptyCaseId, ZeroViewport, NonFinite(String), InvalidDistance, InvalidFov,}
impl std::fmt::Display for CameraCaseError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "{self:?}") }}
impl std::error::Error for CameraCaseError {}
impl CameraCase { pub fn validate(&self) -> Result<(), CameraCaseError> { if self.schema_version != CAMERA_CASE_SCHEMA { return Err(CameraCaseError::UnknownSchema(self.schema_version.clone())); } if self.case_id.is_empty() { return Err(CameraCaseError::EmptyCaseId); } if self.width == 0 || self.height == 0 { return Err(CameraCaseError::ZeroViewport); } for (name, value) in [ ("yaw_degrees", self.yaw_degrees), ("pitch_degrees", self.pitch_degrees), ("roll_degrees", self.roll_degrees), ("fov_degrees", self.fov_degrees), ] { if !value.is_finite() { return Err(CameraCaseError::NonFinite(name.to_owned())); } } for (index, value) in self.target.into_iter().enumerate() { if !value.is_finite() { return Err(CameraCaseError::NonFinite(format!("target[{index}]"))); } } if let Some(distance) = self.distance && (!distance.is_finite() || distance <= 0.0) { return Err(CameraCaseError::InvalidDistance); } if !self.fov_degrees.is_finite() || !(0.0 < self.fov_degrees && self.fov_degrees < 180.0) { return Err(CameraCaseError::InvalidFov); } Ok(()) }
pub fn default_for(case_id: impl Into<String>) -> Self { Self { case_id: case_id.into(), schema_version: CAMERA_CASE_SCHEMA.to_owned(), yaw_degrees: 0.0, pitch_degrees: 0.0, roll_degrees: 0.0, target: [0.0, 0.0, 0.0], distance: None, fov_degrees: 45.0, width: 1280, height: 720, fit_bounds: true, } }
pub fn encoded_json(&self) -> Result<String, CameraCaseError> { self.validate()?; serde_json::to_string(self).map_err(|_| CameraCaseError::NonFinite("serialization".into())) }
#[cfg(not(target_arch = "wasm32"))] pub fn native_pose(&self, fit: NativeCameraFit) -> Result<NativeCameraPose, CameraCaseError> { self.validate()?; use three_d::InnerSpace as _; let target = three_d::vec3( self.target[0] as f32, self.target[1] as f32, self.target[2] as f32, ); let target = if self.fit_bounds { fit.center } else { target }; let distance = self .distance .map(|value| value as f32) .unwrap_or_else(|| (fit.position - fit.center).magnitude()); let mut view = (fit.center - fit.position).normalize(); let world_up = three_d::vec3(0.0, 0.0, 1.0); let rotate = |vector: three_d::Vec3, axis: three_d::Vec3, degrees: f64| { let radians = (degrees as f32).to_radians(); let (sin, cos) = radians.sin_cos(); vector * cos + axis.cross(vector) * sin + axis * axis.dot(vector) * (1.0 - cos) }; view = rotate(view, world_up, self.yaw_degrees); let right = view.cross(world_up).normalize(); view = rotate(view, right, self.pitch_degrees); let up = rotate(world_up, view, self.roll_degrees).normalize(); Ok(NativeCameraPose { target, position: target - view * distance, up, near: fit.near, far: fit.far, fov_degrees: self.fov_degrees as f32, }) }}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Copy, Debug, PartialEq)]pub struct NativeCameraPose { pub target: three_d::Vec3, pub position: three_d::Vec3, pub up: three_d::Vec3, pub near: f32, pub far: f32, pub fov_degrees: f32,}
#[derive(Clone, Debug, Eq, PartialEq)]pub struct SceneVariantBinding { pub variant_id: String, pub occurrence_id: String, pub definition_id: String, pub source_span: Option<[u32; 4]>, pub occurrence_index: usize, pub determinant: i8, pub topology: String, pub material_id: u32,}
impl SceneVariantBinding { pub fn for_case(case_id: &str) -> Result<Self, RendererError> { let reflected = case_id.starts_with("reflected_"); let variant_id = if reflected { "reflected" } else { "identity" }; Ok(Self { variant_id: variant_id.to_owned(), occurrence_id: if reflected { "fixture-reflected" } else { "fixture-identity" } .into(), definition_id: "pm90-lighting-fixture-v1".into(), source_span: Some([1, 1, 1, 1]), occurrence_index: usize::from(reflected), determinant: if reflected { -1 } else { 1 }, topology: "triangles".into(), material_id: 4, }) }}
pub fn named_camera_case(case_id: &str) -> Result<CameraCase, CameraCaseError> { let mut case = CameraCase::default_for(case_id); case.width = 2560; case.height = 1440; match case_id { "identity_front" | "reflected_front" => {} "identity_back" | "reflected_back" => case.yaw_degrees = 180.0, "orbit_left" => case.yaw_degrees = -35.0, "close_range" => { case.distance = Some(0.35); case.fit_bounds = false; } "top_down" => case.pitch_degrees = -90.0, "top" => case.pitch_degrees = -90.0, "bottom" => case.pitch_degrees = 90.0, other => return Err(CameraCaseError::UnknownSchema(other.to_owned())), } case.validate()?; Ok(case)}
#[derive(Clone, Debug, Eq, PartialEq)]pub struct NativeCaptureMetadata { pub case_id: String, pub schema_version: String, pub variant: SceneVariantBinding, pub fixture_hash: String, pub requested_width: u32, pub requested_height: u32, pub actual_width: u32, pub actual_height: u32, pub native_path: String,}
pub const LIGHTING_DIAGNOSTICS_ENV: &str = "POLYMODEL_RENDERER_LIGHTING_DIAGNOSTICS";
pub fn lighting_diagnostics_enabled() -> bool { std::env::var(LIGHTING_DIAGNOSTICS_ENV).as_deref() == Ok("1")}
pub fn emit_lighting_diagnostic(metadata: &NativeCaptureMetadata, scene: &Scene) { if lighting_diagnostics_enabled() { tracing::debug!( "[ldraw-diagnostic] case={} schema={} fixture={} path={} variant={} occurrence={} source_span={:?} determinant={} material_id={} scene_triangles={}", metadata.case_id, metadata.schema_version, metadata.fixture_hash, metadata.native_path, metadata.variant.variant_id, metadata.variant.occurrence_id, metadata.variant.source_span, metadata.variant.determinant, metadata.variant.material_id, scene.stats.triangles, ); }}
pub const PM90_FIXTURE: &[u8] = b"0 PM-90 lighting fixture v1\n0 BFC CERTIFY CCW\n3 4 0 0 0 1 0 0 0 1 0\n3 14 0 0 1 1 0 1 0 1 1\n0 BFC CERTIFY CW\n0 BFC INVERTNEXT\n3 4 0 0 -1 1 0 -1 0 1 -1\n";
pub fn pm90_fixture_hash() -> String { use sha2::{Digest, Sha256}; format!("{:x}", Sha256::digest(PM90_FIXTURE))}
pub const CAMERA_DEPTH_RATIO_LIMIT: f64 = 4096.0;const CAMERA_DEPTH_MARGIN_FRACTION: f64 = 0.01;const CAMERA_DEPTH_ROUNDING_FLOOR: f64 = 0.00034526698;
#[derive(Clone, Copy, Debug, PartialEq)]pub struct CameraDepthInterval { pub min_depth: f64, pub max_depth: f64,}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum CameraDepthError { InvalidBounds, InvalidCamera, InvalidInterval, DepthRangeExceedsPrecision, F32Conversion,}
impl std::fmt::Display for CameraDepthError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "{self:?}") }}
impl std::error::Error for CameraDepthError {}
fn finite_ordered_bounds(bounds: SceneBounds) -> Result<([f64; 3], [f64; 3]), CameraDepthError> { for axis in 0..3 { if !bounds.min[axis].is_finite() || !bounds.max[axis].is_finite() || bounds.min[axis] > bounds.max[axis] { return Err(CameraDepthError::InvalidBounds); } } Ok((bounds.min, bounds.max))}
/// Return the signed f64 camera-space interval for all eight bounds corners.pub fn camera_space_depth_interval( bounds: SceneBounds, position: three_d::Vec3, target: three_d::Vec3,) -> Result<CameraDepthInterval, CameraDepthError> { let (min, max) = finite_ordered_bounds(bounds)?; let position = [ f64::from(position.x), f64::from(position.y), f64::from(position.z), ]; let target = [ f64::from(target.x), f64::from(target.y), f64::from(target.z), ]; if position .iter() .chain(target.iter()) .any(|value| !value.is_finite()) { return Err(CameraDepthError::InvalidCamera); } let mut forward = [ target[0] - position[0], target[1] - position[1], target[2] - position[2], ]; let length = (forward[0] * forward[0] + forward[1] * forward[1] + forward[2] * forward[2]).sqrt(); if !length.is_finite() || length <= f64::EPSILON { return Err(CameraDepthError::InvalidCamera); } for component in &mut forward { *component /= length; } let mut interval = CameraDepthInterval { min_depth: f64::INFINITY, max_depth: f64::NEG_INFINITY, }; for x in [min[0], max[0]] { for y in [min[1], max[1]] { for z in [min[2], max[2]] { let offset = [x - position[0], y - position[1], z - position[2]]; let depth = offset[0] * forward[0] + offset[1] * forward[1] + offset[2] * forward[2]; interval.min_depth = interval.min_depth.min(depth); interval.max_depth = interval.max_depth.max(depth); } } } if interval.min_depth.is_finite() && interval.max_depth.is_finite() && interval.min_depth <= interval.max_depth { Ok(interval) } else { Err(CameraDepthError::InvalidInterval) }}
fn scene_radius_f64(bounds: SceneBounds) -> Result<f64, CameraDepthError> { let (min, max) = finite_ordered_bounds(bounds)?; let half = [ (max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5, ]; let radius = (half[0] * half[0] + half[1] * half[1] + half[2] * half[2]).sqrt(); radius .is_finite() .then_some(radius) .ok_or(CameraDepthError::InvalidBounds)}
fn precision_aware_clip_planes( interval: CameraDepthInterval, camera_target_distance: f64, scene_radius: f64,) -> Result<(f32, f32), CameraDepthError> { if !camera_target_distance.is_finite() || camera_target_distance <= 0.0 || !scene_radius.is_finite() || scene_radius < 0.0 { return Err(CameraDepthError::InvalidCamera); } let min_depth = interval.min_depth; let max_depth = interval.max_depth; if !min_depth.is_finite() || !max_depth.is_finite() || min_depth > max_depth || max_depth <= 0.0 { return Err(CameraDepthError::InvalidInterval); } let span = max_depth - min_depth; let scale = camera_target_distance .max(scene_radius) .max(max_depth) .max(1.0) .min(f64::from(f32::MAX)); let margin = (span * CAMERA_DEPTH_MARGIN_FRACTION).max(scale * f64::from(f32::EPSILON)); let near_floor = scale * f64::from(f32::EPSILON); let mut near = if min_depth > 0.0 { (min_depth - margin).max(near_floor).min(min_depth) } else { scale * CAMERA_DEPTH_ROUNDING_FLOOR }; let far = (max_depth + margin).max(near * (1.0 + f64::from(f32::EPSILON))); near = near.max(far / CAMERA_DEPTH_RATIO_LIMIT); let near_f32 = finite_f32(near).map_err(|_| CameraDepthError::F32Conversion)?; let far_f32 = finite_f32(far).map_err(|_| CameraDepthError::F32Conversion)?; let max_f32 = finite_f32(max_depth).map_err(|_| CameraDepthError::F32Conversion)?; if near_f32 <= 0.0 || far_f32 <= near_f32 || far_f32 < max_f32 { return Err(CameraDepthError::F32Conversion); } Ok((near_f32, far_f32))}
pub fn scene_camera_fit( bounds: SceneBounds, width: u32, height: u32,) -> Result<NativeCameraFit, CameraDepthError> { use three_d::InnerSpace as _; if width == 0 || height == 0 { return Err(CameraDepthError::InvalidBounds); } let (min, max) = finite_ordered_bounds(bounds)?; let center = three_d::vec3( finite_f32((min[0] + max[0]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, finite_f32((min[1] + max[1]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, finite_f32((min[2] + max[2]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, ); let half = three_d::vec3( finite_f32((max[0] - min[0]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, finite_f32((max[1] - min[1]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, finite_f32((max[2] - min[2]) * 0.5).map_err(|_| CameraDepthError::F32Conversion)?, ); let view = three_d::vec3(1.8, -2.4, 1.5).normalize(); let right = view.cross(three_d::vec3(0.0, 0.0, 1.0)).normalize(); let up = right.cross(view).normalize(); let projected_half = |axis: three_d::Vec3| axis.x.abs() * half.x + axis.y.abs() * half.y + axis.z.abs() * half.z; let depth = projected_half(view); let vertical = projected_half(up); let horizontal = projected_half(right); let tan_half_fov = (45.0_f32.to_radians() * 0.5).tan(); let aspect = width as f32 / height as f32; let distance = depth + (vertical / tan_half_fov).max(horizontal / (tan_half_fov * aspect)) * 1.08; if !distance.is_finite() || distance <= 0.0 { return Err(CameraDepthError::InvalidCamera); } let position = center + view * distance; let projection = camera_space_clip_planes(bounds, position, center)?; Ok(NativeCameraFit { center, position, near: projection.near, far: projection.far, control_min: finite_f32( (scene_radius_f64(bounds)? * CAMERA_DEPTH_ROUNDING_FLOOR).max(f64::from(f32::EPSILON)), ) .map_err(|_| CameraDepthError::F32Conversion)? .min(distance), control_max: projection.far, })}
#[cfg(not(target_arch = "wasm32"))]pub fn yaw_camera_position( center: three_d::Vec3, position: three_d::Vec3, degrees: f32,) -> three_d::Vec3 { let offset = position - center; let radians = degrees.to_radians(); let (sin, cos) = radians.sin_cos(); center + three_d::vec3( offset.x * cos - offset.y * sin, offset.x * sin + offset.y * cos, offset.z, )}
#[derive(Clone, Copy, Debug, PartialEq)]pub struct CameraClipPlanes { pub near: f32, pub far: f32, pub min_depth: f32, pub max_depth: f32,}
pub fn camera_clip_planes( distance: f32, scene_radius: f32,) -> Result<(f32, f32), CameraDepthError> { if !distance.is_finite() || distance <= 0.0 || !scene_radius.is_finite() || scene_radius < 0.0 { return Err(CameraDepthError::InvalidCamera); } let min_depth = distance as f64 - scene_radius as f64; let max_depth = distance as f64 + scene_radius as f64; let projection = precision_aware_clip_planes( CameraDepthInterval { min_depth, max_depth, }, f64::from(distance), f64::from(scene_radius), )?; Ok(projection)}
pub fn camera_space_clip_planes( bounds: SceneBounds, position: three_d::Vec3, target: three_d::Vec3,) -> Result<CameraClipPlanes, CameraDepthError> { use three_d::InnerSpace as _; let interval = camera_space_depth_interval(bounds, position, target)?; let (near, far) = precision_aware_clip_planes( interval, f64::from((target - position).magnitude()), scene_radius_f64(bounds)?, )?; Ok(CameraClipPlanes { near, far, min_depth: finite_f32(interval.min_depth).map_err(|_| CameraDepthError::F32Conversion)?, max_depth: finite_f32(interval.max_depth).map_err(|_| CameraDepthError::F32Conversion)?, })}
pub fn scene_radius(bounds: SceneBounds) -> Result<f32, CameraDepthError> { let (min, max) = finite_ordered_bounds(bounds)?; let radius = ((max[0] - min[0]).mul_add(0.5, 0.0).powi(2) + (max[1] - min[1]).mul_add(0.5, 0.0).powi(2) + (max[2] - min[2]).mul_add(0.5, 0.0).powi(2)) .sqrt(); finite_f32(radius).map_err(|_| CameraDepthError::F32Conversion)}
#[cfg(not(target_arch = "wasm32"))]fn official_texture_resources( closure: &polymodel_ldraw_core::ResourceClosure,) -> Result<Vec<ModelResource>, RendererError> { closure.validate().map_err(|error| { RendererError::External(format!("official resource closure rejected: {error}")) })?; let mut resources = Vec::new(); for dependency in closure.texture_dependencies() { let Some(resolved) = closure.resolved.iter().find(|resource| { resource.root == dependency.root && resource.path == dependency.path && resource.mime_type.starts_with("image/") }) else { continue; }; let resource = ModelResource { key: dependency.path.as_str().to_owned(), path: dependency.path.as_str().to_owned(), root: texture_resource_root(dependency.root).to_owned(), byte_length: resolved.byte_length, sha256: resolved.sha256.clone().try_into().map_err(|_| { RendererError::External( "official resource SHA-256 digest has invalid length".into(), ) })?, mime_type: resolved.mime_type.to_owned(), target_cids: Box::default(), bytes: resolved.bytes.clone().into_boxed_slice(), }; resource.validate().map_err(|error| { RendererError::External(format!("official TEXMAP resource rejected: {error:?}")) })?; if !resources.iter().any(|existing: &ModelResource| { existing.root == resource.root && existing.path == resource.path }) { resources.push(resource); } } Ok(resources)}
#[cfg(not(target_arch = "wasm32"))]pub fn prepare_preview_scene( input: &std::path::Path, library_root: &std::path::Path,) -> Result<Scene, RendererError> { use polymodel_ldraw_core::{ AdapterRequest, CancellationPolicy, InProcessRustAdapter, Materialization, NormalizedPath, ParserProfile, RootId, }; let bytes = std::fs::read(input) .map_err(|error| RendererError::Io(format!("read {}: {error}", input.display())))?; let logical_name = input .file_name() .and_then(std::ffi::OsStr::to_str) .ok_or_else(|| { RendererError::External(format!( "preview input has no UTF-8 file name: {}", input.display() )) })?; let path = NormalizedPath::new(logical_name) .map_err(|error| RendererError::External(format!("preview input path: {error}")))?; let started = Instant::now(); let closure = official_materializations(library_root, &path, &bytes)?; tracing::debug!( "[ldraw-preview] preparation-stage=closure elapsed_ms={} materializations={} selections={}", started.elapsed().as_millis(), closure.materializations.len(), closure.target_selections.len(), ); let resources = official_texture_resources(&closure.resource_closure)?; let mut materializations = closure.materializations; let target_selections = closure.target_selections; materializations.retain(|materialization| { !closure .resource_closure .texture_dependencies() .any(|dependency| { materialization.root == dependency.root && materialization.path == dependency.path }) }); materializations.insert( 0, Materialization::from_key( polymodel_ldraw_core::CacheKey::new(path.clone(), RootId::CurrentMpd, &bytes), bytes.clone(), true, Some("pinned-omr".into()), ), ); let mut sink = AdapterSink::new(Some(&resources)); let result = InProcessRustAdapter::parse_with_limits_and_sink( AdapterRequest { bytes: &bytes, profile: ParserProfile::Compatibility, provenance_id: logical_name, root_name: logical_name, root: RootId::CurrentMpd, materializations: &materializations, target_selections: &target_selections, cancellation: CancellationPolicy::default(), }, preview_limits(), &mut sink, ) .map_err(|error| RendererError::ParseMessage(format!("pinned OMR parse: {error}")))?; sink.take_error()?; let scene = adapt_ldraw_parse_result(&result, Some(&resources), sink)?; if scene.stats.triangles == 0 || scene.stats.occurrences == 0 || scene.bounds.is_none() { return Err(RendererError::ParseMessage(format!( "preview root scene incomplete: occurrences={} triangles={} bounds={}", scene.stats.occurrences, scene.stats.triangles, scene.bounds.is_some() ))); } Ok(scene)}
#[cfg(not(target_arch = "wasm32"))]pub fn preview_limits() -> polymodel_ldraw_core::LdrawLimits { polymodel_ldraw_core::LdrawLimits { resource_bytes: 256 * 1024 * 1024, line_bytes: 1024 * 1024, files: 8_192, include_depth: 128, commands: 2_000_000, instances: 500_000, triangles: 1_000_000_000_000, lines: 1_000_000_000, textures: 1_024, fetches: 8, diagnostics: 20_000, }}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Debug)]pub struct OfficialClosure { pub materializations: Vec<Materialization>, pub target_selections: Vec<polymodel_ldraw_core::TargetSelection>, pub resource_closure: polymodel_ldraw_core::ResourceClosure,}
#[cfg(not(target_arch = "wasm32"))]pub fn official_materializations( library_root: &std::path::Path, root_path: &NormalizedPath, root_bytes: &[u8],) -> Result<OfficialClosure, RendererError> { use polymodel_ldraw_core::CacheKey; use std::collections::{HashMap, HashSet, VecDeque};
const MAX_FILES: usize = 4096; const MAX_EDGES: usize = 16_384; const MAX_BYTES: usize = 256 * 1024 * 1024; const MAX_DEPTH: usize = 128;
let canonical_root = library_root.canonicalize().map_err(|error| { RendererError::Io(format!("canonicalize official LDraw library: {error}")) })?; if !canonical_root.is_dir() { return Err(RendererError::External(format!( "official LDraw library is not a directory: {}", library_root.display() ))); } #[derive(Clone)] struct Pending { path: NormalizedPath, bytes: Vec<u8>, key: CacheKey, depth: usize, } let root = if root_path.as_str().starts_with("parts/") || root_path.as_str().starts_with("p/") { RootId::OfficialLibrary } else { RootId::CurrentMpd }; let root_key = CacheKey::new(root_path.clone(), root, root_bytes); let mut queue = VecDeque::from([Pending { path: root_path.clone(), bytes: root_bytes.to_vec(), key: root_key, depth: 0, }]); let mut seen = HashSet::<CacheKey>::new(); let mut materialization_cache = HashMap::<(RootId, NormalizedPath), (CacheKey, Vec<u8>)>::new(); let mut resolution_cache = HashMap::<(String, String), Option<(String, std::path::PathBuf, String)>>::new(); let mut materializations = Vec::new(); let mut target_selections = Vec::new(); let mut resource_closure = polymodel_ldraw_core::ResourceClosure { include_references: Vec::new(), dependencies: Vec::new(), resolved: Vec::new(), needs_ldconfig: false, }; let mut total_bytes = root_bytes.len();
while let Some(current) = queue.pop_front() { if current.depth > MAX_DEPTH { return Err(RendererError::External(format!( "official closure depth exceeded {MAX_DEPTH}" ))); } let inspection = polymodel_ldraw_core::inspect_compound_source( ¤t.bytes, ¤t.path, current.key.resolved_root, ) .map_err(|error| { RendererError::ParseMessage(format!("inspect {}: {error}", current.path.as_str())) })?; let mut inspection = inspection; let source_keys = std::mem::take(&mut inspection.source_keys); let has_virtual_files = inspection.has_virtual_files; let inspection_closure = polymodel_ldraw_core::ResourceClosure::from_inspection( &mut inspection, ¤t.path, current.key.resolved_root, ); let polymodel_ldraw_core::ResourceClosure { include_references: references, dependencies, needs_ldconfig, .. } = inspection_closure; resource_closure.dependencies.extend(dependencies); resource_closure.needs_ldconfig |= needs_ldconfig; for reference in references { if reference.target_virtual_file { continue; } let resolved = polymodel_ldraw_core::resolve_reference_path(¤t.path, &reference) .map_err(|error| { RendererError::ParseMessage(format!("resolve {}: {error}", reference.target)) })?; let unqualified_cache_key = ( "unqualified".to_owned(), reference.target.to_ascii_lowercase(), ); let unqualified_official = if !reference.target.contains('/') && matches!(resolved.root, polymodel_ldraw_core::ReferenceRoot::Models) { if let Some(cached) = resolution_cache.get(&unqualified_cache_key) { cached.clone().map(|(prefix, _canonical, relative)| { ( NormalizedPath::new(&format!("{prefix}/{relative}")) .expect("cached official path remains normalized"), prefix == "parts", ) }) } else { let resolved = ["parts", "p"].iter().find_map(|prefix| { let relative = reference.target.clone(); let candidate = library_root.join(prefix).join(&relative); candidate.canonicalize().ok().and_then(|canonical| { canonical .starts_with(&canonical_root) .then(|| ((*prefix).to_owned(), canonical, relative)) }) }); resolution_cache.insert(unqualified_cache_key, resolved.clone()); resolved.map(|(prefix, _canonical, relative)| { ( NormalizedPath::new(&format!("{prefix}/{relative}")) .expect("resolved official path remains normalized"), prefix == "parts", ) }) } } else { None }; let (path, selected_root) = if let Some((path, is_parts)) = unqualified_official { (path, if is_parts { "parts" } else { "p" }) } else { if !matches!( resolved.root, polymodel_ldraw_core::ReferenceRoot::Parts | polymodel_ldraw_core::ReferenceRoot::P ) { continue; } let path = resolved.path; let selected_root = if path.as_str().starts_with("parts/") { "parts" } else { "p" }; (path, selected_root) }; let source = if has_virtual_files { source_keys .iter() .find(|(name, _)| name == &reference.file_name.to_ascii_lowercase()) .map(|(_, key)| key.clone()) .ok_or_else(|| { RendererError::External(format!( "source section missing: {}", reference.file_name )) })? } else { current.key.clone() }; let relative = path .as_str() .strip_prefix("parts/") .or_else(|| path.as_str().strip_prefix("p/")) .ok_or_else(|| { RendererError::External(format!( "official path outside library roots: {}", path.as_str() )) })?; let fallback_root = if selected_root == "parts" { "p" } else { "parts" }; let authored_target = reference.target.trim().replace('\\', "/"); let authored_subpart = authored_target .split_once('/') .and_then(|(prefix, suffix)| { (prefix.eq_ignore_ascii_case("s") && !suffix.is_empty()).then_some(suffix) }); let mut canonical_path = None; let mut candidates = if selected_root == "parts" { if let Some(authored_suffix) = authored_subpart { vec![ ("parts", format!("s/{authored_suffix}")), ("p", authored_suffix.to_owned()), ("parts", authored_suffix.to_owned()), ] } else { vec![ (selected_root, relative.to_owned()), (fallback_root, relative.to_owned()), ] } } else { vec![ (selected_root, relative.to_owned()), (fallback_root, relative.to_owned()), ] }; if selected_root == "parts" && authored_subpart.is_none() { if !authored_target.contains('/') { candidates.extend([ ("p", authored_target.clone()), ("parts", authored_target.clone()), ]); } candidates.push(("parts", format!("s/{relative}"))); if !relative.starts_with("8/") && !relative.starts_with("48/") { candidates.extend([ ("parts", format!("8/{relative}")), ("parts", format!("48/{relative}")), ]); } } else if selected_root != "parts" && !relative.starts_with("8/") && !relative.starts_with("48/") { candidates.extend([ ("p", format!("8/{relative}")), ("p", format!("48/{relative}")), ]); } let resolution_key = ( format!("{selected_root}:{}", authored_target.to_ascii_lowercase()), relative.to_owned(), ); if let Some(cached) = resolution_cache.get(&resolution_key) { canonical_path = cached.clone(); } else { for (prefix, relative_candidate) in candidates.drain(..) { let candidate = library_root.join(prefix).join(&relative_candidate); if let Ok(path) = candidate.canonicalize() { if !path.starts_with(&canonical_root) { return Err(RendererError::External(format!( "official path escapes library: {}", candidate.display() ))); } canonical_path = Some((prefix.to_owned(), path, relative_candidate)); break; } } resolution_cache.insert(resolution_key, canonical_path.clone()); } let (actual_root, canonical_path, relative_candidate) = canonical_path.ok_or_else(|| { RendererError::External(format!( "official part is missing from parts/ and p/: {}", path.as_str() )) })?; let logical_path = NormalizedPath::new(&format!("{actual_root}/{relative_candidate}")) .map_err(|error| { RendererError::External(format!("normalize official part path: {error}")) })?; let cache_key = (RootId::OfficialLibrary, logical_path.clone()); let (target, child_bytes) = if let Some((target, bytes)) = materialization_cache.get(&cache_key) { (target.clone(), bytes.clone()) } else { let child_bytes = std::fs::read(&canonical_path).map_err(|error| { RendererError::Io(format!("read {}: {error}", canonical_path.display())) })?; let target = CacheKey::new(logical_path.clone(), RootId::OfficialLibrary, &child_bytes); materialization_cache.insert(cache_key, (target.clone(), child_bytes.clone())); (target, child_bytes) }; target_selections.push(polymodel_ldraw_core::TargetSelection { source, span: reference.span, target: target.clone(), }); if seen.insert(target.clone()) { if materializations.len() >= MAX_FILES { return Err(RendererError::External(format!( "official closure file limit exceeded {MAX_FILES}" ))); } total_bytes = total_bytes .checked_add(child_bytes.len()) .ok_or(RendererError::External( "official closure byte count overflow".to_owned(), ))?; if total_bytes > MAX_BYTES { return Err(RendererError::External(format!( "official closure byte limit exceeded {MAX_BYTES}" ))); } materializations.push(Materialization::new( logical_path.clone(), RootId::OfficialLibrary, child_bytes.clone(), true, Some("official-ldraw-library".into()), )); queue.push_back(Pending { path: logical_path, bytes: child_bytes, key: target, depth: current.depth + 1, }); } if target_selections.len() > MAX_EDGES { return Err(RendererError::External(format!( "official closure edge limit exceeded {MAX_EDGES}" ))); } } }
// Resolve auxiliary resources through the same typed closure consumed by // server bundling. Keep texture bytes in the official closure so native // preview can pass the exact admitted assets to scene adaptation. let texture_dependencies = resource_closure .texture_dependencies() .cloned() .collect::<Vec<_>>(); for dependency in texture_dependencies { let candidate = library_root.join(dependency.path.as_str()); if !candidate.is_file() { if dependency.required { return Err(RendererError::External(format!( "required TEXMAP resource is missing: {} (source {})", dependency.path, dependency.source ))); } continue; } if materializations.iter().any(|materialization| { materialization.root == dependency.root && materialization.path == dependency.path }) { continue; } let bytes = std::fs::read(&candidate).map_err(|error| { RendererError::Io(format!( "read TEXMAP resource {}: {error}", candidate.display() )) })?; let sha256 = polymodel_ldraw_core::Sha256Hash::digest(&bytes) .as_bytes() .to_vec(); resource_closure .resolved .push(polymodel_ldraw_core::ResolvedResource { path: dependency.path.clone(), root: dependency.root, sha256, byte_length: bytes.len() as u64, bytes: bytes.clone(), mime_type: "image/png", required: dependency.required, fallback_available: dependency.fallback_available, }); materializations.push(Materialization::new( dependency.path.clone(), dependency.root, bytes, true, Some("official-ldraw-library".into()), )); }
// Seed the colour table from LDConfig.ldr at the library root when the // closure requests the optional official-library colour seed. if resource_closure.needs_ldconfig { let ldconfig_path = library_root.join("LDConfig.ldr"); if let Ok(ldconfig_bytes) = std::fs::read(&ldconfig_path) { if let Ok(ldconfig_normalized) = NormalizedPath::new("LDConfig.ldr") { let ldconfig_key = CacheKey::new( ldconfig_normalized.clone(), RootId::OfficialLibrary, &ldconfig_bytes, ); if seen.insert(ldconfig_key) { materializations.insert( 0, Materialization::new( ldconfig_normalized, RootId::OfficialLibrary, ldconfig_bytes, true, Some("official-ldraw-library".into()), ), ); } } } }
resource_closure.validate().map_err(|error| { RendererError::External(format!("official resource closure rejected: {error}")) })?; Ok(OfficialClosure { materializations, target_selections, resource_closure, })}
#[cfg(not(target_arch = "wasm32"))]pub fn native_png_bytes(width: u32, height: u32, pixels: &[u8]) -> Vec<u8> { try_native_png_bytes(width, height, pixels).expect("valid native PNG data")}
#[cfg(not(target_arch = "wasm32"))]pub fn try_native_png_bytes( width: u32, height: u32, pixels: &[u8],) -> Result<Vec<u8>, RendererError> { let mut output = Vec::new(); let mut encoder = png::Encoder::new(std::io::Cursor::new(&mut output), width, height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder .write_header() .map_err(|error| RendererError::External(format!("encode PNG header: {error}")))?; writer .write_image_data(pixels) .map_err(|error| RendererError::External(format!("encode PNG pixels: {error}")))?; drop(writer); Ok(output)}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]pub enum RenderStyle { #[default] Faces, Edges,}
#[cfg(not(target_arch = "wasm32"))]#[derive(Clone, Debug)]pub struct NativeRenderOptions { pub width: u32, pub height: u32, pub yaw_degrees: f32, pub style: RenderStyle, pub camera_case: Option<CameraCase>, pub variant: Option<SceneVariantBinding>,}
#[cfg(not(target_arch = "wasm32"))]impl Default for NativeRenderOptions { fn default() -> Self { Self { width: 1280, height: 720, yaw_degrees: 0.0, style: RenderStyle::default(), camera_case: None, variant: None, } }}
#[cfg(not(target_arch = "wasm32"))]pub fn render_scene_window( scene: &Scene, output: Option<&std::path::Path>,) -> Result<(), RendererError> { render_scene_window_configured(scene, output, NativeRenderOptions::default())}
#[cfg(not(target_arch = "wasm32"))]pub fn render_scene_window_configured( scene: &Scene, output: Option<&std::path::Path>, options: NativeRenderOptions,) -> Result<(), RendererError> { render_scene_window_options_traced(scene, output, NativeTrace::default(), options)}
#[derive(Debug, Clone, PartialEq)]pub struct SceneMeshTopology { pub positions: Box<[[f32; 3]]>, /// Optional texture coordinates aligned one-to-one with `positions`. pub uvs: Box<[[f32; 2]]>, pub indices: Vec<u32>,}
impl SceneMeshTopology { pub fn triangle_count(&self) -> usize { self.indices.len() / 3 }
pub fn max_index(&self) -> Option<u32> { self.indices.iter().copied().max() }}
pub fn scene_oriented_mesh_topology( geometry: &SceneGeometry, bfc_cull: bool, bfc_ccw: bool, bfc_inverted: bool, reflection: bool,) -> Result<SceneMeshTopology, RendererError> { scene_oriented_mesh_topology_for_view( geometry, bfc_cull, bfc_ccw, bfc_inverted, reflection, [0.0, 0.0, 1.0], )}
fn scene_oriented_mesh_topology_for_view( geometry: &SceneGeometry, bfc_cull: bool, bfc_ccw: bool, bfc_inverted: bool, reflection: bool, view: [f64; 3],) -> Result<SceneMeshTopology, RendererError> { scene_oriented_mesh_topology_for_view_kind( geometry, bfc_cull, bfc_ccw, bfc_inverted, reflection, view, SceneTopologyKind::Complete, )}
fn scene_oriented_mesh_topology_for_view_kind( geometry: &SceneGeometry, bfc_cull: bool, bfc_ccw: bool, bfc_inverted: bool, reflection: bool, view: [f64; 3], kind: SceneTopologyKind,) -> Result<SceneMeshTopology, RendererError> { let mut topology = scene_mesh_topology_for_view_kind(geometry, view, kind)?; // `bfc_inverted` is the authoritative effective winding bit produced by // traversal. It already includes accumulated transform reflection; applying // `reflection` here would cancel that parity for reflected occurrences. let reverse = bfc_inverted ^ !bfc_ccw; if reverse { for triangle in topology.indices.chunks_exact_mut(3) { triangle.swap(0, 2); } } let _ = (bfc_cull, reflection); Ok(topology)}
pub fn scene_mesh_topology(geometry: &SceneGeometry) -> Result<SceneMeshTopology, RendererError> { scene_mesh_topology_for_view(geometry, [0.0, 0.0, 1.0])}
pub fn scene_mesh_topology_for_view( geometry: &SceneGeometry, view: [f64; 3],) -> Result<SceneMeshTopology, RendererError> { scene_mesh_topology_for_view_kind(geometry, view, SceneTopologyKind::Complete)}
#[derive(Clone, Copy)]enum SceneTopologyKind { Faces, #[allow(dead_code)] Edges, Complete,}
fn scene_mesh_topology_for_view_kind( geometry: &SceneGeometry, view: [f64; 3], kind: SceneTopologyKind,) -> Result<SceneMeshTopology, RendererError> { let include_faces = matches!(kind, SceneTopologyKind::Faces | SceneTopologyKind::Complete); let include_explicit = matches!(kind, SceneTopologyKind::Edges | SceneTopologyKind::Complete); let include_conditional = matches!(kind, SceneTopologyKind::Edges | SceneTopologyKind::Complete); let source_positions = geometry .points .iter() .map(|point| { let converted = [point[0] as f32, point[1] as f32, point[2] as f32]; if converted.iter().all(|coordinate| coordinate.is_finite()) { Ok(converted) } else { Err(RendererError::NonFiniteTransform) } }) .collect::<Result<Vec<_>, _>>()?; let mut positions = if include_faces { source_positions.clone() } else { Vec::new() }; let uvs: Vec<[f32; 2]> = if include_faces { if geometry.uvs.is_empty() { Vec::new() } else if geometry.uvs.len() != geometry.points.len() || geometry .uvs .iter() .flatten() .any(|value| !value.is_finite()) { return Err(RendererError::InvalidRange { context: "geometry UV alignment", index: geometry.uvs.len(), len: geometry.points.len(), }); } else { geometry.uvs.to_vec() } } else { Vec::new() }; let mut indices = Vec::with_capacity( usize::from(include_faces) * (geometry.triangles.len() * 3 + geometry.quads.len() * 6) + usize::from(include_explicit) * geometry.lines.len() * 6 + usize::from(include_conditional) * geometry.conditional_lines.len() * 6, ); if include_faces { for triangle in &geometry.triangles { indices.extend_from_slice(triangle); } for quad in &geometry.quads { indices.extend_from_slice(&[quad[0], quad[1], quad[2], quad[0], quad[2], quad[3]]); } } let mut add_line = |line: [u32; 2]| -> Result<(), RendererError> { let start = source_positions .get(line[0] as usize) .copied() .ok_or_else(|| RendererError::InvalidRange { context: "geometry line start", index: line[0] as usize, len: source_positions.len(), })?; let end = source_positions .get(line[1] as usize) .copied() .ok_or_else(|| RendererError::InvalidRange { context: "geometry line end", index: line[1] as usize, len: source_positions.len(), })?; let direction = [ f64::from(end[0] - start[0]), f64::from(end[1] - start[1]), f64::from(end[2] - start[2]), ]; let length = (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]) .sqrt(); if !length.is_finite() || length <= f64::EPSILON { return Ok(()); } let mut side = [ direction[1] * view[2] - direction[2] * view[1], direction[2] * view[0] - direction[0] * view[2], direction[0] * view[1] - direction[1] * view[0], ]; let side_length = (side[0] * side[0] + side[1] * side[1] + side[2] * side[2]).sqrt(); if !side_length.is_finite() || side_length <= f64::EPSILON { // Pick the least-parallel cardinal axis, then cross it with the // line. This handles view-parallel Y edges as well as arbitrary // finite camera directions without a zero-length fallback. let axis = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] .into_iter() .min_by(|left, right| { let left_dot = (direction[0] * left[0] + direction[1] * left[1] + direction[2] * left[2]) .abs(); let right_dot = (direction[0] * right[0] + direction[1] * right[1] + direction[2] * right[2]) .abs(); left_dot .partial_cmp(&right_dot) .unwrap_or(std::cmp::Ordering::Equal) }) .unwrap_or([1.0, 0.0, 0.0]); side = [ direction[1] * axis[2] - direction[2] * axis[1], direction[2] * axis[0] - direction[0] * axis[2], direction[0] * axis[1] - direction[1] * axis[0], ]; } let side_length = (side[0] * side[0] + side[1] * side[1] + side[2] * side[2]).sqrt(); if !side_length.is_finite() || side_length <= f64::EPSILON { return Ok(()); } let radius = 0.02_f64; let offset = [ (side[0] / side_length * radius) as f32, (side[1] / side_length * radius) as f32, (side[2] / side_length * radius) as f32, ]; let base = u32::try_from(positions.len()).map_err(|_| RendererError::IndexOverflow { context: "line vertex range", })?; positions.extend([ [ start[0] - offset[0], start[1] - offset[1], start[2] - offset[2], ], [ start[0] + offset[0], start[1] + offset[1], start[2] + offset[2], ], [end[0] + offset[0], end[1] + offset[1], end[2] + offset[2]], [end[0] - offset[0], end[1] - offset[1], end[2] - offset[2]], ]); indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]); Ok(()) }; if include_explicit { for line in geometry.lines.iter().copied() { add_line(line)?; } } if include_conditional { for conditional in &geometry.conditional_lines { let points = conditional .iter() .map(|index| { geometry .points .get(*index as usize) .copied() .ok_or_else(|| RendererError::InvalidRange { context: "geometry conditional-line index", index: *index as usize, len: geometry.points.len(), }) }) .collect::<Result<Vec<_>, _>>()?; let endpoints = [points[0], points[1]]; let controls = [points[2], points[3]]; if conditional_line_visible(endpoints, controls, view) { add_line([conditional[0], conditional[1]])?; } } } if indices.len() % 3 != 0 { return Err(RendererError::InvalidRange { context: "geometry triangle index stream", index: indices.len(), len: indices.len() + (3 - indices.len() % 3) % 3, }); } if let Some(index) = indices .iter() .copied() .find(|index| *index as usize >= positions.len()) { return Err(RendererError::InvalidRange { context: "geometry index", index: index as usize, len: positions.len(), }); } Ok(SceneMeshTopology { positions: positions.into_boxed_slice(), uvs: uvs.into_boxed_slice(), indices, })}
#[derive(Debug, Clone, Copy, PartialEq)]pub struct PickRay { pub origin: [f64; 3], pub direction: [f64; 3],}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub struct OccurrenceGpuRange { pub batch: usize, pub instance: usize, pub geometry: GeometryId, pub vertex_start: u32, pub vertex_count: u32, pub index_start: u32, pub index_count: u32,}
#[derive(Debug, Clone, Copy, PartialEq)]pub struct ScenePickHit { pub occurrence_index: usize, pub triangle_index: usize, pub distance: f64, /// GPU range when this occurrence was uploaded as face geometry. /// Edge-only and empty-face occurrences are intentionally `None`. pub gpu: Option<OccurrenceGpuRange>,}
fn ray_triangle_distance(ray: PickRay, a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Option<f64> { let sub = |x: [f64; 3], y: [f64; 3]| [x[0] - y[0], x[1] - y[1], x[2] - y[2]]; let cross = |x: [f64; 3], y: [f64; 3]| { [ x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0], ] }; let dot = |x: [f64; 3], y: [f64; 3]| x[0] * y[0] + x[1] * y[1] + x[2] * y[2]; let edge1 = sub(b, a); let edge2 = sub(c, a); let p = cross(ray.direction, edge2); let determinant = dot(edge1, p); if determinant.abs() <= 1.0e-12 { return None; } let inverse = determinant.recip(); let from_a = sub(ray.origin, a); let u = dot(from_a, p) * inverse; if !(0.0..=1.0).contains(&u) { return None; } let q = cross(from_a, edge1); let v = dot(ray.direction, q) * inverse; if v < 0.0 || u + v > 1.0 { return None; } let distance = dot(edge2, q) * inverse; (distance > 1.0e-9 && distance.is_finite()).then_some(distance)}
#[derive(Debug, Clone, PartialEq)]struct SceneIndex { geometries: std::collections::HashMap<GeometryId, usize>, materials: std::collections::HashMap<MaterialId, usize>,}
impl SceneIndex { fn new(scene: &Scene) -> Result<Self, RendererError> { let mut geometries = std::collections::HashMap::with_capacity(scene.geometries.len()); for (index, geometry) in scene.geometries.iter().enumerate() { if geometry.id.0 == 0 { return Err(RendererError::ParseAdaptation( "geometry id zero is invalid", )); } if geometries.insert(geometry.id, index).is_some() { return Err(RendererError::DuplicateGeometry(geometry.id)); } } let mut materials = std::collections::HashMap::with_capacity(scene.materials.len()); for (index, material) in scene.materials.iter().enumerate() { if material.id.0 == 0 { return Err(RendererError::ParseAdaptation( "material id zero is invalid", )); } if materials.insert(material.id, index).is_some() { return Err(RendererError::ParseAdaptation("duplicate material id")); } } let index = Self { geometries, materials, }; index.geometry(scene, scene.root)?; for occurrence in &scene.occurrences { index.geometry(scene, occurrence.node)?; if let Some(material) = occurrence.material { index.material(scene, material)?; } if occurrence .transform .iter() .flatten() .any(|value| !value.is_finite()) { return Err(RendererError::NonFiniteTransform); } } Ok(index) }
fn geometry<'a>( &self, scene: &'a Scene, node: NodeId, ) -> Result<&'a SceneGeometry, RendererError> { let id = GeometryId(node.0); self.geometries .get(&id) .and_then(|index| scene.geometries.get(*index)) .ok_or(RendererError::MissingGeometry(id)) }
fn geometry_index(&self, node: NodeId) -> Result<usize, RendererError> { self.geometries .get(&GeometryId(node.0)) .copied() .ok_or(RendererError::MissingGeometry(GeometryId(node.0))) }
fn material<'a>( &self, scene: &'a Scene, id: MaterialId, ) -> Result<&'a SceneMaterial, RendererError> { self.materials .get(&id) .and_then(|index| scene.materials.get(*index)) .ok_or(RendererError::MissingMaterial(id)) }}
pub fn occurrence_gpu_ranges( scene: &Scene,) -> Result<Vec<Option<OccurrenceGpuRange>>, RendererError> { Ok(scene_render_plan(scene)?.occurrence_ranges.to_vec())}
fn finalized_upload_batches( index: &SceneIndex, batches: &[SceneInstanceBatch], topologies: &[SceneMeshTopology],) -> Result<(Vec<usize>, Vec<Option<usize>>), RendererError> { let mut upload_batches = Vec::new(); let mut planned_to_upload = vec![None; batches.len()]; for (planned_index, batch) in batches.iter().enumerate() { let geometry_index = index.geometry_index(batch.node)?; let topology = topologies .get(geometry_index) .ok_or(RendererError::InvalidRange { context: "render plan geometry topology", index: geometry_index, len: topologies.len(), })?; if topology.indices.is_empty() { continue; } planned_to_upload[planned_index] = Some(upload_batches.len()); upload_batches.push(planned_index); } Ok((upload_batches, planned_to_upload))}
fn occurrence_gpu_ranges_from_batches( scene: &Scene, index: &SceneIndex, batches: &[SceneInstanceBatch], upload_batch_indices: &[usize], topologies: &[SceneMeshTopology],) -> Result<Vec<Option<OccurrenceGpuRange>>, RendererError> { let mut ranges = vec![None; scene.occurrences.len()];
for (upload_batch_index, planned_batch_index) in upload_batch_indices.iter().copied().enumerate() { let batch = batches .get(planned_batch_index) .ok_or(RendererError::InvalidRange { context: "upload batch index", index: planned_batch_index, len: batches.len(), })?; let geometry_index = index.geometry_index(batch.node)?; let topology = topologies .get(geometry_index) .ok_or(RendererError::InvalidRange { context: "render plan geometry topology", index: geometry_index, len: topologies.len(), })?; let vertex_count = u32::try_from(topology.positions.len()).map_err(|_| RendererError::IndexOverflow { context: "vertex range", })?; let index_count = u32::try_from(topology.indices.len()).map_err(|_| RendererError::IndexOverflow { context: "index range", })?; for (instance_index, instance) in batch.instances.iter().enumerate() { let range = ranges .get_mut(instance.occurrence_index) .ok_or(RendererError::InvalidRange { context: "batch occurrence", index: instance.occurrence_index, len: scene.occurrences.len(), })?; if range.is_some() { return Err(RendererError::ParseAdaptation( "occurrence appears in multiple native GPU batches", )); } *range = Some(OccurrenceGpuRange { batch: upload_batch_index, instance: instance_index, geometry: GeometryId(batch.node.0), vertex_start: 0, vertex_count, index_start: 0, index_count, }); } }
Ok(ranges)}
#[derive(Debug, Clone, Copy, PartialEq)]struct PickBounds { min: [f64; 3], max: [f64; 3],}
impl PickBounds { fn transformed(self, transform: [[f64; 4]; 4]) -> Option<Self> { let center = [ (self.min[0] + self.max[0]) * 0.5, (self.min[1] + self.max[1]) * 0.5, (self.min[2] + self.max[2]) * 0.5, ]; let half = [ (self.max[0] - self.min[0]) * 0.5, (self.max[1] - self.min[1]) * 0.5, (self.max[2] - self.min[2]) * 0.5, ]; let center = apply_scene_transform(transform, center).ok()?; let extent = [0, 1, 2].map(|axis| { transform[0][axis].abs() * half[0] + transform[1][axis].abs() * half[1] + transform[2][axis].abs() * half[2] }); center .iter() .chain(extent.iter()) .all(|value| value.is_finite()) .then_some(Self { min: [ center[0] - extent[0], center[1] - extent[1], center[2] - extent[2], ], max: [ center[0] + extent[0], center[1] + extent[1], center[2] + extent[2], ], }) }
fn from_points(points: impl IntoIterator<Item = [f64; 3]>) -> Option<Self> { let mut bounds = Self { min: [f64::INFINITY; 3], max: [f64::NEG_INFINITY; 3], }; let mut any = false; for point in points { if point.iter().any(|value| !value.is_finite()) { return None; } any = true; for (axis, coordinate) in point.into_iter().enumerate() { bounds.min[axis] = bounds.min[axis].min(coordinate); bounds.max[axis] = bounds.max[axis].max(coordinate); } } any.then_some(bounds) }
fn union(self, other: Self) -> Self { let mut result = self; for axis in 0..3 { result.min[axis] = result.min[axis].min(other.min[axis]); result.max[axis] = result.max[axis].max(other.max[axis]); } result }
fn intersects(self, ray: PickRay, maximum: f64) -> bool { let mut near = 0.0_f64; let mut far = maximum; for axis in 0..3 { if ray.direction[axis].abs() <= 1.0e-15 { if ray.origin[axis] < self.min[axis] || ray.origin[axis] > self.max[axis] { return false; } continue; } let inverse = ray.direction[axis].recip(); let mut axis_near = (self.min[axis] - ray.origin[axis]) * inverse; let mut axis_far = (self.max[axis] - ray.origin[axis]) * inverse; if axis_near > axis_far { std::mem::swap(&mut axis_near, &mut axis_far); } near = near.max(axis_near); far = far.min(axis_far); if near > far { return false; } } far >= 0.0 }}
pub type PlannedConditionalEdge = ([[f32; 3]; 2], [[f32; 3]; 2]);
#[derive(Debug, Clone, PartialEq)]pub struct PlannedEdgeSource { pub explicit: Box<[[[f32; 3]; 2]]>, pub conditional: Box<[PlannedConditionalEdge]>,}
#[derive(Debug, Clone, Copy, PartialEq)]pub struct PlannedOccurrence { pub occurrence_index: usize, pub geometry_index: usize, pub material: Option<MaterialId>, pub logical_part_index: u32, pub bounds: Option<SceneBounds>,}
#[derive(Debug, Clone, PartialEq)]pub struct SelectionIdentity { pub canonical_occurrence_id: SmolStr, pub canonical_path: SmolStr, pub logical_part_occurrence_id: SmolStr, pub logical_part_path: SmolStr, pub occurrence_indices: Vec<usize>, pub transforms: Vec<[[f64; 4]; 4]>, pub bounds: Option<SceneBounds>,}
#[derive(Debug, Clone, Copy, PartialEq)]pub enum SelectionFallback { BoundingBox(SceneBounds),}
#[derive(Debug, Clone, Copy, PartialEq)]pub enum SelectionPointerAction { ForwardToOrbit, Select { x: f64, y: f64 }, ClearSelection, WithholdFromOrbit,}
#[derive(Debug, Clone, Copy, Default, PartialEq)]pub struct SelectionInteraction { shift: bool, shift_on_down: bool, down: Option<(f64, f64)>, dragged: bool, pending_select: Option<(f64, f64)>,}
impl SelectionInteraction { pub const DRAG_THRESHOLD: f64 = 4.0;
pub fn set_modifiers(&mut self, shift: bool) { self.shift = shift; }
pub fn pointer_down(&mut self, x: f64, y: f64) -> SelectionPointerAction { self.down = Some((x, y)); self.shift_on_down = self.shift; self.dragged = false; if self.shift_on_down { SelectionPointerAction::WithholdFromOrbit } else { SelectionPointerAction::ForwardToOrbit } }
pub fn pointer_move(&mut self, x: f64, y: f64) -> SelectionPointerAction { let Some((start_x, start_y)) = self.down else { return SelectionPointerAction::ForwardToOrbit; }; if (x - start_x).hypot(y - start_y) >= Self::DRAG_THRESHOLD { self.dragged = true; SelectionPointerAction::ForwardToOrbit } else { SelectionPointerAction::WithholdFromOrbit } }
pub fn pointer_up(&mut self, x: f64, y: f64) -> SelectionPointerAction { let action = match self.down.take() { None => SelectionPointerAction::ForwardToOrbit, Some(_) if self.dragged => SelectionPointerAction::ForwardToOrbit, Some((start_x, start_y)) if self.shift_on_down => SelectionPointerAction::Select { x: (start_x + x) * 0.5, y: (start_y + y) * 0.5, }, Some(_) => SelectionPointerAction::ForwardToOrbit, }; if let SelectionPointerAction::Select { x, y } = action { self.pending_select = Some((x, y)); } self.dragged = false; self.shift_on_down = false; action }
pub fn has_pending_select(&self) -> bool { self.pending_select.is_some() }
pub fn take_pending_select(&mut self) -> Option<(f64, f64)> { self.pending_select.take() }
pub fn cancel(&mut self) { *self = Self::default(); }
pub fn dispose(&mut self) { self.cancel(); }
pub fn has_pending_click(&self) -> bool { self.down.is_some() }}
#[derive(Debug, Clone)]struct PickBvhNode { bounds: PickBounds, children: Option<[usize; 2]>, range: std::ops::Range<usize>,}
#[derive(Debug, Clone)]struct PickBvh { nodes: Vec<PickBvhNode>, indices: Vec<usize>,}
impl PickBvh { const LEAF_SIZE: usize = 16;
fn new(item_bounds: &[PickBounds]) -> Option<Self> { if item_bounds.is_empty() { return None; } let mut bvh = Self { nodes: Vec::with_capacity(item_bounds.len() / Self::LEAF_SIZE * 2), indices: (0..item_bounds.len()).collect(), }; bvh.build_node(0..item_bounds.len(), item_bounds); Some(bvh) }
fn build_node(&mut self, range: std::ops::Range<usize>, item_bounds: &[PickBounds]) -> usize { let bounds = self.indices[range.clone()] .iter() .map(|index| item_bounds[*index]) .reduce(PickBounds::union) .expect("BVH nodes are never empty"); let node_index = self.nodes.len(); self.nodes.push(PickBvhNode { bounds, children: None, range: range.clone(), }); if range.len() <= Self::LEAF_SIZE { return node_index; } let extents = [ bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2], ]; let axis = (0..3) .max_by(|a, b| extents[*a].total_cmp(&extents[*b])) .expect("three axes"); let middle = range.start + range.len() / 2; self.indices[range.clone()].select_nth_unstable_by(middle - range.start, |a, b| { let center = |index: usize| { let bounds = item_bounds[index]; bounds.min[axis] + bounds.max[axis] }; center(*a).total_cmp(¢er(*b)) }); let left = self.build_node(range.start..middle, item_bounds); let right = self.build_node(middle..range.end, item_bounds); self.nodes[node_index].children = Some([left, right]); node_index }
fn candidates(&self, ray: PickRay, maximum: f64, output: &mut Vec<usize>) { let mut stack = vec![0]; while let Some(node_index) = stack.pop() { let node = &self.nodes[node_index]; if !node.bounds.intersects(ray, maximum) { continue; } if let Some([left, right]) = node.children { stack.push(right); stack.push(left); } else { output.extend_from_slice(&self.indices[node.range.clone()]); } } }}
#[derive(Debug, Clone)]pub struct ScenePicker { scene_occurrences: Arc<[SceneOccurrence]>, topologies: Arc<[SceneMeshTopology]>, edge_sources: Arc<[PlannedEdgeSource]>, occurrences: Arc<[PlannedOccurrence]>, logical_parts: Arc<[Box<[usize]>]>, logical_bounds: Arc<[PickBounds]>, ranges: Arc<[Option<OccurrenceGpuRange>]>, logical_part_bounds: Arc<[SceneBounds]>, logical_part_indices: std::collections::HashMap<SmolStr, u32>, bvh: Option<PickBvh>,}
impl ScenePicker { pub fn new(scene: &Scene, plan: SceneRenderPlan) -> Result<Self, RendererError> { Self::from_plan(scene, &plan) }
fn from_plan(scene: &Scene, plan: &SceneRenderPlan) -> Result<Self, RendererError> { if plan.planned_occurrences.len() != scene.occurrences.len() { return Err(RendererError::InvalidRange { context: "render plan pick occurrences", index: plan.planned_occurrences.len(), len: scene.occurrences.len(), }); } let bvh = PickBvh::new(&plan.logical_bounds); Ok(Self { scene_occurrences: scene.occurrences.clone().into(), topologies: plan.topologies.clone(), edge_sources: plan.edge_sources.clone(), occurrences: plan.planned_occurrences.clone(), logical_parts: plan.logical_parts.clone(), logical_bounds: plan.logical_bounds.clone(), ranges: plan.occurrence_ranges.clone(), logical_part_bounds: plan.logical_part_bounds.clone(), logical_part_indices: plan.logical_part_indices.clone(), bvh, }) }
pub fn logical_part_bounds(&self, occurrence_id: &str) -> Option<SceneBounds> { self.logical_part_indices .get(occurrence_id) .and_then(|index| self.logical_part_bounds.get(*index as usize)) .copied() }
pub fn selection(&self, raw_index: usize) -> Result<SelectionIdentity, RendererError> { let raw = self .occurrences .get(raw_index) .ok_or_else(|| RendererError::InvalidRange { context: "raw occurrence", index: raw_index, len: self.occurrences.len(), })?; let scene_occurrence = self.scene_occurrences.get(raw.occurrence_index).ok_or( RendererError::InvalidRange { context: "planned occurrence source", index: raw.occurrence_index, len: self.scene_occurrences.len(), }, )?; let group_index = raw.logical_part_index; let occurrence_indices = self.logical_parts[group_index as usize].clone(); let transforms = occurrence_indices .iter() .map(|index| self.occurrences[*index].occurrence_index) .map(|index| self.scene_occurrences[index].transform) .collect(); Ok(SelectionIdentity { canonical_occurrence_id: scene_occurrence.canonical_occurrence_id.clone(), canonical_path: scene_occurrence.canonical_path.as_str().into(), logical_part_occurrence_id: scene_occurrence .logical_part_occurrence_id .clone() .unwrap_or_else(|| scene_occurrence.canonical_occurrence_id.clone()), logical_part_path: scene_occurrence .logical_part_path .clone() .unwrap_or_else(|| scene_occurrence.canonical_path.clone()), occurrence_indices: occurrence_indices.to_vec(), transforms, bounds: self.logical_part_bounds.get(group_index as usize).copied(), }) }
pub fn edge_sources(&self) -> &Arc<[PlannedEdgeSource]> { &self.edge_sources }
pub fn planned_occurrences(&self) -> &Arc<[PlannedOccurrence]> { &self.occurrences }
pub fn pick(&self, ray: PickRay) -> Result<Option<ScenePickHit>, RendererError> { let mut candidate_parts = Vec::new(); if let Some(bvh) = &self.bvh { bvh.candidates(ray, f64::INFINITY, &mut candidate_parts); } let mut nearest: Option<ScenePickHit> = None; for part_index in candidate_parts { let maximum = nearest.as_ref().map_or(f64::INFINITY, |hit| hit.distance); if !self.logical_bounds[part_index].intersects(ray, maximum) { continue; } for candidate in &self.logical_parts[part_index] { let planned = &self.occurrences[*candidate]; let occurrence = self.scene_occurrences.get(planned.occurrence_index).ok_or( RendererError::InvalidRange { context: "planned occurrence source", index: planned.occurrence_index, len: self.scene_occurrences.len(), }, )?; let maximum = nearest.as_ref().map_or(f64::INFINITY, |hit| hit.distance); let bounds = planned.bounds.map(|bounds| PickBounds { min: bounds.min, max: bounds.max, }); if !bounds.is_some_and(|bounds| bounds.intersects(ray, maximum)) { continue; } let topology = &self.topologies[planned.geometry_index]; let local_ray = InverseSceneTransform::new(occurrence.transform) .map(|inverse| inverse.ray(ray)); for (triangle_index, indices) in topology.indices.chunks_exact(3).enumerate() { let distance = if let Some(local_ray) = local_ray { let point = |index: u32| { let point = topology.positions[index as usize]; [ f64::from(point[0]), f64::from(point[1]), f64::from(point[2]), ] }; ray_triangle_distance( local_ray, point(indices[0]), point(indices[1]), point(indices[2]), ) } else { let point = |index: u32| { let point = topology.positions[index as usize]; apply_scene_transform( occurrence.transform, [ f64::from(point[0]), f64::from(point[1]), f64::from(point[2]), ], ) }; ray_triangle_distance( ray, point(indices[0])?, point(indices[1])?, point(indices[2])?, ) }; if let Some(distance) = distance && nearest.as_ref().is_none_or(|hit| distance < hit.distance) { nearest = Some(ScenePickHit { occurrence_index: planned.occurrence_index, triangle_index, distance, gpu: self.ranges[planned.occurrence_index], }); } } } } Ok(nearest) }}
#[cfg(not(target_arch = "wasm32"))]pub fn build_scene_occurrence_models( context: &three_d::Context, scene: &Scene, occurrence_indices: &[usize],) -> Result<NativeSceneModel, RendererError> { let mut selected = scene.clone(); selected.occurrences = occurrence_indices .iter() .map(|index| { scene .occurrences .get(*index) .cloned() .ok_or(RendererError::InvalidRange { context: "selected occurrence", index: *index, len: scene.occurrences.len(), }) }) .collect::<Result<_, _>>()?; build_scene_model(context, &selected)}
#[cfg(not(target_arch = "wasm32"))]pub fn build_scene_occurrence_model( context: &three_d::Context, scene: &Scene, occurrence_index: usize,) -> Result<NativeSceneModel, RendererError> { build_scene_occurrence_models(context, scene, &[occurrence_index])}
#[derive(Debug, Clone, PartialEq)]pub struct SceneBatchInstance { pub occurrence_index: usize, pub transform: [[f64; 4]; 4],}
#[derive(Debug, Clone, PartialEq)]pub struct SceneInstanceBatch { pub node: NodeId, /// UVs shared by occurrences with identical geometry/transform/TEXMAP keys. pub uvs: Option<Arc<[[f32; 2]]>>, pub material: MaterialId, pub bfc_cull: bool, pub bfc_ccw: bool, pub bfc_inverted: bool, pub reflection: bool, pub instances: Box<[SceneBatchInstance]>,}
struct SceneInstanceBatchBuilder { node: NodeId, uvs: Option<Arc<[[f32; 2]]>>, material: MaterialId, bfc_cull: bool, bfc_ccw: bool, bfc_inverted: bool, reflection: bool, instances: Vec<SceneBatchInstance>,}
impl SceneInstanceBatchBuilder { fn into_batch(self) -> SceneInstanceBatch { SceneInstanceBatch { node: self.node, uvs: self.uvs, material: self.material, bfc_cull: self.bfc_cull, bfc_ccw: self.bfc_ccw, bfc_inverted: self.bfc_inverted, reflection: self.reflection, instances: self.instances.into_boxed_slice(), } }}
#[derive(Debug, Clone, PartialEq)]pub struct SceneRenderPlan { /// Planned batches retain all semantic occurrences for picking and diagnostics. pub batches: Box<[SceneInstanceBatch]>, /// Indices of planned batches that have non-empty face topology. pub upload_batch_indices: Box<[usize]>, /// Mapping from planned batch index to actual upload-object index. pub planned_to_upload: Box<[Option<usize>]>, pub occurrence_ranges: Arc<[Option<OccurrenceGpuRange>]>, pub topologies: Arc<[SceneMeshTopology]>, pub edge_sources: Arc<[PlannedEdgeSource]>, /// Canonical occurrence records shared by face upload, edge output, and picking. pub planned_occurrences: Arc<[PlannedOccurrence]>, logical_parts: Arc<[Box<[usize]>]>, logical_bounds: Arc<[PickBounds]>, logical_part_bounds: Arc<[SceneBounds]>, logical_part_indices: std::collections::HashMap<SmolStr, u32>, index: SceneIndex,}
fn planned_edge_source(geometry: &SceneGeometry) -> Result<PlannedEdgeSource, RendererError> { let explicit: Vec<[[f32; 3]; 2]> = geometry .lines .iter() .enumerate() .filter_map(|(line_index, line)| { let Some(start) = geometry.points.get(line[0] as usize).copied() else { tracing::warn!( edge_kind = "line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; }; let Some(end) = geometry.points.get(line[1] as usize).copied() else { tracing::warn!( edge_kind = "line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; }; let start = match finite_f32_vec3(start) { Ok(start) => start, Err(_) => { tracing::warn!( edge_kind = "line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }; let end = match finite_f32_vec3(end) { Ok(end) => end, Err(_) => { tracing::warn!( edge_kind = "line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }; if start == end { tracing::warn!( edge_kind = "line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } Some([start, end]) }) .collect(); let conditional: Vec<PlannedConditionalEdge> = geometry .conditional_lines .iter() .enumerate() .filter_map(|(line_index, line)| { let Some(points) = line .iter() .map(|index| geometry.points.get(*index as usize).copied()) .collect::<Option<Vec<_>>>() else { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; }; let endpoints = [ match finite_f32_vec3(points[0]) { Ok(point) => point, Err(_) => { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }, match finite_f32_vec3(points[1]) { Ok(point) => point, Err(_) => { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }, ]; let controls = [ match finite_f32_vec3(points[2]) { Ok(point) => point, Err(_) => { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }, match finite_f32_vec3(points[3]) { Ok(point) => point, Err(_) => { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } }, ]; if endpoints[0] == endpoints[1] { tracing::warn!( edge_kind = "conditional-line", record_index = line_index, indices = ?line, "dropping malformed edge geometry" ); return None; } Some((endpoints, controls)) }) .collect(); Ok(PlannedEdgeSource { explicit: explicit.into_boxed_slice(), conditional: conditional.into_boxed_slice(), })}
pub fn scene_render_plan(scene: &Scene) -> Result<SceneRenderPlan, RendererError> { #[cfg(not(target_arch = "wasm32"))] let diagnostics = std::env::var_os("POLYMODEL_LDRAW_PLAN_DIAGNOSTICS").is_some(); #[cfg(target_arch = "wasm32")] let diagnostics = false; let started = Instant::now(); let index = SceneIndex::new(scene)?; let indexed = started.elapsed(); let topologies = scene .geometries .iter() .map(|geometry| { scene_mesh_topology_for_view_kind(geometry, [0.0, 0.0, 1.0], SceneTopologyKind::Faces) }) .collect::<Result<Vec<_>, _>>()?; for occurrence in &scene.occurrences { if let Some(uvs) = &occurrence.uvs { let geometry_index = index.geometry_index(occurrence.node)?; let point_count = scene.geometries[geometry_index].points.len(); if uvs.len() != point_count || uvs.iter().flatten().any(|value| !value.is_finite()) { return Err(RendererError::InvalidRange { context: "occurrence UV alignment", index: uvs.len(), len: point_count, }); } } } let edge_sources = scene .geometries .iter() .map(planned_edge_source) .collect::<Result<Vec<_>, _>>()?; let topology_complete = started.elapsed(); let local_bounds = topologies .iter() .enumerate() .map(|(_geometry_index, topology)| { PickBounds::from_points(topology.positions.iter().map(|point| { [ f64::from(point[0]), f64::from(point[1]), f64::from(point[2]), ] })) .ok_or_else(|| RendererError::NonFiniteTransform) }) .collect::<Result<Vec<_>, _>>()?; let local_bounds_complete = started.elapsed(); let mut pick_occurrences = Vec::with_capacity(scene.occurrences.len()); let mut logical_part_indices = std::collections::HashMap::<SmolStr, u32>::new(); let mut logical_parts = Vec::<Vec<usize>>::new(); let mut logical_bounds = Vec::<PickBounds>::new(); let mut logical_part_bounds = Vec::<SceneBounds>::new(); let mut accumulator = SceneBatchAccumulator { pick_occurrences: &mut pick_occurrences, logical_part_indices: &mut logical_part_indices, logical_parts: &mut logical_parts, logical_bounds: &mut logical_bounds, logical_part_bounds: &mut logical_part_bounds, }; let batches = scene_instance_batches_for_mode(scene, &index, &local_bounds, &mut accumulator)?; let batches_complete = started.elapsed(); let (upload_batches, planned_to_upload) = finalized_upload_batches(&index, &batches, &topologies)?; let occurrence_ranges = occurrence_gpu_ranges_from_batches(scene, &index, &batches, &upload_batches, &topologies)?; let ranges_complete = started.elapsed(); if diagnostics { tracing::debug!( "[ldraw-preview] plan-detail index_ms={} topology_ms={} local_bounds_ms={} batches_ms={} ranges_ms={}", indexed.as_millis(), (topology_complete - indexed).as_millis(), (local_bounds_complete - topology_complete).as_millis(), (batches_complete - local_bounds_complete).as_millis(), (ranges_complete - batches_complete).as_millis(), ); } Ok(SceneRenderPlan { batches: batches.into_boxed_slice(), upload_batch_indices: upload_batches.into_boxed_slice(), planned_to_upload: planned_to_upload.into_boxed_slice(), occurrence_ranges: Arc::from(occurrence_ranges.into_boxed_slice()), topologies: Arc::from(topologies.into_boxed_slice()), edge_sources: Arc::from(edge_sources.into_boxed_slice()), planned_occurrences: Arc::from(pick_occurrences.into_boxed_slice()), logical_parts: Arc::from( logical_parts .into_iter() .map(Vec::into_boxed_slice) .collect::<Vec<_>>() .into_boxed_slice(), ), logical_bounds: Arc::from(logical_bounds.into_boxed_slice()), logical_part_bounds: Arc::from(logical_part_bounds.into_boxed_slice()), logical_part_indices, index, })}
pub fn scene_instance_batches(scene: &Scene) -> Result<Vec<SceneInstanceBatch>, RendererError> { Ok(scene_render_plan(scene)?.batches.into_vec())}
struct SceneBatchAccumulator<'a> { pick_occurrences: &'a mut Vec<PlannedOccurrence>, logical_part_indices: &'a mut std::collections::HashMap<SmolStr, u32>, logical_parts: &'a mut Vec<Vec<usize>>, logical_bounds: &'a mut Vec<PickBounds>, logical_part_bounds: &'a mut Vec<SceneBounds>,}
fn scene_instance_batches_for_mode( scene: &Scene, index: &SceneIndex, local_bounds: &[PickBounds], accumulator: &mut SceneBatchAccumulator<'_>,) -> Result<Vec<SceneInstanceBatch>, RendererError> { #[derive(Clone, Copy, PartialEq, Eq, Hash)] struct BatchKey { node: NodeId, material: MaterialId, bfc_cull: bool, reverse_winding: bool, reflection: bool, textured_occurrence: Option<usize>, }
let default_material = scene .materials .first() .map(|material| material.id) .unwrap_or(MaterialId(1)); let mut batches = Vec::<SceneInstanceBatchBuilder>::new(); let mut batch_indices = std::collections::HashMap::<BatchKey, usize>::new(); for (occurrence_index, occurrence) in scene.occurrences.iter().enumerate() { let geometry_index = index.geometry_index(occurrence.node)?; let local = local_bounds[geometry_index]; let bounds = local .transformed(occurrence.transform) .ok_or_else(|| RendererError::NonFiniteTransform)?; let logical_part_occurrence_id = occurrence .logical_part_occurrence_id .clone() .unwrap_or_else(|| occurrence.canonical_occurrence_id.clone()); let group_index = if let Some(group_index) = accumulator .logical_part_indices .get(&logical_part_occurrence_id) .copied() { group_index } else { let group_index = u32::try_from(accumulator.logical_parts.len()) .map_err(|_| RendererError::ParseAdaptation("logical part group count overflow"))?; accumulator .logical_part_indices .insert(logical_part_occurrence_id.clone(), group_index); accumulator.logical_parts.push(Vec::new()); accumulator.logical_bounds.push(bounds); accumulator.logical_part_bounds.push(SceneBounds { min: bounds.min, max: bounds.max, }); group_index }; accumulator.pick_occurrences.push(PlannedOccurrence { occurrence_index, geometry_index, material: occurrence.material, logical_part_index: group_index, bounds: Some(SceneBounds { min: bounds.min, max: bounds.max, }), }); accumulator.logical_parts[group_index as usize].push(occurrence_index); accumulator.logical_bounds[group_index as usize] = accumulator.logical_bounds[group_index as usize].union(bounds); let aggregate = &mut accumulator.logical_part_bounds[group_index as usize]; for axis in 0..3 { aggregate.min[axis] = aggregate.min[axis].min(bounds.min[axis]); aggregate.max[axis] = aggregate.max[axis].max(bounds.max[axis]); } let material = occurrence.material.unwrap_or(default_material); let reverse_winding = occurrence.bfc_inverted ^ !occurrence.bfc_ccw; let reflection = scene_transform_reflection(occurrence.transform) .ok_or_else(|| RendererError::NonFiniteTransform)?; let key = BatchKey { node: occurrence.node, material, bfc_cull: occurrence.bfc_cull, reverse_winding, reflection, textured_occurrence: occurrence.uvs.as_ref().map(|_| occurrence_index), }; let instance = SceneBatchInstance { occurrence_index, transform: occurrence.transform, }; if let Some(index) = batch_indices.get(&key).copied() { batches[index].instances.push(instance); } else { let index = batches.len(); batches.push(SceneInstanceBatchBuilder { node: occurrence.node, uvs: occurrence.uvs.clone(), material, bfc_cull: occurrence.bfc_cull, bfc_ccw: occurrence.bfc_ccw, bfc_inverted: reverse_winding, reflection, instances: vec![instance], }); batch_indices.insert(key, index); } } Ok(batches .into_iter() .map(SceneInstanceBatchBuilder::into_batch) .collect())}
#[derive(Clone, Copy, Debug, PartialEq)]pub struct LdrawLightingMaterialPolicy { pub version: &'static str, pub ambient_energy: f32, pub ambient_colour: [u8; 3], pub key_energy: f32, pub key_colour: [u8; 3], pub key_direction: [f32; 3], pub fill_energy: f32, pub fill_colour: [u8; 3], pub fill_direction: [f32; 3], pub albedo_is_direct_colour: bool, pub black_albedo: [u8; 3], pub black_roughness: f32, pub roughness: f32, pub metallic: f32, pub cull_back_faces: bool, pub depth_test: bool, pub depth_write: bool, pub alpha_blend: bool,}
impl LdrawLightingMaterialPolicy { pub const VERSION: &'static str = "pm-ldraw-lighting-material-v1";
pub const fn ldraw() -> Self { Self { version: Self::VERSION, ambient_energy: 0.45, ambient_colour: [255, 255, 255], key_energy: 2.6, key_colour: [255, 255, 255], key_direction: [-0.45, -0.55, -0.70], fill_energy: 0.8, fill_colour: [210, 225, 255], fill_direction: [0.55, 0.35, -0.30], albedo_is_direct_colour: true, black_albedo: [28, 38, 40], black_roughness: 0.75, roughness: 0.72, metallic: 0.0, cull_back_faces: true, depth_test: true, depth_write: true, alpha_blend: false, } }}
#[cfg(not(target_arch = "wasm32"))]pub struct NativeLights { ambient: AmbientLight, key: DirectionalLight, fill: DirectionalLight,}
#[cfg(not(target_arch = "wasm32"))]impl NativeLights { pub fn new(context: &three_d::Context) -> Self { let policy = LdrawLightingMaterialPolicy::ldraw(); Self { ambient: AmbientLight::new( context, policy.ambient_energy, Srgba::new_opaque( policy.ambient_colour[0], policy.ambient_colour[1], policy.ambient_colour[2], ), ), key: DirectionalLight::new( context, policy.key_energy, Srgba::new_opaque( policy.key_colour[0], policy.key_colour[1], policy.key_colour[2], ), three_d::vec3( policy.key_direction[0], policy.key_direction[1], policy.key_direction[2], ), ), fill: DirectionalLight::new( context, policy.fill_energy, Srgba::new_opaque( policy.fill_colour[0], policy.fill_colour[1], policy.fill_colour[2], ), three_d::vec3( policy.fill_direction[0], policy.fill_direction[1], policy.fill_direction[2], ), ), } }
pub fn as_refs(&self) -> [&dyn three_d::Light; 3] { [&self.ambient, &self.key, &self.fill] }}
fn ldraw_colour(rgb: [u8; 3]) -> Srgba { ldraw_colour_with_policy(rgb, LdrawLightingMaterialPolicy::ldraw(), u8::MAX)}
/// Hardcoded fallback for common LDraw colour codes when the colour table/// (LDConfig.ldr) is unavailable or doesn't define a code. This keeps models/// visually correct even when the library is incomplete or absent.fn fallback_colour_rgb(code: u32) -> Option<[u8; 3]> { Some(match code { 0 => [33, 33, 33], 1 => [0, 85, 191], 2 => [37, 122, 62], 3 => [44, 148, 180], 4 => [201, 26, 9], 5 => [220, 198, 152], 6 => [150, 157, 161], 7 => [160, 165, 169], 8 => [109, 110, 108], 9 => [180, 210, 227], 10 => [75, 151, 74], 11 => [90, 195, 195], 12 => [225, 213, 237], 13 => [199, 104, 13], 14 => [242, 205, 55], 15 => [242, 243, 242], 16 => [160, 165, 169], 17 => [225, 193, 110], 18 => [170, 125, 90], 19 => [228, 173, 200], 20 => [180, 210, 227], 21 => [221, 117, 46], 22 => [170, 127, 255], 23 => [170, 182, 149], 24 => [160, 143, 95], 25 => [254, 138, 24], 26 => [80, 36, 35], 27 => [105, 64, 39], 28 => [160, 160, 160], 29 => [120, 120, 120], 30 => [95, 124, 150], _ => return None, })}
fn ldraw_colour_with_policy(rgb: [u8; 3], policy: LdrawLightingMaterialPolicy, alpha: u8) -> Srgba { let [r, g, b] = if rgb == [0, 0, 0] { policy.black_albedo } else { rgb }; Srgba::new(r, g, b, alpha)}
pub type NativeSceneModel = Vec<three_d::Gm<three_d::InstancedMesh, Arc<three_d::PhysicalMaterial>>>;
pub fn build_scene_model( context: &three_d::Context, scene: &Scene,) -> Result<NativeSceneModel, RendererError> { build_scene_model_for_view_with_resources(context, scene, None)}
pub fn build_scene_model_for_view_with_resources( context: &three_d::Context, scene: &Scene, resources: Option<&[ModelResource]>,) -> Result<NativeSceneModel, RendererError> { let plan = scene_render_plan(scene)?; build_scene_model_from_batches( context, scene, &plan.index, &plan.batches, &plan.upload_batch_indices, &plan.topologies, resources, )}
pub type EdgeModel = Gm<CompactSegmentGeometry, CompactSegmentMaterial>;
pub type EdgeModelParts = (Option<Vec<EdgeModel>>, Option<Vec<EdgeModel>>);
pub fn update_edge_model_culling_bounds( models: &mut [EdgeModel], viewer: &dyn Viewer,) -> Result<(), RendererError> { for model in models { model.geometry.update_culling_aabb(viewer)?; } Ok(())}
const BASE_RIBBON_WIDTH_PIXELS: f32 = 1.5;const SELECTED_RIBBON_WIDTH_PIXELS: f32 = 3.0;
#[cfg(not(target_arch = "wasm32"))]pub type SelectionModelParts = ( Option<Vec<EdgeModel>>, Option<Vec<EdgeModel>>, Option<SelectionFallback>,);
#[cfg(not(target_arch = "wasm32"))]#[derive(Default)]pub struct NativeSelectionSlots { pub identity: Option<SelectionIdentity>, pub explicit: Option<Vec<EdgeModel>>, pub conditional: Option<Vec<EdgeModel>>, pub fallback: Option<SelectionFallback>, pub generation: u64, pub conditional_generation: u64,}
#[cfg(not(target_arch = "wasm32"))]impl NativeSelectionSlots { pub fn clear(&mut self) { self.identity = None; self.explicit = None; self.conditional = None; self.fallback = None; self.generation = self.generation.wrapping_add(1); self.conditional_generation = self.conditional_generation.wrapping_add(1); }
pub fn install(&mut self, identity: SelectionIdentity, models: SelectionModelParts) { self.generation = self.generation.wrapping_add(1); self.conditional_generation = self.conditional_generation.wrapping_add(1); self.identity = Some(identity); self.explicit = models.0; self.conditional = models.1; self.fallback = models.2; }
pub fn refilter_conditional( &mut self, generation: u64, conditional: Option<Vec<EdgeModel>>, ) -> bool { if self.identity.is_none() || generation != self.generation { return false; } self.conditional_generation = self.conditional_generation.wrapping_add(1); self.conditional = conditional; true }
pub fn live_renderable_count(&self) -> usize { self.explicit.as_ref().map_or(0, Vec::len) + self.conditional.as_ref().map_or(0, Vec::len) + usize::from(self.fallback.is_some()) }}
pub struct CompactSegmentGeometry { context: three_d::Context, ribbon_width: f32, endpoints: VertexBuffer<three_d::Vec3>, paired_endpoints: VertexBuffer<three_d::Vec3>, control1s: VertexBuffer<three_d::Vec3>, control2s: VertexBuffer<three_d::Vec3>, sides: VertexBuffer<f32>, transforms: ( InstanceBuffer<three_d::Vec4>, InstanceBuffer<three_d::Vec4>, InstanceBuffer<three_d::Vec4>, ), local_views: Option<InstanceBuffer<three_d::Vec3>>, instance_count: u32, base_aabb: AxisAlignedBoundingBox, aabb: AxisAlignedBoundingBox,}
fn compact_segment_aabb( endpoints: &[three_d::Vec3], transforms: &[Mat4],) -> Result<AxisAlignedBoundingBox, RendererError> { if endpoints.is_empty() { return Err(RendererError::InvalidRange { context: "compact segment endpoints", index: 0, len: endpoints.len(), }); } if transforms.is_empty() { return Err(RendererError::InvalidRange { context: "compact segment transforms", index: 0, len: transforms.len(), }); } if endpoints.iter().any(|point| { [point.x, point.y, point.z] .into_iter() .any(|value| !value.is_finite()) }) { return Err(RendererError::NonFiniteTransform); } let local = AxisAlignedBoundingBox::new_with_positions(endpoints); let mut world = AxisAlignedBoundingBox::EMPTY; for transform in transforms { let transformed = local.transformed(*transform); if ![ transformed.min().x, transformed.min().y, transformed.min().z, ] .into_iter() .chain([ transformed.max().x, transformed.max().y, transformed.max().z, ]) .all(|value| value.is_finite()) { return Err(RendererError::NonFiniteTransform); } world.expand_with_aabb(transformed); } if [world.min().x, world.min().y, world.min().z] .into_iter() .chain([world.max().x, world.max().y, world.max().z]) .all(|value| value.is_finite()) { Ok(world) } else { Err(RendererError::NonFiniteTransform) }}
fn validate_compact_segment_cardinality( segment_count: u64, control_count: Option<u64>, transform_count: u64, local_view_count: Option<u64>,) -> Result<(u64, u32), RendererError> { let vertex_count = segment_count .checked_mul(6) .ok_or(RendererError::IndexOverflow { context: "compact segment vertex count", })?; if let Some(control_count) = control_count && control_count != segment_count { return Err(RendererError::InvalidRange { context: "compact segment controls", index: control_count as usize, len: segment_count as usize, }); } if let Some(local_view_count) = local_view_count && local_view_count != transform_count { return Err(RendererError::InvalidRange { context: "compact segment local views", index: local_view_count as usize, len: transform_count as usize, }); } let instance_count = u32::try_from(transform_count).map_err(|_| RendererError::IndexOverflow { context: "compact segment instance count", })?; Ok((vertex_count, instance_count))}
impl CompactSegmentGeometry { fn new( context: &three_d::Context, segments: &[[[f32; 3]; 2]], controls: Option<&[[[f32; 3]; 2]]>, transforms: &[Mat4], local_views: Option<&[[f32; 3]]>, ribbon_width: f32, ) -> Result<Self, RendererError> { let (vertex_count, instance_count) = validate_compact_segment_cardinality( u64::try_from(segments.len()).map_err(|_| RendererError::IndexOverflow { context: "compact segment count", })?, controls .map(<[_]>::len) .map(u64::try_from) .transpose() .map_err(|_| RendererError::IndexOverflow { context: "compact segment control count", })?, u64::try_from(transforms.len()).map_err(|_| RendererError::IndexOverflow { context: "compact segment transform count", })?, local_views .map(<[_]>::len) .map(u64::try_from) .transpose() .map_err(|_| RendererError::IndexOverflow { context: "compact segment local-view count", })?, )?; let vertex_count = usize::try_from(vertex_count).map_err(|_| RendererError::IndexOverflow { context: "compact segment vertex address", })?; let mut endpoints = Vec::with_capacity(vertex_count); let mut paired_endpoints = Vec::with_capacity(vertex_count); let mut control1s = Vec::with_capacity(vertex_count); let mut control2s = Vec::with_capacity(vertex_count); let mut sides = Vec::with_capacity(vertex_count); for segment in segments { let a = three_d::vec3(segment[0][0], segment[0][1], segment[0][2]); let b = three_d::vec3(segment[1][0], segment[1][1], segment[1][2]); let control = controls .and_then(|controls| controls.get(endpoints.len() / 6).copied()) .unwrap_or([ [segment[0][0], segment[0][1], segment[0][2]], [segment[1][0], segment[1][1], segment[1][2]], ]); let c1 = three_d::vec3(control[0][0], control[0][1], control[0][2]); let c2 = three_d::vec3(control[1][0], control[1][1], control[1][2]); for (point, other, side) in [ (a, b, -1.0), (a, b, 1.0), (b, a, 1.0), (b, a, 1.0), (a, b, -1.0), (b, a, -1.0), ] { endpoints.push(point); paired_endpoints.push(other); control1s.push(c1); control2s.push(c2); sides.push(side); } } let base_aabb = compact_segment_aabb(&endpoints, transforms)?; let row1: Vec<three_d::Vec4> = transforms.iter().map(|m| m.row(0)).collect(); let row2: Vec<three_d::Vec4> = transforms.iter().map(|m| m.row(1)).collect(); let row3: Vec<three_d::Vec4> = transforms.iter().map(|m| m.row(2)).collect(); Ok(Self { context: context.clone(), ribbon_width, endpoints: VertexBuffer::new_with_data(context, &endpoints), paired_endpoints: VertexBuffer::new_with_data(context, &paired_endpoints), control1s: VertexBuffer::new_with_data(context, &control1s), control2s: VertexBuffer::new_with_data(context, &control2s), sides: VertexBuffer::new_with_data(context, &sides), transforms: ( InstanceBuffer::new_with_data(context, &row1), InstanceBuffer::new_with_data(context, &row2), InstanceBuffer::new_with_data(context, &row3), ), local_views: local_views.map(|views| { InstanceBuffer::new_with_data( context, &views .iter() .map(|view| three_d::vec3(view[0], view[1], view[2])) .collect::<Vec<_>>(), ) }), instance_count, base_aabb, aabb: base_aabb, }) }}
fn compact_segment_vertex_shader(ribbon_width_pixels: f32) -> String { let source = r#"uniform mat4 viewProjection;// Physical framebuffer dimensions in pixels, supplied for every draw.uniform vec2 viewport;
in vec3 endpoint;in vec3 pairedEndpoint;in vec3 control1;in vec3 control2;in float side;in vec3 localView;in vec4 row1;in vec4 row2;in vec4 row3;out vec4 col;out vec3 pos;flat out int instance_id;
bool clip_segment_to_plane(inout vec4 a, inout vec4 b, vec4 plane) { float da = dot(plane, a); float db = dot(plane, b); if (any(isnan(vec4(da, db, a.w, b.w))) || any(isinf(vec4(da, db, a.w, b.w)))) return false; if (da < 0.0 && db < 0.0) return false; if (da < 0.0) { float denominator = da - db; if (!(denominator > 0.0)) return false; a = mix(a, b, da / denominator); } else if (db < 0.0) { float denominator = db - da; if (!(denominator > 0.0)) return false; b = mix(b, a, db / denominator); } return true;}
void main() { mat4 transform = mat4( vec4(row1.x, row2.x, row3.x, 0.0), vec4(row1.y, row2.y, row3.y, 0.0), vec4(row1.z, row2.z, row3.z, 0.0), vec4(row1.w, row2.w, row3.w, 1.0)); vec4 clip = viewProjection * transform * vec4(endpoint, 1.0); vec4 pairedClip = viewProjection * transform * vec4(pairedEndpoint, 1.0); vec3 worldEndpoint = (transform * vec4(endpoint, 1.0)).xyz; vec3 worldPairedEndpoint = (transform * vec4(pairedEndpoint, 1.0)).xyz; if (length(localView) > 0.0) { vec3 edge = pairedEndpoint - endpoint; vec3 c1 = control1 - endpoint; vec3 c2 = control2 - endpoint; float side1 = dot(cross(edge, c1), localView); float side2 = dot(cross(edge, c2), localView); if (side1 != side1 || side2 != side2 || abs(side1) > 1.0e30 || abs(side2) > 1.0e30 || ((side1 >= 0.0) != (side2 >= 0.0))) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); return; } } if (!clip_segment_to_plane(clip, pairedClip, vec4(0.0, 0.0, 1.0, 1.0)) || !clip_segment_to_plane(clip, pairedClip, vec4(0.0, 0.0, 0.0, 1.0)) || !(clip.w > 0.0) || !(pairedClip.w > 0.0) || any(isnan(vec4(clip.xy, pairedClip.xy))) || any(isinf(vec4(clip.xy, pairedClip.xy)))) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); return; } vec2 endpoint_ndc = clip.xy / clip.w; vec2 paired_ndc = pairedClip.xy / pairedClip.w; vec2 window_tangent = (paired_ndc - endpoint_ndc) * (0.5 * viewport); float tangent_length = length(window_tangent); if (!(tangent_length > 0.0) || !(viewport.x > 0.0) || !(viewport.y > 0.0) || any(isnan(vec4(endpoint_ndc, paired_ndc))) || any(isinf(vec4(endpoint_ndc, paired_ndc)))) { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); return; } vec2 window_normal = vec2(-window_tangent.y, window_tangent.x) / tangent_length; vec2 ndc_offset = window_normal * (2.0 / viewport) * (0.5 * __RIBBON_WIDTH_PIXELS__); clip.xy += ndc_offset * side * clip.w; gl_Position = clip; pos = worldEndpoint; col = vec4(1.0); instance_id = gl_InstanceID;}"#; source.replace( "__RIBBON_WIDTH_PIXELS__", &format!("{:.7}", ribbon_width_pixels), )}
impl CompactSegmentGeometry { fn update_culling_aabb(&mut self, viewer: &dyn Viewer) -> Result<(), RendererError> { if self.base_aabb.is_empty() { self.aabb = self.base_aabb; return Ok(()); } let viewport = viewer.viewport(); if viewport.width == 0 || viewport.height == 0 { return Err(RendererError::InvalidRange { context: "ribbon culling viewport", index: 0, len: 0, }); } let view_projection = viewer.projection() * viewer.view(); let corners = [ self.base_aabb.min(), three_d::vec3( self.base_aabb.max().x, self.base_aabb.min().y, self.base_aabb.min().z, ), three_d::vec3( self.base_aabb.min().x, self.base_aabb.max().y, self.base_aabb.min().z, ), three_d::vec3( self.base_aabb.min().x, self.base_aabb.min().y, self.base_aabb.max().z, ), three_d::vec3( self.base_aabb.min().x, self.base_aabb.max().y, self.base_aabb.max().z, ), three_d::vec3( self.base_aabb.max().x, self.base_aabb.min().y, self.base_aabb.max().z, ), three_d::vec3( self.base_aabb.max().x, self.base_aabb.max().y, self.base_aabb.min().z, ), self.base_aabb.max(), ]; let projection_x_scale = view_projection.x.x.abs() + view_projection.y.x.abs() + view_projection.z.x.abs(); let projection_y_scale = view_projection.x.y.abs() + view_projection.y.y.abs() + view_projection.z.y.abs(); let projection_w_scale = view_projection.x.w.abs() + view_projection.y.w.abs() + view_projection.z.w.abs(); if !projection_x_scale.is_finite() || !projection_y_scale.is_finite() || !projection_w_scale.is_finite() || projection_x_scale <= 0.0 || projection_y_scale <= 0.0 { return Err(RendererError::NonFiniteTransform); } let mut radius = 0.0_f32; let mut minimum_w = f32::INFINITY; for corner in corners { let clip = view_projection * corner.extend(1.0); if !clip.x.is_finite() || !clip.y.is_finite() || !clip.w.is_finite() || clip.w <= 0.0 { return Err(RendererError::NonFiniteTransform); } minimum_w = minimum_w.min(clip.w); let radius_x = self.ribbon_width * clip.w / (viewport.width as f32 * projection_x_scale); let radius_y = self.ribbon_width * clip.w / (viewport.height as f32 * projection_y_scale); radius = radius.max(radius_x.max(radius_y)); } if !radius.is_finite() || radius > f32::MAX / 4.0 || minimum_w <= radius * projection_w_scale { return Err(RendererError::NonFiniteTransform); } let delta = three_d::vec3(radius, radius, radius); self.aabb = AxisAlignedBoundingBox::new_with_positions(&[ self.base_aabb.min() - delta, self.base_aabb.max() + delta, ]); if self.aabb.min().x.is_finite() && self.aabb.min().y.is_finite() && self.aabb.min().z.is_finite() && self.aabb.max().x.is_finite() && self.aabb.max().y.is_finite() && self.aabb.max().z.is_finite() { Ok(()) } else { Err(RendererError::NonFiniteTransform) } }}
impl Geometry for CompactSegmentGeometry { fn draw(&self, viewer: &dyn Viewer, program: &Program, render_states: RenderStates) { program.use_uniform("viewProjection", viewer.projection() * viewer.view()); let viewport = viewer.viewport(); program.use_uniform( "viewport", three_d::vec2(viewport.width as f32, viewport.height as f32), ); program.use_vertex_attribute("endpoint", &self.endpoints); program.use_vertex_attribute("pairedEndpoint", &self.paired_endpoints); program.use_vertex_attribute("control1", &self.control1s); program.use_vertex_attribute("control2", &self.control2s); program.use_vertex_attribute("side", &self.sides); if let Some(local_views) = &self.local_views { program.use_instance_attribute("localView", local_views); } program.use_instance_attribute("row1", &self.transforms.0); program.use_instance_attribute("row2", &self.transforms.1); program.use_instance_attribute("row3", &self.transforms.2); program.draw_arrays_instanced( render_states, viewer.viewport(), self.endpoints.vertex_count(), self.instance_count, ); }
fn vertex_shader_source(&self) -> String { compact_segment_vertex_shader(self.ribbon_width) }
fn id(&self) -> ThreeGeometryId { ThreeGeometryId::Sprites } fn render_with_material( &self, material: &dyn Material, viewer: &dyn Viewer, lights: &[&dyn Light], ) { if let Err(error) = three_d::render_with_material(&self.context, viewer, self, material, lights) { tracing::error!(%error, "rendering compact geometry with material failed"); } } fn render_with_effect( &self, material: &dyn Effect, viewer: &dyn Viewer, lights: &[&dyn Light], color_texture: Option<three_d::ColorTexture>, depth_texture: Option<three_d::DepthTexture>, ) { if let Err(error) = three_d::render_with_effect( &self.context, viewer, self, material, lights, color_texture, depth_texture, ) { tracing::error!(%error, "rendering compact geometry with effect failed"); } } fn aabb(&self) -> AxisAlignedBoundingBox { self.aabb }}
#[derive(Clone)]pub struct CompactSegmentMaterial { color: Srgba, render_states: RenderStates, material_type: MaterialType,}
impl Material for CompactSegmentMaterial { fn fragment_shader_source(&self, _lights: &[&dyn Light]) -> String { "in vec4 col;\nuniform vec4 surfaceColor;\nlayout(location = 0) out vec4 outColor;\nvoid main(){outColor = surfaceColor * col;}".to_owned() } fn id(&self) -> EffectMaterialId { EffectMaterialId(0x0001) } fn use_uniforms(&self, program: &Program, _viewer: &dyn Viewer, _lights: &[&dyn Light]) { program.use_uniform("surfaceColor", self.color.to_linear_srgb()); } fn render_states(&self) -> RenderStates { self.render_states } fn material_type(&self) -> MaterialType { self.material_type }}
#[cfg(not(target_arch = "wasm32"))]pub fn diagnostic_edge_models(label: &str, models: &[EdgeModel], camera: &Camera, generation: u64) { if std::env::var_os("POLYMODEL_LDRAW_RUNTIME_DIAGNOSTICS").is_none() { return; } let frustum = three_d::Frustum::new(camera.projection() * camera.view()); tracing::debug!( "[ldraw-runtime] slot={label} generation={generation} objects={} camera_position={:?} camera_target={:?} camera_distance={:.6} viewport={}x{}", models.len(), camera.position(), camera.target(), (camera.position() - camera.target()).magnitude(), camera.viewport().width, camera.viewport().height, ); for (index, model) in models.iter().enumerate() { let aabb = model.aabb(); let center_distance = (aabb.center() - camera.position()).magnitude(); tracing::debug!( "[ldraw-runtime] slot={label} object={index} material_type={:?} alpha={} render_states={:?} aabb_min={:?} aabb_max={:?} center_distance={center_distance:.6} frustum_visible={} segments={} instances={} vertices={}", model.material_type(), model.material.color.a, model.material.render_states, aabb.min(), aabb.max(), frustum.contains(aabb), model.geometry.endpoints.vertex_count() / 6, model.geometry.instance_count, model.geometry.endpoints.vertex_count(), ); }}
#[cfg(not(target_arch = "wasm32"))]pub fn diagnostic_edge_model_slot(label: &str, models: Option<&[EdgeModel]>, generation: u64) { if std::env::var_os("POLYMODEL_LDRAW_RUNTIME_DIAGNOSTICS").is_some() { tracing::debug!( "[ldraw-runtime] slot-summary slot={label} generation={generation} present={} objects={}", models.is_some(), models.map_or(0, <[EdgeModel]>::len), ); }}
pub fn build_edge_models_for_view( context: &three_d::Context, scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3],) -> Result<EdgeModelParts, RendererError> { let topology_started = Instant::now(); let (explicit_inputs, conditional_inputs) = compact_edge_inputs_for_view(scene, plan, view)?; let stats = explicit_inputs.stats; let topology_cpu_ms = topology_started.elapsed().as_millis(); let upload_started = Instant::now(); let explicit = build_compact_edge_models(context, explicit_inputs.batches, false)?; let conditional = build_compact_edge_models(context, conditional_inputs.batches, false)?; let model_upload_ms = upload_started.elapsed().as_millis(); tracing::debug!( "[ldraw-preview] edge-output source_geometries={} planned_occurrences={} explicit_segments={} conditional_visible_segments={} vertices={} indices={} material_groups={} topology_cpu_ms={} model_upload_ms={}", stats.source_geometry_count, stats.planned_occurrences, stats.explicit_segments, stats.visible_conditional_segments, stats.generated_vertices, stats.generated_indices, stats.material_groups, topology_cpu_ms, model_upload_ms, ); Ok(( (!explicit.is_empty()).then_some(explicit), (!conditional.is_empty()).then_some(conditional), ))}
pub fn build_conditional_edge_models_for_view( context: &three_d::Context, scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3],) -> Result<Option<Vec<EdgeModel>>, RendererError> { let conditional_inputs = compact_conditional_edge_inputs_for_membership(scene, plan, view, None)?; let models = build_compact_edge_models(context, conditional_inputs.batches, false)?; Ok((!models.is_empty()).then_some(models))}
#[cfg(not(target_arch = "wasm32"))]fn selection_fallback( explicit_empty: bool, bounds: Option<SceneBounds>,) -> Option<SelectionFallback> { explicit_empty .then(|| bounds.map(SelectionFallback::BoundingBox)) .flatten()}
#[cfg(not(target_arch = "wasm32"))]pub fn build_selected_edge_models_for_view( context: &three_d::Context, scene: &Scene, plan: &SceneRenderPlan, selection: &SelectionIdentity, view: [f64; 3],) -> Result<SelectionModelParts, RendererError> { let (explicit_inputs, _) = compact_edge_inputs_for_membership(scene, plan, view, Some(&selection.occurrence_indices))?; let conditional_inputs = compact_conditional_edge_inputs_for_membership( scene, plan, view, Some(&selection.occurrence_indices), )?; let explicit = build_compact_edge_models(context, explicit_inputs.batches, true)?; let conditional = build_compact_edge_models(context, conditional_inputs.batches, true)?; let fallback = selection_fallback(explicit.is_empty(), selection.bounds); Ok(( (!explicit.is_empty()).then_some(explicit), (!conditional.is_empty()).then_some(conditional), fallback, ))}
#[cfg(not(target_arch = "wasm32"))]pub fn build_selected_conditional_edge_models_for_view( context: &three_d::Context, scene: &Scene, plan: &SceneRenderPlan, selection: &SelectionIdentity, view: [f64; 3],) -> Result<Option<Vec<EdgeModel>>, RendererError> { let inputs = compact_conditional_edge_inputs_for_membership( scene, plan, view, Some(&selection.occurrence_indices), )?; let models = build_compact_edge_models(context, inputs.batches, true)?; Ok((!models.is_empty()).then_some(models))}pub type SceneModelParts = ( NativeSceneModel, EdgeModelParts, ScenePicker, SceneRenderPlan,);
pub fn build_scene_model_parts_for_style( context: &three_d::Context, scene: &Scene, view: [f64; 3], style: RenderStyle,) -> Result<SceneModelParts, RendererError> { build_scene_model_parts_for_style_with_resources(context, scene, view, style, None)}
pub fn build_scene_model_parts_for_style_with_resources( context: &three_d::Context, scene: &Scene, view: [f64; 3], style: RenderStyle, resources: Option<&[ModelResource]>,) -> Result<SceneModelParts, RendererError> { let started = Instant::now(); let plan = scene_render_plan(scene)?; let plan_elapsed = started.elapsed(); tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={} cumulative_ms={}", plan_elapsed.as_millis(), plan_elapsed.as_millis() ); let static_started = Instant::now(); let static_model = build_scene_model_from_batches( context, scene, &plan.index, &plan.batches, &plan.upload_batch_indices, &plan.topologies, resources, )?; let static_elapsed = static_started.elapsed(); tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={} cumulative_ms={}", static_elapsed.as_millis(), started.elapsed().as_millis() ); let (explicit_model, conditional_model) = if style == RenderStyle::Edges { let edge_started = Instant::now(); let models = build_edge_models_for_view(context, scene, &plan, view)?; tracing::debug!( "[ldraw-preview] native-build-stage=edge-output elapsed_ms={} cumulative_ms={}", edge_started.elapsed().as_millis(), started.elapsed().as_millis() ); models } else { (None, None) }; let picker = ScenePicker::from_plan(scene, &plan)?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() ); Ok(( static_model, (explicit_model, conditional_model), picker, plan, ))}
pub fn build_scene_faces_and_picker( context: &three_d::Context, scene: &Scene,) -> Result<(NativeSceneModel, ScenePicker), RendererError> { let started = Instant::now(); let plan = scene_render_plan(scene)?; tracing::debug!( "[ldraw-preview] native-build-stage=plan elapsed_ms={}", started.elapsed().as_millis() ); let static_model = build_scene_model_from_batches( context, scene, &plan.index, &plan.batches, &plan.upload_batch_indices, &plan.topologies, None, )?; tracing::debug!( "[ldraw-preview] native-build-stage=static elapsed_ms={}", started.elapsed().as_millis() ); let picker = ScenePicker::from_plan(scene, &plan)?; tracing::debug!( "[ldraw-preview] native-build-stage=picker elapsed_ms={}", started.elapsed().as_millis() ); Ok((static_model, picker))}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]pub struct EdgeBuildStats { pub source_geometry_count: usize, pub planned_occurrences: usize, pub explicit_segments: usize, pub visible_conditional_segments: usize, pub generated_vertices: usize, pub generated_indices: usize, pub material_groups: usize, pub source_segment_vertices: usize, pub instance_count: usize, pub gpu_bytes: usize,}
#[derive(Clone, Debug)]struct CompactEdgeBatch { material: MaterialId, colour: u32, ribbon_width: f32, segments: Vec<[[f32; 3]; 2]>, controls: Option<Vec<[[f32; 3]; 2]>>, local_views: Option<Vec<[f32; 3]>>, transforms: Vec<Mat4>,}
#[derive(Clone, Debug, Default)]struct CompactEdgeInputs { batches: Vec<CompactEdgeBatch>, stats: EdgeBuildStats,}
fn compact_edge_inputs_for_view( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3],) -> Result<(CompactEdgeInputs, CompactEdgeInputs), RendererError> { compact_edge_inputs_for_membership(scene, plan, view, None)}
fn compact_conditional_edge_inputs_for_membership( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], selected: Option<&[usize]>,) -> Result<CompactEdgeInputs, RendererError> { let indices = selected.unwrap_or(&[]); let occurrences = if selected.is_some() { indices .iter() .map(|index| { plan.planned_occurrences .get(*index) .ok_or(RendererError::InvalidRange { context: "selected occurrence", index: *index, len: plan.planned_occurrences.len(), }) }) .collect::<Result<Vec<_>, _>>()? } else { plan.planned_occurrences.iter().collect() }; let mut conditional = std::collections::BTreeMap::<(usize, MaterialId), CompactEdgeBatch>::new(); let mut stats = EdgeBuildStats { source_geometry_count: plan.edge_sources.len(), planned_occurrences: plan.planned_occurrences.len(), ..Default::default() }; let default_material = scene .materials .first() .map(|material| material.id) .unwrap_or(MaterialId(1)); for occurrence in occurrences { let scene_occurrence = scene.occurrences.get(occurrence.occurrence_index).ok_or( RendererError::InvalidRange { context: "planned occurrence source", index: occurrence.occurrence_index, len: scene.occurrences.len(), }, )?; let material = occurrence.material.unwrap_or(default_material); let colour = scene .materials .iter() .find(|candidate| candidate.id == material) .ok_or(RendererError::MissingMaterial(material))? .colour; let source = &plan.edge_sources[occurrence.geometry_index]; if source.conditional.is_empty() { continue; } let local_view = conditional_line_view_for_transform(scene_occurrence.transform, view).unwrap_or(view); let batch = conditional .entry((occurrence.geometry_index, material)) .or_insert_with(|| CompactEdgeBatch { material, colour, ribbon_width: BASE_RIBBON_WIDTH_PIXELS, segments: source .conditional .iter() .map(|(endpoints, _)| *endpoints) .collect(), controls: Some( source .conditional .iter() .map(|(_, controls)| *controls) .collect(), ), local_views: Some(Vec::new()), transforms: Vec::new(), }); batch .transforms .push(scene_transform_to_mat4(scene_occurrence.transform)?); batch.local_views.as_mut().unwrap().push([ finite_f32(local_view[0])?, finite_f32(local_view[1])?, finite_f32(local_view[2])?, ]); stats.visible_conditional_segments += source.conditional.len(); } let batches = conditional .into_values() .filter(|batch| !batch.segments.is_empty()) .collect::<Vec<_>>(); stats.material_groups = batches.len(); stats.generated_vertices = batches.iter().map(|batch| batch.segments.len() * 6).sum(); stats.source_segment_vertices = stats.generated_vertices; stats.instance_count = batches.iter().map(|batch| batch.transforms.len()).sum(); stats.gpu_bytes = stats.generated_vertices * (3 * 4 + 3 * 4 + 4) + stats.instance_count * (3 * 4 * 4); Ok(CompactEdgeInputs { batches, stats })}
fn compact_edge_inputs_for_membership( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3], selected: Option<&[usize]>,) -> Result<(CompactEdgeInputs, CompactEdgeInputs), RendererError> { let mut stats = EdgeBuildStats { source_geometry_count: plan.edge_sources.len(), planned_occurrences: plan.planned_occurrences.len(), ..Default::default() }; let occurrences = selected .map(|indices| { indices .iter() .map(|index| { plan.planned_occurrences .get(*index) .ok_or(RendererError::InvalidRange { context: "selected occurrence", index: *index, len: plan.planned_occurrences.len(), }) }) .collect::<Result<Vec<_>, _>>() }) .unwrap_or_else(|| Ok(plan.planned_occurrences.iter().collect()))?; let mut explicit = std::collections::BTreeMap::<(usize, MaterialId), CompactEdgeBatch>::new(); let mut conditional = std::collections::BTreeMap::<(usize, MaterialId), CompactEdgeBatch>::new(); let default_material = scene .materials .first() .map(|material| material.id) .unwrap_or(MaterialId(1)); for occurrence in occurrences { let scene_occurrence = scene.occurrences.get(occurrence.occurrence_index).ok_or( RendererError::InvalidRange { context: "planned occurrence source", index: occurrence.occurrence_index, len: scene.occurrences.len(), }, )?; let material = occurrence.material.unwrap_or(default_material); let colour = scene .materials .iter() .find(|candidate| candidate.id == material) .ok_or(RendererError::MissingMaterial(material))? .colour; let source = &plan.edge_sources[occurrence.geometry_index]; let local_view = conditional_line_view_for_transform(scene_occurrence.transform, view).unwrap_or(view); let explicit_batch = explicit .entry((occurrence.geometry_index, material)) .or_insert_with(|| CompactEdgeBatch { material, colour, ribbon_width: BASE_RIBBON_WIDTH_PIXELS, segments: source.explicit.to_vec(), controls: None, local_views: None, transforms: Vec::new(), }); if !explicit_batch.segments.is_empty() { stats.explicit_segments += source.explicit.len(); explicit_batch .transforms .push(scene_transform_to_mat4(scene_occurrence.transform)?); } if !source.conditional.is_empty() { let batch = conditional .entry((occurrence.geometry_index, material)) .or_insert_with(|| CompactEdgeBatch { material, colour, ribbon_width: BASE_RIBBON_WIDTH_PIXELS, segments: source .conditional .iter() .map(|(endpoints, _)| *endpoints) .collect(), controls: Some( source .conditional .iter() .map(|(_, controls)| *controls) .collect(), ), local_views: Some(Vec::new()), transforms: Vec::new(), }); batch .transforms .push(scene_transform_to_mat4(scene_occurrence.transform)?); batch.local_views.as_mut().unwrap().push([ finite_f32(local_view[0])?, finite_f32(local_view[1])?, finite_f32(local_view[2])?, ]); stats.visible_conditional_segments += source.conditional.len(); } } let mut explicit_inputs = CompactEdgeInputs { batches: explicit .into_values() .filter(|batch| !batch.segments.is_empty()) .collect(), stats, }; let mut conditional_stats = stats; let conditional_batches = conditional .into_values() .filter(|batch| !batch.segments.is_empty()) .collect::<Vec<_>>(); explicit_inputs.stats.material_groups = explicit_inputs.batches.len() + conditional_batches.len(); conditional_stats.material_groups = conditional_batches.len(); let explicit_vertices = explicit_inputs .batches .iter() .map(|batch| batch.segments.len() * 6) .sum::<usize>(); let conditional_vertices = conditional_batches .iter() .map(|batch| batch.segments.len() * 6) .sum::<usize>(); let explicit_instances = explicit_inputs .batches .iter() .map(|batch| batch.transforms.len()) .sum::<usize>(); let conditional_instances = conditional_batches .iter() .map(|batch| batch.transforms.len()) .sum::<usize>(); explicit_inputs.stats.generated_vertices = explicit_vertices + conditional_vertices; explicit_inputs.stats.generated_indices = 0; explicit_inputs.stats.source_segment_vertices = explicit_vertices + conditional_vertices; explicit_inputs.stats.instance_count = explicit_instances + conditional_instances; explicit_inputs.stats.gpu_bytes = explicit_inputs.stats.source_segment_vertices * (3 * 4 + 3 * 4 + 4) + explicit_inputs.stats.instance_count * (3 * 4 * 4); conditional_stats.generated_vertices = conditional_vertices; conditional_stats.generated_indices = 0; conditional_stats.source_segment_vertices = conditional_vertices; conditional_stats.instance_count = conditional_instances; conditional_stats.gpu_bytes = conditional_vertices * (3 * 4 + 3 * 4 + 4) + conditional_instances * (3 * 4 * 4); Ok(( explicit_inputs, CompactEdgeInputs { batches: conditional_batches, stats: conditional_stats, }, ))}
fn finite_f32_vec3(point: [f64; 3]) -> Result<[f32; 3], RendererError> { Ok([ finite_f32(point[0])?, finite_f32(point[1])?, finite_f32(point[2])?, ])}
pub fn edge_build_stats( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3],) -> Result<EdgeBuildStats, RendererError> { Ok(compact_edge_inputs_for_view(scene, plan, view)?.0.stats)}
pub fn build_conditional_edge_topology_for_scene( scene: &Scene, plan: &SceneRenderPlan, view: [f64; 3],) -> Result<Vec<(MaterialId, u32, SceneMeshTopology)>, RendererError> { let (_, conditional) = compact_edge_inputs_for_view(scene, plan, view)?; Ok(conditional .batches .into_iter() .map(|batch| { ( batch.material, batch.colour, SceneMeshTopology { positions: batch .segments .iter() .flat_map(|segment| segment.iter().copied()) .collect(), uvs: Box::default(), indices: Vec::new(), }, ) }) .collect())}
fn compact_segment_material(_colour: u32, selected: bool) -> CompactSegmentMaterial { CompactSegmentMaterial { color: if selected { Srgba::new(116, 180, 255, 200) } else { ldraw_colour([160, 165, 169]) }, render_states: if selected { RenderStates { cull: Cull::None, blend: Blend::Enabled { source_rgb_multiplier: BlendMultiplierType::SrcAlpha, source_alpha_multiplier: BlendMultiplierType::One, destination_rgb_multiplier: BlendMultiplierType::OneMinusSrcAlpha, destination_alpha_multiplier: BlendMultiplierType::OneMinusSrcAlpha, rgb_equation: BlendEquationType::Add, alpha_equation: BlendEquationType::Add, }, ..Default::default() } } else { RenderStates { cull: Cull::None, blend: Blend::Disabled, ..Default::default() } }, material_type: if selected { MaterialType::Transparent } else { MaterialType::Opaque }, }}
fn build_compact_edge_models( context: &three_d::Context, batches: Vec<CompactEdgeBatch>, selected: bool,) -> Result<Vec<Gm<CompactSegmentGeometry, CompactSegmentMaterial>>, RendererError> { batches .into_iter() .map(|batch| { let geometry = CompactSegmentGeometry::new( context, &batch.segments, batch.controls.as_deref(), &batch.transforms, batch.local_views.as_deref(), if selected { SELECTED_RIBBON_WIDTH_PIXELS } else { batch.ribbon_width }, )?; Ok(Gm::new( geometry, compact_segment_material(batch.colour, selected), )) }) .collect()}
fn accumulate_scene_stats( stats: &mut SceneStats, geometry: &SceneGeometry,) -> Result<(), RendererError> { let points = u64::try_from(geometry.points.len()) .map_err(|_| RendererError::ParseAdaptation("point count overflow"))?; let triangles = u64::try_from(geometry.triangles.len()) .map_err(|_| RendererError::ParseAdaptation("triangle count overflow"))?; let quads = u64::try_from(geometry.quads.len()) .map_err(|_| RendererError::ParseAdaptation("quad count overflow"))?; let lines = u64::try_from(geometry.lines.len()) .map_err(|_| RendererError::ParseAdaptation("line count overflow"))?; let conditional_lines = u64::try_from(geometry.conditional_lines.len()) .map_err(|_| RendererError::ParseAdaptation("conditional-line count overflow"))?; stats.points = stats .points .checked_add(points) .ok_or(RendererError::ParseAdaptation("point count overflow"))?; stats.triangles = stats .triangles .checked_add(triangles) .and_then(|value| value.checked_add(quads.checked_mul(2)?)) .ok_or(RendererError::ParseAdaptation("triangle count overflow"))?; stats.lines = stats .lines .checked_add(lines) .ok_or(RendererError::ParseAdaptation("line count overflow"))?; stats.conditional_lines = stats .conditional_lines .checked_add(conditional_lines) .ok_or(RendererError::ParseAdaptation( "conditional-line count overflow", ))?; Ok(())}
fn scene_material_translucent(material: &SceneMaterial, texture_translucent: bool) -> bool { material.alpha < u8::MAX || texture_translucent}
fn scene_material_type(translucent: bool) -> MaterialType { if translucent { MaterialType::Transparent } else { MaterialType::Opaque }}
fn scene_material_render_states(translucent: bool, cull_back_faces: bool) -> RenderStates { RenderStates { cull: if cull_back_faces { Cull::Back } else { Cull::None }, blend: if translucent { Blend::Enabled { source_rgb_multiplier: BlendMultiplierType::SrcAlpha, source_alpha_multiplier: BlendMultiplierType::One, destination_rgb_multiplier: BlendMultiplierType::OneMinusSrcAlpha, destination_alpha_multiplier: BlendMultiplierType::OneMinusSrcAlpha, rgb_equation: BlendEquationType::Add, alpha_equation: BlendEquationType::Add, } } else { Blend::Disabled }, write_mask: if translucent { WriteMask::COLOR } else { WriteMask::COLOR_AND_DEPTH }, ..Default::default() }}
fn build_scene_model_from_batches( context: &three_d::Context, scene: &Scene, index: &SceneIndex, batches: &[SceneInstanceBatch], upload_batch_indices: &[usize], topologies: &[SceneMeshTopology], resources: Option<&[ModelResource]>,) -> Result<NativeSceneModel, RendererError> { let resource_bytes = |texture: &polymodel_renderer_protocol::scene::SceneTexture| { let key = texture .resource_key .as_deref() .unwrap_or(texture.uri.as_str()); resources .and_then(|resources| { resources .iter() .find(|resource| resource.key == key || resource.path == key) }) .map(|resource| (key.to_owned(), &*resource.bytes)) .ok_or_else(|| RendererError::MissingTexture(texture.id)) }; let mut albedo_cache = HashMap::<String, Texture2D>::new(); let mut gloss_cache = HashMap::<String, Texture2D>::new(); let mut transparency_cache = HashMap::<String, bool>::new(); let mut material_cache = HashMap::<(MaterialId, bool), Arc<PhysicalMaterial>>::new(); let model = upload_batch_indices .iter() .copied() .map(|batch_index| { let batch = batches .get(batch_index) .ok_or(RendererError::InvalidRange { context: "upload batch index", index: batch_index, len: batches.len(), })?; let _geometry = index.geometry(scene, batch.node)?; let transformations = batch .instances .iter() .map(|instance| { let occurrence = scene.occurrences.get(instance.occurrence_index).ok_or( RendererError::InvalidRange { context: "batch occurrence", index: instance.occurrence_index, len: scene.occurrences.len(), }, )?; scene_transform_to_mat4(occurrence.transform) }) .collect::<Result<Vec<_>, RendererError>>()?; let scene_material = index.material(scene, batch.material)?; let colour_rgb = scene_material .colour_rgb .or_else(|| fallback_colour_rgb(scene_material.colour)) .unwrap_or([160, 165, 169]); let policy = LdrawLightingMaterialPolicy::ldraw(); let alpha = scene_material.alpha; let mut cpu_material = CpuMaterial { albedo: ldraw_colour_with_policy(colour_rgb, policy, alpha), roughness: if colour_rgb == [0, 0, 0] { policy.black_roughness } else { policy.roughness }, metallic: policy.metallic, ..Default::default() }; let mut translucent = scene_material_translucent(scene_material, false); if let Some(texture_id) = scene_material.albedo_texture { let texture = scene .textures .iter() .find(|texture| texture.id == texture_id) .ok_or(RendererError::MissingTexture(texture_id))?; let (key, bytes) = resource_bytes(texture)?; if !albedo_cache.contains_key(&key) { let (width, height, pixels, transparency) = decode_albedo_png(bytes)?; transparency_cache.insert(key.clone(), transparency); albedo_cache.insert( key.clone(), Texture2D { name: texture.uri.to_string(), data: TextureData::RgbaU8(pixels), width, height, ..Default::default() }, ); } translucent |= *transparency_cache .get(&key) .ok_or(RendererError::MissingTexture( scene_material.albedo_texture.unwrap(), ))?; cpu_material.albedo_texture = albedo_cache.get(&key).cloned(); } if let Some(texture_id) = scene_material.metallic_roughness_texture { let texture = scene .textures .iter() .find(|texture| texture.id == texture_id) .ok_or(RendererError::MissingTexture(texture_id))?; let (key, bytes) = resource_bytes(texture)?; if !gloss_cache.contains_key(&key) { let decoded = decode_glossmap_png(bytes)?; gloss_cache.insert( key.clone(), Texture2D { name: texture.uri.to_string(), data: TextureData::RgbU8(decoded.packed_rgb), width: decoded.width, height: decoded.height, ..Default::default() }, ); } cpu_material.metallic_roughness_texture = gloss_cache.get(&key).cloned(); } let material_key = (batch.material, batch.bfc_cull && policy.cull_back_faces); let material = if let Some(material) = material_cache.get(&material_key) { material.clone() } else { let mut material = if matches!(scene_material_type(translucent), MaterialType::Transparent) { PhysicalMaterial::new_transparent(context, &cpu_material) } else { PhysicalMaterial::new_opaque(context, &cpu_material) }; material.render_states = scene_material_render_states(translucent, material_key.1); let material = Arc::new(material); material_cache.insert(material_key, material.clone()); material }; if transformations.is_empty() { return Ok(None); } let geometry_index = index.geometry_index(batch.node)?; let topology = topologies .get(geometry_index) .ok_or(RendererError::InvalidRange { context: "render plan geometry topology", index: geometry_index, len: topologies.len(), })?; if topology.indices.is_empty() { return Ok(None); } let mut indices = topology.indices.clone(); if batch.bfc_inverted ^ !batch.bfc_ccw { for triangle in indices.chunks_exact_mut(3) { triangle.swap(0, 2); } } let mut cpu_mesh = CpuMesh { positions: Positions::F32( topology .positions .iter() .map(|point| three_d::vec3(point[0], point[1], point[2])) .collect(), ), indices: Indices::U32(indices), uvs: batch .uvs .as_deref() .map(|uvs| uvs) .or_else(|| (!topology.uvs.is_empty()).then_some(&*topology.uvs)) .map(|uvs: &[[f32; 2]]| { uvs.iter().map(|uv| three_d::vec2(uv[0], uv[1])).collect() }), ..Default::default() }; cpu_mesh.compute_normals(); if batch.reflection && let Some(normals) = cpu_mesh.normals.as_mut() { for normal in normals { *normal = -*normal; } } if cpu_mesh.normals.as_ref().map(Vec::len) != Some(topology.positions.len()) { return Err(RendererError::InvalidRange { context: "geometry normals", index: cpu_mesh.normals.as_ref().map_or(0, Vec::len), len: topology.positions.len(), }); } Ok(Some(Gm::new( InstancedMesh::new( context, &Instances { transformations, ..Default::default() }, &cpu_mesh, ), material, ))) }) .collect::<Result<Vec<_>, RendererError>>()? .into_iter() .flatten() .collect::<Vec<_>>(); if model.is_empty() { Err(RendererError::ParseAdaptation( "scene contains no renderable instanced geometry", )) } else { Ok(model) }}
#[cfg(not(target_arch = "wasm32"))]pub fn render_scene_window_options_traced( scene: &Scene, output: Option<&std::path::Path>, trace: NativeTrace, options: NativeRenderOptions,) -> Result<(), RendererError> { use std::panic::{AssertUnwindSafe, catch_unwind}; use three_d::{ClearState, FrameOutput, Window, WindowSettings};
trace.record(NativeStage::Started); let bounds = scene.bounds.ok_or_else(|| { trace.finish(NativeTerminal::Failure("scene has no bounds".to_owned())); RendererError::ParseAdaptation("scene has no bounds") })?; if scene.stats.triangles == 0 { let error = RendererError::ParseAdaptation("scene has no triangles"); trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } trace.record(NativeStage::LibraryResolved); trace.record(NativeStage::OmrParsed); trace.record(NativeStage::SceneAdapted); tracing::debug!( "[ldraw-preview] backend=wayland WAYLAND_DISPLAY={} XDG_RUNTIME_DIR={}", std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "<unset>".to_owned()), std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "<unset>".to_owned()), ); let window = Window::new(WindowSettings { title: "Polymodel LDraw Preview".into(), initial_size: Some((options.width, options.height)), max_size: Some((options.width, options.height)), ..Default::default() }) .map_err(|error| { let error = RendererError::External(format!("native window: {error}")); trace.finish(NativeTerminal::Failure(error.to_string())); error })?; trace.record(NativeStage::WindowCreated); tracing::debug!("[ldraw-preview] window/context created"); let context = window.gl(); trace.record(NativeStage::ContextCreated); let (static_model, (explicit_model, conditional_model), _, _) = match catch_unwind(AssertUnwindSafe(|| { build_scene_model_parts_for_style(&context, scene, [0.0, 0.0, 1.0], options.style) })) { Ok(Ok(parts)) => parts, Ok(Err(error)) => { trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } Err(_) => { let error = RendererError::GpuResource("GPU resource creation panicked".to_owned()); trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } }; trace.record(NativeStage::GpuResourcesCreated); let lights = NativeLights::new(&context); let viewport = window.viewport(); if viewport.width != options.width || viewport.height != options.height { let error = RendererError::External(format!( "native viewport mismatch: requested {}x{}, actual {}x{}", options.width, options.height, viewport.width, viewport.height )); trace.finish(NativeTerminal::Failure(error.to_string())); return Err(error); } let fit = scene_camera_fit(bounds, options.width, options.height) .map_err(|error| RendererError::External(error.to_string()))?; let (position, target, up, fov_degrees) = if let Some(case) = options.camera_case.as_ref() { let pose = case .native_pose(fit) .map_err(|error| RendererError::External(error.to_string()))?; (pose.position, pose.target, pose.up, pose.fov_degrees) } else { ( yaw_camera_position(fit.center, fit.position, options.yaw_degrees), fit.center, three_d::vec3(0.0, 0.0, 1.0), 45.0, ) }; if let (Some(case), Some(variant)) = (options.camera_case.as_ref(), options.variant.as_ref()) { let metadata = NativeCaptureMetadata { case_id: case.case_id.clone(), schema_version: case.schema_version.clone(), variant: variant.clone(), fixture_hash: pm90_fixture_hash(), requested_width: options.width, requested_height: options.height, actual_width: viewport.width, actual_height: viewport.height, native_path: "static".into(), }; emit_lighting_diagnostic(&metadata, scene); tracing::debug!( "[ldraw-preview] capture case={} schema={} variant={} occurrence={} fixture={} requested={}x{} actual={}x{}", metadata.case_id, metadata.schema_version, metadata.variant.variant_id, metadata.variant.occurrence_id, metadata.fixture_hash, metadata.requested_width, metadata.requested_height, metadata.actual_width, metadata.actual_height, ); } tracing::debug!( "[ldraw-preview] camera center={:?} position={:?} target={:?} up={:?} fov={:.1} near={:.3} far={:.3} yaw_degrees={:.1} viewport={}x{}", fit.center, position, target, up, fov_degrees, fit.near, fit.far, options.yaw_degrees, viewport.width, viewport.height, ); let projection = camera_space_clip_planes(bounds, position, target) .map_err(|error| RendererError::External(error.to_string()))?; let mut camera = Camera::new_perspective( viewport, position, target, up, three_d::degrees(fov_degrees), projection.near, projection.far, ); let mut control = OrbitControl::new(fit.center, fit.control_min, fit.control_max); let mut output_path = output.map(std::path::Path::to_owned); let callback_trace = trace.clone(); window.render_loop(move |mut frame_input| { let result = catch_unwind(AssertUnwindSafe(|| { callback_trace.record(NativeStage::FrameCallbackEntered); let headless = output_path.is_some(); let viewport = if headless { three_d::Viewport::new_at_origo(options.width, options.height) } else { frame_input.viewport }; camera.set_viewport(viewport); control.handle_events(&mut camera, &mut frame_input.events); let projection = camera_space_clip_planes(bounds, camera.position(), camera.target()) .map_err(|error| RendererError::External(error.to_string()))?; let fov = { let projection_type = camera.projection_type(); match projection_type { three_d_asset::ProjectionType::Perspective { field_of_view_y } => { *field_of_view_y } _ => { return Err(RendererError::External( "camera projection is not perspective".to_owned(), )); } } }; camera.set_perspective_projection(fov, projection.near, projection.far); let target = if headless { three_d::RenderTarget::screen(&context, options.width, options.height) } else { frame_input.screen() }; let screen = target.clear(ClearState::color_and_depth(0.78, 0.80, 0.84, 1.0, 1.0)); let light_refs = lights.as_refs(); screen.render(&camera, &static_model, &light_refs); if let Some(explicit_model) = explicit_model.as_ref() { screen.render(&camera, explicit_model, &light_refs); } if let Some(conditional_model) = conditional_model.as_ref() { screen.render(&camera, conditional_model, &light_refs); } callback_trace.record(NativeStage::DrawCompleted); if frame_input.first_frame || headless { let pixels = screen.read_color::<[u8; 4]>(); callback_trace.record(NativeStage::ReadbackCompleted); if let Some(path) = output_path.take() { std::fs::create_dir_all( path.parent().unwrap_or_else(|| std::path::Path::new(".")), ) .map_err(|error| { RendererError::Io(format!("create preview output directory: {error}")) })?; std::fs::write( path, native_png_bytes( viewport.width, viewport.height, &pixels.into_iter().flatten().collect::<Vec<_>>(), ), ) .map_err(|error| RendererError::Io(format!("write preview output: {error}")))?; callback_trace.record(NativeStage::OutputWritten); callback_trace.finish(NativeTerminal::Success); return Ok(FrameOutput { exit: true, ..Default::default() }); } } Ok(FrameOutput::default()) })); match result { Ok(Ok(output)) => output, Ok(Err(error)) => { callback_trace.finish(NativeTerminal::Failure(error.to_string())); FrameOutput { exit: true, ..Default::default() } } Err(_) => { callback_trace.finish(NativeTerminal::Failure("render callback panicked".into())); FrameOutput { exit: true, ..Default::default() } } } }); let snapshot = trace.snapshot(); match snapshot.terminal { Some(NativeTerminal::Success) | None if output.is_none() => Ok(()), Some(NativeTerminal::Success) => Ok(()), Some(NativeTerminal::Failure(error)) | Some(NativeTerminal::Timeout(error)) => { Err(RendererError::External(error)) } None => Err(RendererError::External( "native render exited without terminal state".to_owned(), )), }}
pub fn conditional_line_visible( endpoints: [[f64; 3]; 2], controls: [[f64; 3]; 2], view: [f64; 3],) -> bool { let edge = [ endpoints[1][0] - endpoints[0][0], endpoints[1][1] - endpoints[0][1], endpoints[1][2] - endpoints[0][2], ]; let projected_side = |point: [f64; 3]| { let to_control = [ point[0] - endpoints[0][0], point[1] - endpoints[0][1], point[2] - endpoints[0][2], ]; let cross = [ edge[1] * to_control[2] - edge[2] * to_control[1], edge[2] * to_control[0] - edge[0] * to_control[2], edge[0] * to_control[1] - edge[1] * to_control[0], ]; cross[0] * view[0] + cross[1] * view[1] + cross[2] * view[2] }; let first = projected_side(controls[0]); let second = projected_side(controls[1]); first.is_finite() && second.is_finite() && (first >= 0.0) == (second >= 0.0)}
pub mod admission;mod texture;pub use texture::{ GlossMapPixels, decode_albedo_png, decode_glossmap_png, packed_roughness_from_alpha, packed_roughness_from_alpha_samples, texmap_uvs,};
#[cfg(test)]mod tests;