camlib.py 295 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. #import traceback
  9. from io import StringIO
  10. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
  11. transpose
  12. from numpy.linalg import solve, norm
  13. import re
  14. import sys
  15. import traceback
  16. from decimal import Decimal
  17. import collections
  18. from rtree import index as rtindex
  19. # See: http://toblerity.org/shapely/manual.html
  20. from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
  21. from shapely.geometry import MultiPoint, MultiPolygon
  22. from shapely.geometry import box as shply_box
  23. from shapely.ops import cascaded_union, unary_union
  24. import shapely.affinity as affinity
  25. from shapely.wkt import loads as sloads
  26. from shapely.wkt import dumps as sdumps
  27. from shapely.geometry.base import BaseGeometry
  28. from shapely.geometry import shape
  29. from shapely import speedups
  30. from collections import Iterable
  31. import numpy as np
  32. import rasterio
  33. from rasterio.features import shapes
  34. from copy import deepcopy
  35. # TODO: Commented for FlatCAM packaging with cx_freeze
  36. from xml.dom.minidom import parseString as parse_xml_string
  37. # from scipy.spatial import KDTree, Delaunay
  38. from ParseSVG import *
  39. from ParseDXF import *
  40. import logging
  41. import os
  42. # import pprint
  43. import platform
  44. import FlatCAMApp
  45. import math
  46. if platform.architecture()[0] == '64bit':
  47. from ortools.constraint_solver import pywrapcp
  48. from ortools.constraint_solver import routing_enums_pb2
  49. log = logging.getLogger('base2')
  50. log.setLevel(logging.DEBUG)
  51. formatter = logging.Formatter('[%(levelname)s] %(message)s')
  52. handler = logging.StreamHandler()
  53. handler.setFormatter(formatter)
  54. log.addHandler(handler)
  55. import gettext
  56. import FlatCAMTranslation as fcTranslate
  57. fcTranslate.apply_language('camlib')
  58. def _tr(text):
  59. try:
  60. return _(text)
  61. except:
  62. return text
  63. class ParseError(Exception):
  64. pass
  65. class Geometry(object):
  66. """
  67. Base geometry class.
  68. """
  69. defaults = {
  70. "units": 'in',
  71. "geo_steps_per_circle": 64
  72. }
  73. def __init__(self, geo_steps_per_circle=None):
  74. # Units (in or mm)
  75. self.units = Geometry.defaults["units"]
  76. # Final geometry: MultiPolygon or list (of geometry constructs)
  77. self.solid_geometry = None
  78. # Final geometry: MultiLineString or list (of LineString or Points)
  79. self.follow_geometry = None
  80. # Attributes to be included in serialization
  81. self.ser_attrs = ["units", 'solid_geometry', 'follow_geometry']
  82. # Flattened geometry (list of paths only)
  83. self.flat_geometry = []
  84. # this is the calculated conversion factor when the file units are different than the ones in the app
  85. self.file_units_factor = 1
  86. # Index
  87. self.index = None
  88. self.geo_steps_per_circle = geo_steps_per_circle
  89. if geo_steps_per_circle is None:
  90. geo_steps_per_circle = int(Geometry.defaults["geo_steps_per_circle"])
  91. self.geo_steps_per_circle = geo_steps_per_circle
  92. def make_index(self):
  93. self.flatten()
  94. self.index = FlatCAMRTree()
  95. for i, g in enumerate(self.flat_geometry):
  96. self.index.insert(i, g)
  97. def add_circle(self, origin, radius):
  98. """
  99. Adds a circle to the object.
  100. :param origin: Center of the circle.
  101. :param radius: Radius of the circle.
  102. :return: None
  103. """
  104. # TODO: Decide what solid_geometry is supposed to be and how we append to it.
  105. if self.solid_geometry is None:
  106. self.solid_geometry = []
  107. if type(self.solid_geometry) is list:
  108. self.solid_geometry.append(Point(origin).buffer(radius, int(int(self.geo_steps_per_circle) / 4)))
  109. return
  110. try:
  111. self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(radius,
  112. int(int(self.geo_steps_per_circle) / 4)))
  113. except:
  114. #print "Failed to run union on polygons."
  115. log.error("Failed to run union on polygons.")
  116. return
  117. def add_polygon(self, points):
  118. """
  119. Adds a polygon to the object (by union)
  120. :param points: The vertices of the polygon.
  121. :return: None
  122. """
  123. if self.solid_geometry is None:
  124. self.solid_geometry = []
  125. if type(self.solid_geometry) is list:
  126. self.solid_geometry.append(Polygon(points))
  127. return
  128. try:
  129. self.solid_geometry = self.solid_geometry.union(Polygon(points))
  130. except:
  131. #print "Failed to run union on polygons."
  132. log.error("Failed to run union on polygons.")
  133. return
  134. def add_polyline(self, points):
  135. """
  136. Adds a polyline to the object (by union)
  137. :param points: The vertices of the polyline.
  138. :return: None
  139. """
  140. if self.solid_geometry is None:
  141. self.solid_geometry = []
  142. if type(self.solid_geometry) is list:
  143. self.solid_geometry.append(LineString(points))
  144. return
  145. try:
  146. self.solid_geometry = self.solid_geometry.union(LineString(points))
  147. except:
  148. #print "Failed to run union on polygons."
  149. log.error("Failed to run union on polylines.")
  150. return
  151. def is_empty(self):
  152. if isinstance(self.solid_geometry, BaseGeometry):
  153. return self.solid_geometry.is_empty
  154. if isinstance(self.solid_geometry, list):
  155. return len(self.solid_geometry) == 0
  156. self.app.inform.emit(_tr("[ERROR_NOTCL] self.solid_geometry is neither BaseGeometry or list."))
  157. return
  158. def subtract_polygon(self, points):
  159. """
  160. Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths.
  161. :param points: The vertices of the polygon.
  162. :return: none
  163. """
  164. if self.solid_geometry is None:
  165. self.solid_geometry = []
  166. #pathonly should be allways True, otherwise polygons are not subtracted
  167. flat_geometry = self.flatten(pathonly=True)
  168. log.debug("%d paths" % len(flat_geometry))
  169. polygon=Polygon(points)
  170. toolgeo=cascaded_union(polygon)
  171. diffs=[]
  172. for target in flat_geometry:
  173. if type(target) == LineString or type(target) == LinearRing:
  174. diffs.append(target.difference(toolgeo))
  175. else:
  176. log.warning("Not implemented.")
  177. self.solid_geometry=cascaded_union(diffs)
  178. def bounds(self):
  179. """
  180. Returns coordinates of rectangular bounds
  181. of geometry: (xmin, ymin, xmax, ymax).
  182. """
  183. # fixed issue of getting bounds only for one level lists of objects
  184. # now it can get bounds for nested lists of objects
  185. log.debug("Geometry->bounds()")
  186. if self.solid_geometry is None:
  187. log.debug("solid_geometry is None")
  188. return 0, 0, 0, 0
  189. def bounds_rec(obj):
  190. if type(obj) is list:
  191. minx = Inf
  192. miny = Inf
  193. maxx = -Inf
  194. maxy = -Inf
  195. for k in obj:
  196. if type(k) is dict:
  197. for key in k:
  198. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  199. minx = min(minx, minx_)
  200. miny = min(miny, miny_)
  201. maxx = max(maxx, maxx_)
  202. maxy = max(maxy, maxy_)
  203. else:
  204. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  205. minx = min(minx, minx_)
  206. miny = min(miny, miny_)
  207. maxx = max(maxx, maxx_)
  208. maxy = max(maxy, maxy_)
  209. return minx, miny, maxx, maxy
  210. else:
  211. # it's a Shapely object, return it's bounds
  212. return obj.bounds
  213. if self.multigeo is True:
  214. minx_list = []
  215. miny_list = []
  216. maxx_list = []
  217. maxy_list = []
  218. for tool in self.tools:
  219. minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry'])
  220. minx_list.append(minx)
  221. miny_list.append(miny)
  222. maxx_list.append(maxx)
  223. maxy_list.append(maxy)
  224. return(min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
  225. else:
  226. bounds_coords = bounds_rec(self.solid_geometry)
  227. return bounds_coords
  228. # try:
  229. # # from here: http://rightfootin.blogspot.com/2006/09/more-on-python-flatten.html
  230. # def flatten(l, ltypes=(list, tuple)):
  231. # ltype = type(l)
  232. # l = list(l)
  233. # i = 0
  234. # while i < len(l):
  235. # while isinstance(l[i], ltypes):
  236. # if not l[i]:
  237. # l.pop(i)
  238. # i -= 1
  239. # break
  240. # else:
  241. # l[i:i + 1] = l[i]
  242. # i += 1
  243. # return ltype(l)
  244. #
  245. # log.debug("Geometry->bounds()")
  246. # if self.solid_geometry is None:
  247. # log.debug("solid_geometry is None")
  248. # return 0, 0, 0, 0
  249. #
  250. # if type(self.solid_geometry) is list:
  251. # # TODO: This can be done faster. See comment from Shapely mailing lists.
  252. # if len(self.solid_geometry) == 0:
  253. # log.debug('solid_geometry is empty []')
  254. # return 0, 0, 0, 0
  255. # return cascaded_union(flatten(self.solid_geometry)).bounds
  256. # else:
  257. # return self.solid_geometry.bounds
  258. # except Exception as e:
  259. # self.app.inform.emit("[ERROR_NOTCL] Error cause: %s" % str(e))
  260. # log.debug("Geometry->bounds()")
  261. # if self.solid_geometry is None:
  262. # log.debug("solid_geometry is None")
  263. # return 0, 0, 0, 0
  264. #
  265. # if type(self.solid_geometry) is list:
  266. # # TODO: This can be done faster. See comment from Shapely mailing lists.
  267. # if len(self.solid_geometry) == 0:
  268. # log.debug('solid_geometry is empty []')
  269. # return 0, 0, 0, 0
  270. # return cascaded_union(self.solid_geometry).bounds
  271. # else:
  272. # return self.solid_geometry.bounds
  273. def find_polygon(self, point, geoset=None):
  274. """
  275. Find an object that object.contains(Point(point)) in
  276. poly, which can can be iterable, contain iterable of, or
  277. be itself an implementer of .contains().
  278. :param poly: See description
  279. :return: Polygon containing point or None.
  280. """
  281. if geoset is None:
  282. geoset = self.solid_geometry
  283. try: # Iterable
  284. for sub_geo in geoset:
  285. p = self.find_polygon(point, geoset=sub_geo)
  286. if p is not None:
  287. return p
  288. except TypeError: # Non-iterable
  289. try: # Implements .contains()
  290. if isinstance(geoset, LinearRing):
  291. geoset = Polygon(geoset)
  292. if geoset.contains(Point(point)):
  293. return geoset
  294. except AttributeError: # Does not implement .contains()
  295. return None
  296. return None
  297. def get_interiors(self, geometry=None):
  298. interiors = []
  299. if geometry is None:
  300. geometry = self.solid_geometry
  301. ## If iterable, expand recursively.
  302. try:
  303. for geo in geometry:
  304. interiors.extend(self.get_interiors(geometry=geo))
  305. ## Not iterable, get the interiors if polygon.
  306. except TypeError:
  307. if type(geometry) == Polygon:
  308. interiors.extend(geometry.interiors)
  309. return interiors
  310. def get_exteriors(self, geometry=None):
  311. """
  312. Returns all exteriors of polygons in geometry. Uses
  313. ``self.solid_geometry`` if geometry is not provided.
  314. :param geometry: Shapely type or list or list of list of such.
  315. :return: List of paths constituting the exteriors
  316. of polygons in geometry.
  317. """
  318. exteriors = []
  319. if geometry is None:
  320. geometry = self.solid_geometry
  321. ## If iterable, expand recursively.
  322. try:
  323. for geo in geometry:
  324. exteriors.extend(self.get_exteriors(geometry=geo))
  325. ## Not iterable, get the exterior if polygon.
  326. except TypeError:
  327. if type(geometry) == Polygon:
  328. exteriors.append(geometry.exterior)
  329. return exteriors
  330. def flatten(self, geometry=None, reset=True, pathonly=False):
  331. """
  332. Creates a list of non-iterable linear geometry objects.
  333. Polygons are expanded into its exterior and interiors if specified.
  334. Results are placed in self.flat_geometry
  335. :param geometry: Shapely type or list or list of list of such.
  336. :param reset: Clears the contents of self.flat_geometry.
  337. :param pathonly: Expands polygons into linear elements.
  338. """
  339. if geometry is None:
  340. geometry = self.solid_geometry
  341. if reset:
  342. self.flat_geometry = []
  343. ## If iterable, expand recursively.
  344. try:
  345. for geo in geometry:
  346. if geo is not None:
  347. self.flatten(geometry=geo,
  348. reset=False,
  349. pathonly=pathonly)
  350. ## Not iterable, do the actual indexing and add.
  351. except TypeError:
  352. if pathonly and type(geometry) == Polygon:
  353. self.flat_geometry.append(geometry.exterior)
  354. self.flatten(geometry=geometry.interiors,
  355. reset=False,
  356. pathonly=True)
  357. else:
  358. self.flat_geometry.append(geometry)
  359. return self.flat_geometry
  360. # def make2Dstorage(self):
  361. #
  362. # self.flatten()
  363. #
  364. # def get_pts(o):
  365. # pts = []
  366. # if type(o) == Polygon:
  367. # g = o.exterior
  368. # pts += list(g.coords)
  369. # for i in o.interiors:
  370. # pts += list(i.coords)
  371. # else:
  372. # pts += list(o.coords)
  373. # return pts
  374. #
  375. # storage = FlatCAMRTreeStorage()
  376. # storage.get_points = get_pts
  377. # for shape in self.flat_geometry:
  378. # storage.insert(shape)
  379. # return storage
  380. # def flatten_to_paths(self, geometry=None, reset=True):
  381. # """
  382. # Creates a list of non-iterable linear geometry elements and
  383. # indexes them in rtree.
  384. #
  385. # :param geometry: Iterable geometry
  386. # :param reset: Wether to clear (True) or append (False) to self.flat_geometry
  387. # :return: self.flat_geometry, self.flat_geometry_rtree
  388. # """
  389. #
  390. # if geometry is None:
  391. # geometry = self.solid_geometry
  392. #
  393. # if reset:
  394. # self.flat_geometry = []
  395. #
  396. # ## If iterable, expand recursively.
  397. # try:
  398. # for geo in geometry:
  399. # self.flatten_to_paths(geometry=geo, reset=False)
  400. #
  401. # ## Not iterable, do the actual indexing and add.
  402. # except TypeError:
  403. # if type(geometry) == Polygon:
  404. # g = geometry.exterior
  405. # self.flat_geometry.append(g)
  406. #
  407. # ## Add first and last points of the path to the index.
  408. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  409. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  410. #
  411. # for interior in geometry.interiors:
  412. # g = interior
  413. # self.flat_geometry.append(g)
  414. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  415. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  416. # else:
  417. # g = geometry
  418. # self.flat_geometry.append(g)
  419. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  420. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  421. #
  422. # return self.flat_geometry, self.flat_geometry_rtree
  423. def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None):
  424. """
  425. Creates contours around geometry at a given
  426. offset distance.
  427. :param offset: Offset distance.
  428. :type offset: float
  429. :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
  430. :type integer
  431. :param corner: type of corner for the isolation: 0 = round; 1 = square; 2= beveled (line that connects the ends)
  432. :return: The buffered geometry.
  433. :rtype: Shapely.MultiPolygon or Shapely.Polygon
  434. """
  435. # geo_iso = []
  436. # In case that the offset value is zero we don't use the buffer as the resulting geometry is actually the
  437. # original solid_geometry
  438. # if offset == 0:
  439. # geo_iso = self.solid_geometry
  440. # else:
  441. # flattened_geo = self.flatten_list(self.solid_geometry)
  442. # try:
  443. # for mp_geo in flattened_geo:
  444. # geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
  445. # except TypeError:
  446. # geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
  447. # return geo_iso
  448. # commented this because of the bug with multiple passes cutting out of the copper
  449. # geo_iso = []
  450. # flattened_geo = self.flatten_list(self.solid_geometry)
  451. # try:
  452. # for mp_geo in flattened_geo:
  453. # geo_iso.append(mp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
  454. # except TypeError:
  455. # geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)))
  456. # the previously commented block is replaced with this block - regression - to solve the bug with multiple
  457. # isolation passes cutting from the copper features
  458. if offset == 0:
  459. if follow:
  460. geo_iso = self.follow_geometry
  461. else:
  462. geo_iso = self.solid_geometry
  463. else:
  464. if follow:
  465. geo_iso = self.follow_geometry
  466. else:
  467. if corner is None:
  468. geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))
  469. else:
  470. geo_iso = self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4),
  471. join_style=corner)
  472. # end of replaced block
  473. if follow:
  474. return geo_iso
  475. elif iso_type == 2:
  476. return geo_iso
  477. elif iso_type == 0:
  478. return self.get_exteriors(geo_iso)
  479. elif iso_type == 1:
  480. return self.get_interiors(geo_iso)
  481. else:
  482. log.debug("Geometry.isolation_geometry() --> Type of isolation not supported")
  483. return "fail"
  484. def flatten_list(self, list):
  485. for item in list:
  486. if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
  487. yield from self.flatten_list(item)
  488. else:
  489. yield item
  490. def import_svg(self, filename, object_type=None, flip=True, units='MM'):
  491. """
  492. Imports shapes from an SVG file into the object's geometry.
  493. :param filename: Path to the SVG file.
  494. :type filename: str
  495. :param flip: Flip the vertically.
  496. :type flip: bool
  497. :return: None
  498. """
  499. # Parse into list of shapely objects
  500. svg_tree = ET.parse(filename)
  501. svg_root = svg_tree.getroot()
  502. # Change origin to bottom left
  503. # h = float(svg_root.get('height'))
  504. # w = float(svg_root.get('width'))
  505. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  506. geos = getsvggeo(svg_root, object_type)
  507. if flip:
  508. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  509. # Add to object
  510. if self.solid_geometry is None:
  511. self.solid_geometry = []
  512. if type(self.solid_geometry) is list:
  513. # self.solid_geometry.append(cascaded_union(geos))
  514. if type(geos) is list:
  515. self.solid_geometry += geos
  516. else:
  517. self.solid_geometry.append(geos)
  518. else: # It's shapely geometry
  519. # self.solid_geometry = cascaded_union([self.solid_geometry,
  520. # cascaded_union(geos)])
  521. self.solid_geometry = [self.solid_geometry, geos]
  522. # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
  523. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  524. self.solid_geometry = cascaded_union(self.solid_geometry)
  525. geos_text = getsvgtext(svg_root, object_type, units=units)
  526. if geos_text is not None:
  527. geos_text_f = []
  528. if flip:
  529. # Change origin to bottom left
  530. for i in geos_text:
  531. _, minimy, _, maximy = i.bounds
  532. h2 = (maximy - minimy) * 0.5
  533. geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
  534. self.solid_geometry = [self.solid_geometry, geos_text_f]
  535. def import_dxf(self, filename, object_type=None, units='MM'):
  536. """
  537. Imports shapes from an DXF file into the object's geometry.
  538. :param filename: Path to the DXF file.
  539. :type filename: str
  540. :param units: Application units
  541. :type flip: str
  542. :return: None
  543. """
  544. # Parse into list of shapely objects
  545. dxf = ezdxf.readfile(filename)
  546. geos = getdxfgeo(dxf)
  547. # Add to object
  548. if self.solid_geometry is None:
  549. self.solid_geometry = []
  550. if type(self.solid_geometry) is list:
  551. if type(geos) is list:
  552. self.solid_geometry += geos
  553. else:
  554. self.solid_geometry.append(geos)
  555. else: # It's shapely geometry
  556. self.solid_geometry = [self.solid_geometry, geos]
  557. # flatten the self.solid_geometry list for import_dxf() to import DXF as Gerber
  558. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  559. if self.solid_geometry is not None:
  560. self.solid_geometry = cascaded_union(self.solid_geometry)
  561. else:
  562. return
  563. # commented until this function is ready
  564. # geos_text = getdxftext(dxf, object_type, units=units)
  565. # if geos_text is not None:
  566. # geos_text_f = []
  567. # self.solid_geometry = [self.solid_geometry, geos_text_f]
  568. def import_image(self, filename, flip=True, units='MM', dpi=96, mode='black', mask=[128, 128, 128, 128]):
  569. """
  570. Imports shapes from an IMAGE file into the object's geometry.
  571. :param filename: Path to the IMAGE file.
  572. :type filename: str
  573. :param flip: Flip the object vertically.
  574. :type flip: bool
  575. :return: None
  576. """
  577. scale_factor = 0.264583333
  578. if units.lower() == 'mm':
  579. scale_factor = 25.4 / dpi
  580. else:
  581. scale_factor = 1 / dpi
  582. geos = []
  583. unscaled_geos = []
  584. with rasterio.open(filename) as src:
  585. # if filename.lower().rpartition('.')[-1] == 'bmp':
  586. # red = green = blue = src.read(1)
  587. # print("BMP")
  588. # elif filename.lower().rpartition('.')[-1] == 'png':
  589. # red, green, blue, alpha = src.read()
  590. # elif filename.lower().rpartition('.')[-1] == 'jpg':
  591. # red, green, blue = src.read()
  592. red = green = blue = src.read(1)
  593. try:
  594. green = src.read(2)
  595. except:
  596. pass
  597. try:
  598. blue= src.read(3)
  599. except:
  600. pass
  601. if mode == 'black':
  602. mask_setting = red <= mask[0]
  603. total = red
  604. log.debug("Image import as monochrome.")
  605. else:
  606. mask_setting = (red <= mask[1]) + (green <= mask[2]) + (blue <= mask[3])
  607. total = np.zeros(red.shape, dtype=float32)
  608. for band in red, green, blue:
  609. total += band
  610. total /= 3
  611. log.debug("Image import as colored. Thresholds are: R = %s , G = %s, B = %s" %
  612. (str(mask[1]), str(mask[2]), str(mask[3])))
  613. for geom, val in shapes(total, mask=mask_setting):
  614. unscaled_geos.append(shape(geom))
  615. for g in unscaled_geos:
  616. geos.append(scale(g, scale_factor, scale_factor, origin=(0, 0)))
  617. if flip:
  618. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0))) for g in geos]
  619. # Add to object
  620. if self.solid_geometry is None:
  621. self.solid_geometry = []
  622. if type(self.solid_geometry) is list:
  623. # self.solid_geometry.append(cascaded_union(geos))
  624. if type(geos) is list:
  625. self.solid_geometry += geos
  626. else:
  627. self.solid_geometry.append(geos)
  628. else: # It's shapely geometry
  629. self.solid_geometry = [self.solid_geometry, geos]
  630. # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
  631. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  632. self.solid_geometry = cascaded_union(self.solid_geometry)
  633. # self.solid_geometry = MultiPolygon(self.solid_geometry)
  634. # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
  635. # self.solid_geometry = self.solid_geometry.buffer(-0.00000001)
  636. def size(self):
  637. """
  638. Returns (width, height) of rectangular
  639. bounds of geometry.
  640. """
  641. if self.solid_geometry is None:
  642. log.warning("Solid_geometry not computed yet.")
  643. return 0
  644. bounds = self.bounds()
  645. return bounds[2] - bounds[0], bounds[3] - bounds[1]
  646. def get_empty_area(self, boundary=None):
  647. """
  648. Returns the complement of self.solid_geometry within
  649. the given boundary polygon. If not specified, it defaults to
  650. the rectangular bounding box of self.solid_geometry.
  651. """
  652. if boundary is None:
  653. boundary = self.solid_geometry.envelope
  654. return boundary.difference(self.solid_geometry)
  655. @staticmethod
  656. def clear_polygon(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True,
  657. contour=True):
  658. """
  659. Creates geometry inside a polygon for a tool to cover
  660. the whole area.
  661. This algorithm shrinks the edges of the polygon and takes
  662. the resulting edges as toolpaths.
  663. :param polygon: Polygon to clear.
  664. :param tooldia: Diameter of the tool.
  665. :param overlap: Overlap of toolpasses.
  666. :param connect: Draw lines between disjoint segments to
  667. minimize tool lifts.
  668. :param contour: Paint around the edges. Inconsequential in
  669. this painting method.
  670. :return:
  671. """
  672. # log.debug("camlib.clear_polygon()")
  673. assert type(polygon) == Polygon or type(polygon) == MultiPolygon, \
  674. "Expected a Polygon or MultiPolygon, got %s" % type(polygon)
  675. ## The toolpaths
  676. # Index first and last points in paths
  677. def get_pts(o):
  678. return [o.coords[0], o.coords[-1]]
  679. geoms = FlatCAMRTreeStorage()
  680. geoms.get_points = get_pts
  681. # Can only result in a Polygon or MultiPolygon
  682. # NOTE: The resulting polygon can be "empty".
  683. current = polygon.buffer((-tooldia / 1.999999), int(int(steps_per_circle) / 4))
  684. if current.area == 0:
  685. # Otherwise, trying to to insert current.exterior == None
  686. # into the FlatCAMStorage will fail.
  687. # print("Area is None")
  688. return None
  689. # current can be a MultiPolygon
  690. try:
  691. for p in current:
  692. geoms.insert(p.exterior)
  693. for i in p.interiors:
  694. geoms.insert(i)
  695. # Not a Multipolygon. Must be a Polygon
  696. except TypeError:
  697. geoms.insert(current.exterior)
  698. for i in current.interiors:
  699. geoms.insert(i)
  700. while True:
  701. # Can only result in a Polygon or MultiPolygon
  702. current = current.buffer(-tooldia * (1 - overlap), int(int(steps_per_circle) / 4))
  703. if current.area > 0:
  704. # current can be a MultiPolygon
  705. try:
  706. for p in current:
  707. geoms.insert(p.exterior)
  708. for i in p.interiors:
  709. geoms.insert(i)
  710. # Not a Multipolygon. Must be a Polygon
  711. except TypeError:
  712. geoms.insert(current.exterior)
  713. for i in current.interiors:
  714. geoms.insert(i)
  715. else:
  716. log.debug("camlib.Geometry.clear_polygon() --> Current Area is zero")
  717. break
  718. # Optimization: Reduce lifts
  719. if connect:
  720. # log.debug("Reducing tool lifts...")
  721. geoms = Geometry.paint_connect(geoms, polygon, tooldia, int(steps_per_circle))
  722. return geoms
  723. @staticmethod
  724. def clear_polygon2(polygon_to_clear, tooldia, steps_per_circle, seedpoint=None, overlap=0.15,
  725. connect=True, contour=True):
  726. """
  727. Creates geometry inside a polygon for a tool to cover
  728. the whole area.
  729. This algorithm starts with a seed point inside the polygon
  730. and draws circles around it. Arcs inside the polygons are
  731. valid cuts. Finalizes by cutting around the inside edge of
  732. the polygon.
  733. :param polygon_to_clear: Shapely.geometry.Polygon
  734. :param tooldia: Diameter of the tool
  735. :param seedpoint: Shapely.geometry.Point or None
  736. :param overlap: Tool fraction overlap bewteen passes
  737. :param connect: Connect disjoint segment to minumize tool lifts
  738. :param contour: Cut countour inside the polygon.
  739. :return: List of toolpaths covering polygon.
  740. :rtype: FlatCAMRTreeStorage | None
  741. """
  742. # log.debug("camlib.clear_polygon2()")
  743. # Current buffer radius
  744. radius = tooldia / 2 * (1 - overlap)
  745. ## The toolpaths
  746. # Index first and last points in paths
  747. def get_pts(o):
  748. return [o.coords[0], o.coords[-1]]
  749. geoms = FlatCAMRTreeStorage()
  750. geoms.get_points = get_pts
  751. # Path margin
  752. path_margin = polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))
  753. if path_margin.is_empty or path_margin is None:
  754. return
  755. # Estimate good seedpoint if not provided.
  756. if seedpoint is None:
  757. seedpoint = path_margin.representative_point()
  758. # Grow from seed until outside the box. The polygons will
  759. # never have an interior, so take the exterior LinearRing.
  760. while 1:
  761. path = Point(seedpoint).buffer(radius, int(steps_per_circle / 4)).exterior
  762. path = path.intersection(path_margin)
  763. # Touches polygon?
  764. if path.is_empty:
  765. break
  766. else:
  767. #geoms.append(path)
  768. #geoms.insert(path)
  769. # path can be a collection of paths.
  770. try:
  771. for p in path:
  772. geoms.insert(p)
  773. except TypeError:
  774. geoms.insert(path)
  775. radius += tooldia * (1 - overlap)
  776. # Clean inside edges (contours) of the original polygon
  777. if contour:
  778. outer_edges = [x.exterior for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4)))]
  779. inner_edges = []
  780. for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle / 4))): # Over resulting polygons
  781. for y in x.interiors: # Over interiors of each polygon
  782. inner_edges.append(y)
  783. #geoms += outer_edges + inner_edges
  784. for g in outer_edges + inner_edges:
  785. geoms.insert(g)
  786. # Optimization connect touching paths
  787. # log.debug("Connecting paths...")
  788. # geoms = Geometry.path_connect(geoms)
  789. # Optimization: Reduce lifts
  790. if connect:
  791. # log.debug("Reducing tool lifts...")
  792. geoms = Geometry.paint_connect(geoms, polygon_to_clear, tooldia, steps_per_circle)
  793. return geoms
  794. @staticmethod
  795. def clear_polygon3(polygon, tooldia, steps_per_circle, overlap=0.15, connect=True,
  796. contour=True):
  797. """
  798. Creates geometry inside a polygon for a tool to cover
  799. the whole area.
  800. This algorithm draws horizontal lines inside the polygon.
  801. :param polygon: The polygon being painted.
  802. :type polygon: shapely.geometry.Polygon
  803. :param tooldia: Tool diameter.
  804. :param overlap: Tool path overlap percentage.
  805. :param connect: Connect lines to avoid tool lifts.
  806. :param contour: Paint around the edges.
  807. :return:
  808. """
  809. # log.debug("camlib.clear_polygon3()")
  810. ## The toolpaths
  811. # Index first and last points in paths
  812. def get_pts(o):
  813. return [o.coords[0], o.coords[-1]]
  814. geoms = FlatCAMRTreeStorage()
  815. geoms.get_points = get_pts
  816. lines = []
  817. # Bounding box
  818. left, bot, right, top = polygon.bounds
  819. # First line
  820. y = top - tooldia / 1.99999999
  821. while y > bot + tooldia / 1.999999999:
  822. line = LineString([(left, y), (right, y)])
  823. lines.append(line)
  824. y -= tooldia * (1 - overlap)
  825. # Last line
  826. y = bot + tooldia / 2
  827. line = LineString([(left, y), (right, y)])
  828. lines.append(line)
  829. # Combine
  830. linesgeo = unary_union(lines)
  831. # Trim to the polygon
  832. margin_poly = polygon.buffer(-tooldia / 1.99999999, (int(steps_per_circle)))
  833. lines_trimmed = linesgeo.intersection(margin_poly)
  834. # Add lines to storage
  835. try:
  836. for line in lines_trimmed:
  837. geoms.insert(line)
  838. except TypeError:
  839. # in case lines_trimmed are not iterable (Linestring, LinearRing)
  840. geoms.insert(lines_trimmed)
  841. # Add margin (contour) to storage
  842. if contour:
  843. geoms.insert(margin_poly.exterior)
  844. for ints in margin_poly.interiors:
  845. geoms.insert(ints)
  846. # Optimization: Reduce lifts
  847. if connect:
  848. # log.debug("Reducing tool lifts...")
  849. geoms = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle)
  850. return geoms
  851. def scale(self, xfactor, yfactor, point=None):
  852. """
  853. Scales all of the object's geometry by a given factor. Override
  854. this method.
  855. :param factor: Number by which to scale.
  856. :type factor: float
  857. :return: None
  858. :rtype: None
  859. """
  860. return
  861. def offset(self, vect):
  862. """
  863. Offset the geometry by the given vector. Override this method.
  864. :param vect: (x, y) vector by which to offset the object.
  865. :type vect: tuple
  866. :return: None
  867. """
  868. return
  869. @staticmethod
  870. def paint_connect(storage, boundary, tooldia, steps_per_circle, max_walk=None):
  871. """
  872. Connects paths that results in a connection segment that is
  873. within the paint area. This avoids unnecessary tool lifting.
  874. :param storage: Geometry to be optimized.
  875. :type storage: FlatCAMRTreeStorage
  876. :param boundary: Polygon defining the limits of the paintable area.
  877. :type boundary: Polygon
  878. :param tooldia: Tool diameter.
  879. :rtype tooldia: float
  880. :param max_walk: Maximum allowable distance without lifting tool.
  881. :type max_walk: float or None
  882. :return: Optimized geometry.
  883. :rtype: FlatCAMRTreeStorage
  884. """
  885. # If max_walk is not specified, the maximum allowed is
  886. # 10 times the tool diameter
  887. max_walk = max_walk or 10 * tooldia
  888. # Assuming geolist is a flat list of flat elements
  889. ## Index first and last points in paths
  890. def get_pts(o):
  891. return [o.coords[0], o.coords[-1]]
  892. # storage = FlatCAMRTreeStorage()
  893. # storage.get_points = get_pts
  894. #
  895. # for shape in geolist:
  896. # if shape is not None: # TODO: This shouldn't have happened.
  897. # # Make LlinearRings into linestrings otherwise
  898. # # When chaining the coordinates path is messed up.
  899. # storage.insert(LineString(shape))
  900. # #storage.insert(shape)
  901. ## Iterate over geometry paths getting the nearest each time.
  902. #optimized_paths = []
  903. optimized_paths = FlatCAMRTreeStorage()
  904. optimized_paths.get_points = get_pts
  905. path_count = 0
  906. current_pt = (0, 0)
  907. pt, geo = storage.nearest(current_pt)
  908. storage.remove(geo)
  909. geo = LineString(geo)
  910. current_pt = geo.coords[-1]
  911. try:
  912. while True:
  913. path_count += 1
  914. #log.debug("Path %d" % path_count)
  915. pt, candidate = storage.nearest(current_pt)
  916. storage.remove(candidate)
  917. candidate = LineString(candidate)
  918. # If last point in geometry is the nearest
  919. # then reverse coordinates.
  920. # but prefer the first one if last == first
  921. if pt != candidate.coords[0] and pt == candidate.coords[-1]:
  922. candidate.coords = list(candidate.coords)[::-1]
  923. # Straight line from current_pt to pt.
  924. # Is the toolpath inside the geometry?
  925. walk_path = LineString([current_pt, pt])
  926. walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle / 4))
  927. if walk_cut.within(boundary) and walk_path.length < max_walk:
  928. #log.debug("Walk to path #%d is inside. Joining." % path_count)
  929. # Completely inside. Append...
  930. geo.coords = list(geo.coords) + list(candidate.coords)
  931. # try:
  932. # last = optimized_paths[-1]
  933. # last.coords = list(last.coords) + list(geo.coords)
  934. # except IndexError:
  935. # optimized_paths.append(geo)
  936. else:
  937. # Have to lift tool. End path.
  938. #log.debug("Path #%d not within boundary. Next." % path_count)
  939. #optimized_paths.append(geo)
  940. optimized_paths.insert(geo)
  941. geo = candidate
  942. current_pt = geo.coords[-1]
  943. # Next
  944. #pt, geo = storage.nearest(current_pt)
  945. except StopIteration: # Nothing left in storage.
  946. #pass
  947. optimized_paths.insert(geo)
  948. return optimized_paths
  949. @staticmethod
  950. def path_connect(storage, origin=(0, 0)):
  951. """
  952. Simplifies paths in the FlatCAMRTreeStorage storage by
  953. connecting paths that touch on their enpoints.
  954. :param storage: Storage containing the initial paths.
  955. :rtype storage: FlatCAMRTreeStorage
  956. :return: Simplified storage.
  957. :rtype: FlatCAMRTreeStorage
  958. """
  959. log.debug("path_connect()")
  960. ## Index first and last points in paths
  961. def get_pts(o):
  962. return [o.coords[0], o.coords[-1]]
  963. #
  964. # storage = FlatCAMRTreeStorage()
  965. # storage.get_points = get_pts
  966. #
  967. # for shape in pathlist:
  968. # if shape is not None: # TODO: This shouldn't have happened.
  969. # storage.insert(shape)
  970. path_count = 0
  971. pt, geo = storage.nearest(origin)
  972. storage.remove(geo)
  973. #optimized_geometry = [geo]
  974. optimized_geometry = FlatCAMRTreeStorage()
  975. optimized_geometry.get_points = get_pts
  976. #optimized_geometry.insert(geo)
  977. try:
  978. while True:
  979. path_count += 1
  980. #print "geo is", geo
  981. _, left = storage.nearest(geo.coords[0])
  982. #print "left is", left
  983. # If left touches geo, remove left from original
  984. # storage and append to geo.
  985. if type(left) == LineString:
  986. if left.coords[0] == geo.coords[0]:
  987. storage.remove(left)
  988. geo.coords = list(geo.coords)[::-1] + list(left.coords)
  989. continue
  990. if left.coords[-1] == geo.coords[0]:
  991. storage.remove(left)
  992. geo.coords = list(left.coords) + list(geo.coords)
  993. continue
  994. if left.coords[0] == geo.coords[-1]:
  995. storage.remove(left)
  996. geo.coords = list(geo.coords) + list(left.coords)
  997. continue
  998. if left.coords[-1] == geo.coords[-1]:
  999. storage.remove(left)
  1000. geo.coords = list(geo.coords) + list(left.coords)[::-1]
  1001. continue
  1002. _, right = storage.nearest(geo.coords[-1])
  1003. #print "right is", right
  1004. # If right touches geo, remove left from original
  1005. # storage and append to geo.
  1006. if type(right) == LineString:
  1007. if right.coords[0] == geo.coords[-1]:
  1008. storage.remove(right)
  1009. geo.coords = list(geo.coords) + list(right.coords)
  1010. continue
  1011. if right.coords[-1] == geo.coords[-1]:
  1012. storage.remove(right)
  1013. geo.coords = list(geo.coords) + list(right.coords)[::-1]
  1014. continue
  1015. if right.coords[0] == geo.coords[0]:
  1016. storage.remove(right)
  1017. geo.coords = list(geo.coords)[::-1] + list(right.coords)
  1018. continue
  1019. if right.coords[-1] == geo.coords[0]:
  1020. storage.remove(right)
  1021. geo.coords = list(left.coords) + list(geo.coords)
  1022. continue
  1023. # right is either a LinearRing or it does not connect
  1024. # to geo (nothing left to connect to geo), so we continue
  1025. # with right as geo.
  1026. storage.remove(right)
  1027. if type(right) == LinearRing:
  1028. optimized_geometry.insert(right)
  1029. else:
  1030. # Cannot exteng geo any further. Put it away.
  1031. optimized_geometry.insert(geo)
  1032. # Continue with right.
  1033. geo = right
  1034. except StopIteration: # Nothing found in storage.
  1035. optimized_geometry.insert(geo)
  1036. #print path_count
  1037. log.debug("path_count = %d" % path_count)
  1038. return optimized_geometry
  1039. def convert_units(self, units):
  1040. """
  1041. Converts the units of the object to ``units`` by scaling all
  1042. the geometry appropriately. This call ``scale()``. Don't call
  1043. it again in descendents.
  1044. :param units: "IN" or "MM"
  1045. :type units: str
  1046. :return: Scaling factor resulting from unit change.
  1047. :rtype: float
  1048. """
  1049. log.debug("Geometry.convert_units()")
  1050. if units.upper() == self.units.upper():
  1051. return 1.0
  1052. if units.upper() == "MM":
  1053. factor = 25.4
  1054. elif units.upper() == "IN":
  1055. factor = 1 / 25.4
  1056. else:
  1057. log.error("Unsupported units: %s" % str(units))
  1058. return 1.0
  1059. self.units = units
  1060. self.scale(factor)
  1061. self.file_units_factor = factor
  1062. return factor
  1063. def to_dict(self):
  1064. """
  1065. Returns a respresentation of the object as a dictionary.
  1066. Attributes to include are listed in ``self.ser_attrs``.
  1067. :return: A dictionary-encoded copy of the object.
  1068. :rtype: dict
  1069. """
  1070. d = {}
  1071. for attr in self.ser_attrs:
  1072. d[attr] = getattr(self, attr)
  1073. return d
  1074. def from_dict(self, d):
  1075. """
  1076. Sets object's attributes from a dictionary.
  1077. Attributes to include are listed in ``self.ser_attrs``.
  1078. This method will look only for only and all the
  1079. attributes in ``self.ser_attrs``. They must all
  1080. be present. Use only for deserializing saved
  1081. objects.
  1082. :param d: Dictionary of attributes to set in the object.
  1083. :type d: dict
  1084. :return: None
  1085. """
  1086. for attr in self.ser_attrs:
  1087. setattr(self, attr, d[attr])
  1088. def union(self):
  1089. """
  1090. Runs a cascaded union on the list of objects in
  1091. solid_geometry.
  1092. :return: None
  1093. """
  1094. self.solid_geometry = [cascaded_union(self.solid_geometry)]
  1095. def export_svg(self, scale_factor=0.00):
  1096. """
  1097. Exports the Geometry Object as a SVG Element
  1098. :return: SVG Element
  1099. """
  1100. # Make sure we see a Shapely Geometry class and not a list
  1101. if str(type(self)) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
  1102. flat_geo = []
  1103. if self.multigeo:
  1104. for tool in self.tools:
  1105. flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
  1106. geom = cascaded_union(flat_geo)
  1107. else:
  1108. geom = cascaded_union(self.flatten())
  1109. else:
  1110. geom = cascaded_union(self.flatten())
  1111. # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
  1112. # If 0 or less which is invalid then default to 0.05
  1113. # This value appears to work for zooming, and getting the output svg line width
  1114. # to match that viewed on screen with FlatCam
  1115. # MS: I choose a factor of 0.01 so the scale is right for PCB UV film
  1116. if scale_factor <= 0:
  1117. scale_factor = 0.01
  1118. # Convert to a SVG
  1119. svg_elem = geom.svg(scale_factor=scale_factor)
  1120. return svg_elem
  1121. def mirror(self, axis, point):
  1122. """
  1123. Mirrors the object around a specified axis passign through
  1124. the given point.
  1125. :param axis: "X" or "Y" indicates around which axis to mirror.
  1126. :type axis: str
  1127. :param point: [x, y] point belonging to the mirror axis.
  1128. :type point: list
  1129. :return: None
  1130. """
  1131. px, py = point
  1132. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1133. def mirror_geom(obj):
  1134. if type(obj) is list:
  1135. new_obj = []
  1136. for g in obj:
  1137. new_obj.append(mirror_geom(g))
  1138. return new_obj
  1139. else:
  1140. return affinity.scale(obj, xscale, yscale, origin=(px,py))
  1141. try:
  1142. if self.multigeo is True:
  1143. for tool in self.tools:
  1144. self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
  1145. else:
  1146. self.solid_geometry = mirror_geom(self.solid_geometry)
  1147. self.app.inform.emit(_tr('[success]Object was mirrored ...'))
  1148. except AttributeError:
  1149. self.app.inform.emit(_tr("[ERROR_NOTCL] Failed to mirror. No object selected"))
  1150. def rotate(self, angle, point):
  1151. """
  1152. Rotate an object by an angle (in degrees) around the provided coordinates.
  1153. Parameters
  1154. ----------
  1155. The angle of rotation are specified in degrees (default). Positive angles are
  1156. counter-clockwise and negative are clockwise rotations.
  1157. The point of origin can be a keyword 'center' for the bounding box
  1158. center (default), 'centroid' for the geometry's centroid, a Point object
  1159. or a coordinate tuple (x0, y0).
  1160. See shapely manual for more information:
  1161. http://toblerity.org/shapely/manual.html#affine-transformations
  1162. """
  1163. px, py = point
  1164. def rotate_geom(obj):
  1165. if type(obj) is list:
  1166. new_obj = []
  1167. for g in obj:
  1168. new_obj.append(rotate_geom(g))
  1169. return new_obj
  1170. else:
  1171. return affinity.rotate(obj, angle, origin=(px, py))
  1172. try:
  1173. if self.multigeo is True:
  1174. for tool in self.tools:
  1175. self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
  1176. else:
  1177. self.solid_geometry = rotate_geom(self.solid_geometry)
  1178. self.app.inform.emit(_tr('[success]Object was rotated ...'))
  1179. except AttributeError:
  1180. self.app.inform.emit(_tr("[ERROR_NOTCL] Failed to rotate. No object selected"))
  1181. def skew(self, angle_x, angle_y, point):
  1182. """
  1183. Shear/Skew the geometries of an object by angles along x and y dimensions.
  1184. Parameters
  1185. ----------
  1186. angle_x, angle_y : float, float
  1187. The shear angle(s) for the x and y axes respectively. These can be
  1188. specified in either degrees (default) or radians by setting
  1189. use_radians=True.
  1190. point: tuple of coordinates (x,y)
  1191. See shapely manual for more information:
  1192. http://toblerity.org/shapely/manual.html#affine-transformations
  1193. """
  1194. px, py = point
  1195. def skew_geom(obj):
  1196. if type(obj) is list:
  1197. new_obj = []
  1198. for g in obj:
  1199. new_obj.append(skew_geom(g))
  1200. return new_obj
  1201. else:
  1202. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  1203. try:
  1204. if self.multigeo is True:
  1205. for tool in self.tools:
  1206. self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
  1207. else:
  1208. self.solid_geometry = skew_geom(self.solid_geometry)
  1209. self.app.inform.emit(_tr('[success]Object was skewed ...'))
  1210. except AttributeError:
  1211. self.app.inform.emit(_tr("[ERROR_NOTCL] Failed to skew. No object selected"))
  1212. # if type(self.solid_geometry) == list:
  1213. # self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py))
  1214. # for g in self.solid_geometry]
  1215. # else:
  1216. # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
  1217. # origin=(px, py))
  1218. class ApertureMacro:
  1219. """
  1220. Syntax of aperture macros.
  1221. <AM command>: AM<Aperture macro name>*<Macro content>
  1222. <Macro content>: {{<Variable definition>*}{<Primitive>*}}
  1223. <Variable definition>: $K=<Arithmetic expression>
  1224. <Primitive>: <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
  1225. <Modifier>: $M|< Arithmetic expression>
  1226. <Comment>: 0 <Text>
  1227. """
  1228. ## Regular expressions
  1229. am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
  1230. am2_re = re.compile(r'(.*)%$')
  1231. amcomm_re = re.compile(r'^0(.*)')
  1232. amprim_re = re.compile(r'^[1-9].*')
  1233. amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
  1234. def __init__(self, name=None):
  1235. self.name = name
  1236. self.raw = ""
  1237. ## These below are recomputed for every aperture
  1238. ## definition, in other words, are temporary variables.
  1239. self.primitives = []
  1240. self.locvars = {}
  1241. self.geometry = None
  1242. def to_dict(self):
  1243. """
  1244. Returns the object in a serializable form. Only the name and
  1245. raw are required.
  1246. :return: Dictionary representing the object. JSON ready.
  1247. :rtype: dict
  1248. """
  1249. return {
  1250. 'name': self.name,
  1251. 'raw': self.raw
  1252. }
  1253. def from_dict(self, d):
  1254. """
  1255. Populates the object from a serial representation created
  1256. with ``self.to_dict()``.
  1257. :param d: Serial representation of an ApertureMacro object.
  1258. :return: None
  1259. """
  1260. for attr in ['name', 'raw']:
  1261. setattr(self, attr, d[attr])
  1262. def parse_content(self):
  1263. """
  1264. Creates numerical lists for all primitives in the aperture
  1265. macro (in ``self.raw``) by replacing all variables by their
  1266. values iteratively and evaluating expressions. Results
  1267. are stored in ``self.primitives``.
  1268. :return: None
  1269. """
  1270. # Cleanup
  1271. self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
  1272. self.primitives = []
  1273. # Separate parts
  1274. parts = self.raw.split('*')
  1275. #### Every part in the macro ####
  1276. for part in parts:
  1277. ### Comments. Ignored.
  1278. match = ApertureMacro.amcomm_re.search(part)
  1279. if match:
  1280. continue
  1281. ### Variables
  1282. # These are variables defined locally inside the macro. They can be
  1283. # numerical constant or defind in terms of previously define
  1284. # variables, which can be defined locally or in an aperture
  1285. # definition. All replacements ocurr here.
  1286. match = ApertureMacro.amvar_re.search(part)
  1287. if match:
  1288. var = match.group(1)
  1289. val = match.group(2)
  1290. # Replace variables in value
  1291. for v in self.locvars:
  1292. # replaced the following line with the next to fix Mentor custom apertures not parsed OK
  1293. # val = re.sub((r'\$'+str(v)+r'(?![0-9a-zA-Z])'), str(self.locvars[v]), val)
  1294. val = val.replace('$' + str(v), str(self.locvars[v]))
  1295. # Make all others 0
  1296. val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
  1297. # Change x with *
  1298. val = re.sub(r'[xX]', "*", val)
  1299. # Eval() and store.
  1300. self.locvars[var] = eval(val)
  1301. continue
  1302. ### Primitives
  1303. # Each is an array. The first identifies the primitive, while the
  1304. # rest depend on the primitive. All are strings representing a
  1305. # number and may contain variable definition. The values of these
  1306. # variables are defined in an aperture definition.
  1307. match = ApertureMacro.amprim_re.search(part)
  1308. if match:
  1309. ## Replace all variables
  1310. for v in self.locvars:
  1311. # replaced the following line with the next to fix Mentor custom apertures not parsed OK
  1312. # part = re.sub(r'\$' + str(v) + r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
  1313. part = part.replace('$' + str(v), str(self.locvars[v]))
  1314. # Make all others 0
  1315. part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
  1316. # Change x with *
  1317. part = re.sub(r'[xX]', "*", part)
  1318. ## Store
  1319. elements = part.split(",")
  1320. self.primitives.append([eval(x) for x in elements])
  1321. continue
  1322. log.warning("Unknown syntax of aperture macro part: %s" % str(part))
  1323. def append(self, data):
  1324. """
  1325. Appends a string to the raw macro.
  1326. :param data: Part of the macro.
  1327. :type data: str
  1328. :return: None
  1329. """
  1330. self.raw += data
  1331. @staticmethod
  1332. def default2zero(n, mods):
  1333. """
  1334. Pads the ``mods`` list with zeros resulting in an
  1335. list of length n.
  1336. :param n: Length of the resulting list.
  1337. :type n: int
  1338. :param mods: List to be padded.
  1339. :type mods: list
  1340. :return: Zero-padded list.
  1341. :rtype: list
  1342. """
  1343. x = [0.0] * n
  1344. na = len(mods)
  1345. x[0:na] = mods
  1346. return x
  1347. @staticmethod
  1348. def make_circle(mods):
  1349. """
  1350. :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
  1351. :return:
  1352. """
  1353. pol, dia, x, y = ApertureMacro.default2zero(4, mods)
  1354. return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
  1355. @staticmethod
  1356. def make_vectorline(mods):
  1357. """
  1358. :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
  1359. rotation angle around origin in degrees)
  1360. :return:
  1361. """
  1362. pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
  1363. line = LineString([(xs, ys), (xe, ye)])
  1364. box = line.buffer(width/2, cap_style=2)
  1365. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  1366. return {"pol": int(pol), "geometry": box_rotated}
  1367. @staticmethod
  1368. def make_centerline(mods):
  1369. """
  1370. :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
  1371. rotation angle around origin in degrees)
  1372. :return:
  1373. """
  1374. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  1375. box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
  1376. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  1377. return {"pol": int(pol), "geometry": box_rotated}
  1378. @staticmethod
  1379. def make_lowerleftline(mods):
  1380. """
  1381. :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
  1382. rotation angle around origin in degrees)
  1383. :return:
  1384. """
  1385. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  1386. box = shply_box(x, y, x+width, y+height)
  1387. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  1388. return {"pol": int(pol), "geometry": box_rotated}
  1389. @staticmethod
  1390. def make_outline(mods):
  1391. """
  1392. :param mods:
  1393. :return:
  1394. """
  1395. pol = mods[0]
  1396. n = mods[1]
  1397. points = [(0, 0)]*(n+1)
  1398. for i in range(n+1):
  1399. points[i] = mods[2*i + 2:2*i + 4]
  1400. angle = mods[2*n + 4]
  1401. poly = Polygon(points)
  1402. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  1403. return {"pol": int(pol), "geometry": poly_rotated}
  1404. @staticmethod
  1405. def make_polygon(mods):
  1406. """
  1407. Note: Specs indicate that rotation is only allowed if the center
  1408. (x, y) == (0, 0). I will tolerate breaking this rule.
  1409. :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
  1410. diameter of circumscribed circle >=0, rotation angle around origin)
  1411. :return:
  1412. """
  1413. pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
  1414. points = [(0, 0)]*nverts
  1415. for i in range(nverts):
  1416. points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
  1417. y + 0.5 * dia * sin(2*pi * i/nverts))
  1418. poly = Polygon(points)
  1419. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  1420. return {"pol": int(pol), "geometry": poly_rotated}
  1421. @staticmethod
  1422. def make_moire(mods):
  1423. """
  1424. Note: Specs indicate that rotation is only allowed if the center
  1425. (x, y) == (0, 0). I will tolerate breaking this rule.
  1426. :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
  1427. gap, max_rings, crosshair_thickness, crosshair_len, rotation
  1428. angle around origin in degrees)
  1429. :return:
  1430. """
  1431. x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
  1432. r = dia/2 - thickness/2
  1433. result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  1434. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) # Need a copy!
  1435. i = 1 # Number of rings created so far
  1436. ## If the ring does not have an interior it means that it is
  1437. ## a disk. Then stop.
  1438. while len(ring.interiors) > 0 and i < nrings:
  1439. r -= thickness + gap
  1440. if r <= 0:
  1441. break
  1442. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  1443. result = cascaded_union([result, ring])
  1444. i += 1
  1445. ## Crosshair
  1446. hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
  1447. ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
  1448. result = cascaded_union([result, hor, ver])
  1449. return {"pol": 1, "geometry": result}
  1450. @staticmethod
  1451. def make_thermal(mods):
  1452. """
  1453. Note: Specs indicate that rotation is only allowed if the center
  1454. (x, y) == (0, 0). I will tolerate breaking this rule.
  1455. :param mods: [x-center, y-center, diameter-outside, diameter-inside,
  1456. gap-thickness, rotation angle around origin]
  1457. :return:
  1458. """
  1459. x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
  1460. ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
  1461. hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
  1462. vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
  1463. thermal = ring.difference(hline.union(vline))
  1464. return {"pol": 1, "geometry": thermal}
  1465. def make_geometry(self, modifiers):
  1466. """
  1467. Runs the macro for the given modifiers and generates
  1468. the corresponding geometry.
  1469. :param modifiers: Modifiers (parameters) for this macro
  1470. :type modifiers: list
  1471. :return: Shapely geometry
  1472. :rtype: shapely.geometry.polygon
  1473. """
  1474. ## Primitive makers
  1475. makers = {
  1476. "1": ApertureMacro.make_circle,
  1477. "2": ApertureMacro.make_vectorline,
  1478. "20": ApertureMacro.make_vectorline,
  1479. "21": ApertureMacro.make_centerline,
  1480. "22": ApertureMacro.make_lowerleftline,
  1481. "4": ApertureMacro.make_outline,
  1482. "5": ApertureMacro.make_polygon,
  1483. "6": ApertureMacro.make_moire,
  1484. "7": ApertureMacro.make_thermal
  1485. }
  1486. ## Store modifiers as local variables
  1487. modifiers = modifiers or []
  1488. modifiers = [float(m) for m in modifiers]
  1489. self.locvars = {}
  1490. for i in range(0, len(modifiers)):
  1491. self.locvars[str(i + 1)] = modifiers[i]
  1492. ## Parse
  1493. self.primitives = [] # Cleanup
  1494. self.geometry = Polygon()
  1495. self.parse_content()
  1496. ## Make the geometry
  1497. for primitive in self.primitives:
  1498. # Make the primitive
  1499. prim_geo = makers[str(int(primitive[0]))](primitive[1:])
  1500. # Add it (according to polarity)
  1501. # if self.geometry is None and prim_geo['pol'] == 1:
  1502. # self.geometry = prim_geo['geometry']
  1503. # continue
  1504. if prim_geo['pol'] == 1:
  1505. self.geometry = self.geometry.union(prim_geo['geometry'])
  1506. continue
  1507. if prim_geo['pol'] == 0:
  1508. self.geometry = self.geometry.difference(prim_geo['geometry'])
  1509. continue
  1510. return self.geometry
  1511. class Gerber (Geometry):
  1512. """
  1513. **ATTRIBUTES**
  1514. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  1515. The values are dictionaries key/value pairs which describe the aperture. The
  1516. type key is always present and the rest depend on the key:
  1517. +-----------+-----------------------------------+
  1518. | Key | Value |
  1519. +===========+===================================+
  1520. | type | (str) "C", "R", "O", "P", or "AP" |
  1521. +-----------+-----------------------------------+
  1522. | others | Depend on ``type`` |
  1523. +-----------+-----------------------------------+
  1524. | solid_geometry | (list) |
  1525. +-----------+-----------------------------------+
  1526. * ``aperture_macros`` (dictionary): Are predefined geometrical structures
  1527. that can be instantiated with different parameters in an aperture
  1528. definition. See ``apertures`` above. The key is the name of the macro,
  1529. and the macro itself, the value, is a ``Aperture_Macro`` object.
  1530. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  1531. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  1532. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  1533. *buffering* (or thickening) the ``paths`` with the aperture. These are
  1534. generated from ``paths`` in ``buffer_paths()``.
  1535. **USAGE**::
  1536. g = Gerber()
  1537. g.parse_file(filename)
  1538. g.create_geometry()
  1539. do_something(s.solid_geometry)
  1540. """
  1541. defaults = {
  1542. "steps_per_circle": 56,
  1543. "use_buffer_for_union": True
  1544. }
  1545. def __init__(self, steps_per_circle=None):
  1546. """
  1547. The constructor takes no parameters. Use ``gerber.parse_files()``
  1548. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  1549. :return: Gerber object
  1550. :rtype: Gerber
  1551. """
  1552. # How to discretize a circle.
  1553. if steps_per_circle is None:
  1554. steps_per_circle = int(Gerber.defaults['steps_per_circle'])
  1555. self.steps_per_circle = int(steps_per_circle)
  1556. # Initialize parent
  1557. Geometry.__init__(self, geo_steps_per_circle=int(steps_per_circle))
  1558. # Number format
  1559. self.int_digits = 3
  1560. """Number of integer digits in Gerber numbers. Used during parsing."""
  1561. self.frac_digits = 4
  1562. """Number of fraction digits in Gerber numbers. Used during parsing."""
  1563. self.gerber_zeros = 'L'
  1564. """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
  1565. """
  1566. ## Gerber elements ##
  1567. '''
  1568. apertures = {
  1569. 'id':{
  1570. 'type':chr,
  1571. 'size':float,
  1572. 'width':float,
  1573. 'height':float,
  1574. 'solid_geometry': [],
  1575. 'follow_geometry': [],
  1576. }
  1577. }
  1578. '''
  1579. # aperture storage
  1580. self.apertures = {}
  1581. # Aperture Macros
  1582. self.aperture_macros = {}
  1583. # will store the Gerber geometry's as solids
  1584. self.solid_geometry = Polygon()
  1585. # will store the Gerber geometry's as paths
  1586. self.follow_geometry = []
  1587. self.source_file = ''
  1588. # Attributes to be included in serialization
  1589. # Always append to it because it carries contents
  1590. # from Geometry.
  1591. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
  1592. 'aperture_macros', 'solid_geometry', 'source_file']
  1593. #### Parser patterns ####
  1594. # FS - Format Specification
  1595. # The format of X and Y must be the same!
  1596. # L-omit leading zeros, T-omit trailing zeros
  1597. # A-absolute notation, I-incremental notation
  1598. self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
  1599. self.fmt_re_alt = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
  1600. self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LT])([AI]).*X(\d)(\d)Y\d\d\*%$')
  1601. # Mode (IN/MM)
  1602. self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
  1603. # Comment G04|G4
  1604. self.comm_re = re.compile(r'^G0?4(.*)$')
  1605. # AD - Aperture definition
  1606. # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
  1607. # NOTE: Adding "-" to support output from Upverter.
  1608. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
  1609. # AM - Aperture Macro
  1610. # Beginning of macro (Ends with *%):
  1611. #self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  1612. # Tool change
  1613. # May begin with G54 but that is deprecated
  1614. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  1615. # G01... - Linear interpolation plus flashes with coordinates
  1616. # Operation code (D0x) missing is deprecated... oh well I will support it.
  1617. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  1618. # Operation code alone, usually just D03 (Flash)
  1619. self.opcode_re = re.compile(r'^D0?([123])\*$')
  1620. # G02/3... - Circular interpolation with coordinates
  1621. # 2-clockwise, 3-counterclockwise
  1622. # Operation code (D0x) missing is deprecated... oh well I will support it.
  1623. # Optional start with G02 or G03, optional end with D01 or D02 with
  1624. # optional coordinates but at least one in any order.
  1625. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' +
  1626. '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  1627. # G01/2/3 Occurring without coordinates
  1628. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  1629. # Single G74 or multi G75 quadrant for circular interpolation
  1630. self.quad_re = re.compile(r'^G7([45]).*\*$')
  1631. # Region mode on
  1632. # In region mode, D01 starts a region
  1633. # and D02 ends it. A new region can be started again
  1634. # with D01. All contours must be closed before
  1635. # D02 or G37.
  1636. self.regionon_re = re.compile(r'^G36\*$')
  1637. # Region mode off
  1638. # Will end a region and come off region mode.
  1639. # All contours must be closed before D02 or G37.
  1640. self.regionoff_re = re.compile(r'^G37\*$')
  1641. # End of file
  1642. self.eof_re = re.compile(r'^M02\*')
  1643. # IP - Image polarity
  1644. self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
  1645. # LP - Level polarity
  1646. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  1647. # Units (OBSOLETE)
  1648. self.units_re = re.compile(r'^G7([01])\*$')
  1649. # Absolute/Relative G90/1 (OBSOLETE)
  1650. self.absrel_re = re.compile(r'^G9([01])\*$')
  1651. # Aperture macros
  1652. self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
  1653. self.am2_re = re.compile(r'(.*)%$')
  1654. self.use_buffer_for_union = self.defaults["use_buffer_for_union"]
  1655. def aperture_parse(self, apertureId, apertureType, apParameters):
  1656. """
  1657. Parse gerber aperture definition into dictionary of apertures.
  1658. The following kinds and their attributes are supported:
  1659. * *Circular (C)*: size (float)
  1660. * *Rectangle (R)*: width (float), height (float)
  1661. * *Obround (O)*: width (float), height (float).
  1662. * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
  1663. * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
  1664. :param apertureId: Id of the aperture being defined.
  1665. :param apertureType: Type of the aperture.
  1666. :param apParameters: Parameters of the aperture.
  1667. :type apertureId: str
  1668. :type apertureType: str
  1669. :type apParameters: str
  1670. :return: Identifier of the aperture.
  1671. :rtype: str
  1672. """
  1673. # Found some Gerber with a leading zero in the aperture id and the
  1674. # referenced it without the zero, so this is a hack to handle that.
  1675. apid = str(int(apertureId))
  1676. try: # Could be empty for aperture macros
  1677. paramList = apParameters.split('X')
  1678. except:
  1679. paramList = None
  1680. if apertureType == "C": # Circle, example: %ADD11C,0.1*%
  1681. self.apertures[apid] = {"type": "C",
  1682. "size": float(paramList[0])}
  1683. return apid
  1684. if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*%
  1685. self.apertures[apid] = {"type": "R",
  1686. "width": float(paramList[0]),
  1687. "height": float(paramList[1]),
  1688. "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack
  1689. return apid
  1690. if apertureType == "O": # Obround
  1691. self.apertures[apid] = {"type": "O",
  1692. "width": float(paramList[0]),
  1693. "height": float(paramList[1]),
  1694. "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack
  1695. return apid
  1696. if apertureType == "P": # Polygon (regular)
  1697. self.apertures[apid] = {"type": "P",
  1698. "diam": float(paramList[0]),
  1699. "nVertices": int(paramList[1]),
  1700. "size": float(paramList[0])} # Hack
  1701. if len(paramList) >= 3:
  1702. self.apertures[apid]["rotation"] = float(paramList[2])
  1703. return apid
  1704. if apertureType in self.aperture_macros:
  1705. self.apertures[apid] = {"type": "AM",
  1706. "macro": self.aperture_macros[apertureType],
  1707. "modifiers": paramList}
  1708. return apid
  1709. log.warning("Aperture not implemented: %s" % str(apertureType))
  1710. return None
  1711. def parse_file(self, filename, follow=False):
  1712. """
  1713. Calls Gerber.parse_lines() with generator of lines
  1714. read from the given file. Will split the lines if multiple
  1715. statements are found in a single original line.
  1716. The following line is split into two::
  1717. G54D11*G36*
  1718. First is ``G54D11*`` and seconds is ``G36*``.
  1719. :param filename: Gerber file to parse.
  1720. :type filename: str
  1721. :param follow: If true, will not create polygons, just lines
  1722. following the gerber path.
  1723. :type follow: bool
  1724. :return: None
  1725. """
  1726. with open(filename, 'r') as gfile:
  1727. def line_generator():
  1728. for line in gfile:
  1729. line = line.strip(' \r\n')
  1730. while len(line) > 0:
  1731. # If ends with '%' leave as is.
  1732. if line[-1] == '%':
  1733. yield line
  1734. break
  1735. # Split after '*' if any.
  1736. starpos = line.find('*')
  1737. if starpos > -1:
  1738. cleanline = line[:starpos + 1]
  1739. yield cleanline
  1740. line = line[starpos + 1:]
  1741. # Otherwise leave as is.
  1742. else:
  1743. # yield cleanline
  1744. yield line
  1745. break
  1746. self.parse_lines(line_generator())
  1747. #@profile
  1748. def parse_lines(self, glines):
  1749. """
  1750. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  1751. ``self.flashes``, ``self.regions`` and ``self.units``.
  1752. :param glines: Gerber code as list of strings, each element being
  1753. one line of the source file.
  1754. :type glines: list
  1755. :return: None
  1756. :rtype: None
  1757. """
  1758. # Coordinates of the current path, each is [x, y]
  1759. path = []
  1760. # this is for temporary storage of geometry until it is added to poly_buffer
  1761. geo = None
  1762. # Polygons are stored here until there is a change in polarity.
  1763. # Only then they are combined via cascaded_union and added or
  1764. # subtracted from solid_geometry. This is ~100 times faster than
  1765. # applying a union for every new polygon.
  1766. poly_buffer = []
  1767. # store here the follow geometry
  1768. follow_buffer = []
  1769. last_path_aperture = None
  1770. current_aperture = None
  1771. # 1,2 or 3 from "G01", "G02" or "G03"
  1772. current_interpolation_mode = None
  1773. # 1 or 2 from "D01" or "D02"
  1774. # Note this is to support deprecated Gerber not putting
  1775. # an operation code at the end of every coordinate line.
  1776. current_operation_code = None
  1777. # Current coordinates
  1778. current_x = None
  1779. current_y = None
  1780. previous_x = None
  1781. previous_y = None
  1782. current_d = None
  1783. # Absolute or Relative/Incremental coordinates
  1784. # Not implemented
  1785. absolute = True
  1786. # How to interpret circular interpolation: SINGLE or MULTI
  1787. quadrant_mode = None
  1788. # Indicates we are parsing an aperture macro
  1789. current_macro = None
  1790. # Indicates the current polarity: D-Dark, C-Clear
  1791. current_polarity = 'D'
  1792. # If a region is being defined
  1793. making_region = False
  1794. #### Parsing starts here ####
  1795. line_num = 0
  1796. gline = ""
  1797. try:
  1798. for gline in glines:
  1799. line_num += 1
  1800. self.source_file += gline + '\n'
  1801. ### Cleanup
  1802. gline = gline.strip(' \r\n')
  1803. # log.debug("Line=%3s %s" % (line_num, gline))
  1804. #### Ignored lines
  1805. ## Comments
  1806. match = self.comm_re.search(gline)
  1807. if match:
  1808. continue
  1809. ### Polarity change
  1810. # Example: %LPD*% or %LPC*%
  1811. # If polarity changes, creates geometry from current
  1812. # buffer, then adds or subtracts accordingly.
  1813. match = self.lpol_re.search(gline)
  1814. if match:
  1815. new_polarity = match.group(1)
  1816. if len(path) > 1 and current_polarity != new_polarity:
  1817. # finish the current path and add it to the storage
  1818. # --- Buffered ----
  1819. width = self.apertures[last_path_aperture]["size"]
  1820. geo = LineString(path)
  1821. if not geo.is_empty:
  1822. follow_buffer.append(geo)
  1823. geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  1824. if not geo.is_empty:
  1825. poly_buffer.append(geo)
  1826. try:
  1827. self.apertures[current_aperture]['solid_geometry'].append(geo)
  1828. except KeyError:
  1829. self.apertures[current_aperture]['solid_geometry'] = []
  1830. self.apertures[current_aperture]['solid_geometry'].append(geo)
  1831. path = [path[-1]]
  1832. # --- Apply buffer ---
  1833. # If added for testing of bug #83
  1834. # TODO: Remove when bug fixed
  1835. if len(poly_buffer) > 0:
  1836. if current_polarity == 'D':
  1837. # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
  1838. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  1839. else:
  1840. # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
  1841. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  1842. # follow_buffer = []
  1843. poly_buffer = []
  1844. current_polarity = new_polarity
  1845. continue
  1846. ### Number format
  1847. # Example: %FSLAX24Y24*%
  1848. # TODO: This is ignoring most of the format. Implement the rest.
  1849. match = self.fmt_re.search(gline)
  1850. if match:
  1851. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
  1852. self.gerber_zeros = match.group(1)
  1853. self.int_digits = int(match.group(3))
  1854. self.frac_digits = int(match.group(4))
  1855. log.debug("Gerber format found. (%s) " % str(gline))
  1856. log.debug(
  1857. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
  1858. self.gerber_zeros)
  1859. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  1860. continue
  1861. ### Mode (IN/MM)
  1862. # Example: %MOIN*%
  1863. match = self.mode_re.search(gline)
  1864. if match:
  1865. gerber_units = match.group(1)
  1866. log.debug("Gerber units found = %s" % gerber_units)
  1867. # Changed for issue #80
  1868. self.convert_units(match.group(1))
  1869. continue
  1870. ### Combined Number format and Mode --- Allegro does this
  1871. match = self.fmt_re_alt.search(gline)
  1872. if match:
  1873. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
  1874. self.gerber_zeros = match.group(1)
  1875. self.int_digits = int(match.group(3))
  1876. self.frac_digits = int(match.group(4))
  1877. log.debug("Gerber format found. (%s) " % str(gline))
  1878. log.debug(
  1879. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
  1880. self.gerber_zeros)
  1881. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  1882. gerber_units = match.group(1)
  1883. log.debug("Gerber units found = %s" % gerber_units)
  1884. # Changed for issue #80
  1885. self.convert_units(match.group(5))
  1886. continue
  1887. ### Search for OrCAD way for having Number format
  1888. match = self.fmt_re_orcad.search(gline)
  1889. if match:
  1890. if match.group(1) is not None:
  1891. if match.group(1) == 'G74':
  1892. quadrant_mode = 'SINGLE'
  1893. elif match.group(1) == 'G75':
  1894. quadrant_mode = 'MULTI'
  1895. absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)]
  1896. self.gerber_zeros = match.group(2)
  1897. self.int_digits = int(match.group(4))
  1898. self.frac_digits = int(match.group(5))
  1899. log.debug("Gerber format found. (%s) " % str(gline))
  1900. log.debug(
  1901. "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros)" %
  1902. self.gerber_zeros)
  1903. log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
  1904. gerber_units = match.group(1)
  1905. log.debug("Gerber units found = %s" % gerber_units)
  1906. # Changed for issue #80
  1907. self.convert_units(match.group(5))
  1908. continue
  1909. ### Units (G70/1) OBSOLETE
  1910. match = self.units_re.search(gline)
  1911. if match:
  1912. obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
  1913. log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
  1914. # Changed for issue #80
  1915. self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
  1916. continue
  1917. ### Absolute/relative coordinates G90/1 OBSOLETE
  1918. match = self.absrel_re.search(gline)
  1919. if match:
  1920. absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
  1921. log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
  1922. continue
  1923. ### Aperture Macros
  1924. # Having this at the beginning will slow things down
  1925. # but macros can have complicated statements than could
  1926. # be caught by other patterns.
  1927. if current_macro is None: # No macro started yet
  1928. match = self.am1_re.search(gline)
  1929. # Start macro if match, else not an AM, carry on.
  1930. if match:
  1931. log.debug("Starting macro. Line %d: %s" % (line_num, gline))
  1932. current_macro = match.group(1)
  1933. self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
  1934. if match.group(2): # Append
  1935. self.aperture_macros[current_macro].append(match.group(2))
  1936. if match.group(3): # Finish macro
  1937. #self.aperture_macros[current_macro].parse_content()
  1938. current_macro = None
  1939. log.debug("Macro complete in 1 line.")
  1940. continue
  1941. else: # Continue macro
  1942. log.debug("Continuing macro. Line %d." % line_num)
  1943. match = self.am2_re.search(gline)
  1944. if match: # Finish macro
  1945. log.debug("End of macro. Line %d." % line_num)
  1946. self.aperture_macros[current_macro].append(match.group(1))
  1947. #self.aperture_macros[current_macro].parse_content()
  1948. current_macro = None
  1949. else: # Append
  1950. self.aperture_macros[current_macro].append(gline)
  1951. continue
  1952. ### Aperture definitions %ADD...
  1953. match = self.ad_re.search(gline)
  1954. if match:
  1955. log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
  1956. self.aperture_parse(match.group(1), match.group(2), match.group(3))
  1957. continue
  1958. ### Operation code alone
  1959. # Operation code alone, usually just D03 (Flash)
  1960. # self.opcode_re = re.compile(r'^D0?([123])\*$')
  1961. match = self.opcode_re.search(gline)
  1962. if match:
  1963. current_operation_code = int(match.group(1))
  1964. current_d = current_operation_code
  1965. if current_operation_code == 3:
  1966. ## --- Buffered ---
  1967. try:
  1968. log.debug("Bare op-code %d." % current_operation_code)
  1969. # flash = Gerber.create_flash_geometry(Point(path[-1]),
  1970. # self.apertures[current_aperture])
  1971. flash = Gerber.create_flash_geometry(
  1972. Point(current_x, current_y), self.apertures[current_aperture],
  1973. int(self.steps_per_circle))
  1974. if not flash.is_empty:
  1975. poly_buffer.append(flash)
  1976. try:
  1977. self.apertures[current_aperture]['solid_geometry'].append(flash)
  1978. except KeyError:
  1979. self.apertures[current_aperture]['solid_geometry'] = []
  1980. self.apertures[current_aperture]['solid_geometry'].append(flash)
  1981. except IndexError:
  1982. log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
  1983. continue
  1984. ### Tool/aperture change
  1985. # Example: D12*
  1986. match = self.tool_re.search(gline)
  1987. if match:
  1988. current_aperture = match.group(1)
  1989. log.debug("Line %d: Aperture change to (%s)" % (line_num, match.group(1)))
  1990. # If the aperture value is zero then make it something quite small but with a non-zero value
  1991. # so it can be processed by FlatCAM.
  1992. # But first test to see if the aperture type is "aperture macro". In that case
  1993. # we should not test for "size" key as it does not exist in this case.
  1994. if self.apertures[current_aperture]["type"] is not "AM":
  1995. if self.apertures[current_aperture]["size"] == 0:
  1996. self.apertures[current_aperture]["size"] = 1e-12
  1997. log.debug(self.apertures[current_aperture])
  1998. # Take care of the current path with the previous tool
  1999. if len(path) > 1:
  2000. if self.apertures[last_path_aperture]["type"] == 'R':
  2001. # do nothing because 'R' type moving aperture is none at once
  2002. pass
  2003. else:
  2004. # --- Buffered ----
  2005. width = self.apertures[last_path_aperture]["size"]
  2006. geo = LineString(path)
  2007. if not geo.is_empty:
  2008. follow_buffer.append(geo)
  2009. geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  2010. if not geo.is_empty:
  2011. poly_buffer.append(geo)
  2012. try:
  2013. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2014. except KeyError:
  2015. self.apertures[last_path_aperture]['solid_geometry'] = []
  2016. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2017. path = [path[-1]]
  2018. continue
  2019. ### G36* - Begin region
  2020. if self.regionon_re.search(gline):
  2021. if len(path) > 1:
  2022. # Take care of what is left in the path
  2023. ## --- Buffered ---
  2024. width = self.apertures[last_path_aperture]["size"]
  2025. geo = LineString(path)
  2026. if not geo.is_empty:
  2027. follow_buffer.append(geo)
  2028. geo = LineString(path).buffer(width/1.999, int(self.steps_per_circle / 4))
  2029. if not geo.is_empty:
  2030. poly_buffer.append(geo)
  2031. try:
  2032. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2033. except KeyError:
  2034. self.apertures[last_path_aperture]['solid_geometry'] = []
  2035. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2036. path = [path[-1]]
  2037. making_region = True
  2038. continue
  2039. ### G37* - End region
  2040. if self.regionoff_re.search(gline):
  2041. making_region = False
  2042. # if D02 happened before G37 we now have a path with 1 element only so we have to add the current
  2043. # geo to the poly_buffer otherwise we loose it
  2044. if current_operation_code == 2:
  2045. if geo:
  2046. if not geo.is_empty:
  2047. follow_buffer.append(geo)
  2048. poly_buffer.append(geo)
  2049. try:
  2050. self.apertures[current_aperture]['solid_geometry'].append(geo)
  2051. except KeyError:
  2052. self.apertures[current_aperture]['solid_geometry'] = []
  2053. self.apertures[current_aperture]['solid_geometry'].append(geo)
  2054. continue
  2055. # Only one path defines region?
  2056. # This can happen if D02 happened before G37 and
  2057. # is not and error.
  2058. if len(path) < 3:
  2059. # print "ERROR: Path contains less than 3 points:"
  2060. # print path
  2061. # print "Line (%d): " % line_num, gline
  2062. # path = []
  2063. #path = [[current_x, current_y]]
  2064. continue
  2065. # For regions we may ignore an aperture that is None
  2066. # self.regions.append({"polygon": Polygon(path),
  2067. # "aperture": last_path_aperture})
  2068. # --- Buffered ---
  2069. region = Polygon()
  2070. if not region.is_empty:
  2071. follow_buffer.append(region)
  2072. region = Polygon(path)
  2073. if not region.is_valid:
  2074. region = region.buffer(0, int(self.steps_per_circle / 4))
  2075. if not region.is_empty:
  2076. poly_buffer.append(region)
  2077. # we do this for the case that a region is done without having defined any aperture
  2078. # Allegro does that
  2079. if current_aperture:
  2080. used_aperture = current_aperture
  2081. elif last_path_aperture:
  2082. used_aperture = last_path_aperture
  2083. else:
  2084. if '0' not in self.apertures:
  2085. self.apertures['0'] = {}
  2086. self.apertures['0']['solid_geometry'] = []
  2087. self.apertures['0']['type'] = 'REG'
  2088. used_aperture = '0'
  2089. try:
  2090. self.apertures[used_aperture]['solid_geometry'].append(region)
  2091. except KeyError:
  2092. self.apertures[used_aperture]['solid_geometry'] = []
  2093. self.apertures[used_aperture]['solid_geometry'].append(region)
  2094. path = [[current_x, current_y]] # Start new path
  2095. continue
  2096. ### G01/2/3* - Interpolation mode change
  2097. # Can occur along with coordinates and operation code but
  2098. # sometimes by itself (handled here).
  2099. # Example: G01*
  2100. match = self.interp_re.search(gline)
  2101. if match:
  2102. current_interpolation_mode = int(match.group(1))
  2103. continue
  2104. ### G01 - Linear interpolation plus flashes
  2105. # Operation code (D0x) missing is deprecated... oh well I will support it.
  2106. # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
  2107. match = self.lin_re.search(gline)
  2108. if match:
  2109. # Dxx alone?
  2110. # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
  2111. # try:
  2112. # current_operation_code = int(match.group(4))
  2113. # except:
  2114. # pass # A line with just * will match too.
  2115. # continue
  2116. # NOTE: Letting it continue allows it to react to the
  2117. # operation code.
  2118. # Parse coordinates
  2119. if match.group(2) is not None:
  2120. linear_x = parse_gerber_number(match.group(2),
  2121. self.int_digits, self.frac_digits, self.gerber_zeros)
  2122. current_x = linear_x
  2123. else:
  2124. linear_x = current_x
  2125. if match.group(3) is not None:
  2126. linear_y = parse_gerber_number(match.group(3),
  2127. self.int_digits, self.frac_digits, self.gerber_zeros)
  2128. current_y = linear_y
  2129. else:
  2130. linear_y = current_y
  2131. # Parse operation code
  2132. if match.group(4) is not None:
  2133. current_operation_code = int(match.group(4))
  2134. # Pen down: add segment
  2135. if current_operation_code == 1:
  2136. # if linear_x or linear_y are None, ignore those
  2137. if linear_x is not None and linear_y is not None:
  2138. # only add the point if it's a new one otherwise skip it (harder to process)
  2139. if path[-1] != [linear_x, linear_y]:
  2140. path.append([linear_x, linear_y])
  2141. if making_region is False:
  2142. # if the aperture is rectangle then add a rectangular shape having as parameters the
  2143. # coordinates of the start and end point and also the width and height
  2144. # of the 'R' aperture
  2145. try:
  2146. if self.apertures[current_aperture]["type"] == 'R':
  2147. width = self.apertures[current_aperture]['width']
  2148. height = self.apertures[current_aperture]['height']
  2149. minx = min(path[0][0], path[1][0]) - width / 2
  2150. maxx = max(path[0][0], path[1][0]) + width / 2
  2151. miny = min(path[0][1], path[1][1]) - height / 2
  2152. maxy = max(path[0][1], path[1][1]) + height / 2
  2153. log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
  2154. geo = shply_box(minx, miny, maxx, maxy)
  2155. poly_buffer.append(geo)
  2156. try:
  2157. self.apertures[current_aperture]['solid_geometry'].append(geo)
  2158. except KeyError:
  2159. self.apertures[current_aperture]['solid_geometry'] = []
  2160. self.apertures[current_aperture]['solid_geometry'].append(geo)
  2161. except:
  2162. pass
  2163. last_path_aperture = current_aperture
  2164. else:
  2165. self.app.inform.emit(_tr("[WARNING] Coordinates missing, line ignored: %s") % str(gline))
  2166. self.app.inform.emit(_tr("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!"))
  2167. elif current_operation_code == 2:
  2168. if len(path) > 1:
  2169. geo = None
  2170. ## --- BUFFERED ---
  2171. # this treats the case when we are storing geometry as paths only
  2172. if making_region:
  2173. geo = Polygon()
  2174. else:
  2175. geo = LineString(path)
  2176. try:
  2177. if self.apertures[last_path_aperture]["type"] != 'R':
  2178. if not geo.is_empty:
  2179. follow_buffer.append(geo)
  2180. except:
  2181. follow_buffer.append(geo)
  2182. # this treats the case when we are storing geometry as solids
  2183. if making_region:
  2184. elem = [linear_x, linear_y]
  2185. if elem != path[-1]:
  2186. path.append([linear_x, linear_y])
  2187. try:
  2188. geo = Polygon(path)
  2189. except ValueError:
  2190. log.warning("Problem %s %s" % (gline, line_num))
  2191. self.app.inform.emit(_tr("[ERROR] Region does not have enough points. "
  2192. "File will be processed but there are parser errors. "
  2193. "Line number: %s") % str(line_num))
  2194. else:
  2195. if last_path_aperture is None:
  2196. log.warning("No aperture defined for curent path. (%d)" % line_num)
  2197. width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
  2198. geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  2199. try:
  2200. if self.apertures[last_path_aperture]["type"] != 'R':
  2201. if not geo.is_empty:
  2202. poly_buffer.append(geo)
  2203. try:
  2204. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2205. except KeyError:
  2206. self.apertures[last_path_aperture]['solid_geometry'] = []
  2207. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2208. except:
  2209. poly_buffer.append(geo)
  2210. try:
  2211. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2212. except KeyError:
  2213. self.apertures[last_path_aperture]['solid_geometry'] = []
  2214. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2215. # if linear_x or linear_y are None, ignore those
  2216. if linear_x is not None and linear_y is not None:
  2217. path = [[linear_x, linear_y]] # Start new path
  2218. else:
  2219. self.app.inform.emit(_tr("[WARNING] Coordinates missing, line ignored: %s") % str(gline))
  2220. self.app.inform.emit(_tr("[WARNING_NOTCL] GERBER file might be CORRUPT. Check the file !!!"))
  2221. # Flash
  2222. # Not allowed in region mode.
  2223. elif current_operation_code == 3:
  2224. # Create path draw so far.
  2225. if len(path) > 1:
  2226. # --- Buffered ----
  2227. # this treats the case when we are storing geometry as paths
  2228. geo = LineString(path)
  2229. if not geo.is_empty:
  2230. try:
  2231. if self.apertures[last_path_aperture]["type"] != 'R':
  2232. follow_buffer.append(geo)
  2233. except:
  2234. follow_buffer.append(geo)
  2235. # this treats the case when we are storing geometry as solids
  2236. width = self.apertures[last_path_aperture]["size"]
  2237. geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  2238. if not geo.is_empty:
  2239. try:
  2240. if self.apertures[last_path_aperture]["type"] != 'R':
  2241. poly_buffer.append(geo)
  2242. try:
  2243. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2244. except KeyError:
  2245. self.apertures[last_path_aperture]['solid_geometry'] = []
  2246. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2247. except:
  2248. poly_buffer.append(geo)
  2249. try:
  2250. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2251. except KeyError:
  2252. self.apertures[last_path_aperture]['solid_geometry'] = []
  2253. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2254. # Reset path starting point
  2255. path = [[linear_x, linear_y]]
  2256. # --- BUFFERED ---
  2257. # Draw the flash
  2258. # this treats the case when we are storing geometry as paths
  2259. follow_buffer.append(Point([linear_x, linear_y]))
  2260. # this treats the case when we are storing geometry as solids
  2261. flash = Gerber.create_flash_geometry(
  2262. Point( [linear_x, linear_y]),
  2263. self.apertures[current_aperture],
  2264. int(self.steps_per_circle)
  2265. )
  2266. if not flash.is_empty:
  2267. poly_buffer.append(flash)
  2268. try:
  2269. self.apertures[current_aperture]['solid_geometry'].append(flash)
  2270. except KeyError:
  2271. self.apertures[current_aperture]['solid_geometry'] = []
  2272. self.apertures[current_aperture]['solid_geometry'].append(flash)
  2273. # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
  2274. # are used in case that circular interpolation is encountered within the Gerber file
  2275. current_x = linear_x
  2276. current_y = linear_y
  2277. # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
  2278. continue
  2279. ### G74/75* - Single or multiple quadrant arcs
  2280. match = self.quad_re.search(gline)
  2281. if match:
  2282. if match.group(1) == '4':
  2283. quadrant_mode = 'SINGLE'
  2284. else:
  2285. quadrant_mode = 'MULTI'
  2286. continue
  2287. ### G02/3 - Circular interpolation
  2288. # 2-clockwise, 3-counterclockwise
  2289. # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
  2290. match = self.circ_re.search(gline)
  2291. if match:
  2292. arcdir = [None, None, "cw", "ccw"]
  2293. mode, circular_x, circular_y, i, j, d = match.groups()
  2294. try:
  2295. circular_x = parse_gerber_number(circular_x,
  2296. self.int_digits, self.frac_digits, self.gerber_zeros)
  2297. except:
  2298. circular_x = current_x
  2299. try:
  2300. circular_y = parse_gerber_number(circular_y,
  2301. self.int_digits, self.frac_digits, self.gerber_zeros)
  2302. except:
  2303. circular_y = current_y
  2304. # According to Gerber specification i and j are not modal, which means that when i or j are missing,
  2305. # they are to be interpreted as being zero
  2306. try:
  2307. i = parse_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
  2308. except:
  2309. i = 0
  2310. try:
  2311. j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
  2312. except:
  2313. j = 0
  2314. if quadrant_mode is None:
  2315. log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
  2316. log.error(gline)
  2317. continue
  2318. if mode is None and current_interpolation_mode not in [2, 3]:
  2319. log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  2320. log.error(gline)
  2321. continue
  2322. elif mode is not None:
  2323. current_interpolation_mode = int(mode)
  2324. # Set operation code if provided
  2325. try:
  2326. current_operation_code = int(d)
  2327. current_d = current_operation_code
  2328. except:
  2329. current_operation_code = current_d
  2330. # Nothing created! Pen Up.
  2331. if current_operation_code == 2:
  2332. log.warning("Arc with D2. (%d)" % line_num)
  2333. if len(path) > 1:
  2334. if last_path_aperture is None:
  2335. log.warning("No aperture defined for curent path. (%d)" % line_num)
  2336. # --- BUFFERED ---
  2337. width = self.apertures[last_path_aperture]["size"]
  2338. # this treats the case when we are storing geometry as paths
  2339. geo = LineString(path)
  2340. if not geo.is_empty:
  2341. follow_buffer.append(geo)
  2342. # this treats the case when we are storing geometry as solids
  2343. buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
  2344. if not buffered.is_empty:
  2345. poly_buffer.append(buffered)
  2346. try:
  2347. self.apertures[last_path_aperture]['solid_geometry'].append(buffered)
  2348. except KeyError:
  2349. self.apertures[last_path_aperture]['solid_geometry'] = []
  2350. self.apertures[last_path_aperture]['solid_geometry'].append(buffered)
  2351. current_x = circular_x
  2352. current_y = circular_y
  2353. path = [[current_x, current_y]] # Start new path
  2354. continue
  2355. # Flash should not happen here
  2356. if current_operation_code == 3:
  2357. log.error("Trying to flash within arc. (%d)" % line_num)
  2358. continue
  2359. if quadrant_mode == 'MULTI':
  2360. center = [i + current_x, j + current_y]
  2361. radius = sqrt(i ** 2 + j ** 2)
  2362. start = arctan2(-j, -i) # Start angle
  2363. # Numerical errors might prevent start == stop therefore
  2364. # we check ahead of time. This should result in a
  2365. # 360 degree arc.
  2366. if current_x == circular_x and current_y == circular_y:
  2367. stop = start
  2368. else:
  2369. stop = arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  2370. this_arc = arc(center, radius, start, stop,
  2371. arcdir[current_interpolation_mode],
  2372. int(self.steps_per_circle))
  2373. # The last point in the computed arc can have
  2374. # numerical errors. The exact final point is the
  2375. # specified (x, y). Replace.
  2376. this_arc[-1] = (circular_x, circular_y)
  2377. # Last point in path is current point
  2378. # current_x = this_arc[-1][0]
  2379. # current_y = this_arc[-1][1]
  2380. current_x, current_y = circular_x, circular_y
  2381. # Append
  2382. path += this_arc
  2383. last_path_aperture = current_aperture
  2384. continue
  2385. if quadrant_mode == 'SINGLE':
  2386. center_candidates = [
  2387. [i + current_x, j + current_y],
  2388. [-i + current_x, j + current_y],
  2389. [i + current_x, -j + current_y],
  2390. [-i + current_x, -j + current_y]
  2391. ]
  2392. valid = False
  2393. log.debug("I: %f J: %f" % (i, j))
  2394. for center in center_candidates:
  2395. radius = sqrt(i ** 2 + j ** 2)
  2396. # Make sure radius to start is the same as radius to end.
  2397. radius2 = sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
  2398. if radius2 < radius * 0.95 or radius2 > radius * 1.05:
  2399. continue # Not a valid center.
  2400. # Correct i and j and continue as with multi-quadrant.
  2401. i = center[0] - current_x
  2402. j = center[1] - current_y
  2403. start = arctan2(-j, -i) # Start angle
  2404. stop = arctan2(-center[1] + circular_y, -center[0] + circular_x) # Stop angle
  2405. angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  2406. log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  2407. (current_x, current_y, center[0], center[1], circular_x, circular_y))
  2408. log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  2409. (start * 180 / pi, stop * 180 / pi, arcdir[current_interpolation_mode],
  2410. angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2))
  2411. if angle <= (pi + 1e-6) / 2:
  2412. log.debug("########## ACCEPTING ARC ############")
  2413. this_arc = arc(center, radius, start, stop,
  2414. arcdir[current_interpolation_mode],
  2415. int(self.steps_per_circle))
  2416. # Replace with exact values
  2417. this_arc[-1] = (circular_x, circular_y)
  2418. # current_x = this_arc[-1][0]
  2419. # current_y = this_arc[-1][1]
  2420. current_x, current_y = circular_x, circular_y
  2421. path += this_arc
  2422. last_path_aperture = current_aperture
  2423. valid = True
  2424. break
  2425. if valid:
  2426. continue
  2427. else:
  2428. log.warning("Invalid arc in line %d." % line_num)
  2429. ## EOF
  2430. match = self.eof_re.search(gline)
  2431. if match:
  2432. continue
  2433. ### Line did not match any pattern. Warn user.
  2434. log.warning("Line ignored (%d): %s" % (line_num, gline))
  2435. if len(path) > 1:
  2436. # In case that G01 (moving) aperture is rectangular, there is no need to still create
  2437. # another geo since we already created a shapely box using the start and end coordinates found in
  2438. # path variable. We do it only for other apertures than 'R' type
  2439. if self.apertures[last_path_aperture]["type"] == 'R':
  2440. pass
  2441. else:
  2442. # EOF, create shapely LineString if something still in path
  2443. ## --- Buffered ---
  2444. # this treats the case when we are storing geometry as paths
  2445. geo = LineString(path)
  2446. if not geo.is_empty:
  2447. follow_buffer.append(geo)
  2448. # this treats the case when we are storing geometry as solids
  2449. width = self.apertures[last_path_aperture]["size"]
  2450. geo = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
  2451. if not geo.is_empty:
  2452. poly_buffer.append(geo)
  2453. try:
  2454. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2455. except KeyError:
  2456. self.apertures[last_path_aperture]['solid_geometry'] = []
  2457. self.apertures[last_path_aperture]['solid_geometry'].append(geo)
  2458. # --- Apply buffer ---
  2459. # this treats the case when we are storing geometry as paths
  2460. self.follow_geometry = follow_buffer
  2461. # this treats the case when we are storing geometry as solids
  2462. log.warning("Joining %d polygons." % len(poly_buffer))
  2463. if len(poly_buffer) == 0:
  2464. log.error("Object is not Gerber file or empty. Aborting Object creation.")
  2465. return
  2466. if self.use_buffer_for_union:
  2467. log.debug("Union by buffer...")
  2468. new_poly = MultiPolygon(poly_buffer)
  2469. new_poly = new_poly.buffer(0.00000001)
  2470. new_poly = new_poly.buffer(-0.00000001)
  2471. log.warning("Union(buffer) done.")
  2472. else:
  2473. log.debug("Union by union()...")
  2474. new_poly = cascaded_union(poly_buffer)
  2475. new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
  2476. log.warning("Union done.")
  2477. if current_polarity == 'D':
  2478. self.solid_geometry = self.solid_geometry.union(new_poly)
  2479. else:
  2480. self.solid_geometry = self.solid_geometry.difference(new_poly)
  2481. except Exception as err:
  2482. ex_type, ex, tb = sys.exc_info()
  2483. traceback.print_tb(tb)
  2484. #print traceback.format_exc()
  2485. log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
  2486. loc = 'Gerber Line #%d Gerber Line Content: %s\n' % (line_num, gline) + repr(err)
  2487. self.app.inform.emit(_tr("[ERROR]Gerber Parser ERROR.\n%s:") % loc)
  2488. @staticmethod
  2489. def create_flash_geometry(location, aperture, steps_per_circle=None):
  2490. # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
  2491. if steps_per_circle is None:
  2492. steps_per_circle = 64
  2493. if type(location) == list:
  2494. location = Point(location)
  2495. if aperture['type'] == 'C': # Circles
  2496. return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4))
  2497. if aperture['type'] == 'R': # Rectangles
  2498. loc = location.coords[0]
  2499. width = aperture['width']
  2500. height = aperture['height']
  2501. minx = loc[0] - width / 2
  2502. maxx = loc[0] + width / 2
  2503. miny = loc[1] - height / 2
  2504. maxy = loc[1] + height / 2
  2505. return shply_box(minx, miny, maxx, maxy)
  2506. if aperture['type'] == 'O': # Obround
  2507. loc = location.coords[0]
  2508. width = aperture['width']
  2509. height = aperture['height']
  2510. if width > height:
  2511. p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
  2512. p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
  2513. c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4))
  2514. c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4))
  2515. else:
  2516. p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
  2517. p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
  2518. c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
  2519. c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
  2520. return cascaded_union([c1, c2]).convex_hull
  2521. if aperture['type'] == 'P': # Regular polygon
  2522. loc = location.coords[0]
  2523. diam = aperture['diam']
  2524. n_vertices = aperture['nVertices']
  2525. points = []
  2526. for i in range(0, n_vertices):
  2527. x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices))
  2528. y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices))
  2529. points.append((x, y))
  2530. ply = Polygon(points)
  2531. if 'rotation' in aperture:
  2532. ply = affinity.rotate(ply, aperture['rotation'])
  2533. return ply
  2534. if aperture['type'] == 'AM': # Aperture Macro
  2535. loc = location.coords[0]
  2536. flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
  2537. if flash_geo.is_empty:
  2538. log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
  2539. return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
  2540. log.warning("Unknown aperture type: %s" % aperture['type'])
  2541. return None
  2542. def create_geometry(self):
  2543. """
  2544. Geometry from a Gerber file is made up entirely of polygons.
  2545. Every stroke (linear or circular) has an aperture which gives
  2546. it thickness. Additionally, aperture strokes have non-zero area,
  2547. and regions naturally do as well.
  2548. :rtype : None
  2549. :return: None
  2550. """
  2551. # self.buffer_paths()
  2552. #
  2553. # self.fix_regions()
  2554. #
  2555. # self.do_flashes()
  2556. #
  2557. # self.solid_geometry = cascaded_union(self.buffered_paths +
  2558. # [poly['polygon'] for poly in self.regions] +
  2559. # self.flash_geometry)
  2560. def get_bounding_box(self, margin=0.0, rounded=False):
  2561. """
  2562. Creates and returns a rectangular polygon bounding at a distance of
  2563. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  2564. can optionally have rounded corners of radius equal to margin.
  2565. :param margin: Distance to enlarge the rectangular bounding
  2566. box in both positive and negative, x and y axes.
  2567. :type margin: float
  2568. :param rounded: Wether or not to have rounded corners.
  2569. :type rounded: bool
  2570. :return: The bounding box.
  2571. :rtype: Shapely.Polygon
  2572. """
  2573. bbox = self.solid_geometry.envelope.buffer(margin)
  2574. if not rounded:
  2575. bbox = bbox.envelope
  2576. return bbox
  2577. def bounds(self):
  2578. """
  2579. Returns coordinates of rectangular bounds
  2580. of Gerber geometry: (xmin, ymin, xmax, ymax).
  2581. """
  2582. # fixed issue of getting bounds only for one level lists of objects
  2583. # now it can get bounds for nested lists of objects
  2584. log.debug("Gerber->bounds()")
  2585. if self.solid_geometry is None:
  2586. log.debug("solid_geometry is None")
  2587. return 0, 0, 0, 0
  2588. def bounds_rec(obj):
  2589. if type(obj) is list and type(obj) is not MultiPolygon:
  2590. minx = Inf
  2591. miny = Inf
  2592. maxx = -Inf
  2593. maxy = -Inf
  2594. for k in obj:
  2595. if type(k) is dict:
  2596. for key in k:
  2597. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  2598. minx = min(minx, minx_)
  2599. miny = min(miny, miny_)
  2600. maxx = max(maxx, maxx_)
  2601. maxy = max(maxy, maxy_)
  2602. else:
  2603. try:
  2604. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  2605. except Exception as e:
  2606. log.debug("camlib.Geometry.bounds() --> %s" % str(e))
  2607. return
  2608. minx = min(minx, minx_)
  2609. miny = min(miny, miny_)
  2610. maxx = max(maxx, maxx_)
  2611. maxy = max(maxy, maxy_)
  2612. return minx, miny, maxx, maxy
  2613. else:
  2614. # it's a Shapely object, return it's bounds
  2615. return obj.bounds
  2616. bounds_coords = bounds_rec(self.solid_geometry)
  2617. return bounds_coords
  2618. def scale(self, xfactor, yfactor=None, point=None):
  2619. """
  2620. Scales the objects' geometry on the XY plane by a given factor.
  2621. These are:
  2622. * ``buffered_paths``
  2623. * ``flash_geometry``
  2624. * ``solid_geometry``
  2625. * ``regions``
  2626. NOTE:
  2627. Does not modify the data used to create these elements. If these
  2628. are recreated, the scaling will be lost. This behavior was modified
  2629. because of the complexity reached in this class.
  2630. :param factor: Number by which to scale.
  2631. :type factor: float
  2632. :rtype : None
  2633. """
  2634. log.debug("camlib.Gerber.scale()")
  2635. try:
  2636. xfactor = float(xfactor)
  2637. except:
  2638. self.app.inform.emit(_tr("[ERROR_NOTCL] Scale factor has to be a number: integer or float."))
  2639. return
  2640. if yfactor is None:
  2641. yfactor = xfactor
  2642. else:
  2643. try:
  2644. yfactor = float(yfactor)
  2645. except:
  2646. self.app.inform.emit(_tr("[ERROR_NOTCL] Scale factor has to be a number: integer or float."))
  2647. return
  2648. if point is None:
  2649. px = 0
  2650. py = 0
  2651. else:
  2652. px, py = point
  2653. def scale_geom(obj):
  2654. if type(obj) is list:
  2655. new_obj = []
  2656. for g in obj:
  2657. new_obj.append(scale_geom(g))
  2658. return new_obj
  2659. else:
  2660. return affinity.scale(obj, xfactor,
  2661. yfactor, origin=(px, py))
  2662. self.solid_geometry = scale_geom(self.solid_geometry)
  2663. self.follow_geometry = scale_geom(self.follow_geometry)
  2664. # we need to scale the geometry stored in the Gerber apertures, too
  2665. try:
  2666. for apid in self.apertures:
  2667. self.apertures[apid]['solid_geometry'] = scale_geom(self.apertures[apid]['solid_geometry'])
  2668. except Exception as e:
  2669. log.debug('FlatCAMGeometry.scale() --> %s' % str(e))
  2670. self.app.inform.emit(_tr("[success]Gerber Scale done."))
  2671. ## solid_geometry ???
  2672. # It's a cascaded union of objects.
  2673. # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  2674. # factor, origin=(0, 0))
  2675. # # Now buffered_paths, flash_geometry and solid_geometry
  2676. # self.create_geometry()
  2677. def offset(self, vect):
  2678. """
  2679. Offsets the objects' geometry on the XY plane by a given vector.
  2680. These are:
  2681. * ``buffered_paths``
  2682. * ``flash_geometry``
  2683. * ``solid_geometry``
  2684. * ``regions``
  2685. NOTE:
  2686. Does not modify the data used to create these elements. If these
  2687. are recreated, the scaling will be lost. This behavior was modified
  2688. because of the complexity reached in this class.
  2689. :param vect: (x, y) offset vector.
  2690. :type vect: tuple
  2691. :return: None
  2692. """
  2693. try:
  2694. dx, dy = vect
  2695. except TypeError:
  2696. self.app.inform.emit(_tr("[ERROR_NOTCL]An (x,y) pair of values are needed. "
  2697. "Probable you entered only one value in the Offset field."))
  2698. return
  2699. def offset_geom(obj):
  2700. if type(obj) is list:
  2701. new_obj = []
  2702. for g in obj:
  2703. new_obj.append(offset_geom(g))
  2704. return new_obj
  2705. else:
  2706. return affinity.translate(obj, xoff=dx, yoff=dy)
  2707. ## Solid geometry
  2708. self.solid_geometry = offset_geom(self.solid_geometry)
  2709. self.follow_geometry = offset_geom(self.follow_geometry)
  2710. # we need to offset the geometry stored in the Gerber apertures, too
  2711. try:
  2712. for apid in self.apertures:
  2713. self.apertures[apid]['solid_geometry'] = offset_geom(self.apertures[apid]['solid_geometry'])
  2714. except Exception as e:
  2715. log.debug('FlatCAMGeometry.offset() --> %s' % str(e))
  2716. self.app.inform.emit(_tr("[success]Gerber Offset done."))
  2717. def mirror(self, axis, point):
  2718. """
  2719. Mirrors the object around a specified axis passing through
  2720. the given point. What is affected:
  2721. * ``buffered_paths``
  2722. * ``flash_geometry``
  2723. * ``solid_geometry``
  2724. * ``regions``
  2725. NOTE:
  2726. Does not modify the data used to create these elements. If these
  2727. are recreated, the scaling will be lost. This behavior was modified
  2728. because of the complexity reached in this class.
  2729. :param axis: "X" or "Y" indicates around which axis to mirror.
  2730. :type axis: str
  2731. :param point: [x, y] point belonging to the mirror axis.
  2732. :type point: list
  2733. :return: None
  2734. """
  2735. px, py = point
  2736. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  2737. def mirror_geom(obj):
  2738. if type(obj) is list:
  2739. new_obj = []
  2740. for g in obj:
  2741. new_obj.append(mirror_geom(g))
  2742. return new_obj
  2743. else:
  2744. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  2745. self.solid_geometry = mirror_geom(self.solid_geometry)
  2746. self.follow_geometry = mirror_geom(self.follow_geometry)
  2747. # we need to mirror the geometry stored in the Gerber apertures, too
  2748. try:
  2749. for apid in self.apertures:
  2750. self.apertures[apid]['solid_geometry'] = mirror_geom(self.apertures[apid]['solid_geometry'])
  2751. except Exception as e:
  2752. log.debug('FlatCAMGeometry.mirror() --> %s' % str(e))
  2753. # It's a cascaded union of objects.
  2754. # self.solid_geometry = affinity.scale(self.solid_geometry,
  2755. # xscale, yscale, origin=(px, py))
  2756. def skew(self, angle_x, angle_y, point):
  2757. """
  2758. Shear/Skew the geometries of an object by angles along x and y dimensions.
  2759. Parameters
  2760. ----------
  2761. xs, ys : float, float
  2762. The shear angle(s) for the x and y axes respectively. These can be
  2763. specified in either degrees (default) or radians by setting
  2764. use_radians=True.
  2765. See shapely manual for more information:
  2766. http://toblerity.org/shapely/manual.html#affine-transformations
  2767. """
  2768. px, py = point
  2769. def skew_geom(obj):
  2770. if type(obj) is list:
  2771. new_obj = []
  2772. for g in obj:
  2773. new_obj.append(skew_geom(g))
  2774. return new_obj
  2775. else:
  2776. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  2777. self.solid_geometry = skew_geom(self.solid_geometry)
  2778. self.follow_geometry = skew_geom(self.follow_geometry)
  2779. # we need to skew the geometry stored in the Gerber apertures, too
  2780. try:
  2781. for apid in self.apertures:
  2782. self.apertures[apid]['solid_geometry'] = skew_geom(self.apertures[apid]['solid_geometry'])
  2783. except Exception as e:
  2784. log.debug('FlatCAMGeometry.skew() --> %s' % str(e))
  2785. # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, origin=(px, py))
  2786. def rotate(self, angle, point):
  2787. """
  2788. Rotate an object by a given angle around given coords (point)
  2789. :param angle:
  2790. :param point:
  2791. :return:
  2792. """
  2793. px, py = point
  2794. def rotate_geom(obj):
  2795. if type(obj) is list:
  2796. new_obj = []
  2797. for g in obj:
  2798. new_obj.append(rotate_geom(g))
  2799. return new_obj
  2800. else:
  2801. return affinity.rotate(obj, angle, origin=(px, py))
  2802. self.solid_geometry = rotate_geom(self.solid_geometry)
  2803. self.follow_geometry = rotate_geom(self.follow_geometry)
  2804. # we need to rotate the geometry stored in the Gerber apertures, too
  2805. try:
  2806. for apid in self.apertures:
  2807. self.apertures[apid]['solid_geometry'] = rotate_geom(self.apertures[apid]['solid_geometry'])
  2808. except Exception as e:
  2809. log.debug('FlatCAMGeometry.rotate() --> %s' % str(e))
  2810. # self.solid_geometry = affinity.rotate(self.solid_geometry, angle, origin=(px, py))
  2811. class Excellon(Geometry):
  2812. """
  2813. *ATTRIBUTES*
  2814. * ``tools`` (dict): The key is the tool name and the value is
  2815. a dictionary specifying the tool:
  2816. ================ ====================================
  2817. Key Value
  2818. ================ ====================================
  2819. C Diameter of the tool
  2820. solid_geometry Geometry list for each tool
  2821. Others Not supported (Ignored).
  2822. ================ ====================================
  2823. * ``drills`` (list): Each is a dictionary:
  2824. ================ ====================================
  2825. Key Value
  2826. ================ ====================================
  2827. point (Shapely.Point) Where to drill
  2828. tool (str) A key in ``tools``
  2829. ================ ====================================
  2830. * ``slots`` (list): Each is a dictionary
  2831. ================ ====================================
  2832. Key Value
  2833. ================ ====================================
  2834. start (Shapely.Point) Start point of the slot
  2835. stop (Shapely.Point) Stop point of the slot
  2836. tool (str) A key in ``tools``
  2837. ================ ====================================
  2838. """
  2839. defaults = {
  2840. "zeros": "L",
  2841. "excellon_format_upper_mm": '3',
  2842. "excellon_format_lower_mm": '3',
  2843. "excellon_format_upper_in": '2',
  2844. "excellon_format_lower_in": '4',
  2845. "excellon_units": 'INCH',
  2846. "geo_steps_per_circle": '64'
  2847. }
  2848. def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None,
  2849. excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None,
  2850. geo_steps_per_circle=None):
  2851. """
  2852. The constructor takes no parameters.
  2853. :return: Excellon object.
  2854. :rtype: Excellon
  2855. """
  2856. if geo_steps_per_circle is None:
  2857. geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
  2858. self.geo_steps_per_circle = int(geo_steps_per_circle)
  2859. Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle))
  2860. # dictionary to store tools, see above for description
  2861. self.tools = {}
  2862. # list to store the drills, see above for description
  2863. self.drills = []
  2864. # self.slots (list) to store the slots; each is a dictionary
  2865. self.slots = []
  2866. self.source_file = ''
  2867. # it serve to flag if a start routing or a stop routing was encountered
  2868. # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
  2869. self.routing_flag = 1
  2870. self.match_routing_start = None
  2871. self.match_routing_stop = None
  2872. self.num_tools = [] # List for keeping the tools sorted
  2873. self.index_per_tool = {} # Dictionary to store the indexed points for each tool
  2874. ## IN|MM -> Units are inherited from Geometry
  2875. #self.units = units
  2876. # Trailing "T" or leading "L" (default)
  2877. #self.zeros = "T"
  2878. self.zeros = zeros or self.defaults["zeros"]
  2879. self.zeros_found = self.zeros
  2880. self.units_found = self.units
  2881. # Excellon format
  2882. self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"]
  2883. self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"]
  2884. self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
  2885. self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
  2886. self.excellon_units = excellon_units or self.defaults["excellon_units"]
  2887. # Attributes to be included in serialization
  2888. # Always append to it because it carries contents
  2889. # from Geometry.
  2890. self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
  2891. 'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots',
  2892. 'source_file']
  2893. #### Patterns ####
  2894. # Regex basics:
  2895. # ^ - beginning
  2896. # $ - end
  2897. # *: 0 or more, +: 1 or more, ?: 0 or 1
  2898. # M48 - Beginning of Part Program Header
  2899. self.hbegin_re = re.compile(r'^M48$')
  2900. # ;HEADER - Beginning of Allegro Program Header
  2901. self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)')
  2902. # M95 or % - End of Part Program Header
  2903. # NOTE: % has different meaning in the body
  2904. self.hend_re = re.compile(r'^(?:M95|%)$')
  2905. # FMAT Excellon format
  2906. # Ignored in the parser
  2907. #self.fmat_re = re.compile(r'^FMAT,([12])$')
  2908. # Number format and units
  2909. # INCH uses 6 digits
  2910. # METRIC uses 5/6
  2911. self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?.*$')
  2912. # Tool definition/parameters (?= is look-ahead
  2913. # NOTE: This might be an overkill!
  2914. # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
  2915. # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  2916. # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  2917. # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  2918. self.toolset_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))?' +
  2919. r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  2920. r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  2921. r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  2922. self.detect_gcode_re = re.compile(r'^G2([01])$')
  2923. # Tool select
  2924. # Can have additional data after tool number but
  2925. # is ignored if present in the header.
  2926. # Warning: This will match toolset_re too.
  2927. # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
  2928. self.toolsel_re = re.compile(r'^T(\d+)')
  2929. # Headerless toolset
  2930. self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))')
  2931. # Comment
  2932. self.comm_re = re.compile(r'^;(.*)$')
  2933. # Absolute/Incremental G90/G91
  2934. self.absinc_re = re.compile(r'^G9([01])$')
  2935. # Modes of operation
  2936. # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
  2937. self.modes_re = re.compile(r'^G0([012345])')
  2938. # Measuring mode
  2939. # 1-metric, 2-inch
  2940. self.meas_re = re.compile(r'^M7([12])$')
  2941. # Coordinates
  2942. # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
  2943. # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
  2944. coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]'
  2945. self.coordsperiod_re = re.compile(coordsperiod_re_string)
  2946. coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]'
  2947. self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string)
  2948. # Slots parsing
  2949. slots_re_string = r'^([^G]+)G85(.*)$'
  2950. self.slots_re = re.compile(slots_re_string)
  2951. # R - Repeat hole (# times, X offset, Y offset)
  2952. self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
  2953. # Various stop/pause commands
  2954. self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
  2955. # Allegro Excellon format support
  2956. self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
  2957. # Parse coordinates
  2958. self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
  2959. # Repeating command
  2960. self.repeat_re = re.compile(r'R(\d+)')
  2961. def parse_file(self, filename):
  2962. """
  2963. Reads the specified file as array of lines as
  2964. passes it to ``parse_lines()``.
  2965. :param filename: The file to be read and parsed.
  2966. :type filename: str
  2967. :return: None
  2968. """
  2969. efile = open(filename, 'r')
  2970. estr = efile.readlines()
  2971. efile.close()
  2972. try:
  2973. self.parse_lines(estr)
  2974. except:
  2975. return "fail"
  2976. def parse_lines(self, elines):
  2977. """
  2978. Main Excellon parser.
  2979. :param elines: List of strings, each being a line of Excellon code.
  2980. :type elines: list
  2981. :return: None
  2982. """
  2983. # State variables
  2984. current_tool = ""
  2985. in_header = False
  2986. headerless = False
  2987. current_x = None
  2988. current_y = None
  2989. slot_current_x = None
  2990. slot_current_y = None
  2991. name_tool = 0
  2992. allegro_warning = False
  2993. line_units_found = False
  2994. repeating_x = 0
  2995. repeating_y = 0
  2996. repeat = 0
  2997. line_units = ''
  2998. #### Parsing starts here ####
  2999. line_num = 0 # Line number
  3000. eline = ""
  3001. try:
  3002. for eline in elines:
  3003. line_num += 1
  3004. # log.debug("%3d %s" % (line_num, str(eline)))
  3005. self.source_file += eline
  3006. # Cleanup lines
  3007. eline = eline.strip(' \r\n')
  3008. # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe
  3009. # and we need to exit from here
  3010. if self.detect_gcode_re.search(eline):
  3011. log.warning("This is GCODE mark: %s" % eline)
  3012. self.app.inform.emit(_tr('[ERROR_NOTCL] This is GCODE mark: %s') % eline)
  3013. return
  3014. # Header Begin (M48) #
  3015. if self.hbegin_re.search(eline):
  3016. in_header = True
  3017. log.warning("Found start of the header: %s" % eline)
  3018. continue
  3019. # Allegro Header Begin (;HEADER) #
  3020. if self.allegro_hbegin_re.search(eline):
  3021. in_header = True
  3022. allegro_warning = True
  3023. log.warning("Found ALLEGRO start of the header: %s" % eline)
  3024. continue
  3025. # Header End #
  3026. # Since there might be comments in the header that include char % or M95
  3027. # we ignore the lines starting with ';' which show they are comments
  3028. if self.comm_re.search(eline):
  3029. match = self.tool_units_re.search(eline)
  3030. if match:
  3031. if line_units_found is False:
  3032. line_units_found = True
  3033. line_units = match.group(3)
  3034. self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
  3035. log.warning("Type of Allegro UNITS found inline: %s" % line_units)
  3036. if match.group(2):
  3037. name_tool += 1
  3038. if line_units == 'MILS':
  3039. spec = {"C": (float(match.group(2)) / 1000)}
  3040. self.tools[str(name_tool)] = spec
  3041. log.debug(" Tool definition: %s %s" % (name_tool, spec))
  3042. else:
  3043. spec = {"C": float(match.group(2))}
  3044. self.tools[str(name_tool)] = spec
  3045. log.debug(" Tool definition: %s %s" % (name_tool, spec))
  3046. spec['solid_geometry'] = []
  3047. continue
  3048. else:
  3049. log.warning("Line ignored, it's a comment: %s" % eline)
  3050. else:
  3051. if self.hend_re.search(eline):
  3052. if in_header is False:
  3053. log.warning("Found end of the header but there is no header: %s" % eline)
  3054. log.warning("The only useful data in header are tools, units and format.")
  3055. log.warning("Therefore we will create units and format based on defaults.")
  3056. headerless = True
  3057. try:
  3058. self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units])
  3059. except Exception as e:
  3060. log.warning("Units could not be converted: %s" % str(e))
  3061. in_header = False
  3062. # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange
  3063. if allegro_warning is True:
  3064. name_tool = 0
  3065. log.warning("Found end of the header: %s" % eline)
  3066. continue
  3067. ## Alternative units format M71/M72
  3068. # Supposed to be just in the body (yes, the body)
  3069. # but some put it in the header (PADS for example).
  3070. # Will detect anywhere. Occurrence will change the
  3071. # object's units.
  3072. match = self.meas_re.match(eline)
  3073. if match:
  3074. #self.units = {"1": "MM", "2": "IN"}[match.group(1)]
  3075. # Modified for issue #80
  3076. self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
  3077. log.debug(" Units: %s" % self.units)
  3078. if self.units == 'MM':
  3079. log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \
  3080. ':' + str(self.excellon_format_lower_mm))
  3081. else:
  3082. log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \
  3083. ':' + str(self.excellon_format_lower_in))
  3084. continue
  3085. #### Body ####
  3086. if not in_header:
  3087. ## Tool change ##
  3088. match = self.toolsel_re.search(eline)
  3089. if match:
  3090. current_tool = str(int(match.group(1)))
  3091. log.debug("Tool change: %s" % current_tool)
  3092. if headerless is True:
  3093. match = self.toolset_hl_re.search(eline)
  3094. if match:
  3095. name = str(int(match.group(1)))
  3096. spec = {
  3097. "C": float(match.group(2)),
  3098. }
  3099. spec['solid_geometry'] = []
  3100. self.tools[name] = spec
  3101. log.debug(" Tool definition out of header: %s %s" % (name, spec))
  3102. continue
  3103. ## Allegro Type Tool change ##
  3104. if allegro_warning is True:
  3105. match = self.absinc_re.search(eline)
  3106. match1 = self.stop_re.search(eline)
  3107. if match or match1:
  3108. name_tool += 1
  3109. current_tool = str(name_tool)
  3110. log.debug(" Tool change for Allegro type of Excellon: %s" % current_tool)
  3111. continue
  3112. ## Slots parsing for drilled slots (contain G85)
  3113. # a Excellon drilled slot line may look like this:
  3114. # X01125Y0022244G85Y0027756
  3115. match = self.slots_re.search(eline)
  3116. if match:
  3117. # signal that there are milling slots operations
  3118. self.defaults['excellon_drills'] = False
  3119. # the slot start coordinates group is to the left of G85 command (group(1) )
  3120. # the slot stop coordinates group is to the right of G85 command (group(2) )
  3121. start_coords_match = match.group(1)
  3122. stop_coords_match = match.group(2)
  3123. # Slot coordinates without period ##
  3124. # get the coordinates for slot start and for slot stop into variables
  3125. start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match)
  3126. stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match)
  3127. if start_coords_noperiod:
  3128. try:
  3129. slot_start_x = self.parse_number(start_coords_noperiod.group(1))
  3130. slot_current_x = slot_start_x
  3131. except TypeError:
  3132. slot_start_x = slot_current_x
  3133. except:
  3134. return
  3135. try:
  3136. slot_start_y = self.parse_number(start_coords_noperiod.group(2))
  3137. slot_current_y = slot_start_y
  3138. except TypeError:
  3139. slot_start_y = slot_current_y
  3140. except:
  3141. return
  3142. try:
  3143. slot_stop_x = self.parse_number(stop_coords_noperiod.group(1))
  3144. slot_current_x = slot_stop_x
  3145. except TypeError:
  3146. slot_stop_x = slot_current_x
  3147. except:
  3148. return
  3149. try:
  3150. slot_stop_y = self.parse_number(stop_coords_noperiod.group(2))
  3151. slot_current_y = slot_stop_y
  3152. except TypeError:
  3153. slot_stop_y = slot_current_y
  3154. except:
  3155. return
  3156. if (slot_start_x is None or slot_start_y is None or
  3157. slot_stop_x is None or slot_stop_y is None):
  3158. log.error("Slots are missing some or all coordinates.")
  3159. continue
  3160. # we have a slot
  3161. log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
  3162. slot_start_y, slot_stop_x,
  3163. slot_stop_y]))
  3164. # store current tool diameter as slot diameter
  3165. slot_dia = 0.05
  3166. try:
  3167. slot_dia = float(self.tools[current_tool]['C'])
  3168. except:
  3169. pass
  3170. log.debug(
  3171. 'Milling/Drilling slot with tool %s, diam=%f' % (
  3172. current_tool,
  3173. slot_dia
  3174. )
  3175. )
  3176. self.slots.append(
  3177. {
  3178. 'start': Point(slot_start_x, slot_start_y),
  3179. 'stop': Point(slot_stop_x, slot_stop_y),
  3180. 'tool': current_tool
  3181. }
  3182. )
  3183. continue
  3184. # Slot coordinates with period: Use literally. ##
  3185. # get the coordinates for slot start and for slot stop into variables
  3186. start_coords_period = self.coordsperiod_re.search(start_coords_match)
  3187. stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
  3188. if start_coords_period:
  3189. try:
  3190. slot_start_x = float(start_coords_period.group(1))
  3191. slot_current_x = slot_start_x
  3192. except TypeError:
  3193. slot_start_x = slot_current_x
  3194. except:
  3195. return
  3196. try:
  3197. slot_start_y = float(start_coords_period.group(2))
  3198. slot_current_y = slot_start_y
  3199. except TypeError:
  3200. slot_start_y = slot_current_y
  3201. except:
  3202. return
  3203. try:
  3204. slot_stop_x = float(stop_coords_period.group(1))
  3205. slot_current_x = slot_stop_x
  3206. except TypeError:
  3207. slot_stop_x = slot_current_x
  3208. except:
  3209. return
  3210. try:
  3211. slot_stop_y = float(stop_coords_period.group(2))
  3212. slot_current_y = slot_stop_y
  3213. except TypeError:
  3214. slot_stop_y = slot_current_y
  3215. except:
  3216. return
  3217. if (slot_start_x is None or slot_start_y is None or
  3218. slot_stop_x is None or slot_stop_y is None):
  3219. log.error("Slots are missing some or all coordinates.")
  3220. continue
  3221. # we have a slot
  3222. log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
  3223. slot_start_y, slot_stop_x, slot_stop_y]))
  3224. # store current tool diameter as slot diameter
  3225. slot_dia = 0.05
  3226. try:
  3227. slot_dia = float(self.tools[current_tool]['C'])
  3228. except:
  3229. pass
  3230. log.debug(
  3231. 'Milling/Drilling slot with tool %s, diam=%f' % (
  3232. current_tool,
  3233. slot_dia
  3234. )
  3235. )
  3236. self.slots.append(
  3237. {
  3238. 'start': Point(slot_start_x, slot_start_y),
  3239. 'stop': Point(slot_stop_x, slot_stop_y),
  3240. 'tool': current_tool
  3241. }
  3242. )
  3243. continue
  3244. ## Coordinates without period ##
  3245. match = self.coordsnoperiod_re.search(eline)
  3246. if match:
  3247. matchr = self.repeat_re.search(eline)
  3248. if matchr:
  3249. repeat = int(matchr.group(1))
  3250. try:
  3251. x = self.parse_number(match.group(1))
  3252. repeating_x = current_x
  3253. current_x = x
  3254. except TypeError:
  3255. x = current_x
  3256. repeating_x = 0
  3257. except:
  3258. return
  3259. try:
  3260. y = self.parse_number(match.group(2))
  3261. repeating_y = current_y
  3262. current_y = y
  3263. except TypeError:
  3264. y = current_y
  3265. repeating_y = 0
  3266. except:
  3267. return
  3268. if x is None or y is None:
  3269. log.error("Missing coordinates")
  3270. continue
  3271. ## Excellon Routing parse
  3272. if len(re.findall("G00", eline)) > 0:
  3273. self.match_routing_start = 'G00'
  3274. # signal that there are milling slots operations
  3275. self.defaults['excellon_drills'] = False
  3276. self.routing_flag = 0
  3277. slot_start_x = x
  3278. slot_start_y = y
  3279. continue
  3280. if self.routing_flag == 0:
  3281. if len(re.findall("G01", eline)) > 0:
  3282. self.match_routing_stop = 'G01'
  3283. # signal that there are milling slots operations
  3284. self.defaults['excellon_drills'] = False
  3285. self.routing_flag = 1
  3286. slot_stop_x = x
  3287. slot_stop_y = y
  3288. self.slots.append(
  3289. {
  3290. 'start': Point(slot_start_x, slot_start_y),
  3291. 'stop': Point(slot_stop_x, slot_stop_y),
  3292. 'tool': current_tool
  3293. }
  3294. )
  3295. continue
  3296. if self.match_routing_start is None and self.match_routing_stop is None:
  3297. if repeat == 0:
  3298. # signal that there are drill operations
  3299. self.defaults['excellon_drills'] = True
  3300. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  3301. else:
  3302. coordx = x
  3303. coordy = y
  3304. while repeat > 0:
  3305. if repeating_x:
  3306. coordx = (repeat * x) + repeating_x
  3307. if repeating_y:
  3308. coordy = (repeat * y) + repeating_y
  3309. self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
  3310. repeat -= 1
  3311. repeating_x = repeating_y = 0
  3312. # log.debug("{:15} {:8} {:8}".format(eline, x, y))
  3313. continue
  3314. ## Coordinates with period: Use literally. ##
  3315. match = self.coordsperiod_re.search(eline)
  3316. if match:
  3317. matchr = self.repeat_re.search(eline)
  3318. if matchr:
  3319. repeat = int(matchr.group(1))
  3320. if match:
  3321. # signal that there are drill operations
  3322. self.defaults['excellon_drills'] = True
  3323. try:
  3324. x = float(match.group(1))
  3325. repeating_x = current_x
  3326. current_x = x
  3327. except TypeError:
  3328. x = current_x
  3329. repeating_x = 0
  3330. try:
  3331. y = float(match.group(2))
  3332. repeating_y = current_y
  3333. current_y = y
  3334. except TypeError:
  3335. y = current_y
  3336. repeating_y = 0
  3337. if x is None or y is None:
  3338. log.error("Missing coordinates")
  3339. continue
  3340. ## Excellon Routing parse
  3341. if len(re.findall("G00", eline)) > 0:
  3342. self.match_routing_start = 'G00'
  3343. # signal that there are milling slots operations
  3344. self.defaults['excellon_drills'] = False
  3345. self.routing_flag = 0
  3346. slot_start_x = x
  3347. slot_start_y = y
  3348. continue
  3349. if self.routing_flag == 0:
  3350. if len(re.findall("G01", eline)) > 0:
  3351. self.match_routing_stop = 'G01'
  3352. # signal that there are milling slots operations
  3353. self.defaults['excellon_drills'] = False
  3354. self.routing_flag = 1
  3355. slot_stop_x = x
  3356. slot_stop_y = y
  3357. self.slots.append(
  3358. {
  3359. 'start': Point(slot_start_x, slot_start_y),
  3360. 'stop': Point(slot_stop_x, slot_stop_y),
  3361. 'tool': current_tool
  3362. }
  3363. )
  3364. continue
  3365. if self.match_routing_start is None and self.match_routing_stop is None:
  3366. # signal that there are drill operations
  3367. if repeat == 0:
  3368. # signal that there are drill operations
  3369. self.defaults['excellon_drills'] = True
  3370. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  3371. else:
  3372. coordx = x
  3373. coordy = y
  3374. while repeat > 0:
  3375. if repeating_x:
  3376. coordx = (repeat * x) + repeating_x
  3377. if repeating_y:
  3378. coordy = (repeat * y) + repeating_y
  3379. self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
  3380. repeat -= 1
  3381. repeating_x = repeating_y = 0
  3382. # log.debug("{:15} {:8} {:8}".format(eline, x, y))
  3383. continue
  3384. #### Header ####
  3385. if in_header:
  3386. ## Tool definitions ##
  3387. match = self.toolset_re.search(eline)
  3388. if match:
  3389. name = str(int(match.group(1)))
  3390. spec = {
  3391. "C": float(match.group(2)),
  3392. # "F": float(match.group(3)),
  3393. # "S": float(match.group(4)),
  3394. # "B": float(match.group(5)),
  3395. # "H": float(match.group(6)),
  3396. # "Z": float(match.group(7))
  3397. }
  3398. spec['solid_geometry'] = []
  3399. self.tools[name] = spec
  3400. log.debug(" Tool definition: %s %s" % (name, spec))
  3401. continue
  3402. ## Units and number format ##
  3403. match = self.units_re.match(eline)
  3404. if match:
  3405. self.units_found = match.group(1)
  3406. self.zeros = match.group(2) # "T" or "L". Might be empty
  3407. # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  3408. # Modified for issue #80
  3409. self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
  3410. # log.warning(" Units/Format: %s %s" % (self.units, self.zeros))
  3411. log.warning("Units: %s" % self.units)
  3412. if self.units == 'MM':
  3413. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
  3414. ':' + str(self.excellon_format_lower_mm))
  3415. else:
  3416. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
  3417. ':' + str(self.excellon_format_lower_in))
  3418. log.warning("Type of zeros found inline: %s" % self.zeros)
  3419. continue
  3420. # Search for units type again it might be alone on the line
  3421. if "INCH" in eline:
  3422. line_units = "INCH"
  3423. # Modified for issue #80
  3424. self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
  3425. log.warning("Type of UNITS found inline: %s" % line_units)
  3426. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
  3427. ':' + str(self.excellon_format_lower_in))
  3428. # TODO: not working
  3429. #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
  3430. continue
  3431. elif "METRIC" in eline:
  3432. line_units = "METRIC"
  3433. # Modified for issue #80
  3434. self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
  3435. log.warning("Type of UNITS found inline: %s" % line_units)
  3436. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
  3437. ':' + str(self.excellon_format_lower_mm))
  3438. # TODO: not working
  3439. #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
  3440. continue
  3441. # Search for zeros type again because it might be alone on the line
  3442. match = re.search(r'[LT]Z',eline)
  3443. if match:
  3444. self.zeros = match.group()
  3445. log.warning("Type of zeros found: %s" % self.zeros)
  3446. continue
  3447. ## Units and number format outside header##
  3448. match = self.units_re.match(eline)
  3449. if match:
  3450. self.units_found = match.group(1)
  3451. self.zeros = match.group(2) # "T" or "L". Might be empty
  3452. # self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  3453. # Modified for issue #80
  3454. self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
  3455. # log.warning(" Units/Format: %s %s" % (self.units, self.zeros))
  3456. log.warning("Units: %s" % self.units)
  3457. if self.units == 'MM':
  3458. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
  3459. ':' + str(self.excellon_format_lower_mm))
  3460. else:
  3461. log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
  3462. ':' + str(self.excellon_format_lower_in))
  3463. log.warning("Type of zeros found outside header, inline: %s" % self.zeros)
  3464. log.warning("UNITS found outside header")
  3465. continue
  3466. log.warning("Line ignored: %s" % eline)
  3467. # make sure that since we are in headerless mode, we convert the tools only after the file parsing
  3468. # is finished since the tools definitions are spread in the Excellon body. We use as units the value
  3469. # from self.defaults['excellon_units']
  3470. log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
  3471. except Exception as e:
  3472. log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
  3473. msg = _tr("[ERROR_NOTCL] An internal error has ocurred. See shell.\n")
  3474. msg += _tr('[ERROR] Excellon Parser error.\nParsing Failed. Line %d: %s\n') % (line_num, eline)
  3475. msg += traceback.format_exc()
  3476. self.app.inform.emit(msg)
  3477. return "fail"
  3478. def parse_number(self, number_str):
  3479. """
  3480. Parses coordinate numbers without period.
  3481. :param number_str: String representing the numerical value.
  3482. :type number_str: str
  3483. :return: Floating point representation of the number
  3484. :rtype: float
  3485. """
  3486. match = self.leadingzeros_re.search(number_str)
  3487. nr_length = len(match.group(1)) + len(match.group(2))
  3488. try:
  3489. if self.zeros == "L" or self.zeros == "LZ":
  3490. # With leading zeros, when you type in a coordinate,
  3491. # the leading zeros must always be included. Trailing zeros
  3492. # are unneeded and may be left off. The CNC-7 will automatically add them.
  3493. # r'^[-\+]?(0*)(\d*)'
  3494. # 6 digits are divided by 10^4
  3495. # If less than size digits, they are automatically added,
  3496. # 5 digits then are divided by 10^3 and so on.
  3497. if self.units.lower() == "in":
  3498. result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in)))
  3499. else:
  3500. result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm)))
  3501. return result
  3502. else: # Trailing
  3503. # You must show all zeros to the right of the number and can omit
  3504. # all zeros to the left of the number. The CNC-7 will count the number
  3505. # of digits you typed and automatically fill in the missing zeros.
  3506. ## flatCAM expects 6digits
  3507. # flatCAM expects the number of digits entered into the defaults
  3508. if self.units.lower() == "in": # Inches is 00.0000
  3509. result = float(number_str) / (10 ** (float(self.excellon_format_lower_in)))
  3510. else: # Metric is 000.000
  3511. result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm)))
  3512. return result
  3513. except Exception as e:
  3514. log.error("Aborted. Operation could not be completed due of %s" % str(e))
  3515. return
  3516. def create_geometry(self):
  3517. """
  3518. Creates circles of the tool diameter at every point
  3519. specified in ``self.drills``. Also creates geometries (polygons)
  3520. for the slots as specified in ``self.slots``
  3521. All the resulting geometry is stored into self.solid_geometry list.
  3522. The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
  3523. and second element is another similar dict that contain the slots geometry.
  3524. Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries
  3525. ================ ====================================
  3526. Key Value
  3527. ================ ====================================
  3528. tool_diameter list of (Shapely.Point) Where to drill
  3529. ================ ====================================
  3530. :return: None
  3531. """
  3532. self.solid_geometry = []
  3533. try:
  3534. # clear the solid_geometry in self.tools
  3535. for tool in self.tools:
  3536. self.tools[tool]['solid_geometry'][:] = []
  3537. for drill in self.drills:
  3538. # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
  3539. if drill['tool'] is '':
  3540. self.app.inform.emit(_tr("[WARNING] Excellon.create_geometry() -> a drill location was skipped "
  3541. "due of not having a tool associated.\n"
  3542. "Check the resulting GCode."))
  3543. log.debug("Excellon.create_geometry() -> a drill location was skipped "
  3544. "due of not having a tool associated")
  3545. continue
  3546. tooldia = self.tools[drill['tool']]['C']
  3547. poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
  3548. # self.solid_geometry.append(poly)
  3549. self.tools[drill['tool']]['solid_geometry'].append(poly)
  3550. for slot in self.slots:
  3551. slot_tooldia = self.tools[slot['tool']]['C']
  3552. start = slot['start']
  3553. stop = slot['stop']
  3554. lines_string = LineString([start, stop])
  3555. poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
  3556. # self.solid_geometry.append(poly)
  3557. self.tools[slot['tool']]['solid_geometry'].append(poly)
  3558. except Exception as e:
  3559. log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e))
  3560. return "fail"
  3561. # drill_geometry = {}
  3562. # slot_geometry = {}
  3563. #
  3564. # def insertIntoDataStruct(dia, drill_geo, aDict):
  3565. # if not dia in aDict:
  3566. # aDict[dia] = [drill_geo]
  3567. # else:
  3568. # aDict[dia].append(drill_geo)
  3569. #
  3570. # for tool in self.tools:
  3571. # tooldia = self.tools[tool]['C']
  3572. # for drill in self.drills:
  3573. # if drill['tool'] == tool:
  3574. # poly = drill['point'].buffer(tooldia / 2.0)
  3575. # insertIntoDataStruct(tooldia, poly, drill_geometry)
  3576. #
  3577. # for tool in self.tools:
  3578. # slot_tooldia = self.tools[tool]['C']
  3579. # for slot in self.slots:
  3580. # if slot['tool'] == tool:
  3581. # start = slot['start']
  3582. # stop = slot['stop']
  3583. # lines_string = LineString([start, stop])
  3584. # poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
  3585. # insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
  3586. #
  3587. # self.solid_geometry = [drill_geometry, slot_geometry]
  3588. def bounds(self):
  3589. """
  3590. Returns coordinates of rectangular bounds
  3591. of Gerber geometry: (xmin, ymin, xmax, ymax).
  3592. """
  3593. # fixed issue of getting bounds only for one level lists of objects
  3594. # now it can get bounds for nested lists of objects
  3595. log.debug("Excellon() -> bounds()")
  3596. # if self.solid_geometry is None:
  3597. # log.debug("solid_geometry is None")
  3598. # return 0, 0, 0, 0
  3599. def bounds_rec(obj):
  3600. if type(obj) is list:
  3601. minx = Inf
  3602. miny = Inf
  3603. maxx = -Inf
  3604. maxy = -Inf
  3605. for k in obj:
  3606. if type(k) is dict:
  3607. for key in k:
  3608. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  3609. minx = min(minx, minx_)
  3610. miny = min(miny, miny_)
  3611. maxx = max(maxx, maxx_)
  3612. maxy = max(maxy, maxy_)
  3613. else:
  3614. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  3615. minx = min(minx, minx_)
  3616. miny = min(miny, miny_)
  3617. maxx = max(maxx, maxx_)
  3618. maxy = max(maxy, maxy_)
  3619. return minx, miny, maxx, maxy
  3620. else:
  3621. # it's a Shapely object, return it's bounds
  3622. return obj.bounds
  3623. minx_list = []
  3624. miny_list = []
  3625. maxx_list = []
  3626. maxy_list = []
  3627. for tool in self.tools:
  3628. minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry'])
  3629. minx_list.append(minx)
  3630. miny_list.append(miny)
  3631. maxx_list.append(maxx)
  3632. maxy_list.append(maxy)
  3633. return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
  3634. def convert_units(self, units):
  3635. """
  3636. This function first convert to the the units found in the Excellon file but it converts tools that
  3637. are not there yet so it has no effect other than it signal that the units are the ones in the file.
  3638. On object creation, in new_object(), true conversion is done because this is done at the end of the
  3639. Excellon file parsing, the tools are inside and self.tools is really converted from the units found
  3640. inside the file to the FlatCAM units.
  3641. Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file
  3642. will have detected the units before the tools are parsed and stored in self.tools
  3643. :param units:
  3644. :type str: IN or MM
  3645. :return:
  3646. """
  3647. factor = Geometry.convert_units(self, units)
  3648. # Tools
  3649. for tname in self.tools:
  3650. self.tools[tname]["C"] *= factor
  3651. self.create_geometry()
  3652. return factor
  3653. def scale(self, xfactor, yfactor=None, point=None):
  3654. """
  3655. Scales geometry on the XY plane in the object by a given factor.
  3656. Tool sizes, feedrates an Z-plane dimensions are untouched.
  3657. :param factor: Number by which to scale the object.
  3658. :type factor: float
  3659. :return: None
  3660. :rtype: NOne
  3661. """
  3662. if yfactor is None:
  3663. yfactor = xfactor
  3664. if point is None:
  3665. px = 0
  3666. py = 0
  3667. else:
  3668. px, py = point
  3669. def scale_geom(obj):
  3670. if type(obj) is list:
  3671. new_obj = []
  3672. for g in obj:
  3673. new_obj.append(scale_geom(g))
  3674. return new_obj
  3675. else:
  3676. return affinity.scale(obj, xfactor,
  3677. yfactor, origin=(px, py))
  3678. # Drills
  3679. for drill in self.drills:
  3680. drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
  3681. # scale solid_geometry
  3682. for tool in self.tools:
  3683. self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
  3684. # Slots
  3685. for slot in self.slots:
  3686. slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py))
  3687. slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py))
  3688. self.create_geometry()
  3689. def offset(self, vect):
  3690. """
  3691. Offsets geometry on the XY plane in the object by a given vector.
  3692. :param vect: (x, y) offset vector.
  3693. :type vect: tuple
  3694. :return: None
  3695. """
  3696. dx, dy = vect
  3697. def offset_geom(obj):
  3698. if type(obj) is list:
  3699. new_obj = []
  3700. for g in obj:
  3701. new_obj.append(offset_geom(g))
  3702. return new_obj
  3703. else:
  3704. return affinity.translate(obj, xoff=dx, yoff=dy)
  3705. # Drills
  3706. for drill in self.drills:
  3707. drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
  3708. # offset solid_geometry
  3709. for tool in self.tools:
  3710. self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
  3711. # Slots
  3712. for slot in self.slots:
  3713. slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy)
  3714. slot['start'] = affinity.translate(slot['start'],xoff=dx, yoff=dy)
  3715. # Recreate geometry
  3716. self.create_geometry()
  3717. def mirror(self, axis, point):
  3718. """
  3719. :param axis: "X" or "Y" indicates around which axis to mirror.
  3720. :type axis: str
  3721. :param point: [x, y] point belonging to the mirror axis.
  3722. :type point: list
  3723. :return: None
  3724. """
  3725. px, py = point
  3726. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  3727. def mirror_geom(obj):
  3728. if type(obj) is list:
  3729. new_obj = []
  3730. for g in obj:
  3731. new_obj.append(mirror_geom(g))
  3732. return new_obj
  3733. else:
  3734. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  3735. # Modify data
  3736. # Drills
  3737. for drill in self.drills:
  3738. drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
  3739. # mirror solid_geometry
  3740. for tool in self.tools:
  3741. self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
  3742. # Slots
  3743. for slot in self.slots:
  3744. slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py))
  3745. slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py))
  3746. # Recreate geometry
  3747. self.create_geometry()
  3748. def skew(self, angle_x=None, angle_y=None, point=None):
  3749. """
  3750. Shear/Skew the geometries of an object by angles along x and y dimensions.
  3751. Tool sizes, feedrates an Z-plane dimensions are untouched.
  3752. Parameters
  3753. ----------
  3754. xs, ys : float, float
  3755. The shear angle(s) for the x and y axes respectively. These can be
  3756. specified in either degrees (default) or radians by setting
  3757. use_radians=True.
  3758. See shapely manual for more information:
  3759. http://toblerity.org/shapely/manual.html#affine-transformations
  3760. """
  3761. if angle_x is None:
  3762. angle_x = 0.0
  3763. if angle_y is None:
  3764. angle_y = 0.0
  3765. def skew_geom(obj):
  3766. if type(obj) is list:
  3767. new_obj = []
  3768. for g in obj:
  3769. new_obj.append(skew_geom(g))
  3770. return new_obj
  3771. else:
  3772. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  3773. if point is None:
  3774. px, py = 0, 0
  3775. # Drills
  3776. for drill in self.drills:
  3777. drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
  3778. origin=(px, py))
  3779. # skew solid_geometry
  3780. for tool in self.tools:
  3781. self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
  3782. # Slots
  3783. for slot in self.slots:
  3784. slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
  3785. slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
  3786. else:
  3787. px, py = point
  3788. # Drills
  3789. for drill in self.drills:
  3790. drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
  3791. origin=(px, py))
  3792. # skew solid_geometry
  3793. for tool in self.tools:
  3794. self.tools[tool]['solid_geometry'] = skew_geom( self.tools[tool]['solid_geometry'])
  3795. # Slots
  3796. for slot in self.slots:
  3797. slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
  3798. slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
  3799. self.create_geometry()
  3800. def rotate(self, angle, point=None):
  3801. """
  3802. Rotate the geometry of an object by an angle around the 'point' coordinates
  3803. :param angle:
  3804. :param point: tuple of coordinates (x, y)
  3805. :return:
  3806. """
  3807. def rotate_geom(obj, origin=None):
  3808. if type(obj) is list:
  3809. new_obj = []
  3810. for g in obj:
  3811. new_obj.append(rotate_geom(g))
  3812. return new_obj
  3813. else:
  3814. if origin:
  3815. return affinity.rotate(obj, angle, origin=origin)
  3816. else:
  3817. return affinity.rotate(obj, angle, origin=(px, py))
  3818. if point is None:
  3819. # Drills
  3820. for drill in self.drills:
  3821. drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
  3822. # rotate solid_geometry
  3823. for tool in self.tools:
  3824. self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center')
  3825. # Slots
  3826. for slot in self.slots:
  3827. slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center')
  3828. slot['start'] = affinity.rotate(slot['start'], angle, origin='center')
  3829. else:
  3830. px, py = point
  3831. # Drills
  3832. for drill in self.drills:
  3833. drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py))
  3834. # rotate solid_geometry
  3835. for tool in self.tools:
  3836. self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
  3837. # Slots
  3838. for slot in self.slots:
  3839. slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py))
  3840. slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
  3841. self.create_geometry()
  3842. class AttrDict(dict):
  3843. def __init__(self, *args, **kwargs):
  3844. super(AttrDict, self).__init__(*args, **kwargs)
  3845. self.__dict__ = self
  3846. class CNCjob(Geometry):
  3847. """
  3848. Represents work to be done by a CNC machine.
  3849. *ATTRIBUTES*
  3850. * ``gcode_parsed`` (list): Each is a dictionary:
  3851. ===================== =========================================
  3852. Key Value
  3853. ===================== =========================================
  3854. geom (Shapely.LineString) Tool path (XY plane)
  3855. kind (string) "AB", A is "T" (travel) or
  3856. "C" (cut). B is "F" (fast) or "S" (slow).
  3857. ===================== =========================================
  3858. """
  3859. defaults = {
  3860. "global_zdownrate": None,
  3861. "pp_geometry_name":'default',
  3862. "pp_excellon_name":'default',
  3863. "excellon_optimization_type": "B",
  3864. "steps_per_circle": 64
  3865. }
  3866. def __init__(self,
  3867. units="in", kind="generic", tooldia=0.0,
  3868. z_cut=-0.002, z_move=0.1,
  3869. feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0, feedrate_probe=3.0,
  3870. pp_geometry_name='default', pp_excellon_name='default',
  3871. depthpercut=0.1,z_pdepth=-0.02,
  3872. spindlespeed=None, dwell=True, dwelltime=1000,
  3873. toolchangez=0.787402, toolchange_xy=[0.0, 0.0],
  3874. endz=2.0,
  3875. segx=None,
  3876. segy=None,
  3877. steps_per_circle=None):
  3878. # Used when parsing G-code arcs
  3879. if steps_per_circle is None:
  3880. steps_per_circle = int(CNCjob.defaults["steps_per_circle"])
  3881. self.steps_per_circle = int(steps_per_circle)
  3882. Geometry.__init__(self, geo_steps_per_circle=int(steps_per_circle))
  3883. self.kind = kind
  3884. self.units = units
  3885. self.z_cut = z_cut
  3886. self.tool_offset = {}
  3887. self.z_move = z_move
  3888. self.feedrate = feedrate
  3889. self.z_feedrate = feedrate_z
  3890. self.feedrate_rapid = feedrate_rapid
  3891. self.tooldia = tooldia
  3892. self.z_toolchange = toolchangez
  3893. self.xy_toolchange = toolchange_xy
  3894. self.toolchange_xy_type = None
  3895. self.toolC = tooldia
  3896. self.z_end = endz
  3897. self.z_depthpercut = depthpercut
  3898. self.unitcode = {"IN": "G20", "MM": "G21"}
  3899. self.feedminutecode = "G94"
  3900. self.absolutecode = "G90"
  3901. self.gcode = ""
  3902. self.gcode_parsed = None
  3903. self.pp_geometry_name = pp_geometry_name
  3904. self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
  3905. self.pp_excellon_name = pp_excellon_name
  3906. self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
  3907. self.pp_solderpaste_name = None
  3908. # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1
  3909. self.f_plunge = None
  3910. # Controls if the move from Z_Cutto Z_Move is done fast with G0 or G1 until zero and then G0 to Z_move
  3911. self.f_retract = None
  3912. # how much depth the probe can probe before error
  3913. self.z_pdepth = z_pdepth if z_pdepth else None
  3914. # the feedrate(speed) with which the probel travel while probing
  3915. self.feedrate_probe = feedrate_probe if feedrate_probe else None
  3916. self.spindlespeed = spindlespeed
  3917. self.dwell = dwell
  3918. self.dwelltime = dwelltime
  3919. self.segx = float(segx) if segx is not None else 0.0
  3920. self.segy = float(segy) if segy is not None else 0.0
  3921. self.input_geometry_bounds = None
  3922. self.oldx = None
  3923. self.oldy = None
  3924. self.tool = 0.0
  3925. # search for toolchange parameters in the Toolchange Custom Code
  3926. self.re_toolchange_custom = re.compile(r'(%[a-zA-Z0-9\-_]+%)')
  3927. # search for toolchange code: M6
  3928. self.re_toolchange = re.compile(r'^\s*(M6)$')
  3929. # Attributes to be included in serialization
  3930. # Always append to it because it carries contents
  3931. # from Geometry.
  3932. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'z_toolchange', 'feedrate', 'z_feedrate', 'feedrate_rapid',
  3933. 'tooldia', 'gcode', 'input_geometry_bounds', 'gcode_parsed', 'steps_per_circle',
  3934. 'z_depthpercut', 'spindlespeed', 'dwell', 'dwelltime']
  3935. @property
  3936. def postdata(self):
  3937. return self.__dict__
  3938. def convert_units(self, units):
  3939. factor = Geometry.convert_units(self, units)
  3940. log.debug("CNCjob.convert_units()")
  3941. self.z_cut = float(self.z_cut) * factor
  3942. self.z_move *= factor
  3943. self.feedrate *= factor
  3944. self.z_feedrate *= factor
  3945. self.feedrate_rapid *= factor
  3946. self.tooldia *= factor
  3947. self.z_toolchange *= factor
  3948. self.z_end *= factor
  3949. self.z_depthpercut = float(self.z_depthpercut) * factor
  3950. return factor
  3951. def doformat(self, fun, **kwargs):
  3952. return self.doformat2(fun, **kwargs) + "\n"
  3953. def doformat2(self, fun, **kwargs):
  3954. attributes = AttrDict()
  3955. attributes.update(self.postdata)
  3956. attributes.update(kwargs)
  3957. try:
  3958. returnvalue = fun(attributes)
  3959. return returnvalue
  3960. except Exception as e:
  3961. self.app.log.error('Exception occurred within a postprocessor: ' + traceback.format_exc())
  3962. return ''
  3963. def parse_custom_toolchange_code(self, data):
  3964. text = data
  3965. match_list = self.re_toolchange_custom.findall(text)
  3966. if match_list:
  3967. for match in match_list:
  3968. command = match.strip('%')
  3969. try:
  3970. value = getattr(self, command)
  3971. except AttributeError:
  3972. self.app.inform.emit(_tr("[ERROR] There is no such parameter: %s") % str(match))
  3973. log.debug("CNCJob.parse_custom_toolchange_code() --> AttributeError ")
  3974. return 'fail'
  3975. text = text.replace(match, str(value))
  3976. return text
  3977. def optimized_travelling_salesman(self, points, start=None):
  3978. """
  3979. As solving the problem in the brute force way is too slow,
  3980. this function implements a simple heuristic: always
  3981. go to the nearest city.
  3982. Even if this algorithm is extremely simple, it works pretty well
  3983. giving a solution only about 25% longer than the optimal one (cit. Wikipedia),
  3984. and runs very fast in O(N^2) time complexity.
  3985. >>> optimized_travelling_salesman([[i,j] for i in range(5) for j in range(5)])
  3986. [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 4], [1, 3], [1, 2], [1, 1], [1, 0], [2, 0], [2, 1], [2, 2],
  3987. [2, 3], [2, 4], [3, 4], [3, 3], [3, 2], [3, 1], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]]
  3988. >>> optimized_travelling_salesman([[0,0],[10,0],[6,0]])
  3989. [[0, 0], [6, 0], [10, 0]]
  3990. """
  3991. if start is None:
  3992. start = points[0]
  3993. must_visit = points
  3994. path = [start]
  3995. # must_visit.remove(start)
  3996. while must_visit:
  3997. nearest = min(must_visit, key=lambda x: distance(path[-1], x))
  3998. path.append(nearest)
  3999. must_visit.remove(nearest)
  4000. return path
  4001. def generate_from_excellon_by_tool(self, exobj, tools="all", drillz = 3.0,
  4002. toolchange=False, toolchangez=0.1, toolchangexy='',
  4003. endz=2.0, startz=None,
  4004. excellon_optimization_type='B'):
  4005. """
  4006. Creates gcode for this object from an Excellon object
  4007. for the specified tools.
  4008. :param exobj: Excellon object to process
  4009. :type exobj: Excellon
  4010. :param tools: Comma separated tool names
  4011. :type: tools: str
  4012. :param drillz: drill Z depth
  4013. :type drillz: float
  4014. :param toolchange: Use tool change sequence between tools.
  4015. :type toolchange: bool
  4016. :param toolchangez: Height at which to perform the tool change.
  4017. :type toolchangez: float
  4018. :param toolchangexy: Toolchange X,Y position
  4019. :type toolchangexy: String containing 2 floats separated by comma
  4020. :param startz: Z position just before starting the job
  4021. :type startz: float
  4022. :param endz: final Z position to move to at the end of the CNC job
  4023. :type endz: float
  4024. :param excellon_optimization_type: Single character that defines which drill re-ordering optimisation algorithm
  4025. is to be used: 'M' for meta-heuristic and 'B' for basic
  4026. :type excellon_optimization_type: string
  4027. :return: None
  4028. :rtype: None
  4029. """
  4030. if drillz > 0:
  4031. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter has positive value. "
  4032. "It is the depth value to drill into material.\n"
  4033. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  4034. "therefore the app will convert the value to negative. "
  4035. "Check the resulting CNC code (Gcode etc)."))
  4036. self.z_cut = -drillz
  4037. elif drillz == 0:
  4038. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter is zero. "
  4039. "There will be no cut, skipping %s file") % exobj.options['name'])
  4040. return 'fail'
  4041. else:
  4042. self.z_cut = drillz
  4043. self.z_toolchange = toolchangez
  4044. try:
  4045. if toolchangexy == '':
  4046. self.xy_toolchange = None
  4047. else:
  4048. self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
  4049. if len(self.xy_toolchange) < 2:
  4050. self.app.inform.emit(_tr("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
  4051. "in the format (x, y) \nbut now there is only one value, not two. "))
  4052. return 'fail'
  4053. except Exception as e:
  4054. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
  4055. pass
  4056. self.startz = startz
  4057. self.z_end = endz
  4058. self.pp_excellon = self.app.postprocessors[self.pp_excellon_name]
  4059. p = self.pp_excellon
  4060. log.debug("Creating CNC Job from Excellon...")
  4061. # Tools
  4062. # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
  4063. # so we actually are sorting the tools by diameter
  4064. #sorted_tools = sorted(exobj.tools.items(), key=lambda t1: t1['C'])
  4065. sort = []
  4066. for k, v in list(exobj.tools.items()):
  4067. sort.append((k, v.get('C')))
  4068. sorted_tools = sorted(sort,key=lambda t1: t1[1])
  4069. if tools == "all":
  4070. tools = [i[0] for i in sorted_tools] # we get a array of ordered tools
  4071. log.debug("Tools 'all' and sorted are: %s" % str(tools))
  4072. else:
  4073. selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ','
  4074. selected_tools = [t1 for t1 in selected_tools if t1 in selected_tools]
  4075. # Create a sorted list of selected tools from the sorted_tools list
  4076. tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
  4077. log.debug("Tools selected and sorted are: %s" % str(tools))
  4078. # Points (Group by tool)
  4079. points = {}
  4080. for drill in exobj.drills:
  4081. if drill['tool'] in tools:
  4082. try:
  4083. points[drill['tool']].append(drill['point'])
  4084. except KeyError:
  4085. points[drill['tool']] = [drill['point']]
  4086. #log.debug("Found %d drills." % len(points))
  4087. self.gcode = []
  4088. self.f_plunge = self.app.defaults["excellon_f_plunge"]
  4089. self.f_retract = self.app.defaults["excellon_f_retract"]
  4090. # Initialization
  4091. gcode = self.doformat(p.start_code)
  4092. gcode += self.doformat(p.feedrate_code)
  4093. if toolchange is False:
  4094. if self.xy_toolchange is not None:
  4095. gcode += self.doformat(p.lift_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4096. gcode += self.doformat(p.startz_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4097. else:
  4098. gcode += self.doformat(p.lift_code, x=0.0, y=0.0)
  4099. gcode += self.doformat(p.startz_code, x=0.0, y=0.0)
  4100. # Distance callback
  4101. class CreateDistanceCallback(object):
  4102. """Create callback to calculate distances between points."""
  4103. def __init__(self):
  4104. """Initialize distance array."""
  4105. locations = create_data_array()
  4106. size = len(locations)
  4107. self.matrix = {}
  4108. for from_node in range(size):
  4109. self.matrix[from_node] = {}
  4110. for to_node in range(size):
  4111. if from_node == to_node:
  4112. self.matrix[from_node][to_node] = 0
  4113. else:
  4114. x1 = locations[from_node][0]
  4115. y1 = locations[from_node][1]
  4116. x2 = locations[to_node][0]
  4117. y2 = locations[to_node][1]
  4118. self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
  4119. def Distance(self, from_node, to_node):
  4120. return int(self.matrix[from_node][to_node])
  4121. # Create the data.
  4122. def create_data_array():
  4123. locations = []
  4124. for point in points[tool]:
  4125. locations.append((point.coords.xy[0][0], point.coords.xy[1][0]))
  4126. return locations
  4127. if self.xy_toolchange is not None:
  4128. self.oldx = self.xy_toolchange[0]
  4129. self.oldy = self.xy_toolchange[1]
  4130. else:
  4131. self.oldx = 0.0
  4132. self.oldy = 0.0
  4133. measured_distance = 0
  4134. current_platform = platform.architecture()[0]
  4135. if current_platform == '64bit':
  4136. if excellon_optimization_type == 'M':
  4137. log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
  4138. if exobj.drills:
  4139. for tool in tools:
  4140. self.tool=tool
  4141. self.postdata['toolC'] = exobj.tools[tool]["C"]
  4142. self.tooldia = exobj.tools[tool]["C"]
  4143. ################################################
  4144. # Create the data.
  4145. node_list = []
  4146. locations = create_data_array()
  4147. tsp_size = len(locations)
  4148. num_routes = 1 # The number of routes, which is 1 in the TSP.
  4149. # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
  4150. depot = 0
  4151. # Create routing model.
  4152. if tsp_size > 0:
  4153. routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
  4154. search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
  4155. search_parameters.local_search_metaheuristic = (
  4156. routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
  4157. # Set search time limit in milliseconds.
  4158. if float(self.app.defaults["excellon_search_time"]) != 0:
  4159. search_parameters.time_limit_ms = int(
  4160. float(self.app.defaults["excellon_search_time"]) * 1000)
  4161. else:
  4162. search_parameters.time_limit_ms = 3000
  4163. # Callback to the distance function. The callback takes two
  4164. # arguments (the from and to node indices) and returns the distance between them.
  4165. dist_between_locations = CreateDistanceCallback()
  4166. dist_callback = dist_between_locations.Distance
  4167. routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
  4168. # Solve, returns a solution if any.
  4169. assignment = routing.SolveWithParameters(search_parameters)
  4170. if assignment:
  4171. # Solution cost.
  4172. log.info("Total distance: " + str(assignment.ObjectiveValue()))
  4173. # Inspect solution.
  4174. # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
  4175. route_number = 0
  4176. node = routing.Start(route_number)
  4177. start_node = node
  4178. while not routing.IsEnd(node):
  4179. node_list.append(node)
  4180. node = assignment.Value(routing.NextVar(node))
  4181. else:
  4182. log.warning('No solution found.')
  4183. else:
  4184. log.warning('Specify an instance greater than 0.')
  4185. ################################################
  4186. # Only if tool has points.
  4187. if tool in points:
  4188. # Tool change sequence (optional)
  4189. if toolchange:
  4190. gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy))
  4191. gcode += self.doformat(p.spindle_code) # Spindle start
  4192. if self.dwell is True:
  4193. gcode += self.doformat(p.dwell_code) # Dwell time
  4194. else:
  4195. gcode += self.doformat(p.spindle_code)
  4196. if self.dwell is True:
  4197. gcode += self.doformat(p.dwell_code) # Dwell time
  4198. if self.units == 'MM':
  4199. current_tooldia = float('%.2f' % float(exobj.tools[tool]["C"]))
  4200. else:
  4201. current_tooldia = float('%.3f' % float(exobj.tools[tool]["C"]))
  4202. z_offset = float(self.tool_offset[current_tooldia]) * (-1)
  4203. self.z_cut += z_offset
  4204. # Drillling!
  4205. for k in node_list:
  4206. locx = locations[k][0]
  4207. locy = locations[k][1]
  4208. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4209. gcode += self.doformat(p.down_code, x=locx, y=locy)
  4210. if self.f_retract is False:
  4211. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4212. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4213. measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  4214. self.oldx = locx
  4215. self.oldy = locy
  4216. else:
  4217. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  4218. "The loaded Excellon file has no drills ...")
  4219. self.app.inform.emit(_tr('[ERROR_NOTCL]The loaded Excellon file has no drills ...'))
  4220. return 'fail'
  4221. log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
  4222. elif excellon_optimization_type == 'B':
  4223. log.debug("Using OR-Tools Basic drill path optimization.")
  4224. if exobj.drills:
  4225. for tool in tools:
  4226. self.tool=tool
  4227. self.postdata['toolC']=exobj.tools[tool]["C"]
  4228. self.tooldia = exobj.tools[tool]["C"]
  4229. ################################################
  4230. node_list = []
  4231. locations = create_data_array()
  4232. tsp_size = len(locations)
  4233. num_routes = 1 # The number of routes, which is 1 in the TSP.
  4234. # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
  4235. depot = 0
  4236. # Create routing model.
  4237. if tsp_size > 0:
  4238. routing = pywrapcp.RoutingModel(tsp_size, num_routes, depot)
  4239. search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
  4240. # Callback to the distance function. The callback takes two
  4241. # arguments (the from and to node indices) and returns the distance between them.
  4242. dist_between_locations = CreateDistanceCallback()
  4243. dist_callback = dist_between_locations.Distance
  4244. routing.SetArcCostEvaluatorOfAllVehicles(dist_callback)
  4245. # Solve, returns a solution if any.
  4246. assignment = routing.SolveWithParameters(search_parameters)
  4247. if assignment:
  4248. # Solution cost.
  4249. log.info("Total distance: " + str(assignment.ObjectiveValue()))
  4250. # Inspect solution.
  4251. # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
  4252. route_number = 0
  4253. node = routing.Start(route_number)
  4254. start_node = node
  4255. while not routing.IsEnd(node):
  4256. node_list.append(node)
  4257. node = assignment.Value(routing.NextVar(node))
  4258. else:
  4259. log.warning('No solution found.')
  4260. else:
  4261. log.warning('Specify an instance greater than 0.')
  4262. ################################################
  4263. # Only if tool has points.
  4264. if tool in points:
  4265. # Tool change sequence (optional)
  4266. if toolchange:
  4267. gcode += self.doformat(p.toolchange_code,toolchangexy=(self.oldx, self.oldy))
  4268. gcode += self.doformat(p.spindle_code) # Spindle start)
  4269. if self.dwell is True:
  4270. gcode += self.doformat(p.dwell_code) # Dwell time
  4271. else:
  4272. gcode += self.doformat(p.spindle_code)
  4273. if self.dwell is True:
  4274. gcode += self.doformat(p.dwell_code) # Dwell time
  4275. if self.units == 'MM':
  4276. current_tooldia = float('%.2f' % float(exobj.tools[tool]["C"]))
  4277. else:
  4278. current_tooldia = float('%.3f' % float(exobj.tools[tool]["C"]))
  4279. z_offset = float(self.tool_offset[current_tooldia]) * (-1)
  4280. self.z_cut += z_offset
  4281. # Drillling!
  4282. for k in node_list:
  4283. locx = locations[k][0]
  4284. locy = locations[k][1]
  4285. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4286. gcode += self.doformat(p.down_code, x=locx, y=locy)
  4287. if self.f_retract is False:
  4288. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4289. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4290. measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  4291. self.oldx = locx
  4292. self.oldy = locy
  4293. else:
  4294. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  4295. "The loaded Excellon file has no drills ...")
  4296. self.app.inform.emit(_tr('[ERROR_NOTCL]The loaded Excellon file has no drills ...'))
  4297. return 'fail'
  4298. log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
  4299. else:
  4300. self.app.inform.emit(_tr("[ERROR_NOTCL] Wrong optimization type selected."))
  4301. return 'fail'
  4302. else:
  4303. log.debug("Using Travelling Salesman drill path optimization.")
  4304. for tool in tools:
  4305. if exobj.drills:
  4306. self.tool = tool
  4307. self.postdata['toolC'] = exobj.tools[tool]["C"]
  4308. self.tooldia = exobj.tools[tool]["C"]
  4309. # Only if tool has points.
  4310. if tool in points:
  4311. # Tool change sequence (optional)
  4312. if toolchange:
  4313. gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
  4314. gcode += self.doformat(p.spindle_code) # Spindle start)
  4315. if self.dwell is True:
  4316. gcode += self.doformat(p.dwell_code) # Dwell time
  4317. else:
  4318. gcode += self.doformat(p.spindle_code)
  4319. if self.dwell is True:
  4320. gcode += self.doformat(p.dwell_code) # Dwell time
  4321. if self.units == 'MM':
  4322. current_tooldia = float('%.2f' % float(exobj.tools[tool]["C"]))
  4323. else:
  4324. current_tooldia = float('%.3f' % float(exobj.tools[tool]["C"]))
  4325. z_offset = float(self.tool_offset[current_tooldia]) * (-1)
  4326. self.z_cut += z_offset
  4327. # Drillling!
  4328. altPoints = []
  4329. for point in points[tool]:
  4330. altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
  4331. for point in self.optimized_travelling_salesman(altPoints):
  4332. gcode += self.doformat(p.rapid_code, x=point[0], y=point[1])
  4333. gcode += self.doformat(p.down_code, x=point[0], y=point[1])
  4334. if self.f_retract is False:
  4335. gcode += self.doformat(p.up_to_zero_code, x=point[0], y=point[1])
  4336. gcode += self.doformat(p.lift_code, x=point[0], y=point[1])
  4337. measured_distance += abs(distance_euclidian(point[0], point[1], self.oldx, self.oldy))
  4338. self.oldx = point[0]
  4339. self.oldy = point[1]
  4340. else:
  4341. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  4342. "The loaded Excellon file has no drills ...")
  4343. self.app.inform.emit(_tr('[ERROR_NOTCL]The loaded Excellon file has no drills ...'))
  4344. return 'fail'
  4345. log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
  4346. gcode += self.doformat(p.spindle_stop_code) # Spindle stop
  4347. gcode += self.doformat(p.end_code, x=0, y=0)
  4348. measured_distance += abs(distance_euclidian(self.oldx, self.oldy, 0, 0))
  4349. log.debug("The total travel distance including travel to end position is: %s" %
  4350. str(measured_distance) + '\n')
  4351. self.travel_distance = measured_distance
  4352. self.gcode = gcode
  4353. return 'OK'
  4354. def generate_from_multitool_geometry(self, geometry, append=True,
  4355. tooldia=None, offset=0.0, tolerance=0, z_cut=1.0, z_move=2.0,
  4356. feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
  4357. spindlespeed=None, dwell=False, dwelltime=1.0,
  4358. multidepth=False, depthpercut=None,
  4359. toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0", extracut=False,
  4360. startz=None, endz=2.0, pp_geometry_name=None, tool_no=1):
  4361. """
  4362. Algorithm to generate from multitool Geometry.
  4363. Algorithm description:
  4364. ----------------------
  4365. Uses RTree to find the nearest path to follow.
  4366. :param geometry:
  4367. :param append:
  4368. :param tooldia:
  4369. :param tolerance:
  4370. :param multidepth: If True, use multiple passes to reach
  4371. the desired depth.
  4372. :param depthpercut: Maximum depth in each pass.
  4373. :param extracut: Adds (or not) an extra cut at the end of each path
  4374. overlapping the first point in path to ensure complete copper removal
  4375. :return: GCode - string
  4376. """
  4377. log.debug("Generate_from_multitool_geometry()")
  4378. temp_solid_geometry = []
  4379. if offset != 0.0:
  4380. for it in geometry:
  4381. # if the geometry is a closed shape then create a Polygon out of it
  4382. if isinstance(it, LineString):
  4383. c = it.coords
  4384. if c[0] == c[-1]:
  4385. it = Polygon(it)
  4386. temp_solid_geometry.append(it.buffer(offset, join_style=2))
  4387. else:
  4388. temp_solid_geometry = geometry
  4389. ## Flatten the geometry. Only linear elements (no polygons) remain.
  4390. flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
  4391. log.debug("%d paths" % len(flat_geometry))
  4392. self.tooldia = float(tooldia) if tooldia else None
  4393. self.z_cut = float(z_cut) if z_cut else None
  4394. self.z_move = float(z_move) if z_move else None
  4395. self.feedrate = float(feedrate) if feedrate else None
  4396. self.z_feedrate = float(feedrate_z) if feedrate_z else None
  4397. self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
  4398. self.spindlespeed = int(spindlespeed) if spindlespeed else None
  4399. self.dwell = dwell
  4400. self.dwelltime = float(dwelltime) if dwelltime else None
  4401. self.startz = float(startz) if startz else None
  4402. self.z_end = float(endz) if endz else None
  4403. self.z_depthpercut = float(depthpercut) if depthpercut else None
  4404. self.multidepth = multidepth
  4405. self.z_toolchange = float(toolchangez) if toolchangez else None
  4406. # it servers in the postprocessor file
  4407. self.tool = tool_no
  4408. try:
  4409. if toolchangexy == '':
  4410. self.xy_toolchange = None
  4411. else:
  4412. self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
  4413. if len(self.xy_toolchange) < 2:
  4414. self.app.inform.emit(_tr("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
  4415. "in the format (x, y) \nbut now there is only one value, not two. "))
  4416. return 'fail'
  4417. except Exception as e:
  4418. log.debug("camlib.CNCJob.generate_from_multitool_geometry() --> %s" % str(e))
  4419. pass
  4420. self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
  4421. self.f_plunge = self.app.defaults["geometry_f_plunge"]
  4422. if self.z_cut is None:
  4423. self.app.inform.emit(_tr("[ERROR_NOTCL] Cut_Z parameter is None or zero. Most likely a bad combinations of "
  4424. "other parameters."))
  4425. return 'fail'
  4426. if self.z_cut > 0:
  4427. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter has positive value. "
  4428. "It is the depth value to cut into material.\n"
  4429. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  4430. "therefore the app will convert the value to negative."
  4431. "Check the resulting CNC code (Gcode etc)."))
  4432. self.z_cut = -self.z_cut
  4433. elif self.z_cut == 0:
  4434. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter is zero. "
  4435. "There will be no cut, skipping %s file") % self.options['name'])
  4436. return 'fail'
  4437. if self.z_move is None:
  4438. self.app.inform.emit(_tr("[ERROR_NOTCL] Travel Z parameter is None or zero."))
  4439. return 'fail'
  4440. if self.z_move < 0:
  4441. self.app.inform.emit(_tr("[WARNING] The Travel Z parameter has negative value. "
  4442. "It is the height value to travel between cuts.\n"
  4443. "The Z Travel parameter needs to have a positive value, assuming it is a typo "
  4444. "therefore the app will convert the value to positive."
  4445. "Check the resulting CNC code (Gcode etc)."))
  4446. self.z_move = -self.z_move
  4447. elif self.z_move == 0:
  4448. self.app.inform.emit(_tr("[WARNING] The Z Travel parameter is zero. "
  4449. "This is dangerous, skipping %s file") % self.options['name'])
  4450. return 'fail'
  4451. ## Index first and last points in paths
  4452. # What points to index.
  4453. def get_pts(o):
  4454. return [o.coords[0], o.coords[-1]]
  4455. # Create the indexed storage.
  4456. storage = FlatCAMRTreeStorage()
  4457. storage.get_points = get_pts
  4458. # Store the geometry
  4459. log.debug("Indexing geometry before generating G-Code...")
  4460. for shape in flat_geometry:
  4461. if shape is not None: # TODO: This shouldn't have happened.
  4462. storage.insert(shape)
  4463. # self.input_geometry_bounds = geometry.bounds()
  4464. if not append:
  4465. self.gcode = ""
  4466. # tell postprocessor the number of tool (for toolchange)
  4467. self.tool = tool_no
  4468. # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
  4469. # given under the name 'toolC'
  4470. self.postdata['toolC'] = self.tooldia
  4471. # Initial G-Code
  4472. self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
  4473. p = self.pp_geometry
  4474. self.gcode = self.doformat(p.start_code)
  4475. self.gcode += self.doformat(p.feedrate_code) # sets the feed rate
  4476. if toolchange is False:
  4477. self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height
  4478. self.gcode += self.doformat(p.startz_code, x=0, y=0)
  4479. if toolchange:
  4480. # if "line_xyz" in self.pp_geometry_name:
  4481. # self.gcode += self.doformat(p.toolchange_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4482. # else:
  4483. # self.gcode += self.doformat(p.toolchange_code)
  4484. self.gcode += self.doformat(p.toolchange_code)
  4485. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4486. if self.dwell is True:
  4487. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4488. else:
  4489. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4490. if self.dwell is True:
  4491. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4492. ## Iterate over geometry paths getting the nearest each time.
  4493. log.debug("Starting G-Code...")
  4494. path_count = 0
  4495. current_pt = (0, 0)
  4496. pt, geo = storage.nearest(current_pt)
  4497. try:
  4498. while True:
  4499. path_count += 1
  4500. # Remove before modifying, otherwise deletion will fail.
  4501. storage.remove(geo)
  4502. # If last point in geometry is the nearest but prefer the first one if last point == first point
  4503. # then reverse coordinates.
  4504. if pt != geo.coords[0] and pt == geo.coords[-1]:
  4505. geo.coords = list(geo.coords)[::-1]
  4506. #---------- Single depth/pass --------
  4507. if not multidepth:
  4508. self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
  4509. #--------- Multi-pass ---------
  4510. else:
  4511. self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
  4512. postproc=p, current_point=current_pt)
  4513. current_pt = geo.coords[-1]
  4514. pt, geo = storage.nearest(current_pt) # Next
  4515. except StopIteration: # Nothing found in storage.
  4516. pass
  4517. log.debug("Finishing G-Code... %s paths traced." % path_count)
  4518. # Finish
  4519. self.gcode += self.doformat(p.spindle_stop_code)
  4520. self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
  4521. self.gcode += self.doformat(p.end_code, x=0, y=0)
  4522. return self.gcode
  4523. def generate_from_geometry_2(self, geometry, append=True,
  4524. tooldia=None, offset=0.0, tolerance=0,
  4525. z_cut=1.0, z_move=2.0,
  4526. feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
  4527. spindlespeed=None, dwell=False, dwelltime=1.0,
  4528. multidepth=False, depthpercut=None,
  4529. toolchange=False, toolchangez=1.0, toolchangexy="0.0, 0.0",
  4530. extracut=False, startz=None, endz=2.0,
  4531. pp_geometry_name=None, tool_no=1):
  4532. """
  4533. Second algorithm to generate from Geometry.
  4534. Algorithm description:
  4535. ----------------------
  4536. Uses RTree to find the nearest path to follow.
  4537. :param geometry:
  4538. :param append:
  4539. :param tooldia:
  4540. :param tolerance:
  4541. :param multidepth: If True, use multiple passes to reach
  4542. the desired depth.
  4543. :param depthpercut: Maximum depth in each pass.
  4544. :param extracut: Adds (or not) an extra cut at the end of each path
  4545. overlapping the first point in path to ensure complete copper removal
  4546. :return: None
  4547. """
  4548. if not isinstance(geometry, Geometry):
  4549. self.app.inform.emit(_tr("[ERROR]Expected a Geometry, got %s") % type(geometry))
  4550. return 'fail'
  4551. log.debug("Generate_from_geometry_2()")
  4552. # if solid_geometry is empty raise an exception
  4553. if not geometry.solid_geometry:
  4554. self.app.inform.emit(_tr("[ERROR_NOTCL]Trying to generate a CNC Job "
  4555. "from a Geometry object without solid_geometry."))
  4556. temp_solid_geometry = []
  4557. def bounds_rec(obj):
  4558. if type(obj) is list:
  4559. minx = Inf
  4560. miny = Inf
  4561. maxx = -Inf
  4562. maxy = -Inf
  4563. for k in obj:
  4564. if type(k) is dict:
  4565. for key in k:
  4566. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  4567. minx = min(minx, minx_)
  4568. miny = min(miny, miny_)
  4569. maxx = max(maxx, maxx_)
  4570. maxy = max(maxy, maxy_)
  4571. else:
  4572. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  4573. minx = min(minx, minx_)
  4574. miny = min(miny, miny_)
  4575. maxx = max(maxx, maxx_)
  4576. maxy = max(maxy, maxy_)
  4577. return minx, miny, maxx, maxy
  4578. else:
  4579. # it's a Shapely object, return it's bounds
  4580. return obj.bounds
  4581. if offset != 0.0:
  4582. offset_for_use = offset
  4583. if offset <0:
  4584. a, b, c, d = bounds_rec(geometry.solid_geometry)
  4585. # if the offset is less than half of the total length or less than half of the total width of the
  4586. # solid geometry it's obvious we can't do the offset
  4587. if -offset > ((c - a) / 2) or -offset > ((d - b) / 2):
  4588. self.app.inform.emit(_tr("[ERROR_NOTCL]The Tool Offset value is too negative to use "
  4589. "for the current_geometry.\n"
  4590. "Raise the value (in module) and try again."))
  4591. return 'fail'
  4592. # hack: make offset smaller by 0.0000000001 which is insignificant difference but allow the job
  4593. # to continue
  4594. elif -offset == ((c - a) / 2) or -offset == ((d - b) / 2):
  4595. offset_for_use = offset - 0.0000000001
  4596. for it in geometry.solid_geometry:
  4597. # if the geometry is a closed shape then create a Polygon out of it
  4598. if isinstance(it, LineString):
  4599. c = it.coords
  4600. if c[0] == c[-1]:
  4601. it = Polygon(it)
  4602. temp_solid_geometry.append(it.buffer(offset_for_use, join_style=2))
  4603. else:
  4604. temp_solid_geometry = geometry.solid_geometry
  4605. ## Flatten the geometry. Only linear elements (no polygons) remain.
  4606. flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
  4607. log.debug("%d paths" % len(flat_geometry))
  4608. self.tooldia = float(tooldia) if tooldia else None
  4609. self.z_cut = float(z_cut) if z_cut else None
  4610. self.z_move = float(z_move) if z_move else None
  4611. self.feedrate = float(feedrate) if feedrate else None
  4612. self.z_feedrate = float(feedrate_z) if feedrate_z else None
  4613. self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else None
  4614. self.spindlespeed = int(spindlespeed) if spindlespeed else None
  4615. self.dwell = dwell
  4616. self.dwelltime = float(dwelltime) if dwelltime else None
  4617. self.startz = float(startz) if startz else None
  4618. self.z_end = float(endz) if endz else None
  4619. self.z_depthpercut = float(depthpercut) if depthpercut else None
  4620. self.multidepth = multidepth
  4621. self.z_toolchange = float(toolchangez) if toolchangez else None
  4622. try:
  4623. if toolchangexy == '':
  4624. self.xy_toolchange = None
  4625. else:
  4626. self.xy_toolchange = [float(eval(a)) for a in toolchangexy.split(",")]
  4627. if len(self.xy_toolchange) < 2:
  4628. self.app.inform.emit(_tr("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
  4629. "in the format (x, y) \nbut now there is only one value, not two. "))
  4630. return 'fail'
  4631. except Exception as e:
  4632. log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))
  4633. pass
  4634. self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
  4635. self.f_plunge = self.app.defaults["geometry_f_plunge"]
  4636. if self.z_cut is None:
  4637. self.app.inform.emit(_tr("[ERROR_NOTCL] Cut_Z parameter is None or zero. Most likely a bad combinations of "
  4638. "other parameters."))
  4639. return 'fail'
  4640. if self.z_cut > 0:
  4641. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter has positive value. "
  4642. "It is the depth value to cut into material.\n"
  4643. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  4644. "therefore the app will convert the value to negative."
  4645. "Check the resulting CNC code (Gcode etc)."))
  4646. self.z_cut = -self.z_cut
  4647. elif self.z_cut == 0:
  4648. self.app.inform.emit(_tr("[WARNING] The Cut Z parameter is zero. "
  4649. "There will be no cut, skipping %s file") % geometry.options['name'])
  4650. return 'fail'
  4651. if self.z_move is None:
  4652. self.app.inform.emit(_tr("[ERROR_NOTCL] Travel Z parameter is None or zero."))
  4653. return 'fail'
  4654. if self.z_move < 0:
  4655. self.app.inform.emit(_tr("[WARNING] The Travel Z parameter has negative value. "
  4656. "It is the height value to travel between cuts.\n"
  4657. "The Z Travel parameter needs to have a positive value, assuming it is a typo "
  4658. "therefore the app will convert the value to positive."
  4659. "Check the resulting CNC code (Gcode etc)."))
  4660. self.z_move = -self.z_move
  4661. elif self.z_move == 0:
  4662. self.app.inform.emit(_tr("[WARNING] The Z Travel parameter is zero. "
  4663. "This is dangerous, skipping %s file") % self.options['name'])
  4664. return 'fail'
  4665. ## Index first and last points in paths
  4666. # What points to index.
  4667. def get_pts(o):
  4668. return [o.coords[0], o.coords[-1]]
  4669. # Create the indexed storage.
  4670. storage = FlatCAMRTreeStorage()
  4671. storage.get_points = get_pts
  4672. # Store the geometry
  4673. log.debug("Indexing geometry before generating G-Code...")
  4674. for shape in flat_geometry:
  4675. if shape is not None: # TODO: This shouldn't have happened.
  4676. storage.insert(shape)
  4677. # self.input_geometry_bounds = geometry.bounds()
  4678. if not append:
  4679. self.gcode = ""
  4680. # tell postprocessor the number of tool (for toolchange)
  4681. self.tool = tool_no
  4682. # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
  4683. # given under the name 'toolC'
  4684. self.postdata['toolC'] = self.tooldia
  4685. # Initial G-Code
  4686. self.pp_geometry = self.app.postprocessors[self.pp_geometry_name]
  4687. p = self.pp_geometry
  4688. self.oldx = 0.0
  4689. self.oldy = 0.0
  4690. self.gcode = self.doformat(p.start_code)
  4691. self.gcode += self.doformat(p.feedrate_code) # sets the feed rate
  4692. if toolchange is False:
  4693. self.gcode += self.doformat(p.lift_code, x=self.oldx , y=self.oldy ) # Move (up) to travel height
  4694. self.gcode += self.doformat(p.startz_code, x=self.oldx , y=self.oldy )
  4695. if toolchange:
  4696. # if "line_xyz" in self.pp_geometry_name:
  4697. # self.gcode += self.doformat(p.toolchange_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4698. # else:
  4699. # self.gcode += self.doformat(p.toolchange_code)
  4700. self.gcode += self.doformat(p.toolchange_code)
  4701. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4702. if self.dwell is True:
  4703. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4704. else:
  4705. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4706. if self.dwell is True:
  4707. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4708. ## Iterate over geometry paths getting the nearest each time.
  4709. log.debug("Starting G-Code...")
  4710. path_count = 0
  4711. current_pt = (0, 0)
  4712. pt, geo = storage.nearest(current_pt)
  4713. try:
  4714. while True:
  4715. path_count += 1
  4716. # Remove before modifying, otherwise deletion will fail.
  4717. storage.remove(geo)
  4718. # If last point in geometry is the nearest but prefer the first one if last point == first point
  4719. # then reverse coordinates.
  4720. if pt != geo.coords[0] and pt == geo.coords[-1]:
  4721. geo.coords = list(geo.coords)[::-1]
  4722. #---------- Single depth/pass --------
  4723. if not multidepth:
  4724. self.gcode += self.create_gcode_single_pass(geo, extracut, tolerance)
  4725. #--------- Multi-pass ---------
  4726. else:
  4727. self.gcode += self.create_gcode_multi_pass(geo, extracut, tolerance,
  4728. postproc=p, current_point=current_pt)
  4729. current_pt = geo.coords[-1]
  4730. pt, geo = storage.nearest(current_pt) # Next
  4731. except StopIteration: # Nothing found in storage.
  4732. pass
  4733. log.debug("Finishing G-Code... %s paths traced." % path_count)
  4734. # Finish
  4735. self.gcode += self.doformat(p.spindle_stop_code)
  4736. self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
  4737. self.gcode += self.doformat(p.end_code, x=0, y=0)
  4738. return self.gcode
  4739. def generate_gcode_from_solderpaste_geo(self, **kwargs):
  4740. """
  4741. Algorithm to generate from multitool Geometry.
  4742. Algorithm description:
  4743. ----------------------
  4744. Uses RTree to find the nearest path to follow.
  4745. :return: Gcode string
  4746. """
  4747. log.debug("Generate_from_solderpaste_geometry()")
  4748. ## Index first and last points in paths
  4749. # What points to index.
  4750. def get_pts(o):
  4751. return [o.coords[0], o.coords[-1]]
  4752. self.gcode = ""
  4753. if not kwargs:
  4754. log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.")
  4755. self.app.inform.emit(_tr("[ERROR_NOTCL] There is no tool data in the SolderPaste geometry."))
  4756. # this is the tool diameter, it is used as such to accommodate the postprocessor who need the tool diameter
  4757. # given under the name 'toolC'
  4758. self.postdata['z_start'] = kwargs['data']['tools_solderpaste_z_start']
  4759. self.postdata['z_dispense'] = kwargs['data']['tools_solderpaste_z_dispense']
  4760. self.postdata['z_stop'] = kwargs['data']['tools_solderpaste_z_stop']
  4761. self.postdata['z_travel'] = kwargs['data']['tools_solderpaste_z_travel']
  4762. self.postdata['z_toolchange'] = kwargs['data']['tools_solderpaste_z_toolchange']
  4763. self.postdata['xy_toolchange'] = kwargs['data']['tools_solderpaste_xy_toolchange']
  4764. self.postdata['frxy'] = kwargs['data']['tools_solderpaste_frxy']
  4765. self.postdata['frz'] = kwargs['data']['tools_solderpaste_frz']
  4766. self.postdata['frz_dispense'] = kwargs['data']['tools_solderpaste_frz_dispense']
  4767. self.postdata['speedfwd'] = kwargs['data']['tools_solderpaste_speedfwd']
  4768. self.postdata['dwellfwd'] = kwargs['data']['tools_solderpaste_dwellfwd']
  4769. self.postdata['speedrev'] = kwargs['data']['tools_solderpaste_speedrev']
  4770. self.postdata['dwellrev'] = kwargs['data']['tools_solderpaste_dwellrev']
  4771. self.postdata['pp_solderpaste_name'] = kwargs['data']['tools_solderpaste_pp']
  4772. self.postdata['toolC'] = kwargs['tooldia']
  4773. self.pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] \
  4774. else self.app.defaults['tools_solderpaste_pp']
  4775. p = self.app.postprocessors[self.pp_solderpaste_name]
  4776. ## Flatten the geometry. Only linear elements (no polygons) remain.
  4777. flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True)
  4778. log.debug("%d paths" % len(flat_geometry))
  4779. # Create the indexed storage.
  4780. storage = FlatCAMRTreeStorage()
  4781. storage.get_points = get_pts
  4782. # Store the geometry
  4783. log.debug("Indexing geometry before generating G-Code...")
  4784. for shape in flat_geometry:
  4785. if shape is not None:
  4786. storage.insert(shape)
  4787. # Initial G-Code
  4788. self.gcode = self.doformat(p.start_code)
  4789. self.gcode += self.doformat(p.spindle_off_code)
  4790. self.gcode += self.doformat(p.toolchange_code)
  4791. ## Iterate over geometry paths getting the nearest each time.
  4792. log.debug("Starting SolderPaste G-Code...")
  4793. path_count = 0
  4794. current_pt = (0, 0)
  4795. pt, geo = storage.nearest(current_pt)
  4796. try:
  4797. while True:
  4798. path_count += 1
  4799. # Remove before modifying, otherwise deletion will fail.
  4800. storage.remove(geo)
  4801. # If last point in geometry is the nearest but prefer the first one if last point == first point
  4802. # then reverse coordinates.
  4803. if pt != geo.coords[0] and pt == geo.coords[-1]:
  4804. geo.coords = list(geo.coords)[::-1]
  4805. self.gcode += self.create_soldepaste_gcode(geo, p=p)
  4806. current_pt = geo.coords[-1]
  4807. pt, geo = storage.nearest(current_pt) # Next
  4808. except StopIteration: # Nothing found in storage.
  4809. pass
  4810. log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count)
  4811. # Finish
  4812. self.gcode += self.doformat(p.lift_code)
  4813. self.gcode += self.doformat(p.end_code)
  4814. return self.gcode
  4815. def create_soldepaste_gcode(self, geometry, p):
  4816. gcode = ''
  4817. path = geometry.coords
  4818. if type(geometry) == LineString or type(geometry) == LinearRing:
  4819. # Move fast to 1st point
  4820. gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point
  4821. # Move down to cutting depth
  4822. gcode += self.doformat(p.z_feedrate_code)
  4823. gcode += self.doformat(p.down_z_start_code)
  4824. gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
  4825. gcode += self.doformat(p.dwell_fwd_code)
  4826. gcode += self.doformat(p.z_feedrate_dispense_code)
  4827. gcode += self.doformat(p.lift_z_dispense_code)
  4828. gcode += self.doformat(p.feedrate_xy_code)
  4829. # Cutting...
  4830. for pt in path[1:]:
  4831. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1]) # Linear motion to point
  4832. # Up to travelling height.
  4833. gcode += self.doformat(p.spindle_off_code) # Stop dispensing
  4834. gcode += self.doformat(p.spindle_rev_code)
  4835. gcode += self.doformat(p.down_z_stop_code)
  4836. gcode += self.doformat(p.spindle_off_code)
  4837. gcode += self.doformat(p.dwell_rev_code)
  4838. gcode += self.doformat(p.z_feedrate_code)
  4839. gcode += self.doformat(p.lift_code)
  4840. elif type(geometry) == Point:
  4841. gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1]) # Move to first point
  4842. gcode += self.doformat(p.z_feedrate_dispense_code)
  4843. gcode += self.doformat(p.down_z_start_code)
  4844. gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
  4845. gcode += self.doformat(p.dwell_fwd_code)
  4846. gcode += self.doformat(p.lift_z_dispense_code)
  4847. gcode += self.doformat(p.spindle_off_code) # Stop dispensing
  4848. gcode += self.doformat(p.spindle_rev_code)
  4849. gcode += self.doformat(p.spindle_off_code)
  4850. gcode += self.doformat(p.down_z_stop_code)
  4851. gcode += self.doformat(p.dwell_rev_code)
  4852. gcode += self.doformat(p.z_feedrate_code)
  4853. gcode += self.doformat(p.lift_code)
  4854. return gcode
  4855. def create_gcode_single_pass(self, geometry, extracut, tolerance):
  4856. # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
  4857. gcode_single_pass = ''
  4858. if type(geometry) == LineString or type(geometry) == LinearRing:
  4859. if extracut is False:
  4860. gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance)
  4861. else:
  4862. if geometry.is_ring:
  4863. gcode_single_pass = self.linear2gcode_extra(geometry, tolerance=tolerance)
  4864. else:
  4865. gcode_single_pass = self.linear2gcode(geometry, tolerance=tolerance)
  4866. elif type(geometry) == Point:
  4867. gcode_single_pass = self.point2gcode(geometry)
  4868. else:
  4869. log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
  4870. return
  4871. return gcode_single_pass
  4872. def create_gcode_multi_pass(self, geometry, extracut, tolerance, postproc, current_point):
  4873. gcode_multi_pass = ''
  4874. if isinstance(self.z_cut, Decimal):
  4875. z_cut = self.z_cut
  4876. else:
  4877. z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001'))
  4878. if self.z_depthpercut is None:
  4879. self.z_depthpercut = z_cut
  4880. elif not isinstance(self.z_depthpercut, Decimal):
  4881. self.z_depthpercut = Decimal(self.z_depthpercut).quantize(Decimal('0.000000001'))
  4882. depth = 0
  4883. reverse = False
  4884. while depth > z_cut:
  4885. # Increase depth. Limit to z_cut.
  4886. depth -= self.z_depthpercut
  4887. if depth < z_cut:
  4888. depth = z_cut
  4889. # Cut at specific depth and do not lift the tool.
  4890. # Note: linear2gcode() will use G00 to move to the first point in the path, but it should be already
  4891. # at the first point if the tool is down (in the material). So, an extra G00 should show up but
  4892. # is inconsequential.
  4893. if type(geometry) == LineString or type(geometry) == LinearRing:
  4894. if extracut is False:
  4895. gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
  4896. else:
  4897. if geometry.is_ring:
  4898. gcode_multi_pass += self.linear2gcode_extra(geometry, tolerance=tolerance, z_cut=depth, up=False)
  4899. else:
  4900. gcode_multi_pass += self.linear2gcode(geometry, tolerance=tolerance, z_cut=depth, up=False)
  4901. # Ignore multi-pass for points.
  4902. elif type(geometry) == Point:
  4903. gcode_multi_pass += self.point2gcode(geometry)
  4904. break # Ignoring ...
  4905. else:
  4906. log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
  4907. # Reverse coordinates if not a loop so we can continue cutting without returning to the beginning.
  4908. if type(geometry) == LineString:
  4909. geometry.coords = list(geometry.coords)[::-1]
  4910. reverse = True
  4911. # If geometry is reversed, revert.
  4912. if reverse:
  4913. if type(geometry) == LineString:
  4914. geometry.coords = list(geometry.coords)[::-1]
  4915. # Lift the tool
  4916. gcode_multi_pass += self.doformat(postproc.lift_code, x=current_point[0], y=current_point[1])
  4917. return gcode_multi_pass
  4918. def codes_split(self, gline):
  4919. """
  4920. Parses a line of G-Code such as "G01 X1234 Y987" into
  4921. a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
  4922. :param gline: G-Code line string
  4923. :return: Dictionary with parsed line.
  4924. """
  4925. command = {}
  4926. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  4927. match_z = re.search(r"^Z(\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?)*;$", gline)
  4928. if match_z:
  4929. command['G'] = 0
  4930. command['X'] = float(match_z.group(1).replace(" ", "")) * 0.025
  4931. command['Y'] = float(match_z.group(2).replace(" ", "")) * 0.025
  4932. command['Z'] = float(match_z.group(3).replace(" ", "")) * 0.025
  4933. elif 'hpgl' in self.pp_excellon_name or 'hpgl' in self.pp_geometry_name:
  4934. match_pa = re.search(r"^PA(\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?)*;$", gline)
  4935. if match_pa:
  4936. command['G'] = 0
  4937. command['X'] = float(match_pa.group(1).replace(" ", ""))
  4938. command['Y'] = float(match_pa.group(2).replace(" ", ""))
  4939. match_pen = re.search(r"^(P[U|D])", gline)
  4940. if match_pen:
  4941. if match_pen.group(1) == 'PU':
  4942. # the value does not matter, only that it is positive so the gcode_parse() know it is > 0,
  4943. # therefore the move is of kind T (travel)
  4944. command['Z'] = 1
  4945. else:
  4946. command['Z'] = 0
  4947. elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name or \
  4948. (self.pp_solderpaste_name is not None and 'Paste' in self.pp_solderpaste_name):
  4949. match_lsr = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
  4950. if match_lsr:
  4951. command['X'] = float(match_lsr.group(1).replace(" ", ""))
  4952. command['Y'] = float(match_lsr.group(2).replace(" ", ""))
  4953. match_lsr_pos = re.search(r"^(M0[3|5])", gline)
  4954. if match_lsr_pos:
  4955. if match_lsr_pos.group(1) == 'M05':
  4956. # the value does not matter, only that it is positive so the gcode_parse() know it is > 0,
  4957. # therefore the move is of kind T (travel)
  4958. command['Z'] = 1
  4959. else:
  4960. command['Z'] = 0
  4961. elif self.pp_solderpaste_name is not None:
  4962. if 'Paste' in self.pp_solderpaste_name:
  4963. match_paste = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
  4964. if match_paste:
  4965. command['X'] = float(match_paste.group(1).replace(" ", ""))
  4966. command['Y'] = float(match_paste.group(2).replace(" ", ""))
  4967. else:
  4968. match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
  4969. while match:
  4970. command[match.group(1)] = float(match.group(2).replace(" ", ""))
  4971. gline = gline[match.end():]
  4972. match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
  4973. return command
  4974. def gcode_parse(self):
  4975. """
  4976. G-Code parser (from self.gcode). Generates dictionary with
  4977. single-segment LineString's and "kind" indicating cut or travel,
  4978. fast or feedrate speed.
  4979. """
  4980. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  4981. # Results go here
  4982. geometry = []
  4983. # Last known instruction
  4984. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  4985. # Current path: temporary storage until tool is
  4986. # lifted or lowered.
  4987. if self.toolchange_xy_type == "excellon":
  4988. if self.app.defaults["excellon_toolchangexy"] == '':
  4989. pos_xy = [0, 0]
  4990. else:
  4991. pos_xy = [float(eval(a)) for a in self.app.defaults["excellon_toolchangexy"].split(",")]
  4992. else:
  4993. if self.app.defaults["geometry_toolchangexy"] == '':
  4994. pos_xy = [0, 0]
  4995. else:
  4996. pos_xy = [float(eval(a)) for a in self.app.defaults["geometry_toolchangexy"].split(",")]
  4997. path = [pos_xy]
  4998. # path = [(0, 0)]
  4999. # Process every instruction
  5000. for line in StringIO(self.gcode):
  5001. if '%MO' in line or '%' in line:
  5002. return "fail"
  5003. gobj = self.codes_split(line)
  5004. ## Units
  5005. if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
  5006. self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
  5007. continue
  5008. ## Changing height
  5009. if 'Z' in gobj:
  5010. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  5011. pass
  5012. elif 'hpgl' in self.pp_excellon_name or 'hpgl' in self.pp_geometry_name:
  5013. pass
  5014. elif 'grbl_laser' in self.pp_excellon_name or 'grbl_laser' in self.pp_geometry_name:
  5015. pass
  5016. elif ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  5017. if self.pp_geometry_name == 'line_xyz' or self.pp_excellon_name == 'line_xyz':
  5018. pass
  5019. else:
  5020. log.warning("Non-orthogonal motion: From %s" % str(current))
  5021. log.warning(" To: %s" % str(gobj))
  5022. current['Z'] = gobj['Z']
  5023. # Store the path into geometry and reset path
  5024. if len(path) > 1:
  5025. geometry.append({"geom": LineString(path),
  5026. "kind": kind})
  5027. path = [path[-1]] # Start with the last point of last path.
  5028. if 'G' in gobj:
  5029. current['G'] = int(gobj['G'])
  5030. if 'X' in gobj or 'Y' in gobj:
  5031. # TODO: I think there is a problem here, current['X] (and the rest of current[...] are not initialized
  5032. if 'X' in gobj:
  5033. x = gobj['X']
  5034. # current['X'] = x
  5035. else:
  5036. x = current['X']
  5037. if 'Y' in gobj:
  5038. y = gobj['Y']
  5039. else:
  5040. y = current['Y']
  5041. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5042. if current['Z'] > 0:
  5043. kind[0] = 'T'
  5044. if current['G'] > 0:
  5045. kind[1] = 'S'
  5046. if current['G'] in [0, 1]: # line
  5047. path.append((x, y))
  5048. arcdir = [None, None, "cw", "ccw"]
  5049. if current['G'] in [2, 3]: # arc
  5050. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  5051. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  5052. start = arctan2(-gobj['J'], -gobj['I'])
  5053. stop = arctan2(-center[1] + y, -center[0] + x)
  5054. path += arc(center, radius, start, stop,
  5055. arcdir[current['G']],
  5056. int(self.steps_per_circle / 4))
  5057. # Update current instruction
  5058. for code in gobj:
  5059. current[code] = gobj[code]
  5060. # There might not be a change in height at the
  5061. # end, therefore, see here too if there is
  5062. # a final path.
  5063. if len(path) > 1:
  5064. geometry.append({"geom": LineString(path),
  5065. "kind": kind})
  5066. self.gcode_parsed = geometry
  5067. return geometry
  5068. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  5069. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  5070. # alpha={"T": 0.3, "C": 1.0}):
  5071. # """
  5072. # Creates a Matplotlib figure with a plot of the
  5073. # G-code job.
  5074. # """
  5075. # if tooldia is None:
  5076. # tooldia = self.tooldia
  5077. #
  5078. # fig = Figure(dpi=dpi)
  5079. # ax = fig.add_subplot(111)
  5080. # ax.set_aspect(1)
  5081. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  5082. # ax.set_xlim(xmin-margin, xmax+margin)
  5083. # ax.set_ylim(ymin-margin, ymax+margin)
  5084. #
  5085. # if tooldia == 0:
  5086. # for geo in self.gcode_parsed:
  5087. # linespec = '--'
  5088. # linecolor = color[geo['kind'][0]][1]
  5089. # if geo['kind'][0] == 'C':
  5090. # linespec = 'k-'
  5091. # x, y = geo['geom'].coords.xy
  5092. # ax.plot(x, y, linespec, color=linecolor)
  5093. # else:
  5094. # for geo in self.gcode_parsed:
  5095. # poly = geo['geom'].buffer(tooldia/2.0)
  5096. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  5097. # edgecolor=color[geo['kind'][0]][1],
  5098. # alpha=alpha[geo['kind'][0]], zorder=2)
  5099. # ax.add_patch(patch)
  5100. #
  5101. # return fig
  5102. def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None,
  5103. color={"T": ["#F0E24D4C", "#B5AB3A4C"], "C": ["#5E6CFFFF", "#4650BDFF"]},
  5104. alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False, kind='all'):
  5105. """
  5106. Plots the G-code job onto the given axes.
  5107. :param tooldia: Tool diameter.
  5108. :param dpi: Not used!
  5109. :param margin: Not used!
  5110. :param color: Color specification.
  5111. :param alpha: Transparency specification.
  5112. :param tool_tolerance: Tolerance when drawing the toolshape.
  5113. :return: None
  5114. """
  5115. gcode_parsed = gcode_parsed if gcode_parsed else self.gcode_parsed
  5116. path_num = 0
  5117. if tooldia is None:
  5118. tooldia = self.tooldia
  5119. if tooldia == 0:
  5120. for geo in gcode_parsed:
  5121. if kind == 'all':
  5122. obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
  5123. elif kind == 'travel':
  5124. if geo['kind'][0] == 'T':
  5125. obj.add_shape(shape=geo['geom'], color=color['T'][1], visible=visible)
  5126. elif kind == 'cut':
  5127. if geo['kind'][0] == 'C':
  5128. obj.add_shape(shape=geo['geom'], color=color['C'][1], visible=visible)
  5129. else:
  5130. text = []
  5131. pos = []
  5132. for geo in gcode_parsed:
  5133. path_num += 1
  5134. text.append(str(path_num))
  5135. pos.append(geo['geom'].coords[0])
  5136. poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
  5137. if kind == 'all':
  5138. obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
  5139. visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
  5140. elif kind == 'travel':
  5141. if geo['kind'][0] == 'T':
  5142. obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0],
  5143. visible=visible, layer=2)
  5144. elif kind == 'cut':
  5145. if geo['kind'][0] == 'C':
  5146. obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
  5147. visible=visible, layer=1)
  5148. obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'])
  5149. def create_geometry(self):
  5150. # TODO: This takes forever. Too much data?
  5151. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  5152. return self.solid_geometry
  5153. # code snippet added by Lei Zheng in a rejected pull request on FlatCAM https://bitbucket.org/realthunder/
  5154. def segment(self, coords):
  5155. """
  5156. break long linear lines to make it more auto level friendly
  5157. """
  5158. if len(coords) < 2 or self.segx <= 0 and self.segy <= 0:
  5159. return list(coords)
  5160. path = [coords[0]]
  5161. # break the line in either x or y dimension only
  5162. def linebreak_single(line, dim, dmax):
  5163. if dmax <= 0:
  5164. return None
  5165. if line[1][dim] > line[0][dim]:
  5166. sign = 1.0
  5167. d = line[1][dim] - line[0][dim]
  5168. else:
  5169. sign = -1.0
  5170. d = line[0][dim] - line[1][dim]
  5171. if d > dmax:
  5172. # make sure we don't make any new lines too short
  5173. if d > dmax * 2:
  5174. dd = dmax
  5175. else:
  5176. dd = d / 2
  5177. other = dim ^ 1
  5178. return (line[0][dim] + dd * sign, line[0][other] + \
  5179. dd * (line[1][other] - line[0][other]) / d)
  5180. return None
  5181. # recursively breaks down a given line until it is within the
  5182. # required step size
  5183. def linebreak(line):
  5184. pt_new = linebreak_single(line, 0, self.segx)
  5185. if pt_new is None:
  5186. pt_new2 = linebreak_single(line, 1, self.segy)
  5187. else:
  5188. pt_new2 = linebreak_single((line[0], pt_new), 1, self.segy)
  5189. if pt_new2 is not None:
  5190. pt_new = pt_new2[::-1]
  5191. if pt_new is None:
  5192. path.append(line[1])
  5193. else:
  5194. path.append(pt_new)
  5195. linebreak((pt_new, line[1]))
  5196. for pt in coords[1:]:
  5197. linebreak((path[-1], pt))
  5198. return path
  5199. def linear2gcode(self, linear, tolerance=0, down=True, up=True,
  5200. z_cut=None, z_move=None, zdownrate=None,
  5201. feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False):
  5202. """
  5203. Generates G-code to cut along the linear feature.
  5204. :param linear: The path to cut along.
  5205. :type: Shapely.LinearRing or Shapely.Linear String
  5206. :param tolerance: All points in the simplified object will be within the
  5207. tolerance distance of the original geometry.
  5208. :type tolerance: float
  5209. :param feedrate: speed for cut on X - Y plane
  5210. :param feedrate_z: speed for cut on Z plane
  5211. :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
  5212. :return: G-code to cut along the linear feature.
  5213. :rtype: str
  5214. """
  5215. if z_cut is None:
  5216. z_cut = self.z_cut
  5217. if z_move is None:
  5218. z_move = self.z_move
  5219. #
  5220. # if zdownrate is None:
  5221. # zdownrate = self.zdownrate
  5222. if feedrate is None:
  5223. feedrate = self.feedrate
  5224. if feedrate_z is None:
  5225. feedrate_z = self.z_feedrate
  5226. if feedrate_rapid is None:
  5227. feedrate_rapid = self.feedrate_rapid
  5228. # Simplify paths?
  5229. if tolerance > 0:
  5230. target_linear = linear.simplify(tolerance)
  5231. else:
  5232. target_linear = linear
  5233. gcode = ""
  5234. # path = list(target_linear.coords)
  5235. path = self.segment(target_linear.coords)
  5236. p = self.pp_geometry
  5237. # Move fast to 1st point
  5238. if not cont:
  5239. gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point
  5240. # Move down to cutting depth
  5241. if down:
  5242. # Different feedrate for vertical cut?
  5243. gcode += self.doformat(p.z_feedrate_code)
  5244. # gcode += self.doformat(p.feedrate_code)
  5245. gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut)
  5246. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  5247. # Cutting...
  5248. for pt in path[1:]:
  5249. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1], z=z_cut) # Linear motion to point
  5250. # Up to travelling height.
  5251. if up:
  5252. gcode += self.doformat(p.lift_code, x=pt[0], y=pt[1], z_move=z_move) # Stop cutting
  5253. return gcode
  5254. def linear2gcode_extra(self, linear, tolerance=0, down=True, up=True,
  5255. z_cut=None, z_move=None, zdownrate=None,
  5256. feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False):
  5257. """
  5258. Generates G-code to cut along the linear feature.
  5259. :param linear: The path to cut along.
  5260. :type: Shapely.LinearRing or Shapely.Linear String
  5261. :param tolerance: All points in the simplified object will be within the
  5262. tolerance distance of the original geometry.
  5263. :type tolerance: float
  5264. :param feedrate: speed for cut on X - Y plane
  5265. :param feedrate_z: speed for cut on Z plane
  5266. :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
  5267. :return: G-code to cut along the linear feature.
  5268. :rtype: str
  5269. """
  5270. if z_cut is None:
  5271. z_cut = self.z_cut
  5272. if z_move is None:
  5273. z_move = self.z_move
  5274. #
  5275. # if zdownrate is None:
  5276. # zdownrate = self.zdownrate
  5277. if feedrate is None:
  5278. feedrate = self.feedrate
  5279. if feedrate_z is None:
  5280. feedrate_z = self.z_feedrate
  5281. if feedrate_rapid is None:
  5282. feedrate_rapid = self.feedrate_rapid
  5283. # Simplify paths?
  5284. if tolerance > 0:
  5285. target_linear = linear.simplify(tolerance)
  5286. else:
  5287. target_linear = linear
  5288. gcode = ""
  5289. path = list(target_linear.coords)
  5290. p = self.pp_geometry
  5291. # Move fast to 1st point
  5292. if not cont:
  5293. gcode += self.doformat(p.rapid_code, x=path[0][0], y=path[0][1]) # Move to first point
  5294. # Move down to cutting depth
  5295. if down:
  5296. # Different feedrate for vertical cut?
  5297. if self.z_feedrate is not None:
  5298. gcode += self.doformat(p.z_feedrate_code)
  5299. # gcode += self.doformat(p.feedrate_code)
  5300. gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut)
  5301. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  5302. else:
  5303. gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut=z_cut) # Start cutting
  5304. # Cutting...
  5305. for pt in path[1:]:
  5306. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1], z=z_cut) # Linear motion to point
  5307. # this line is added to create an extra cut over the first point in patch
  5308. # to make sure that we remove the copper leftovers
  5309. gcode += self.doformat(p.linear_code, x=path[1][0], y=path[1][1]) # Linear motion to the 1st point in the cut path
  5310. # Up to travelling height.
  5311. if up:
  5312. gcode += self.doformat(p.lift_code, x=path[1][0], y=path[1][1], z_move=z_move) # Stop cutting
  5313. return gcode
  5314. def point2gcode(self, point):
  5315. gcode = ""
  5316. path = list(point.coords)
  5317. p = self.pp_geometry
  5318. gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1]) # Move to first point
  5319. if self.z_feedrate is not None:
  5320. gcode += self.doformat(p.z_feedrate_code)
  5321. gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut = self.z_cut)
  5322. gcode += self.doformat(p.feedrate_code)
  5323. else:
  5324. gcode += self.doformat(p.down_code, x=path[0][0], y=path[0][1], z_cut = self.z_cut) # Start cutting
  5325. gcode += self.doformat(p.lift_code, x=path[0][0], y=path[0][1]) # Stop cutting
  5326. return gcode
  5327. def export_svg(self, scale_factor=0.00):
  5328. """
  5329. Exports the CNC Job as a SVG Element
  5330. :scale_factor: float
  5331. :return: SVG Element string
  5332. """
  5333. # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
  5334. # If not specified then try and use the tool diameter
  5335. # This way what is on screen will match what is outputed for the svg
  5336. # This is quite a useful feature for svg's used with visicut
  5337. if scale_factor <= 0:
  5338. scale_factor = self.options['tooldia'] / 2
  5339. # If still 0 then default to 0.05
  5340. # This value appears to work for zooming, and getting the output svg line width
  5341. # to match that viewed on screen with FlatCam
  5342. if scale_factor == 0:
  5343. scale_factor = 0.01
  5344. # Separate the list of cuts and travels into 2 distinct lists
  5345. # This way we can add different formatting / colors to both
  5346. cuts = []
  5347. travels = []
  5348. for g in self.gcode_parsed:
  5349. if g['kind'][0] == 'C': cuts.append(g)
  5350. if g['kind'][0] == 'T': travels.append(g)
  5351. # Used to determine the overall board size
  5352. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  5353. # Convert the cuts and travels into single geometry objects we can render as svg xml
  5354. if travels:
  5355. travelsgeom = cascaded_union([geo['geom'] for geo in travels])
  5356. if cuts:
  5357. cutsgeom = cascaded_union([geo['geom'] for geo in cuts])
  5358. # Render the SVG Xml
  5359. # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
  5360. # It's better to have the travels sitting underneath the cuts for visicut
  5361. svg_elem = ""
  5362. if travels:
  5363. svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D")
  5364. if cuts:
  5365. svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF")
  5366. return svg_elem
  5367. def bounds(self):
  5368. """
  5369. Returns coordinates of rectangular bounds
  5370. of geometry: (xmin, ymin, xmax, ymax).
  5371. """
  5372. # fixed issue of getting bounds only for one level lists of objects
  5373. # now it can get bounds for nested lists of objects
  5374. def bounds_rec(obj):
  5375. if type(obj) is list:
  5376. minx = Inf
  5377. miny = Inf
  5378. maxx = -Inf
  5379. maxy = -Inf
  5380. for k in obj:
  5381. if type(k) is dict:
  5382. for key in k:
  5383. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  5384. minx = min(minx, minx_)
  5385. miny = min(miny, miny_)
  5386. maxx = max(maxx, maxx_)
  5387. maxy = max(maxy, maxy_)
  5388. else:
  5389. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  5390. minx = min(minx, minx_)
  5391. miny = min(miny, miny_)
  5392. maxx = max(maxx, maxx_)
  5393. maxy = max(maxy, maxy_)
  5394. return minx, miny, maxx, maxy
  5395. else:
  5396. # it's a Shapely object, return it's bounds
  5397. return obj.bounds
  5398. if self.multitool is False:
  5399. log.debug("CNCJob->bounds()")
  5400. if self.solid_geometry is None:
  5401. log.debug("solid_geometry is None")
  5402. return 0, 0, 0, 0
  5403. bounds_coords = bounds_rec(self.solid_geometry)
  5404. else:
  5405. for k, v in self.cnc_tools.items():
  5406. minx = Inf
  5407. miny = Inf
  5408. maxx = -Inf
  5409. maxy = -Inf
  5410. try:
  5411. for k in v['solid_geometry']:
  5412. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  5413. minx = min(minx, minx_)
  5414. miny = min(miny, miny_)
  5415. maxx = max(maxx, maxx_)
  5416. maxy = max(maxy, maxy_)
  5417. except TypeError:
  5418. minx_, miny_, maxx_, maxy_ = bounds_rec(v['solid_geometry'])
  5419. minx = min(minx, minx_)
  5420. miny = min(miny, miny_)
  5421. maxx = max(maxx, maxx_)
  5422. maxy = max(maxy, maxy_)
  5423. bounds_coords = minx, miny, maxx, maxy
  5424. return bounds_coords
  5425. # TODO This function should be replaced at some point with a "real" function. Until then it's an ugly hack ...
  5426. def scale(self, xfactor, yfactor=None, point=None):
  5427. """
  5428. Scales all the geometry on the XY plane in the object by the
  5429. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  5430. not altered.
  5431. :param factor: Number by which to scale the object.
  5432. :type factor: float
  5433. :param point: the (x,y) coords for the point of origin of scale
  5434. :type tuple of floats
  5435. :return: None
  5436. :rtype: None
  5437. """
  5438. if yfactor is None:
  5439. yfactor = xfactor
  5440. if point is None:
  5441. px = 0
  5442. py = 0
  5443. else:
  5444. px, py = point
  5445. def scale_g(g):
  5446. """
  5447. :param g: 'g' parameter it's a gcode string
  5448. :return: scaled gcode string
  5449. """
  5450. temp_gcode = ''
  5451. header_start = False
  5452. header_stop = False
  5453. units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
  5454. lines = StringIO(g)
  5455. for line in lines:
  5456. # this changes the GCODE header ---- UGLY HACK
  5457. if "TOOL DIAMETER" in line or "Feedrate:" in line:
  5458. header_start = True
  5459. if "G20" in line or "G21" in line:
  5460. header_start = False
  5461. header_stop = True
  5462. if header_start is True:
  5463. header_stop = False
  5464. if "in" in line:
  5465. if units == 'MM':
  5466. line = line.replace("in", "mm")
  5467. if "mm" in line:
  5468. if units == 'IN':
  5469. line = line.replace("mm", "in")
  5470. # find any float number in header (even multiple on the same line) and convert it
  5471. numbers_in_header = re.findall(self.g_nr_re, line)
  5472. if numbers_in_header:
  5473. for nr in numbers_in_header:
  5474. new_nr = float(nr) * xfactor
  5475. # replace the updated string
  5476. line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
  5477. )
  5478. # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
  5479. if header_stop is True:
  5480. if "G20" in line:
  5481. if units == 'MM':
  5482. line = line.replace("G20", "G21")
  5483. if "G21" in line:
  5484. if units == 'IN':
  5485. line = line.replace("G21", "G20")
  5486. # find the X group
  5487. match_x = self.g_x_re.search(line)
  5488. if match_x:
  5489. if match_x.group(1) is not None:
  5490. new_x = float(match_x.group(1)[1:]) * xfactor
  5491. # replace the updated string
  5492. line = line.replace(
  5493. match_x.group(1),
  5494. 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
  5495. )
  5496. # find the Y group
  5497. match_y = self.g_y_re.search(line)
  5498. if match_y:
  5499. if match_y.group(1) is not None:
  5500. new_y = float(match_y.group(1)[1:]) * yfactor
  5501. line = line.replace(
  5502. match_y.group(1),
  5503. 'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
  5504. )
  5505. # find the Z group
  5506. match_z = self.g_z_re.search(line)
  5507. if match_z:
  5508. if match_z.group(1) is not None:
  5509. new_z = float(match_z.group(1)[1:]) * xfactor
  5510. line = line.replace(
  5511. match_z.group(1),
  5512. 'Z%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_z)
  5513. )
  5514. # find the F group
  5515. match_f = self.g_f_re.search(line)
  5516. if match_f:
  5517. if match_f.group(1) is not None:
  5518. new_f = float(match_f.group(1)[1:]) * xfactor
  5519. line = line.replace(
  5520. match_f.group(1),
  5521. 'F%.*f' % (self.app.defaults["cncjob_fr_decimals"], new_f)
  5522. )
  5523. # find the T group (tool dia on toolchange)
  5524. match_t = self.g_t_re.search(line)
  5525. if match_t:
  5526. if match_t.group(1) is not None:
  5527. new_t = float(match_t.group(1)[1:]) * xfactor
  5528. line = line.replace(
  5529. match_t.group(1),
  5530. '= %.*f' % (self.app.defaults["cncjob_coords_decimals"], new_t)
  5531. )
  5532. temp_gcode += line
  5533. lines.close()
  5534. header_stop = False
  5535. return temp_gcode
  5536. if self.multitool is False:
  5537. # offset Gcode
  5538. self.gcode = scale_g(self.gcode)
  5539. # offset geometry
  5540. for g in self.gcode_parsed:
  5541. g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
  5542. self.create_geometry()
  5543. else:
  5544. for k, v in self.cnc_tools.items():
  5545. # scale Gcode
  5546. v['gcode'] = scale_g(v['gcode'])
  5547. # scale gcode_parsed
  5548. for g in v['gcode_parsed']:
  5549. g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
  5550. v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
  5551. self.create_geometry()
  5552. def offset(self, vect):
  5553. """
  5554. Offsets all the geometry on the XY plane in the object by the
  5555. given vector.
  5556. Offsets all the GCODE on the XY plane in the object by the
  5557. given vector.
  5558. g_offsetx_re, g_offsety_re, multitool, cnnc_tools are attributes of FlatCAMCNCJob class in camlib
  5559. :param vect: (x, y) offset vector.
  5560. :type vect: tuple
  5561. :return: None
  5562. """
  5563. dx, dy = vect
  5564. def offset_g(g):
  5565. """
  5566. :param g: 'g' parameter it's a gcode string
  5567. :return: offseted gcode string
  5568. """
  5569. temp_gcode = ''
  5570. lines = StringIO(g)
  5571. for line in lines:
  5572. # find the X group
  5573. match_x = self.g_x_re.search(line)
  5574. if match_x:
  5575. if match_x.group(1) is not None:
  5576. # get the coordinate and add X offset
  5577. new_x = float(match_x.group(1)[1:]) + dx
  5578. # replace the updated string
  5579. line = line.replace(
  5580. match_x.group(1),
  5581. 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
  5582. )
  5583. match_y = self.g_y_re.search(line)
  5584. if match_y:
  5585. if match_y.group(1) is not None:
  5586. new_y = float(match_y.group(1)[1:]) + dy
  5587. line = line.replace(
  5588. match_y.group(1),
  5589. 'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
  5590. )
  5591. temp_gcode += line
  5592. lines.close()
  5593. return temp_gcode
  5594. if self.multitool is False:
  5595. # offset Gcode
  5596. self.gcode = offset_g(self.gcode)
  5597. # offset geometry
  5598. for g in self.gcode_parsed:
  5599. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  5600. self.create_geometry()
  5601. else:
  5602. for k, v in self.cnc_tools.items():
  5603. # offset Gcode
  5604. v['gcode'] = offset_g(v['gcode'])
  5605. # offset gcode_parsed
  5606. for g in v['gcode_parsed']:
  5607. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  5608. v['solid_geometry'] = cascaded_union([geo['geom'] for geo in v['gcode_parsed']])
  5609. def mirror(self, axis, point):
  5610. """
  5611. Mirror the geometrys of an object by an given axis around the coordinates of the 'point'
  5612. :param angle:
  5613. :param point: tupple of coordinates (x,y)
  5614. :return:
  5615. """
  5616. px, py = point
  5617. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  5618. for g in self.gcode_parsed:
  5619. g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py))
  5620. self.create_geometry()
  5621. def skew(self, angle_x, angle_y, point):
  5622. """
  5623. Shear/Skew the geometries of an object by angles along x and y dimensions.
  5624. Parameters
  5625. ----------
  5626. angle_x, angle_y : float, float
  5627. The shear angle(s) for the x and y axes respectively. These can be
  5628. specified in either degrees (default) or radians by setting
  5629. use_radians=True.
  5630. point: tupple of coordinates (x,y)
  5631. See shapely manual for more information:
  5632. http://toblerity.org/shapely/manual.html#affine-transformations
  5633. """
  5634. px, py = point
  5635. for g in self.gcode_parsed:
  5636. g['geom'] = affinity.skew(g['geom'], angle_x, angle_y,
  5637. origin=(px, py))
  5638. self.create_geometry()
  5639. def rotate(self, angle, point):
  5640. """
  5641. Rotate the geometrys of an object by an given angle around the coordinates of the 'point'
  5642. :param angle:
  5643. :param point: tupple of coordinates (x,y)
  5644. :return:
  5645. """
  5646. px, py = point
  5647. for g in self.gcode_parsed:
  5648. g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py))
  5649. self.create_geometry()
  5650. def get_bounds(geometry_list):
  5651. xmin = Inf
  5652. ymin = Inf
  5653. xmax = -Inf
  5654. ymax = -Inf
  5655. #print "Getting bounds of:", str(geometry_set)
  5656. for gs in geometry_list:
  5657. try:
  5658. gxmin, gymin, gxmax, gymax = gs.bounds()
  5659. xmin = min([xmin, gxmin])
  5660. ymin = min([ymin, gymin])
  5661. xmax = max([xmax, gxmax])
  5662. ymax = max([ymax, gymax])
  5663. except:
  5664. log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.")
  5665. return [xmin, ymin, xmax, ymax]
  5666. def arc(center, radius, start, stop, direction, steps_per_circ):
  5667. """
  5668. Creates a list of point along the specified arc.
  5669. :param center: Coordinates of the center [x, y]
  5670. :type center: list
  5671. :param radius: Radius of the arc.
  5672. :type radius: float
  5673. :param start: Starting angle in radians
  5674. :type start: float
  5675. :param stop: End angle in radians
  5676. :type stop: float
  5677. :param direction: Orientation of the arc, "CW" or "CCW"
  5678. :type direction: string
  5679. :param steps_per_circ: Number of straight line segments to
  5680. represent a circle.
  5681. :type steps_per_circ: int
  5682. :return: The desired arc, as list of tuples
  5683. :rtype: list
  5684. """
  5685. # TODO: Resolution should be established by maximum error from the exact arc.
  5686. da_sign = {"cw": -1.0, "ccw": 1.0}
  5687. points = []
  5688. if direction == "ccw" and stop <= start:
  5689. stop += 2 * pi
  5690. if direction == "cw" and stop >= start:
  5691. stop -= 2 * pi
  5692. angle = abs(stop - start)
  5693. #angle = stop-start
  5694. steps = max([int(ceil(angle / (2 * pi) * steps_per_circ)), 2])
  5695. delta_angle = da_sign[direction] * angle * 1.0 / steps
  5696. for i in range(steps + 1):
  5697. theta = start + delta_angle * i
  5698. points.append((center[0] + radius * cos(theta), center[1] + radius * sin(theta)))
  5699. return points
  5700. def arc2(p1, p2, center, direction, steps_per_circ):
  5701. r = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
  5702. start = arctan2(p1[1] - center[1], p1[0] - center[0])
  5703. stop = arctan2(p2[1] - center[1], p2[0] - center[0])
  5704. return arc(center, r, start, stop, direction, steps_per_circ)
  5705. def arc_angle(start, stop, direction):
  5706. if direction == "ccw" and stop <= start:
  5707. stop += 2 * pi
  5708. if direction == "cw" and stop >= start:
  5709. stop -= 2 * pi
  5710. angle = abs(stop - start)
  5711. return angle
  5712. # def find_polygon(poly, point):
  5713. # """
  5714. # Find an object that object.contains(Point(point)) in
  5715. # poly, which can can be iterable, contain iterable of, or
  5716. # be itself an implementer of .contains().
  5717. #
  5718. # :param poly: See description
  5719. # :return: Polygon containing point or None.
  5720. # """
  5721. #
  5722. # if poly is None:
  5723. # return None
  5724. #
  5725. # try:
  5726. # for sub_poly in poly:
  5727. # p = find_polygon(sub_poly, point)
  5728. # if p is not None:
  5729. # return p
  5730. # except TypeError:
  5731. # try:
  5732. # if poly.contains(Point(point)):
  5733. # return poly
  5734. # except AttributeError:
  5735. # return None
  5736. #
  5737. # return None
  5738. def to_dict(obj):
  5739. """
  5740. Makes the following types into serializable form:
  5741. * ApertureMacro
  5742. * BaseGeometry
  5743. :param obj: Shapely geometry.
  5744. :type obj: BaseGeometry
  5745. :return: Dictionary with serializable form if ``obj`` was
  5746. BaseGeometry or ApertureMacro, otherwise returns ``obj``.
  5747. """
  5748. if isinstance(obj, ApertureMacro):
  5749. return {
  5750. "__class__": "ApertureMacro",
  5751. "__inst__": obj.to_dict()
  5752. }
  5753. if isinstance(obj, BaseGeometry):
  5754. return {
  5755. "__class__": "Shply",
  5756. "__inst__": sdumps(obj)
  5757. }
  5758. return obj
  5759. def dict2obj(d):
  5760. """
  5761. Default deserializer.
  5762. :param d: Serializable dictionary representation of an object
  5763. to be reconstructed.
  5764. :return: Reconstructed object.
  5765. """
  5766. if '__class__' in d and '__inst__' in d:
  5767. if d['__class__'] == "Shply":
  5768. return sloads(d['__inst__'])
  5769. if d['__class__'] == "ApertureMacro":
  5770. am = ApertureMacro()
  5771. am.from_dict(d['__inst__'])
  5772. return am
  5773. return d
  5774. else:
  5775. return d
  5776. # def plotg(geo, solid_poly=False, color="black"):
  5777. # try:
  5778. # _ = iter(geo)
  5779. # except:
  5780. # geo = [geo]
  5781. #
  5782. # for g in geo:
  5783. # if type(g) == Polygon:
  5784. # if solid_poly:
  5785. # patch = PolygonPatch(g,
  5786. # facecolor="#BBF268",
  5787. # edgecolor="#006E20",
  5788. # alpha=0.75,
  5789. # zorder=2)
  5790. # ax = subplot(111)
  5791. # ax.add_patch(patch)
  5792. # else:
  5793. # x, y = g.exterior.coords.xy
  5794. # plot(x, y, color=color)
  5795. # for ints in g.interiors:
  5796. # x, y = ints.coords.xy
  5797. # plot(x, y, color=color)
  5798. # continue
  5799. #
  5800. # if type(g) == LineString or type(g) == LinearRing:
  5801. # x, y = g.coords.xy
  5802. # plot(x, y, color=color)
  5803. # continue
  5804. #
  5805. # if type(g) == Point:
  5806. # x, y = g.coords.xy
  5807. # plot(x, y, 'o')
  5808. # continue
  5809. #
  5810. # try:
  5811. # _ = iter(g)
  5812. # plotg(g, color=color)
  5813. # except:
  5814. # log.error("Cannot plot: " + str(type(g)))
  5815. # continue
  5816. def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
  5817. """
  5818. Parse a single number of Gerber coordinates.
  5819. :param strnumber: String containing a number in decimal digits
  5820. from a coordinate data block, possibly with a leading sign.
  5821. :type strnumber: str
  5822. :param int_digits: Number of digits used for the integer
  5823. part of the number
  5824. :type frac_digits: int
  5825. :param frac_digits: Number of digits used for the fractional
  5826. part of the number
  5827. :type frac_digits: int
  5828. :param zeros: If 'L', leading zeros are removed and trailing zeros are kept. If 'T', is in reverse.
  5829. :type zeros: str
  5830. :return: The number in floating point.
  5831. :rtype: float
  5832. """
  5833. if zeros == 'L':
  5834. ret_val = int(strnumber) * (10 ** (-frac_digits))
  5835. if zeros == 'T':
  5836. int_val = int(strnumber)
  5837. ret_val = (int_val * (10 ** ((int_digits + frac_digits) - len(strnumber)))) * (10 ** (-frac_digits))
  5838. return ret_val
  5839. # def voronoi(P):
  5840. # """
  5841. # Returns a list of all edges of the voronoi diagram for the given input points.
  5842. # """
  5843. # delauny = Delaunay(P)
  5844. # triangles = delauny.points[delauny.vertices]
  5845. #
  5846. # circum_centers = np.array([triangle_csc(tri) for tri in triangles])
  5847. # long_lines_endpoints = []
  5848. #
  5849. # lineIndices = []
  5850. # for i, triangle in enumerate(triangles):
  5851. # circum_center = circum_centers[i]
  5852. # for j, neighbor in enumerate(delauny.neighbors[i]):
  5853. # if neighbor != -1:
  5854. # lineIndices.append((i, neighbor))
  5855. # else:
  5856. # ps = triangle[(j+1)%3] - triangle[(j-1)%3]
  5857. # ps = np.array((ps[1], -ps[0]))
  5858. #
  5859. # middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
  5860. # di = middle - triangle[j]
  5861. #
  5862. # ps /= np.linalg.norm(ps)
  5863. # di /= np.linalg.norm(di)
  5864. #
  5865. # if np.dot(di, ps) < 0.0:
  5866. # ps *= -1000.0
  5867. # else:
  5868. # ps *= 1000.0
  5869. #
  5870. # long_lines_endpoints.append(circum_center + ps)
  5871. # lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
  5872. #
  5873. # vertices = np.vstack((circum_centers, long_lines_endpoints))
  5874. #
  5875. # # filter out any duplicate lines
  5876. # lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
  5877. # lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
  5878. # lineIndicesUnique = np.unique(lineIndicesTupled)
  5879. #
  5880. # return vertices, lineIndicesUnique
  5881. #
  5882. #
  5883. # def triangle_csc(pts):
  5884. # rows, cols = pts.shape
  5885. #
  5886. # A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
  5887. # [np.ones((1, rows)), np.zeros((1, 1))]])
  5888. #
  5889. # b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
  5890. # x = np.linalg.solve(A,b)
  5891. # bary_coords = x[:-1]
  5892. # return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
  5893. #
  5894. #
  5895. # def voronoi_cell_lines(points, vertices, lineIndices):
  5896. # """
  5897. # Returns a mapping from a voronoi cell to its edges.
  5898. #
  5899. # :param points: shape (m,2)
  5900. # :param vertices: shape (n,2)
  5901. # :param lineIndices: shape (o,2)
  5902. # :rtype: dict point index -> list of shape (n,2) with vertex indices
  5903. # """
  5904. # kd = KDTree(points)
  5905. #
  5906. # cells = collections.defaultdict(list)
  5907. # for i1, i2 in lineIndices:
  5908. # v1, v2 = vertices[i1], vertices[i2]
  5909. # mid = (v1+v2)/2
  5910. # _, (p1Idx, p2Idx) = kd.query(mid, 2)
  5911. # cells[p1Idx].append((i1, i2))
  5912. # cells[p2Idx].append((i1, i2))
  5913. #
  5914. # return cells
  5915. #
  5916. #
  5917. # def voronoi_edges2polygons(cells):
  5918. # """
  5919. # Transforms cell edges into polygons.
  5920. #
  5921. # :param cells: as returned from voronoi_cell_lines
  5922. # :rtype: dict point index -> list of vertex indices which form a polygon
  5923. # """
  5924. #
  5925. # # first, close the outer cells
  5926. # for pIdx, lineIndices_ in cells.items():
  5927. # dangling_lines = []
  5928. # for i1, i2 in lineIndices_:
  5929. # p = (i1, i2)
  5930. # connections = filter(lambda k: p != k and (p[0] == k[0] or p[0] == k[1] or p[1] == k[0] or p[1] == k[1]), lineIndices_)
  5931. # # connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
  5932. # assert 1 <= len(connections) <= 2
  5933. # if len(connections) == 1:
  5934. # dangling_lines.append((i1, i2))
  5935. # assert len(dangling_lines) in [0, 2]
  5936. # if len(dangling_lines) == 2:
  5937. # (i11, i12), (i21, i22) = dangling_lines
  5938. # s = (i11, i12)
  5939. # t = (i21, i22)
  5940. #
  5941. # # determine which line ends are unconnected
  5942. # connected = filter(lambda k: k != s and (k[0] == s[0] or k[1] == s[0]), lineIndices_)
  5943. # # connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
  5944. # i11Unconnected = len(connected) == 0
  5945. #
  5946. # connected = filter(lambda k: k != t and (k[0] == t[0] or k[1] == t[0]), lineIndices_)
  5947. # # connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
  5948. # i21Unconnected = len(connected) == 0
  5949. #
  5950. # startIdx = i11 if i11Unconnected else i12
  5951. # endIdx = i21 if i21Unconnected else i22
  5952. #
  5953. # cells[pIdx].append((startIdx, endIdx))
  5954. #
  5955. # # then, form polygons by storing vertex indices in (counter-)clockwise order
  5956. # polys = dict()
  5957. # for pIdx, lineIndices_ in cells.items():
  5958. # # get a directed graph which contains both directions and arbitrarily follow one of both
  5959. # directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
  5960. # directedGraphMap = collections.defaultdict(list)
  5961. # for (i1, i2) in directedGraph:
  5962. # directedGraphMap[i1].append(i2)
  5963. # orderedEdges = []
  5964. # currentEdge = directedGraph[0]
  5965. # while len(orderedEdges) < len(lineIndices_):
  5966. # i1 = currentEdge[1]
  5967. # i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
  5968. # nextEdge = (i1, i2)
  5969. # orderedEdges.append(nextEdge)
  5970. # currentEdge = nextEdge
  5971. #
  5972. # polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
  5973. #
  5974. # return polys
  5975. #
  5976. #
  5977. # def voronoi_polygons(points):
  5978. # """
  5979. # Returns the voronoi polygon for each input point.
  5980. #
  5981. # :param points: shape (n,2)
  5982. # :rtype: list of n polygons where each polygon is an array of vertices
  5983. # """
  5984. # vertices, lineIndices = voronoi(points)
  5985. # cells = voronoi_cell_lines(points, vertices, lineIndices)
  5986. # polys = voronoi_edges2polygons(cells)
  5987. # polylist = []
  5988. # for i in range(len(points)):
  5989. # poly = vertices[np.asarray(polys[i])]
  5990. # polylist.append(poly)
  5991. # return polylist
  5992. #
  5993. #
  5994. # class Zprofile:
  5995. # def __init__(self):
  5996. #
  5997. # # data contains lists of [x, y, z]
  5998. # self.data = []
  5999. #
  6000. # # Computed voronoi polygons (shapely)
  6001. # self.polygons = []
  6002. # pass
  6003. #
  6004. # # def plot_polygons(self):
  6005. # # axes = plt.subplot(1, 1, 1)
  6006. # #
  6007. # # plt.axis([-0.05, 1.05, -0.05, 1.05])
  6008. # #
  6009. # # for poly in self.polygons:
  6010. # # p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
  6011. # # axes.add_patch(p)
  6012. #
  6013. # def init_from_csv(self, filename):
  6014. # pass
  6015. #
  6016. # def init_from_string(self, zpstring):
  6017. # pass
  6018. #
  6019. # def init_from_list(self, zplist):
  6020. # self.data = zplist
  6021. #
  6022. # def generate_polygons(self):
  6023. # self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
  6024. #
  6025. # def normalize(self, origin):
  6026. # pass
  6027. #
  6028. # def paste(self, path):
  6029. # """
  6030. # Return a list of dictionaries containing the parts of the original
  6031. # path and their z-axis offset.
  6032. # """
  6033. #
  6034. # # At most one region/polygon will contain the path
  6035. # containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
  6036. #
  6037. # if len(containing) > 0:
  6038. # return [{"path": path, "z": self.data[containing[0]][2]}]
  6039. #
  6040. # # All region indexes that intersect with the path
  6041. # crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
  6042. #
  6043. # return [{"path": path.intersection(self.polygons[i]),
  6044. # "z": self.data[i][2]} for i in crossing]
  6045. def autolist(obj):
  6046. try:
  6047. _ = iter(obj)
  6048. return obj
  6049. except TypeError:
  6050. return [obj]
  6051. def three_point_circle(p1, p2, p3):
  6052. """
  6053. Computes the center and radius of a circle from
  6054. 3 points on its circumference.
  6055. :param p1: Point 1
  6056. :param p2: Point 2
  6057. :param p3: Point 3
  6058. :return: center, radius
  6059. """
  6060. # Midpoints
  6061. a1 = (p1 + p2) / 2.0
  6062. a2 = (p2 + p3) / 2.0
  6063. # Normals
  6064. b1 = dot((p2 - p1), array([[0, -1], [1, 0]], dtype=float32))
  6065. b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32))
  6066. # Params
  6067. T = solve(transpose(array([-b1, b2])), a1 - a2)
  6068. # Center
  6069. center = a1 + b1 * T[0]
  6070. # Radius
  6071. radius = norm(center - p1)
  6072. return center, radius, T[0]
  6073. def distance(pt1, pt2):
  6074. return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
  6075. def distance_euclidian(x1, y1, x2, y2):
  6076. return sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
  6077. class FlatCAMRTree(object):
  6078. """
  6079. Indexes geometry (Any object with "cooords" property containing
  6080. a list of tuples with x, y values). Objects are indexed by
  6081. all their points by default. To index by arbitrary points,
  6082. override self.points2obj.
  6083. """
  6084. def __init__(self):
  6085. # Python RTree Index
  6086. self.rti = rtindex.Index()
  6087. ## Track object-point relationship
  6088. # Each is list of points in object.
  6089. self.obj2points = []
  6090. # Index is index in rtree, value is index of
  6091. # object in obj2points.
  6092. self.points2obj = []
  6093. self.get_points = lambda go: go.coords
  6094. def grow_obj2points(self, idx):
  6095. """
  6096. Increases the size of self.obj2points to fit
  6097. idx + 1 items.
  6098. :param idx: Index to fit into list.
  6099. :return: None
  6100. """
  6101. if len(self.obj2points) > idx:
  6102. # len == 2, idx == 1, ok.
  6103. return
  6104. else:
  6105. # len == 2, idx == 2, need 1 more.
  6106. # range(2, 3)
  6107. for i in range(len(self.obj2points), idx + 1):
  6108. self.obj2points.append([])
  6109. def insert(self, objid, obj):
  6110. self.grow_obj2points(objid)
  6111. self.obj2points[objid] = []
  6112. for pt in self.get_points(obj):
  6113. self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
  6114. self.obj2points[objid].append(len(self.points2obj))
  6115. self.points2obj.append(objid)
  6116. def remove_obj(self, objid, obj):
  6117. # Use all ptids to delete from index
  6118. for i, pt in enumerate(self.get_points(obj)):
  6119. self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
  6120. def nearest(self, pt):
  6121. """
  6122. Will raise StopIteration if no items are found.
  6123. :param pt:
  6124. :return:
  6125. """
  6126. return next(self.rti.nearest(pt, objects=True))
  6127. class FlatCAMRTreeStorage(FlatCAMRTree):
  6128. """
  6129. Just like FlatCAMRTree it indexes geometry, but also serves
  6130. as storage for the geometry.
  6131. """
  6132. def __init__(self):
  6133. # super(FlatCAMRTreeStorage, self).__init__()
  6134. super().__init__()
  6135. self.objects = []
  6136. # Optimization attempt!
  6137. self.indexes = {}
  6138. def insert(self, obj):
  6139. self.objects.append(obj)
  6140. idx = len(self.objects) - 1
  6141. # Note: Shapely objects are not hashable any more, althought
  6142. # there seem to be plans to re-introduce the feature in
  6143. # version 2.0. For now, we will index using the object's id,
  6144. # but it's important to remember that shapely geometry is
  6145. # mutable, ie. it can be modified to a totally different shape
  6146. # and continue to have the same id.
  6147. # self.indexes[obj] = idx
  6148. self.indexes[id(obj)] = idx
  6149. # super(FlatCAMRTreeStorage, self).insert(idx, obj)
  6150. super().insert(idx, obj)
  6151. #@profile
  6152. def remove(self, obj):
  6153. # See note about self.indexes in insert().
  6154. # objidx = self.indexes[obj]
  6155. objidx = self.indexes[id(obj)]
  6156. # Remove from list
  6157. self.objects[objidx] = None
  6158. # Remove from index
  6159. self.remove_obj(objidx, obj)
  6160. def get_objects(self):
  6161. return (o for o in self.objects if o is not None)
  6162. def nearest(self, pt):
  6163. """
  6164. Returns the nearest matching points and the object
  6165. it belongs to.
  6166. :param pt: Query point.
  6167. :return: (match_x, match_y), Object owner of
  6168. matching point.
  6169. :rtype: tuple
  6170. """
  6171. tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
  6172. return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
  6173. # class myO:
  6174. # def __init__(self, coords):
  6175. # self.coords = coords
  6176. #
  6177. #
  6178. # def test_rti():
  6179. #
  6180. # o1 = myO([(0, 0), (0, 1), (1, 1)])
  6181. # o2 = myO([(2, 0), (2, 1), (2, 1)])
  6182. # o3 = myO([(2, 0), (2, 1), (3, 1)])
  6183. #
  6184. # os = [o1, o2]
  6185. #
  6186. # idx = FlatCAMRTree()
  6187. #
  6188. # for o in range(len(os)):
  6189. # idx.insert(o, os[o])
  6190. #
  6191. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  6192. #
  6193. # idx.remove_obj(0, o1)
  6194. #
  6195. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  6196. #
  6197. # idx.remove_obj(1, o2)
  6198. #
  6199. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  6200. #
  6201. #
  6202. # def test_rtis():
  6203. #
  6204. # o1 = myO([(0, 0), (0, 1), (1, 1)])
  6205. # o2 = myO([(2, 0), (2, 1), (2, 1)])
  6206. # o3 = myO([(2, 0), (2, 1), (3, 1)])
  6207. #
  6208. # os = [o1, o2]
  6209. #
  6210. # idx = FlatCAMRTreeStorage()
  6211. #
  6212. # for o in range(len(os)):
  6213. # idx.insert(os[o])
  6214. #
  6215. # #os = None
  6216. # #o1 = None
  6217. # #o2 = None
  6218. #
  6219. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  6220. #
  6221. # idx.remove(idx.nearest((2,0))[1])
  6222. #
  6223. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  6224. #
  6225. # idx.remove(idx.nearest((0,0))[1])
  6226. #
  6227. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]