camlib.py 361 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743
  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. # ##########################################################
  9. # File Modified : Marcos Dumay de Medeiros #
  10. # Modifications under GPLv3 #
  11. # ##########################################################
  12. from PyQt5 import QtWidgets, QtCore
  13. from io import StringIO
  14. from numpy.linalg import solve, norm
  15. import platform
  16. from copy import deepcopy
  17. import traceback
  18. from decimal import Decimal
  19. from rtree import index as rtindex
  20. from lxml import etree as ET
  21. # See: http://toblerity.org/shapely/manual.html
  22. from shapely.geometry import Polygon, Point, LinearRing
  23. from shapely.geometry import box as shply_box
  24. from shapely.ops import unary_union, substring, linemerge
  25. import shapely.affinity as affinity
  26. from shapely.wkt import loads as sloads
  27. from shapely.wkt import dumps as sdumps
  28. from shapely.geometry.base import BaseGeometry
  29. from shapely.geometry import shape
  30. # ---------------------------------------
  31. # NEEDED for Legacy mode
  32. # Used for solid polygons in Matplotlib
  33. from descartes.patch import PolygonPatch
  34. # ---------------------------------------
  35. # Fix for python 3.10
  36. try:
  37. from collections import Iterable
  38. except ImportError:
  39. from collections.abc import Iterable
  40. import rasterio
  41. from rasterio.features import shapes
  42. import ezdxf
  43. from appCommon.Common import GracefulException as grace
  44. # Commented for FlatCAM packaging with cx_freeze
  45. # from scipy.spatial import KDTree, Delaunay
  46. # from scipy.spatial import Delaunay
  47. from appParsers.ParseSVG import *
  48. from appParsers.ParseDXF import *
  49. if platform.architecture()[0] == '64bit':
  50. from ortools.constraint_solver import pywrapcp
  51. from ortools.constraint_solver import routing_enums_pb2
  52. import logging
  53. import gettext
  54. import appTranslation as fcTranslate
  55. import builtins
  56. fcTranslate.apply_language('strings')
  57. log = logging.getLogger('base2')
  58. log.setLevel(logging.DEBUG)
  59. formatter = logging.Formatter('[%(levelname)s] %(message)s')
  60. handler = logging.StreamHandler()
  61. handler.setFormatter(formatter)
  62. log.addHandler(handler)
  63. if '_' not in builtins.__dict__:
  64. _ = gettext.gettext
  65. class ParseError(Exception):
  66. pass
  67. class ApertureMacro:
  68. """
  69. Syntax of aperture macros.
  70. <AM command>: AM<Aperture macro name>*<Macro content>
  71. <Macro content>: {{<Variable definition>*}{<Primitive>*}}
  72. <Variable definition>: $K=<Arithmetic expression>
  73. <Primitive>: <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
  74. <Modifier>: $M|< Arithmetic expression>
  75. <Comment>: 0 <Text>
  76. """
  77. # ## Regular expressions
  78. am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
  79. am2_re = re.compile(r'(.*)%$')
  80. amcomm_re = re.compile(r'^0(.*)')
  81. amprim_re = re.compile(r'^[1-9].*')
  82. amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
  83. def __init__(self, name=None):
  84. self.name = name
  85. self.raw = ""
  86. # ## These below are recomputed for every aperture
  87. # ## definition, in other words, are temporary variables.
  88. self.primitives = []
  89. self.locvars = {}
  90. self.geometry = None
  91. def to_dict(self):
  92. """
  93. Returns the object in a serializable form. Only the name and
  94. raw are required.
  95. :return: Dictionary representing the object. JSON ready.
  96. :rtype: dict
  97. """
  98. return {
  99. 'name': self.name,
  100. 'raw': self.raw
  101. }
  102. def from_dict(self, d):
  103. """
  104. Populates the object from a serial representation created
  105. with ``self.to_dict()``.
  106. :param d: Serial representation of an ApertureMacro object.
  107. :return: None
  108. """
  109. for attr in ['name', 'raw']:
  110. setattr(self, attr, d[attr])
  111. def parse_content(self):
  112. """
  113. Creates numerical lists for all primitives in the aperture
  114. macro (in ``self.raw``) by replacing all variables by their
  115. values iteratively and evaluating expressions. Results
  116. are stored in ``self.primitives``.
  117. :return: None
  118. """
  119. # Cleanup
  120. self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
  121. self.primitives = []
  122. # Separate parts
  123. parts = self.raw.split('*')
  124. # ### Every part in the macro ####
  125. for part in parts:
  126. # ## Comments. Ignored.
  127. match = ApertureMacro.amcomm_re.search(part)
  128. if match:
  129. continue
  130. # ## Variables
  131. # These are variables defined locally inside the macro. They can be
  132. # numerical constant or defined in terms of previously define
  133. # variables, which can be defined locally or in an aperture
  134. # definition. All replacements occur here.
  135. match = ApertureMacro.amvar_re.search(part)
  136. if match:
  137. var = match.group(1)
  138. val = match.group(2)
  139. # Replace variables in value
  140. for v in self.locvars:
  141. # replaced the following line with the next to fix Mentor custom apertures not parsed OK
  142. # val = re.sub((r'\$'+str(v)+r'(?![0-9a-zA-Z])'), str(self.locvars[v]), val)
  143. val = val.replace('$' + str(v), str(self.locvars[v]))
  144. # Make all others 0
  145. val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
  146. # Change x with *
  147. val = re.sub(r'[xX]', "*", val)
  148. # Eval() and store.
  149. self.locvars[var] = eval(val)
  150. continue
  151. # ## Primitives
  152. # Each is an array. The first identifies the primitive, while the
  153. # rest depend on the primitive. All are strings representing a
  154. # number and may contain variable definition. The values of these
  155. # variables are defined in an aperture definition.
  156. match = ApertureMacro.amprim_re.search(part)
  157. if match:
  158. # ## Replace all variables
  159. for v in self.locvars:
  160. # replaced the following line with the next to fix Mentor custom apertures not parsed OK
  161. # part = re.sub(r'\$' + str(v) + r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
  162. part = part.replace('$' + str(v), str(self.locvars[v]))
  163. # Make all others 0
  164. part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
  165. # Change x with *
  166. part = re.sub(r'[xX]', "*", part)
  167. # ## Store
  168. elements = part.split(",")
  169. self.primitives.append([eval(x) for x in elements])
  170. continue
  171. log.warning("Unknown syntax of aperture macro part: %s" % str(part))
  172. def append(self, data):
  173. """
  174. Appends a string to the raw macro.
  175. :param data: Part of the macro.
  176. :type data: str
  177. :return: None
  178. """
  179. self.raw += data
  180. @staticmethod
  181. def default2zero(n, mods):
  182. """
  183. Pads the ``mods`` list with zeros resulting in an
  184. list of length n.
  185. :param n: Length of the resulting list.
  186. :type n: int
  187. :param mods: List to be padded.
  188. :type mods: list
  189. :return: Zero-padded list.
  190. :rtype: list
  191. """
  192. x = [0.0] * n
  193. na = len(mods)
  194. x[0:na] = mods
  195. return x
  196. @staticmethod
  197. def make_circle(mods):
  198. """
  199. :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
  200. :return:
  201. """
  202. val = ApertureMacro.default2zero(4, mods)
  203. pol = val[0]
  204. dia = val[1]
  205. x = val[2]
  206. y = val[3]
  207. # pol, dia, x, y = ApertureMacro.default2zero(4, mods)
  208. return {"pol": int(pol), "geometry": Point(x, y).buffer(dia / 2)}
  209. @staticmethod
  210. def make_vectorline(mods):
  211. """
  212. :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
  213. rotation angle around origin in degrees)
  214. :return:
  215. """
  216. val = ApertureMacro.default2zero(7, mods)
  217. pol = val[0]
  218. width = val[1]
  219. xs = val[2]
  220. ys = val[3]
  221. xe = val[4]
  222. ye = val[5]
  223. angle = val[6]
  224. # pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
  225. line = LineString([(xs, ys), (xe, ye)])
  226. box = line.buffer(width / 2, cap_style=2)
  227. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  228. return {"pol": int(pol), "geometry": box_rotated}
  229. @staticmethod
  230. def make_centerline(mods):
  231. """
  232. :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
  233. rotation angle around origin in degrees)
  234. :return:
  235. """
  236. # pol, width, height, x, y, angle = ApertureMacro.default2zero(4, mods)
  237. val = ApertureMacro.default2zero(4, mods)
  238. pol = val[0]
  239. width = val[1]
  240. height = val[2]
  241. x = val[3]
  242. y = val[4]
  243. angle = val[5]
  244. box = shply_box(x - width / 2, y - height / 2, x + width / 2, y + height / 2)
  245. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  246. return {"pol": int(pol), "geometry": box_rotated}
  247. @staticmethod
  248. def make_lowerleftline(mods):
  249. """
  250. :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
  251. rotation angle around origin in degrees)
  252. :return:
  253. """
  254. # pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  255. val = ApertureMacro.default2zero(6, mods)
  256. pol = val[0]
  257. width = val[1]
  258. height = val[2]
  259. x = val[3]
  260. y = val[4]
  261. angle = val[5]
  262. box = shply_box(x, y, x + width, y + height)
  263. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  264. return {"pol": int(pol), "geometry": box_rotated}
  265. @staticmethod
  266. def make_outline(mods):
  267. """
  268. :param mods:
  269. :return:
  270. """
  271. pol = mods[0]
  272. n = mods[1]
  273. points = [(0, 0)] * (n + 1)
  274. for i in range(n + 1):
  275. points[i] = mods[2 * i + 2:2 * i + 4]
  276. angle = mods[2 * n + 4]
  277. poly = Polygon(points)
  278. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  279. return {"pol": int(pol), "geometry": poly_rotated}
  280. @staticmethod
  281. def make_polygon(mods):
  282. """
  283. Note: Specs indicate that rotation is only allowed if the center
  284. (x, y) == (0, 0). I will tolerate breaking this rule.
  285. :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
  286. diameter of circumscribed circle >=0, rotation angle around origin)
  287. :return:
  288. """
  289. # pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
  290. val = ApertureMacro.default2zero(6, mods)
  291. pol = val[0]
  292. nverts = val[1]
  293. x = val[2]
  294. y = val[3]
  295. dia = val[4]
  296. angle = val[5]
  297. points = [(0, 0)] * nverts
  298. for i in range(nverts):
  299. points[i] = (x + 0.5 * dia * np.cos(2 * np.pi * i / nverts),
  300. y + 0.5 * dia * np.sin(2 * np.pi * i / nverts))
  301. poly = Polygon(points)
  302. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  303. return {"pol": int(pol), "geometry": poly_rotated}
  304. @staticmethod
  305. def make_moire(mods):
  306. """
  307. Note: Specs indicate that rotation is only allowed if the center
  308. (x, y) == (0, 0). I will tolerate breaking this rule.
  309. :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
  310. gap, max_rings, crosshair_thickness, crosshair_len, rotation
  311. angle around origin in degrees)
  312. :return:
  313. """
  314. # x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
  315. val = ApertureMacro.default2zero(9, mods)
  316. x = val[0]
  317. y = val[1]
  318. dia = val[2]
  319. thickness = val[3]
  320. gap = val[4]
  321. nrings = val[5]
  322. cross_th = val[6]
  323. cross_len = val[7]
  324. angle = val[8]
  325. r = dia / 2 - thickness / 2
  326. result = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
  327. ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0) # Need a copy!
  328. i = 1 # Number of rings created so far
  329. # ## If the ring does not have an interior it means that it is
  330. # ## a disk. Then stop.
  331. while len(ring.interiors) > 0 and i < nrings:
  332. r -= thickness + gap
  333. if r <= 0:
  334. break
  335. ring = Point((x, y)).buffer(r).exterior.buffer(thickness / 2.0)
  336. result = unary_union([result, ring])
  337. i += 1
  338. # ## Crosshair
  339. hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th / 2.0, cap_style=2)
  340. ver = LineString([(x, y - cross_len), (x, y + cross_len)]).buffer(cross_th / 2.0, cap_style=2)
  341. result = unary_union([result, hor, ver])
  342. return {"pol": 1, "geometry": result}
  343. @staticmethod
  344. def make_thermal(mods):
  345. """
  346. Note: Specs indicate that rotation is only allowed if the center
  347. (x, y) == (0, 0). I will tolerate breaking this rule.
  348. :param mods: [x-center, y-center, diameter-outside, diameter-inside,
  349. gap-thickness, rotation angle around origin]
  350. :return:
  351. """
  352. # x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
  353. val = ApertureMacro.default2zero(6, mods)
  354. x = val[0]
  355. y = val[1]
  356. dout = val[2]
  357. din = val[3]
  358. t = val[4]
  359. angle = val[5]
  360. ring = Point((x, y)).buffer(dout / 2.0).difference(Point((x, y)).buffer(din / 2.0))
  361. hline = LineString([(x - dout / 2.0, y), (x + dout / 2.0, y)]).buffer(t / 2.0, cap_style=3)
  362. vline = LineString([(x, y - dout / 2.0), (x, y + dout / 2.0)]).buffer(t / 2.0, cap_style=3)
  363. thermal = ring.difference(hline.union(vline))
  364. return {"pol": 1, "geometry": thermal}
  365. def make_geometry(self, modifiers):
  366. """
  367. Runs the macro for the given modifiers and generates
  368. the corresponding geometry.
  369. :param modifiers: Modifiers (parameters) for this macro
  370. :type modifiers: list
  371. :return: Shapely geometry
  372. :rtype: shapely.geometry.polygon
  373. """
  374. # ## Primitive makers
  375. makers = {
  376. "1": ApertureMacro.make_circle,
  377. "2": ApertureMacro.make_vectorline,
  378. "20": ApertureMacro.make_vectorline,
  379. "21": ApertureMacro.make_centerline,
  380. "22": ApertureMacro.make_lowerleftline,
  381. "4": ApertureMacro.make_outline,
  382. "5": ApertureMacro.make_polygon,
  383. "6": ApertureMacro.make_moire,
  384. "7": ApertureMacro.make_thermal
  385. }
  386. # ## Store modifiers as local variables
  387. modifiers = modifiers or []
  388. modifiers = [float(m) for m in modifiers]
  389. self.locvars = {}
  390. for i in range(0, len(modifiers)):
  391. self.locvars[str(i + 1)] = modifiers[i]
  392. # ## Parse
  393. self.primitives = [] # Cleanup
  394. self.geometry = Polygon()
  395. self.parse_content()
  396. # ## Make the geometry
  397. for primitive in self.primitives:
  398. # Make the primitive
  399. prim_geo = makers[str(int(primitive[0]))](primitive[1:])
  400. # Add it (according to polarity)
  401. # if self.geometry is None and prim_geo['pol'] == 1:
  402. # self.geometry = prim_geo['geometry']
  403. # continue
  404. if prim_geo['pol'] == 1:
  405. self.geometry = self.geometry.union(prim_geo['geometry'])
  406. continue
  407. if prim_geo['pol'] == 0:
  408. self.geometry = self.geometry.difference(prim_geo['geometry'])
  409. continue
  410. return self.geometry
  411. class Geometry(object):
  412. """
  413. Base geometry class.
  414. """
  415. defaults = {
  416. "units": 'mm',
  417. # "geo_steps_per_circle": 128
  418. }
  419. def __init__(self, geo_steps_per_circle=None):
  420. # Units (in or mm)
  421. self.units = self.app.defaults["units"]
  422. self.decimals = self.app.decimals
  423. self.drawing_tolerance = 0.0
  424. self.tools = None
  425. # Final geometry: MultiPolygon or list (of geometry constructs)
  426. self.solid_geometry = None
  427. # Final geometry: MultiLineString or list (of LineString or Points)
  428. self.follow_geometry = None
  429. # Flattened geometry (list of paths only)
  430. self.flat_geometry = []
  431. # this is the calculated conversion factor when the file units are different than the ones in the app
  432. self.file_units_factor = 1
  433. # Index
  434. self.index = None
  435. self.geo_steps_per_circle = geo_steps_per_circle
  436. # variables to display the percentage of work done
  437. self.geo_len = 0
  438. self.old_disp_number = 0
  439. self.el_count = 0
  440. if self.app.is_legacy is False:
  441. self.temp_shapes = self.app.plotcanvas.new_shape_collection(layers=1)
  442. else:
  443. from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
  444. self.temp_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='camlib.geometry')
  445. # Attributes to be included in serialization
  446. self.ser_attrs = ["units", 'solid_geometry', 'follow_geometry', 'tools']
  447. def plot_temp_shapes(self, element, color='red'):
  448. try:
  449. for sub_el in element:
  450. self.plot_temp_shapes(sub_el)
  451. except TypeError: # Element is not iterable...
  452. # self.add_shape(shape=element, color=color, visible=visible, layer=0)
  453. self.temp_shapes.add(tolerance=float(self.app.defaults["global_tolerance"]),
  454. shape=element, color=color, visible=True, layer=0)
  455. def make_index(self):
  456. self.flatten()
  457. self.index = FlatCAMRTree()
  458. for i, g in enumerate(self.flat_geometry):
  459. self.index.insert(i, g)
  460. def add_circle(self, origin, radius, tool=None):
  461. """
  462. Adds a circle to the object.
  463. :param origin: Center of the circle.
  464. :param radius: Radius of the circle.
  465. :param tool: A tool in the Tools dictionary attribute of the object
  466. :return: None
  467. """
  468. if self.solid_geometry is None:
  469. self.solid_geometry = []
  470. new_circle = Point(origin).buffer(radius, int(self.geo_steps_per_circle))
  471. if not new_circle.is_valid:
  472. return "fail"
  473. # add to the solid_geometry
  474. try:
  475. self.solid_geometry.append(new_circle)
  476. except TypeError:
  477. try:
  478. self.solid_geometry = self.solid_geometry.union(new_circle)
  479. except Exception as e:
  480. log.error("Failed to run union on polygons. %s" % str(e))
  481. return "fail"
  482. # add in tools solid_geometry
  483. if tool is None or tool not in self.tools:
  484. tool = 1
  485. self.tools[tool]['solid_geometry'].append(new_circle)
  486. # calculate bounds
  487. try:
  488. xmin, ymin, xmax, ymax = self.bounds()
  489. self.options['xmin'] = xmin
  490. self.options['ymin'] = ymin
  491. self.options['xmax'] = xmax
  492. self.options['ymax'] = ymax
  493. except Exception as e:
  494. log.error("Failed. The object has no bounds properties. %s" % str(e))
  495. def add_polygon(self, points, tool=None):
  496. """
  497. Adds a polygon to the object (by union)
  498. :param points: The vertices of the polygon.
  499. :param tool: A tool in the Tools dictionary attribute of the object
  500. :return: None
  501. """
  502. if self.solid_geometry is None:
  503. self.solid_geometry = []
  504. new_poly = Polygon(points)
  505. if not new_poly.is_valid:
  506. return "fail"
  507. # add to the solid_geometry
  508. if type(self.solid_geometry) is list:
  509. self.solid_geometry.append(new_poly)
  510. else:
  511. try:
  512. self.solid_geometry = self.solid_geometry.union(Polygon(points))
  513. except Exception as e:
  514. log.error("Failed to run union on polygons. %s" % str(e))
  515. return "fail"
  516. # add in tools solid_geometry
  517. if tool is None or tool not in self.tools:
  518. tool = 1
  519. self.tools[tool]['solid_geometry'].append(new_poly)
  520. # calculate bounds
  521. try:
  522. xmin, ymin, xmax, ymax = self.bounds()
  523. self.options['xmin'] = xmin
  524. self.options['ymin'] = ymin
  525. self.options['xmax'] = xmax
  526. self.options['ymax'] = ymax
  527. except Exception as e:
  528. log.error("Failed. The object has no bounds properties. %s" % str(e))
  529. def add_polyline(self, points, tool=None):
  530. """
  531. Adds a polyline to the object (by union)
  532. :param points: The vertices of the polyline.
  533. :param tool: A tool in the Tools dictionary attribute of the object
  534. :return: None
  535. """
  536. if self.solid_geometry is None:
  537. self.solid_geometry = []
  538. new_line = LineString(points)
  539. if not new_line.is_valid:
  540. return "fail"
  541. # add to the solid_geometry
  542. if type(self.solid_geometry) is list:
  543. self.solid_geometry.append(new_line)
  544. else:
  545. try:
  546. self.solid_geometry = self.solid_geometry.union(new_line)
  547. except Exception as e:
  548. log.error("Failed to run union on polylines. %s" % str(e))
  549. return "fail"
  550. # add in tools solid_geometry
  551. if tool is None or tool not in self.tools:
  552. tool = 1
  553. self.tools[tool]['solid_geometry'].append(new_line)
  554. # calculate bounds
  555. try:
  556. xmin, ymin, xmax, ymax = self.bounds()
  557. self.options['xmin'] = xmin
  558. self.options['ymin'] = ymin
  559. self.options['xmax'] = xmax
  560. self.options['ymax'] = ymax
  561. except Exception as e:
  562. log.error("Failed. The object has no bounds properties. %s" % str(e))
  563. def is_empty(self):
  564. if isinstance(self.solid_geometry, BaseGeometry) or isinstance(self.solid_geometry, Polygon) or \
  565. isinstance(self.solid_geometry, MultiPolygon):
  566. return self.solid_geometry.is_empty
  567. if isinstance(self.solid_geometry, list):
  568. return len(self.solid_geometry) == 0
  569. self.app.inform.emit('[ERROR_NOTCL] %s' % _("self.solid_geometry is neither BaseGeometry or list."))
  570. return
  571. def subtract_polygon(self, points):
  572. """
  573. Subtract polygon from the given object. This only operates on the paths in the original geometry,
  574. i.e. it converts polygons into paths.
  575. :param points: The vertices of the polygon.
  576. :return: none
  577. """
  578. if self.solid_geometry is None:
  579. self.solid_geometry = []
  580. # pathonly should be allways True, otherwise polygons are not subtracted
  581. flat_geometry = self.flatten(pathonly=True)
  582. log.debug("%d paths" % len(flat_geometry))
  583. if not isinstance(points, Polygon):
  584. polygon = Polygon(points)
  585. else:
  586. polygon = points
  587. toolgeo = unary_union(polygon)
  588. diffs = []
  589. for target in flat_geometry:
  590. if isinstance(target, LineString) or isinstance(target, LineString) or isinstance(target, MultiLineString):
  591. diffs.append(target.difference(toolgeo))
  592. else:
  593. log.warning("Not implemented.")
  594. self.solid_geometry = unary_union(diffs)
  595. def bounds(self, flatten=False):
  596. """
  597. Returns coordinates of rectangular bounds
  598. of geometry: (xmin, ymin, xmax, ymax).
  599. :param flatten: will flatten the solid_geometry if True
  600. :return:
  601. """
  602. # fixed issue of getting bounds only for one level lists of objects
  603. # now it can get bounds for nested lists of objects
  604. log.debug("camlib.Geometry.bounds()")
  605. if self.solid_geometry is None:
  606. log.debug("solid_geometry is None")
  607. return 0, 0, 0, 0
  608. def bounds_rec(obj):
  609. if type(obj) is list:
  610. gminx = np.inf
  611. gminy = np.inf
  612. gmaxx = -np.inf
  613. gmaxy = -np.inf
  614. for k in obj:
  615. if type(k) is dict:
  616. for key in k:
  617. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  618. gminx = min(gminx, minx_)
  619. gminy = min(gminy, miny_)
  620. gmaxx = max(gmaxx, maxx_)
  621. gmaxy = max(gmaxy, maxy_)
  622. else:
  623. try:
  624. if k.is_empty:
  625. continue
  626. except Exception:
  627. pass
  628. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  629. gminx = min(gminx, minx_)
  630. gminy = min(gminy, miny_)
  631. gmaxx = max(gmaxx, maxx_)
  632. gmaxy = max(gmaxy, maxy_)
  633. return gminx, gminy, gmaxx, gmaxy
  634. else:
  635. # it's a Shapely object, return it's bounds
  636. return obj.bounds
  637. if self.multigeo is True:
  638. minx_list = []
  639. miny_list = []
  640. maxx_list = []
  641. maxy_list = []
  642. for tool in self.tools:
  643. working_geo = self.tools[tool]['solid_geometry']
  644. if flatten:
  645. self.flatten(geometry=working_geo, reset=True)
  646. working_geo = self.flat_geometry
  647. minx, miny, maxx, maxy = bounds_rec(working_geo)
  648. minx_list.append(minx)
  649. miny_list.append(miny)
  650. maxx_list.append(maxx)
  651. maxy_list.append(maxy)
  652. return min(minx_list), min(miny_list), max(maxx_list), max(maxy_list)
  653. else:
  654. if flatten:
  655. self.flatten(reset=True)
  656. self.solid_geometry = self.flat_geometry
  657. bounds_coords = bounds_rec(self.solid_geometry)
  658. return bounds_coords
  659. # try:
  660. # # from here: http://rightfootin.blogspot.com/2006/09/more-on-python-flatten.html
  661. # def flatten(l, ltypes=(list, tuple)):
  662. # ltype = type(l)
  663. # l = list(l)
  664. # i = 0
  665. # while i < len(l):
  666. # while isinstance(l[i], ltypes):
  667. # if not l[i]:
  668. # l.pop(i)
  669. # i -= 1
  670. # break
  671. # else:
  672. # l[i:i + 1] = l[i]
  673. # i += 1
  674. # return ltype(l)
  675. #
  676. # log.debug("Geometry->bounds()")
  677. # if self.solid_geometry is None:
  678. # log.debug("solid_geometry is None")
  679. # return 0, 0, 0, 0
  680. #
  681. # if type(self.solid_geometry) is list:
  682. # if len(self.solid_geometry) == 0:
  683. # log.debug('solid_geometry is empty []')
  684. # return 0, 0, 0, 0
  685. # return unary_union(flatten(self.solid_geometry)).bounds
  686. # else:
  687. # return self.solid_geometry.bounds
  688. # except Exception as e:
  689. # self.app.inform.emit("[ERROR_NOTCL] Error cause: %s" % str(e))
  690. # log.debug("Geometry->bounds()")
  691. # if self.solid_geometry is None:
  692. # log.debug("solid_geometry is None")
  693. # return 0, 0, 0, 0
  694. #
  695. # if type(self.solid_geometry) is list:
  696. # if len(self.solid_geometry) == 0:
  697. # log.debug('solid_geometry is empty []')
  698. # return 0, 0, 0, 0
  699. # return unary_union(self.solid_geometry).bounds
  700. # else:
  701. # return self.solid_geometry.bounds
  702. def find_polygon(self, point, geoset=None):
  703. """
  704. Find an object that object.contains(Point(point)) in
  705. poly, which can can be iterable, contain iterable of, or
  706. be itself an implementer of .contains().
  707. :param point: See description
  708. :param geoset: a polygon or list of polygons where to find if the param point is contained
  709. :return: Polygon containing point or None.
  710. """
  711. if geoset is None:
  712. geoset = self.solid_geometry
  713. try: # Iterable
  714. for sub_geo in geoset:
  715. p = self.find_polygon(point, geoset=sub_geo)
  716. if p is not None:
  717. return p
  718. except TypeError: # Non-iterable
  719. try: # Implements .contains()
  720. if isinstance(geoset, LinearRing):
  721. geoset = Polygon(geoset)
  722. if geoset.contains(Point(point)):
  723. return geoset
  724. except AttributeError: # Does not implement .contains()
  725. return None
  726. return None
  727. def get_interiors(self, geometry=None):
  728. interiors = []
  729. if geometry is None:
  730. geometry = self.solid_geometry
  731. # ## If iterable, expand recursively.
  732. try:
  733. for geo in geometry:
  734. interiors.extend(self.get_interiors(geometry=geo))
  735. # ## Not iterable, get the interiors if polygon.
  736. except TypeError:
  737. if type(geometry) == Polygon:
  738. interiors.extend(geometry.interiors)
  739. return interiors
  740. def get_exteriors(self, geometry=None):
  741. """
  742. Returns all exteriors of polygons in geometry. Uses
  743. ``self.solid_geometry`` if geometry is not provided.
  744. :param geometry: Shapely type or list or list of list of such.
  745. :return: List of paths constituting the exteriors
  746. of polygons in geometry.
  747. """
  748. exteriors = []
  749. if geometry is None:
  750. geometry = self.solid_geometry
  751. # ## If iterable, expand recursively.
  752. try:
  753. for geo in geometry:
  754. exteriors.extend(self.get_exteriors(geometry=geo))
  755. # ## Not iterable, get the exterior if polygon.
  756. except TypeError:
  757. if type(geometry) == Polygon:
  758. exteriors.append(geometry.exterior)
  759. return exteriors
  760. def flatten(self, geometry=None, reset=True, pathonly=False):
  761. """
  762. Creates a list of non-iterable linear geometry objects.
  763. Polygons are expanded into its exterior and interiors if specified.
  764. Results are placed in self.flat_geometry
  765. :param geometry: Shapely type or list or list of list of such.
  766. :param reset: Clears the contents of self.flat_geometry.
  767. :param pathonly: Expands polygons into linear elements.
  768. """
  769. if geometry is None:
  770. geometry = self.solid_geometry
  771. if reset:
  772. self.flat_geometry = []
  773. # ## If iterable, expand recursively.
  774. try:
  775. for geo in geometry:
  776. if geo is not None:
  777. self.flatten(geometry=geo,
  778. reset=False,
  779. pathonly=pathonly)
  780. # ## Not iterable, do the actual indexing and add.
  781. except TypeError:
  782. if pathonly and type(geometry) == Polygon:
  783. self.flat_geometry.append(geometry.exterior)
  784. self.flatten(geometry=geometry.interiors,
  785. reset=False,
  786. pathonly=True)
  787. else:
  788. self.flat_geometry.append(geometry)
  789. return self.flat_geometry
  790. # def make2Dstorage(self):
  791. #
  792. # self.flatten()
  793. #
  794. # def get_pts(o):
  795. # pts = []
  796. # if type(o) == Polygon:
  797. # g = o.exterior
  798. # pts += list(g.coords)
  799. # for i in o.interiors:
  800. # pts += list(i.coords)
  801. # else:
  802. # pts += list(o.coords)
  803. # return pts
  804. #
  805. # storage = FlatCAMRTreeStorage()
  806. # storage.get_points = get_pts
  807. # for shape in self.flat_geometry:
  808. # storage.insert(shape)
  809. # return storage
  810. # def flatten_to_paths(self, geometry=None, reset=True):
  811. # """
  812. # Creates a list of non-iterable linear geometry elements and
  813. # indexes them in rtree.
  814. #
  815. # :param geometry: Iterable geometry
  816. # :param reset: Wether to clear (True) or append (False) to self.flat_geometry
  817. # :return: self.flat_geometry, self.flat_geometry_rtree
  818. # """
  819. #
  820. # if geometry is None:
  821. # geometry = self.solid_geometry
  822. #
  823. # if reset:
  824. # self.flat_geometry = []
  825. #
  826. # # ## If iterable, expand recursively.
  827. # try:
  828. # for geo in geometry:
  829. # self.flatten_to_paths(geometry=geo, reset=False)
  830. #
  831. # # ## Not iterable, do the actual indexing and add.
  832. # except TypeError:
  833. # if type(geometry) == Polygon:
  834. # g = geometry.exterior
  835. # self.flat_geometry.append(g)
  836. #
  837. # # ## Add first and last points of the path to the index.
  838. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  839. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  840. #
  841. # for interior in geometry.interiors:
  842. # g = interior
  843. # self.flat_geometry.append(g)
  844. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  845. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  846. # else:
  847. # g = geometry
  848. # self.flat_geometry.append(g)
  849. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  850. # self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  851. #
  852. # return self.flat_geometry, self.flat_geometry_rtree
  853. def isolation_geometry(self, offset, geometry=None, iso_type=2, corner=None, follow=None, passes=0,
  854. prog_plot=False):
  855. """
  856. Creates contours around geometry at a given
  857. offset distance.
  858. :param offset: Offset distance.
  859. :type offset: float
  860. :param geometry The geometry to work with
  861. :param iso_type: type of isolation, can be 0 = exteriors or 1 = interiors or 2 = both (complete)
  862. :param corner: type of corner for the isolation:
  863. 0 = round; 1 = square; 2= beveled (line that connects the ends)
  864. :param follow: whether the geometry to be isolated is a follow_geometry
  865. :param passes: current pass out of possible multiple passes for which the isolation is done
  866. :param prog_plot: type of plotting: "normal" or "progressive"
  867. :return: The buffered geometry.
  868. :rtype: Shapely.MultiPolygon or Shapely.Polygon
  869. """
  870. if self.app.abort_flag:
  871. # graceful abort requested by the user
  872. raise grace
  873. geo_iso = []
  874. if follow:
  875. return geometry
  876. if geometry:
  877. working_geo = geometry
  878. else:
  879. working_geo = self.solid_geometry
  880. try:
  881. geo_len = len(working_geo)
  882. except TypeError:
  883. geo_len = 1
  884. old_disp_number = 0
  885. pol_nr = 0
  886. # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo
  887. try:
  888. for pol in working_geo:
  889. if self.app.abort_flag:
  890. # graceful abort requested by the user
  891. raise grace
  892. if offset == 0:
  893. temp_geo = pol
  894. else:
  895. corner_type = 1 if corner is None else corner
  896. temp_geo = pol.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type)
  897. geo_iso.append(temp_geo)
  898. pol_nr += 1
  899. # activity view update
  900. disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100]))
  901. if old_disp_number < disp_number <= 100:
  902. self.app.proc_container.update_view_text(' %s %d: %d%%' %
  903. (_("Pass"), int(passes + 1), int(disp_number)))
  904. old_disp_number = disp_number
  905. except TypeError:
  906. # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a
  907. # MultiPolygon (not an iterable)
  908. if offset == 0:
  909. temp_geo = working_geo
  910. else:
  911. corner_type = 1 if corner is None else corner
  912. temp_geo = working_geo.buffer(offset, int(self.geo_steps_per_circle), join_style=corner_type)
  913. geo_iso.append(temp_geo)
  914. self.app.proc_container.update_view_text(' %s' % _("Buffering"))
  915. geo_iso = unary_union(geo_iso)
  916. self.app.proc_container.update_view_text('')
  917. # end of replaced block
  918. if iso_type == 2:
  919. ret_geo = geo_iso
  920. elif iso_type == 0:
  921. self.app.proc_container.update_view_text(' %s' % _("Get Exteriors"))
  922. ret_geo = self.get_exteriors(geo_iso)
  923. elif iso_type == 1:
  924. self.app.proc_container.update_view_text(' %s' % _("Get Interiors"))
  925. ret_geo = self.get_interiors(geo_iso)
  926. else:
  927. log.debug("Geometry.isolation_geometry() --> Type of isolation not supported")
  928. return "fail"
  929. if prog_plot == 'progressive':
  930. for elem in ret_geo:
  931. self.plot_temp_shapes(elem)
  932. return ret_geo
  933. def flatten_list(self, obj_list):
  934. for item in obj_list:
  935. if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
  936. yield from self.flatten_list(item)
  937. else:
  938. yield item
  939. def import_svg(self, filename, object_type=None, flip=True, units=None):
  940. """
  941. Imports shapes from an SVG file into the object's geometry.
  942. :param filename: Path to the SVG file.
  943. :type filename: str
  944. :param object_type: parameter passed further along
  945. :param flip: Flip the vertically.
  946. :type flip: bool
  947. :param units: FlatCAM units
  948. :return: None
  949. """
  950. log.debug("camlib.Geometry.import_svg()")
  951. # Parse into list of shapely objects
  952. svg_tree = ET.parse(filename)
  953. svg_root = svg_tree.getroot()
  954. # Change origin to bottom left
  955. # h = float(svg_root.get('height'))
  956. # w = float(svg_root.get('width'))
  957. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  958. units = self.app.defaults['units'] if units is None else units
  959. res = self.app.defaults['geometry_circle_steps']
  960. factor = svgparse_viewbox(svg_root)
  961. geos = getsvggeo(svg_root, object_type, units=units, res=res, factor=factor)
  962. if flip:
  963. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  964. # trying to optimize the resulting geometry by merging contiguous lines
  965. geos = list(self.flatten_list(geos))
  966. geos_polys = []
  967. geos_lines = []
  968. for g in geos:
  969. if isinstance(g, Polygon):
  970. geos_polys.append(g)
  971. else:
  972. geos_lines.append(g)
  973. merged_lines = linemerge(geos_lines)
  974. geos = geos_polys
  975. try:
  976. for l in merged_lines:
  977. geos.append(l)
  978. except TypeError:
  979. geos.append(merged_lines)
  980. # Add to object
  981. if self.solid_geometry is None:
  982. self.solid_geometry = []
  983. if type(self.solid_geometry) is list:
  984. if type(geos) is list:
  985. self.solid_geometry += geos
  986. else:
  987. self.solid_geometry.append(geos)
  988. else: # It's shapely geometry
  989. self.solid_geometry = [self.solid_geometry, geos]
  990. # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
  991. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  992. geos_text = getsvgtext(svg_root, object_type, units=units)
  993. if geos_text is not None:
  994. geos_text_f = []
  995. if flip:
  996. # Change origin to bottom left
  997. for i in geos_text:
  998. __, minimy, __, maximy = i.bounds
  999. h2 = (maximy - minimy) * 0.5
  1000. geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
  1001. if geos_text_f:
  1002. self.solid_geometry = self.solid_geometry + geos_text_f
  1003. tooldia = float(self.app.defaults["geometry_cnctooldia"])
  1004. tooldia = float('%.*f' % (self.decimals, tooldia))
  1005. new_data = {k: v for k, v in self.options.items()}
  1006. self.tools.update({
  1007. 1: {
  1008. 'tooldia': tooldia,
  1009. 'offset': 'Path',
  1010. 'offset_value': 0.0,
  1011. 'type': 'Rough',
  1012. 'tool_type': 'C1',
  1013. 'data': deepcopy(new_data),
  1014. 'solid_geometry': self.solid_geometry
  1015. }
  1016. })
  1017. self.tools[1]['data']['name'] = self.options['name']
  1018. def import_dxf_as_geo(self, filename, units='MM'):
  1019. """
  1020. Imports shapes from an DXF file into the object's geometry.
  1021. :param filename: Path to the DXF file.
  1022. :type filename: str
  1023. :param units: Application units
  1024. :return: None
  1025. """
  1026. log.debug("Parsing DXF file geometry into a Geometry object solid geometry.")
  1027. # Parse into list of shapely objects
  1028. dxf = ezdxf.readfile(filename)
  1029. geos = getdxfgeo(dxf)
  1030. # trying to optimize the resulting geometry by merging contiguous lines
  1031. geos = list(self.flatten_list(geos))
  1032. geos_polys = []
  1033. geos_lines = []
  1034. for g in geos:
  1035. if isinstance(g, Polygon):
  1036. geos_polys.append(g)
  1037. else:
  1038. geos_lines.append(g)
  1039. merged_lines = linemerge(geos_lines)
  1040. geos = geos_polys
  1041. for l in list(map(LineString, zip(merged_lines.coords[:-1], merged_lines.coords[1:]))):
  1042. geos.append(l)
  1043. # Add to object
  1044. if self.solid_geometry is None:
  1045. self.solid_geometry = []
  1046. if type(self.solid_geometry) is list:
  1047. if type(geos) is list:
  1048. self.solid_geometry += geos
  1049. else:
  1050. self.solid_geometry.append(geos)
  1051. else: # It's shapely geometry
  1052. self.solid_geometry = [self.solid_geometry, geos]
  1053. tooldia = float(self.app.defaults["geometry_cnctooldia"])
  1054. tooldia = float('%.*f' % (self.decimals, tooldia))
  1055. new_data = {k: v for k, v in self.options.items()}
  1056. self.tools.update({
  1057. 1: {
  1058. 'tooldia': tooldia,
  1059. 'offset': 'Path',
  1060. 'offset_value': 0.0,
  1061. 'type': 'Rough',
  1062. 'tool_type': 'C1',
  1063. 'data': deepcopy(new_data),
  1064. 'solid_geometry': self.solid_geometry
  1065. }
  1066. })
  1067. self.tools[1]['data']['name'] = self.options['name']
  1068. # commented until this function is ready
  1069. # geos_text = getdxftext(dxf, object_type, units=units)
  1070. # if geos_text is not None:
  1071. # geos_text_f = []
  1072. # self.solid_geometry = [self.solid_geometry, geos_text_f]
  1073. def import_image(self, filename, flip=True, units='MM', dpi=96, mode='black', mask=None):
  1074. """
  1075. Imports shapes from an IMAGE file into the object's geometry.
  1076. :param filename: Path to the IMAGE file.
  1077. :type filename: str
  1078. :param flip: Flip the object vertically.
  1079. :type flip: bool
  1080. :param units: FlatCAM units
  1081. :type units: str
  1082. :param dpi: dots per inch on the imported image
  1083. :param mode: how to import the image: as 'black' or 'color'
  1084. :type mode: str
  1085. :param mask: level of detail for the import
  1086. :return: None
  1087. """
  1088. if mask is None:
  1089. mask = [128, 128, 128, 128]
  1090. scale_factor = 25.4 / dpi if units.lower() == 'mm' else 1 / dpi
  1091. geos = []
  1092. unscaled_geos = []
  1093. with rasterio.open(filename) as src:
  1094. # if filename.lower().rpartition('.')[-1] == 'bmp':
  1095. # red = green = blue = src.read(1)
  1096. # print("BMP")
  1097. # elif filename.lower().rpartition('.')[-1] == 'png':
  1098. # red, green, blue, alpha = src.read()
  1099. # elif filename.lower().rpartition('.')[-1] == 'jpg':
  1100. # red, green, blue = src.read()
  1101. red = green = blue = src.read(1)
  1102. try:
  1103. green = src.read(2)
  1104. except Exception:
  1105. pass
  1106. try:
  1107. blue = src.read(3)
  1108. except Exception:
  1109. pass
  1110. if mode == 'black':
  1111. mask_setting = red <= mask[0]
  1112. total = red
  1113. log.debug("Image import as monochrome.")
  1114. else:
  1115. mask_setting = (red <= mask[1]) + (green <= mask[2]) + (blue <= mask[3])
  1116. total = np.zeros(red.shape, dtype=np.float32)
  1117. for band in red, green, blue:
  1118. total += band
  1119. total /= 3
  1120. log.debug("Image import as colored. Thresholds are: R = %s , G = %s, B = %s" %
  1121. (str(mask[1]), str(mask[2]), str(mask[3])))
  1122. for geom, val in shapes(total, mask=mask_setting):
  1123. unscaled_geos.append(shape(geom))
  1124. for g in unscaled_geos:
  1125. geos.append(scale(g, scale_factor, scale_factor, origin=(0, 0)))
  1126. if flip:
  1127. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0))) for g in geos]
  1128. # Add to object
  1129. if self.solid_geometry is None:
  1130. self.solid_geometry = []
  1131. if type(self.solid_geometry) is list:
  1132. # self.solid_geometry.append(unary_union(geos))
  1133. if type(geos) is list:
  1134. self.solid_geometry += geos
  1135. else:
  1136. self.solid_geometry.append(geos)
  1137. else: # It's shapely geometry
  1138. self.solid_geometry = [self.solid_geometry, geos]
  1139. # flatten the self.solid_geometry list for import_svg() to import SVG as Gerber
  1140. self.solid_geometry = list(self.flatten_list(self.solid_geometry))
  1141. self.solid_geometry = unary_union(self.solid_geometry)
  1142. # self.solid_geometry = MultiPolygon(self.solid_geometry)
  1143. # self.solid_geometry = self.solid_geometry.buffer(0.00000001)
  1144. # self.solid_geometry = self.solid_geometry.buffer(-0.00000001)
  1145. def size(self):
  1146. """
  1147. Returns (width, height) of rectangular
  1148. bounds of geometry.
  1149. """
  1150. if self.solid_geometry is None:
  1151. log.warning("Solid_geometry not computed yet.")
  1152. return 0
  1153. bounds = self.bounds()
  1154. return bounds[2] - bounds[0], bounds[3] - bounds[1]
  1155. def get_empty_area(self, boundary=None):
  1156. """
  1157. Returns the complement of self.solid_geometry within
  1158. the given boundary polygon. If not specified, it defaults to
  1159. the rectangular bounding box of self.solid_geometry.
  1160. """
  1161. if boundary is None:
  1162. boundary = self.solid_geometry.envelope
  1163. return boundary.difference(self.solid_geometry)
  1164. def clear_polygon(self, polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True,
  1165. prog_plot=False):
  1166. """
  1167. Creates geometry inside a polygon for a tool to cover
  1168. the whole area.
  1169. This algorithm shrinks the edges of the polygon and takes
  1170. the resulting edges as toolpaths.
  1171. :param polygon: Polygon to clear.
  1172. :param tooldia: Diameter of the tool.
  1173. :param steps_per_circle: number of linear segments to be used to approximate a circle
  1174. :param overlap: Overlap of toolpasses.
  1175. :param connect: Draw lines between disjoint segments to
  1176. minimize tool lifts.
  1177. :param contour: Paint around the edges. Inconsequential in
  1178. this painting method.
  1179. :param prog_plot: boolean; if Ture use the progressive plotting
  1180. :return:
  1181. """
  1182. # log.debug("camlib.clear_polygon()")
  1183. assert type(polygon) == Polygon or type(polygon) == MultiPolygon, \
  1184. "Expected a Polygon or MultiPolygon, got %s" % type(polygon)
  1185. # ## The toolpaths
  1186. # Index first and last points in paths
  1187. def get_pts(o):
  1188. return [o.coords[0], o.coords[-1]]
  1189. geoms = FlatCAMRTreeStorage()
  1190. geoms.get_points = get_pts
  1191. # Can only result in a Polygon or MultiPolygon
  1192. # NOTE: The resulting polygon can be "empty".
  1193. current = polygon.buffer((-tooldia / 1.999999), int(steps_per_circle))
  1194. if current.area == 0:
  1195. # Otherwise, trying to to insert current.exterior == None
  1196. # into the FlatCAMStorage will fail.
  1197. # print("Area is None")
  1198. return None
  1199. # current can be a MultiPolygon
  1200. try:
  1201. for p in current:
  1202. geoms.insert(p.exterior)
  1203. for i in p.interiors:
  1204. geoms.insert(i)
  1205. # Not a Multipolygon. Must be a Polygon
  1206. except TypeError:
  1207. geoms.insert(current.exterior)
  1208. for i in current.interiors:
  1209. geoms.insert(i)
  1210. while True:
  1211. if self.app.abort_flag:
  1212. # graceful abort requested by the user
  1213. raise grace
  1214. # provide the app with a way to process the GUI events when in a blocking loop
  1215. QtWidgets.QApplication.processEvents()
  1216. # Can only result in a Polygon or MultiPolygon
  1217. current = current.buffer(-tooldia * (1 - overlap), int(steps_per_circle))
  1218. if current.area > 0:
  1219. # current can be a MultiPolygon
  1220. try:
  1221. for p in current:
  1222. geoms.insert(p.exterior)
  1223. for i in p.interiors:
  1224. geoms.insert(i)
  1225. if prog_plot:
  1226. self.plot_temp_shapes(p)
  1227. # Not a Multipolygon. Must be a Polygon
  1228. except TypeError:
  1229. geoms.insert(current.exterior)
  1230. if prog_plot:
  1231. self.plot_temp_shapes(current.exterior)
  1232. for i in current.interiors:
  1233. geoms.insert(i)
  1234. if prog_plot:
  1235. self.plot_temp_shapes(i)
  1236. else:
  1237. log.debug("camlib.Geometry.clear_polygon() --> Current Area is zero")
  1238. break
  1239. if prog_plot:
  1240. self.temp_shapes.redraw()
  1241. # Optimization: Reduce lifts
  1242. if connect:
  1243. # log.debug("Reducing tool lifts...")
  1244. geoms = Geometry.paint_connect(geoms, polygon, tooldia, int(steps_per_circle))
  1245. return geoms
  1246. def clear_polygon2(self, polygon_to_clear, tooldia, steps_per_circle, seedpoint=None, overlap=0.15,
  1247. connect=True, contour=True, prog_plot=False):
  1248. """
  1249. Creates geometry inside a polygon for a tool to cover
  1250. the whole area.
  1251. This algorithm starts with a seed point inside the polygon
  1252. and draws circles around it. Arcs inside the polygons are
  1253. valid cuts. Finalizes by cutting around the inside edge of
  1254. the polygon.
  1255. :param polygon_to_clear: Shapely.geometry.Polygon
  1256. :param steps_per_circle: how many linear segments to use to approximate a circle
  1257. :param tooldia: Diameter of the tool
  1258. :param seedpoint: Shapely.geometry.Point or None
  1259. :param overlap: Tool fraction overlap bewteen passes
  1260. :param connect: Connect disjoint segment to minumize tool lifts
  1261. :param contour: Cut countour inside the polygon.
  1262. :param prog_plot: boolean; if True use the progressive plotting
  1263. :return: List of toolpaths covering polygon.
  1264. :rtype: FlatCAMRTreeStorage | None
  1265. """
  1266. # log.debug("camlib.clear_polygon2()")
  1267. # Current buffer radius
  1268. radius = tooldia / 2 * (1 - overlap)
  1269. # ## The toolpaths
  1270. # Index first and last points in paths
  1271. def get_pts(o):
  1272. return [o.coords[0], o.coords[-1]]
  1273. geoms = FlatCAMRTreeStorage()
  1274. geoms.get_points = get_pts
  1275. # Path margin
  1276. path_margin = polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))
  1277. if path_margin.is_empty or path_margin is None:
  1278. return None
  1279. # Estimate good seedpoint if not provided.
  1280. if seedpoint is None:
  1281. seedpoint = path_margin.representative_point()
  1282. # Grow from seed until outside the box. The polygons will
  1283. # never have an interior, so take the exterior LinearRing.
  1284. while True:
  1285. if self.app.abort_flag:
  1286. # graceful abort requested by the user
  1287. raise grace
  1288. # provide the app with a way to process the GUI events when in a blocking loop
  1289. QtWidgets.QApplication.processEvents()
  1290. path = Point(seedpoint).buffer(radius, int(steps_per_circle)).exterior
  1291. path = path.intersection(path_margin)
  1292. # Touches polygon?
  1293. if path.is_empty:
  1294. break
  1295. else:
  1296. # geoms.append(path)
  1297. # geoms.insert(path)
  1298. # path can be a collection of paths.
  1299. try:
  1300. for p in path:
  1301. geoms.insert(p)
  1302. if prog_plot:
  1303. self.plot_temp_shapes(p)
  1304. except TypeError:
  1305. geoms.insert(path)
  1306. if prog_plot:
  1307. self.plot_temp_shapes(path)
  1308. if prog_plot:
  1309. self.temp_shapes.redraw()
  1310. radius += tooldia * (1 - overlap)
  1311. # Clean inside edges (contours) of the original polygon
  1312. if contour:
  1313. buffered_poly = autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle)))
  1314. outer_edges = [x.exterior for x in buffered_poly]
  1315. inner_edges = []
  1316. # Over resulting polygons
  1317. for x in buffered_poly:
  1318. for y in x.interiors: # Over interiors of each polygon
  1319. inner_edges.append(y)
  1320. # geoms += outer_edges + inner_edges
  1321. for g in outer_edges + inner_edges:
  1322. if g and not g.is_empty:
  1323. geoms.insert(g)
  1324. if prog_plot:
  1325. self.plot_temp_shapes(g)
  1326. if prog_plot:
  1327. self.temp_shapes.redraw()
  1328. # Optimization connect touching paths
  1329. # log.debug("Connecting paths...")
  1330. # geoms = Geometry.path_connect(geoms)
  1331. # Optimization: Reduce lifts
  1332. if connect:
  1333. # log.debug("Reducing tool lifts...")
  1334. geoms_conn = Geometry.paint_connect(geoms, polygon_to_clear, tooldia, steps_per_circle)
  1335. if geoms_conn:
  1336. return geoms_conn
  1337. return geoms
  1338. def clear_polygon3(self, polygon, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True,
  1339. prog_plot=False):
  1340. """
  1341. Creates geometry inside a polygon for a tool to cover
  1342. the whole area.
  1343. This algorithm draws horizontal lines inside the polygon.
  1344. :param polygon: The polygon being painted.
  1345. :type polygon: shapely.geometry.Polygon
  1346. :param tooldia: Tool diameter.
  1347. :param steps_per_circle: how many linear segments to use to approximate a circle
  1348. :param overlap: Tool path overlap percentage.
  1349. :param connect: Connect lines to avoid tool lifts.
  1350. :param contour: Paint around the edges.
  1351. :param prog_plot: boolean; if to use the progressive plotting
  1352. :return:
  1353. """
  1354. # log.debug("camlib.clear_polygon3()")
  1355. if not isinstance(polygon, Polygon):
  1356. log.debug("camlib.Geometry.clear_polygon3() --> Not a Polygon but %s" % str(type(polygon)))
  1357. return None
  1358. # ## The toolpaths
  1359. # Index first and last points in paths
  1360. def get_pts(o):
  1361. return [o.coords[0], o.coords[-1]]
  1362. geoms = FlatCAMRTreeStorage()
  1363. geoms.get_points = get_pts
  1364. lines_trimmed = []
  1365. # Bounding box
  1366. left, bot, right, top = polygon.bounds
  1367. try:
  1368. margin_poly = polygon.buffer(-tooldia / 1.99999999, (int(steps_per_circle)))
  1369. except Exception:
  1370. log.debug("camlib.Geometry.clear_polygon3() --> Could not buffer the Polygon")
  1371. return None
  1372. # decide the direction of the lines
  1373. if abs(left - right) >= abs(top - bot):
  1374. # First line
  1375. try:
  1376. y = top - tooldia / 1.99999999
  1377. while y > bot + tooldia / 1.999999999:
  1378. if self.app.abort_flag:
  1379. # graceful abort requested by the user
  1380. raise grace
  1381. # provide the app with a way to process the GUI events when in a blocking loop
  1382. QtWidgets.QApplication.processEvents()
  1383. line = LineString([(left, y), (right, y)])
  1384. line = line.intersection(margin_poly)
  1385. lines_trimmed.append(line)
  1386. y -= tooldia * (1 - overlap)
  1387. if prog_plot:
  1388. self.plot_temp_shapes(line)
  1389. self.temp_shapes.redraw()
  1390. # Last line
  1391. y = bot + tooldia / 2
  1392. line = LineString([(left, y), (right, y)])
  1393. line = line.intersection(margin_poly)
  1394. try:
  1395. for ll in line:
  1396. lines_trimmed.append(ll)
  1397. if prog_plot:
  1398. self.plot_temp_shapes(ll)
  1399. except TypeError:
  1400. lines_trimmed.append(line)
  1401. if prog_plot:
  1402. self.plot_temp_shapes(line)
  1403. except Exception as e:
  1404. log.debug('camlib.Geometry.clear_polygon3() Processing poly --> %s' % str(e))
  1405. return None
  1406. else:
  1407. # First line
  1408. try:
  1409. x = left + tooldia / 1.99999999
  1410. while x < right - tooldia / 1.999999999:
  1411. if self.app.abort_flag:
  1412. # graceful abort requested by the user
  1413. raise grace
  1414. # provide the app with a way to process the GUI events when in a blocking loop
  1415. QtWidgets.QApplication.processEvents()
  1416. line = LineString([(x, top), (x, bot)])
  1417. line = line.intersection(margin_poly)
  1418. lines_trimmed.append(line)
  1419. x += tooldia * (1 - overlap)
  1420. if prog_plot:
  1421. self.plot_temp_shapes(line)
  1422. self.temp_shapes.redraw()
  1423. # Last line
  1424. x = right + tooldia / 2
  1425. line = LineString([(x, top), (x, bot)])
  1426. line = line.intersection(margin_poly)
  1427. try:
  1428. for ll in line:
  1429. lines_trimmed.append(ll)
  1430. if prog_plot:
  1431. self.plot_temp_shapes(ll)
  1432. except TypeError:
  1433. lines_trimmed.append(line)
  1434. if prog_plot:
  1435. self.plot_temp_shapes(line)
  1436. except Exception as e:
  1437. log.debug('camlib.Geometry.clear_polygon3() Processing poly --> %s' % str(e))
  1438. return None
  1439. if prog_plot:
  1440. self.temp_shapes.redraw()
  1441. lines_trimmed = unary_union(lines_trimmed)
  1442. # Add lines to storage
  1443. try:
  1444. for line in lines_trimmed:
  1445. if isinstance(line, LineString) or isinstance(line, LinearRing):
  1446. if not line.is_empty:
  1447. geoms.insert(line)
  1448. else:
  1449. log.debug("camlib.Geometry.clear_polygon3(). Not a line: %s" % str(type(line)))
  1450. except TypeError:
  1451. # in case lines_trimmed are not iterable (Linestring, LinearRing)
  1452. if not lines_trimmed.is_empty:
  1453. geoms.insert(lines_trimmed)
  1454. # Add margin (contour) to storage
  1455. if contour:
  1456. try:
  1457. for poly in margin_poly:
  1458. if isinstance(poly, Polygon) and not poly.is_empty:
  1459. geoms.insert(poly.exterior)
  1460. if prog_plot:
  1461. self.plot_temp_shapes(poly.exterior)
  1462. for ints in poly.interiors:
  1463. geoms.insert(ints)
  1464. if prog_plot:
  1465. self.plot_temp_shapes(ints)
  1466. except TypeError:
  1467. if isinstance(margin_poly, Polygon) and not margin_poly.is_empty:
  1468. marg_ext = margin_poly.exterior
  1469. geoms.insert(marg_ext)
  1470. if prog_plot:
  1471. self.plot_temp_shapes(margin_poly.exterior)
  1472. for ints in margin_poly.interiors:
  1473. geoms.insert(ints)
  1474. if prog_plot:
  1475. self.plot_temp_shapes(ints)
  1476. if prog_plot:
  1477. self.temp_shapes.redraw()
  1478. # Optimization: Reduce lifts
  1479. if connect:
  1480. # log.debug("Reducing tool lifts...")
  1481. geoms_conn = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle)
  1482. if geoms_conn:
  1483. return geoms_conn
  1484. return geoms
  1485. def fill_with_lines(self, line, aperture_size, tooldia, steps_per_circle, overlap=0.15, connect=True, contour=True,
  1486. prog_plot=False):
  1487. """
  1488. Creates geometry of lines inside a polygon for a tool to cover
  1489. the whole area.
  1490. This algorithm draws parallel lines inside the polygon.
  1491. :param line: The target line that create painted polygon.
  1492. :param aperture_size: the size of the aperture that is used to draw the 'line' as a polygon
  1493. :type line: shapely.geometry.LineString or shapely.geometry.MultiLineString
  1494. :param tooldia: Tool diameter.
  1495. :param steps_per_circle: how many linear segments to use to approximate a circle
  1496. :param overlap: Tool path overlap percentage.
  1497. :param connect: Connect lines to avoid tool lifts.
  1498. :param contour: Paint around the edges.
  1499. :param prog_plot: boolean; if to use the progressive plotting
  1500. :return:
  1501. """
  1502. # log.debug("camlib.fill_with_lines()")
  1503. if not isinstance(line, LineString):
  1504. log.debug("camlib.Geometry.fill_with_lines() --> Not a LineString/MultiLineString but %s" % str(type(line)))
  1505. return None
  1506. # ## The toolpaths
  1507. # Index first and last points in paths
  1508. def get_pts(o):
  1509. return [o.coords[0], o.coords[-1]]
  1510. geoms = FlatCAMRTreeStorage()
  1511. geoms.get_points = get_pts
  1512. lines_trimmed = []
  1513. polygon = line.buffer(aperture_size / 2.0, int(steps_per_circle))
  1514. try:
  1515. margin_poly = polygon.buffer(-tooldia / 2.0, int(steps_per_circle))
  1516. except Exception:
  1517. log.debug("camlib.Geometry.fill_with_lines() --> Could not buffer the Polygon, tool diameter too high")
  1518. return None
  1519. # First line
  1520. try:
  1521. delta = 0
  1522. while delta < aperture_size / 2:
  1523. if self.app.abort_flag:
  1524. # graceful abort requested by the user
  1525. raise grace
  1526. # provide the app with a way to process the GUI events when in a blocking loop
  1527. QtWidgets.QApplication.processEvents()
  1528. new_line = line.parallel_offset(distance=delta, side='left', resolution=int(steps_per_circle))
  1529. new_line = new_line.intersection(margin_poly)
  1530. lines_trimmed.append(new_line)
  1531. new_line = line.parallel_offset(distance=delta, side='right', resolution=int(steps_per_circle))
  1532. new_line = new_line.intersection(margin_poly)
  1533. lines_trimmed.append(new_line)
  1534. delta += tooldia * (1 - overlap)
  1535. if prog_plot:
  1536. self.plot_temp_shapes(new_line)
  1537. self.temp_shapes.redraw()
  1538. # Last line
  1539. delta = (aperture_size / 2) - (tooldia / 2.00000001)
  1540. new_line = line.parallel_offset(distance=delta, side='left', resolution=int(steps_per_circle))
  1541. new_line = new_line.intersection(margin_poly)
  1542. except Exception as e:
  1543. log.debug('camlib.Geometry.fill_with_lines() Processing poly --> %s' % str(e))
  1544. return None
  1545. try:
  1546. for ll in new_line:
  1547. lines_trimmed.append(ll)
  1548. if prog_plot:
  1549. self.plot_temp_shapes(ll)
  1550. except TypeError:
  1551. lines_trimmed.append(new_line)
  1552. if prog_plot:
  1553. self.plot_temp_shapes(new_line)
  1554. new_line = line.parallel_offset(distance=delta, side='right', resolution=int(steps_per_circle))
  1555. new_line = new_line.intersection(margin_poly)
  1556. try:
  1557. for ll in new_line:
  1558. lines_trimmed.append(ll)
  1559. if prog_plot:
  1560. self.plot_temp_shapes(ll)
  1561. except TypeError:
  1562. lines_trimmed.append(new_line)
  1563. if prog_plot:
  1564. self.plot_temp_shapes(new_line)
  1565. if prog_plot:
  1566. self.temp_shapes.redraw()
  1567. lines_trimmed = unary_union(lines_trimmed)
  1568. # Add lines to storage
  1569. try:
  1570. for line in lines_trimmed:
  1571. if isinstance(line, LineString) or isinstance(line, LinearRing):
  1572. geoms.insert(line)
  1573. else:
  1574. log.debug("camlib.Geometry.fill_with_lines(). Not a line: %s" % str(type(line)))
  1575. except TypeError:
  1576. # in case lines_trimmed are not iterable (Linestring, LinearRing)
  1577. geoms.insert(lines_trimmed)
  1578. # Add margin (contour) to storage
  1579. if contour:
  1580. try:
  1581. for poly in margin_poly:
  1582. if isinstance(poly, Polygon) and not poly.is_empty:
  1583. geoms.insert(poly.exterior)
  1584. if prog_plot:
  1585. self.plot_temp_shapes(poly.exterior)
  1586. for ints in poly.interiors:
  1587. geoms.insert(ints)
  1588. if prog_plot:
  1589. self.plot_temp_shapes(ints)
  1590. except TypeError:
  1591. if isinstance(margin_poly, Polygon) and not margin_poly.is_empty:
  1592. marg_ext = margin_poly.exterior
  1593. geoms.insert(marg_ext)
  1594. if prog_plot:
  1595. self.plot_temp_shapes(margin_poly.exterior)
  1596. for ints in margin_poly.interiors:
  1597. geoms.insert(ints)
  1598. if prog_plot:
  1599. self.plot_temp_shapes(ints)
  1600. if prog_plot:
  1601. self.temp_shapes.redraw()
  1602. # Optimization: Reduce lifts
  1603. if connect:
  1604. # log.debug("Reducing tool lifts...")
  1605. geoms_conn = Geometry.paint_connect(geoms, polygon, tooldia, steps_per_circle)
  1606. if geoms_conn:
  1607. return geoms_conn
  1608. return geoms
  1609. def scale(self, xfactor, yfactor, point=None):
  1610. """
  1611. Scales all of the object's geometry by a given factor. Override
  1612. this method.
  1613. :param xfactor: Number by which to scale on X axis.
  1614. :type xfactor: float
  1615. :param yfactor: Number by which to scale on Y axis.
  1616. :type yfactor: float
  1617. :param point: point to be used as reference for scaling; a tuple
  1618. :return: None
  1619. :rtype: None
  1620. """
  1621. return
  1622. def offset(self, vect):
  1623. """
  1624. Offset the geometry by the given vector. Override this method.
  1625. :param vect: (x, y) vector by which to offset the object.
  1626. :type vect: tuple
  1627. :return: None
  1628. """
  1629. return
  1630. @staticmethod
  1631. def paint_connect(storage, boundary, tooldia, steps_per_circle, max_walk=None):
  1632. """
  1633. Connects paths that results in a connection segment that is
  1634. within the paint area. This avoids unnecessary tool lifting.
  1635. :param storage: Geometry to be optimized.
  1636. :type storage: FlatCAMRTreeStorage
  1637. :param boundary: Polygon defining the limits of the paintable area.
  1638. :type boundary: Polygon
  1639. :param tooldia: Tool diameter.
  1640. :rtype tooldia: float
  1641. :param steps_per_circle: how many linear segments to use to approximate a circle
  1642. :param max_walk: Maximum allowable distance without lifting tool.
  1643. :type max_walk: float or None
  1644. :return: Optimized geometry.
  1645. :rtype: FlatCAMRTreeStorage
  1646. """
  1647. # If max_walk is not specified, the maximum allowed is
  1648. # 10 times the tool diameter
  1649. max_walk = max_walk or 10 * tooldia
  1650. # Assuming geolist is a flat list of flat elements
  1651. # ## Index first and last points in paths
  1652. def get_pts(o):
  1653. return [o.coords[0], o.coords[-1]]
  1654. # storage = FlatCAMRTreeStorage()
  1655. # storage.get_points = get_pts
  1656. #
  1657. # for shape in geolist:
  1658. # if shape is not None:
  1659. # # Make LlinearRings into linestrings otherwise
  1660. # # When chaining the coordinates path is messed up.
  1661. # storage.insert(LineString(shape))
  1662. # #storage.insert(shape)
  1663. # ## Iterate over geometry paths getting the nearest each time.
  1664. # optimized_paths = []
  1665. optimized_paths = FlatCAMRTreeStorage()
  1666. optimized_paths.get_points = get_pts
  1667. path_count = 0
  1668. current_pt = (0, 0)
  1669. try:
  1670. pt, geo = storage.nearest(current_pt)
  1671. except StopIteration:
  1672. log.debug("camlib.Geometry.paint_connect(). Storage empty")
  1673. return None
  1674. storage.remove(geo)
  1675. geo = LineString(geo)
  1676. current_pt = geo.coords[-1]
  1677. try:
  1678. while True:
  1679. path_count += 1
  1680. # log.debug("Path %d" % path_count)
  1681. pt, candidate = storage.nearest(current_pt)
  1682. storage.remove(candidate)
  1683. candidate = LineString(candidate)
  1684. # If last point in geometry is the nearest
  1685. # then reverse coordinates.
  1686. # but prefer the first one if last == first
  1687. if pt != candidate.coords[0] and pt == candidate.coords[-1]:
  1688. # in place coordinates update deprecated in Shapely 2.0
  1689. # candidate.coords = list(candidate.coords)[::-1]
  1690. candidate = LineString(list(candidate.coords)[::-1])
  1691. # Straight line from current_pt to pt.
  1692. # Is the toolpath inside the geometry?
  1693. walk_path = LineString([current_pt, pt])
  1694. walk_cut = walk_path.buffer(tooldia / 2, int(steps_per_circle))
  1695. if walk_cut.within(boundary) and walk_path.length < max_walk:
  1696. # log.debug("Walk to path #%d is inside. Joining." % path_count)
  1697. # Completely inside. Append...
  1698. # in place coordinates update deprecated in Shapely 2.0
  1699. # geo.coords = list(geo.coords) + list(candidate.coords)
  1700. geo = LineString(list(geo.coords) + list(candidate.coords))
  1701. # try:
  1702. # last = optimized_paths[-1]
  1703. # last.coords = list(last.coords) + list(geo.coords)
  1704. # except IndexError:
  1705. # optimized_paths.append(geo)
  1706. else:
  1707. # Have to lift tool. End path.
  1708. # log.debug("Path #%d not within boundary. Next." % path_count)
  1709. # optimized_paths.append(geo)
  1710. optimized_paths.insert(geo)
  1711. geo = candidate
  1712. current_pt = geo.coords[-1]
  1713. # Next
  1714. # pt, geo = storage.nearest(current_pt)
  1715. except StopIteration: # Nothing left in storage.
  1716. # pass
  1717. optimized_paths.insert(geo)
  1718. return optimized_paths
  1719. @staticmethod
  1720. def path_connect(storage, origin=(0, 0)):
  1721. """
  1722. Simplifies paths in the FlatCAMRTreeStorage storage by
  1723. connecting paths that touch on their endpoints.
  1724. :param storage: Storage containing the initial paths.
  1725. :rtype storage: FlatCAMRTreeStorage
  1726. :param origin: tuple; point from which to calculate the nearest point
  1727. :return: Simplified storage.
  1728. :rtype: FlatCAMRTreeStorage
  1729. """
  1730. log.debug("path_connect()")
  1731. # ## Index first and last points in paths
  1732. def get_pts(o):
  1733. return [o.coords[0], o.coords[-1]]
  1734. #
  1735. # storage = FlatCAMRTreeStorage()
  1736. # storage.get_points = get_pts
  1737. #
  1738. # for shape in pathlist:
  1739. # if shape is not None:
  1740. # storage.insert(shape)
  1741. path_count = 0
  1742. pt, geo = storage.nearest(origin)
  1743. storage.remove(geo)
  1744. # optimized_geometry = [geo]
  1745. optimized_geometry = FlatCAMRTreeStorage()
  1746. optimized_geometry.get_points = get_pts
  1747. # optimized_geometry.insert(geo)
  1748. try:
  1749. while True:
  1750. path_count += 1
  1751. _, left = storage.nearest(geo.coords[0])
  1752. # If left touches geo, remove left from original
  1753. # storage and append to geo.
  1754. if type(left) == LineString:
  1755. if left.coords[0] == geo.coords[0]:
  1756. storage.remove(left)
  1757. # geo.coords = list(geo.coords)[::-1] + list(left.coords) # Shapely 2.0
  1758. geo = LineString(list(geo.coords)[::-1] + list(left.coords))
  1759. continue
  1760. if left.coords[-1] == geo.coords[0]:
  1761. storage.remove(left)
  1762. # geo.coords = list(left.coords) + list(geo.coords) # Shapely 2.0
  1763. geo = LineString(list(geo.coords)[::-1] + list(left.coords))
  1764. continue
  1765. if left.coords[0] == geo.coords[-1]:
  1766. storage.remove(left)
  1767. # geo.coords = list(geo.coords) + list(left.coords) # Shapely 2.0
  1768. geo = LineString(list(geo.coords) + list(left.coords))
  1769. continue
  1770. if left.coords[-1] == geo.coords[-1]:
  1771. storage.remove(left)
  1772. # geo.coords = list(geo.coords) + list(left.coords)[::-1] # Shapely 2.0
  1773. geo = LineString(list(geo.coords) + list(left.coords)[::-1])
  1774. continue
  1775. _, right = storage.nearest(geo.coords[-1])
  1776. # If right touches geo, remove left from original
  1777. # storage and append to geo.
  1778. if type(right) == LineString:
  1779. if right.coords[0] == geo.coords[-1]:
  1780. storage.remove(right)
  1781. # geo.coords = list(geo.coords) + list(right.coords) # Shapely 2.0
  1782. geo = LineString(list(geo.coords) + list(right.coords))
  1783. continue
  1784. if right.coords[-1] == geo.coords[-1]:
  1785. storage.remove(right)
  1786. # geo.coords = list(geo.coords) + list(right.coords)[::-1] # Shapely 2.0
  1787. geo = LineString(list(geo.coords) + list(right.coords)[::-1])
  1788. continue
  1789. if right.coords[0] == geo.coords[0]:
  1790. storage.remove(right)
  1791. # geo.coords = list(geo.coords)[::-1] + list(right.coords) # Shapely 2.0
  1792. geo = LineString(list(geo.coords)[::-1] + list(right.coords))
  1793. continue
  1794. if right.coords[-1] == geo.coords[0]:
  1795. storage.remove(right)
  1796. # geo.coords = list(left.coords) + list(geo.coords) # Shapely 2.0
  1797. geo = LineString(list(left.coords) + list(geo.coords))
  1798. continue
  1799. # right is either a LinearRing or it does not connect
  1800. # to geo (nothing left to connect to geo), so we continue
  1801. # with right as geo.
  1802. storage.remove(right)
  1803. if type(right) == LinearRing:
  1804. optimized_geometry.insert(right)
  1805. else:
  1806. # Cannot extend geo any further. Put it away.
  1807. optimized_geometry.insert(geo)
  1808. # Continue with right.
  1809. geo = right
  1810. except StopIteration: # Nothing found in storage.
  1811. optimized_geometry.insert(geo)
  1812. # print path_count
  1813. log.debug("path_count = %d" % path_count)
  1814. return optimized_geometry
  1815. def convert_units(self, obj_units):
  1816. """
  1817. Converts the units of the object to ``units`` by scaling all
  1818. the geometry appropriately. This call ``scale()``. Don't call
  1819. it again in descendents.
  1820. :param obj_units: "IN" or "MM"
  1821. :type obj_units: str
  1822. :return: Scaling factor resulting from unit change.
  1823. :rtype: float
  1824. """
  1825. if obj_units.upper() == self.units.upper():
  1826. log.debug("camlib.Geometry.convert_units() --> Factor: 1")
  1827. return 1.0
  1828. if obj_units.upper() == "MM":
  1829. factor = 25.4
  1830. log.debug("camlib.Geometry.convert_units() --> Factor: 25.4")
  1831. elif obj_units.upper() == "IN":
  1832. factor = 1 / 25.4
  1833. log.debug("camlib.Geometry.convert_units() --> Factor: %s" % str(1 / 25.4))
  1834. else:
  1835. log.error("Unsupported units: %s" % str(obj_units))
  1836. log.debug("camlib.Geometry.convert_units() --> Factor: 1")
  1837. return 1.0
  1838. self.units = obj_units
  1839. self.scale(factor, factor)
  1840. self.file_units_factor = factor
  1841. return factor
  1842. def to_dict(self):
  1843. """
  1844. Returns a representation of the object as a dictionary.
  1845. Attributes to include are listed in ``self.ser_attrs``.
  1846. :return: A dictionary-encoded copy of the object.
  1847. :rtype: dict
  1848. """
  1849. d = {}
  1850. for attr in self.ser_attrs:
  1851. d[attr] = getattr(self, attr)
  1852. return d
  1853. def from_dict(self, d):
  1854. """
  1855. Sets object's attributes from a dictionary.
  1856. Attributes to include are listed in ``self.ser_attrs``.
  1857. This method will look only for only and all the
  1858. attributes in ``self.ser_attrs``. They must all
  1859. be present. Use only for deserializing saved
  1860. objects.
  1861. :param d: Dictionary of attributes to set in the object.
  1862. :type d: dict
  1863. :return: None
  1864. """
  1865. for attr in self.ser_attrs:
  1866. setattr(self, attr, d[attr])
  1867. def union(self):
  1868. """
  1869. Runs a unary_union on the list of objects in
  1870. solid_geometry.
  1871. :return: None
  1872. """
  1873. self.solid_geometry = [unary_union(self.solid_geometry)]
  1874. def export_svg(self, scale_stroke_factor=0.00,
  1875. scale_factor_x=None, scale_factor_y=None,
  1876. skew_factor_x=None, skew_factor_y=None,
  1877. skew_reference='center', scale_reference='center',
  1878. mirror=None):
  1879. """
  1880. Exports the Geometry Object as a SVG Element
  1881. :return: SVG Element
  1882. """
  1883. # Make sure we see a Shapely Geometry class and not a list
  1884. if self.kind.lower() == 'geometry':
  1885. flat_geo = []
  1886. if self.multigeo:
  1887. for tool in self.tools:
  1888. flat_geo += self.flatten(self.tools[tool]['solid_geometry'])
  1889. geom_svg = unary_union(flat_geo)
  1890. else:
  1891. geom_svg = unary_union(self.flatten())
  1892. else:
  1893. geom_svg = unary_union(self.flatten())
  1894. skew_ref = 'center'
  1895. if skew_reference != 'center':
  1896. xmin, ymin, xmax, ymax = geom_svg.bounds
  1897. if skew_reference == 'topleft':
  1898. skew_ref = (xmin, ymax)
  1899. elif skew_reference == 'bottomleft':
  1900. skew_ref = (xmin, ymin)
  1901. elif skew_reference == 'topright':
  1902. skew_ref = (xmax, ymax)
  1903. elif skew_reference == 'bottomright':
  1904. skew_ref = (xmax, ymin)
  1905. geom = geom_svg
  1906. if scale_factor_x and not scale_factor_y:
  1907. geom = affinity.scale(geom_svg, scale_factor_x, 1.0, origin=scale_reference)
  1908. elif not scale_factor_x and scale_factor_y:
  1909. geom = affinity.scale(geom_svg, 1.0, scale_factor_y, origin=scale_reference)
  1910. elif scale_factor_x and scale_factor_y:
  1911. geom = affinity.scale(geom_svg, scale_factor_x, scale_factor_y, origin=scale_reference)
  1912. if skew_factor_x and not skew_factor_y:
  1913. geom = affinity.skew(geom_svg, skew_factor_x, 0.0, origin=skew_ref)
  1914. elif not skew_factor_x and skew_factor_y:
  1915. geom = affinity.skew(geom_svg, 0.0, skew_factor_y, origin=skew_ref)
  1916. elif skew_factor_x and skew_factor_y:
  1917. geom = affinity.skew(geom_svg, skew_factor_x, skew_factor_y, origin=skew_ref)
  1918. if mirror:
  1919. if mirror == 'x':
  1920. geom = affinity.scale(geom_svg, 1.0, -1.0)
  1921. if mirror == 'y':
  1922. geom = affinity.scale(geom_svg, -1.0, 1.0)
  1923. if mirror == 'both':
  1924. geom = affinity.scale(geom_svg, -1.0, -1.0)
  1925. # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
  1926. # If 0 or less which is invalid then default to 0.01
  1927. # This value appears to work for zooming, and getting the output svg line width
  1928. # to match that viewed on screen with FlatCam
  1929. # MS: I choose a factor of 0.01 so the scale is right for PCB UV film
  1930. if scale_stroke_factor <= 0:
  1931. scale_stroke_factor = 0.01
  1932. # Convert to a SVG
  1933. svg_elem = geom.svg(scale_factor=scale_stroke_factor)
  1934. return svg_elem
  1935. def mirror(self, axis, point):
  1936. """
  1937. Mirrors the object around a specified axis passign through
  1938. the given point.
  1939. :param axis: "X" or "Y" indicates around which axis to mirror.
  1940. :type axis: str
  1941. :param point: [x, y] point belonging to the mirror axis.
  1942. :type point: list
  1943. :return: None
  1944. """
  1945. log.debug("camlib.Geometry.mirror()")
  1946. px, py = point
  1947. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1948. def mirror_geom(obj):
  1949. if type(obj) is list:
  1950. new_obj = []
  1951. for g in obj:
  1952. new_obj.append(mirror_geom(g))
  1953. return new_obj
  1954. else:
  1955. try:
  1956. self.el_count += 1
  1957. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  1958. if self.old_disp_number < disp_number <= 100:
  1959. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  1960. self.old_disp_number = disp_number
  1961. return affinity.scale(obj, xscale, yscale, origin=(px, py))
  1962. except AttributeError:
  1963. return obj
  1964. try:
  1965. if self.multigeo is True:
  1966. for tool in self.tools:
  1967. # variables to display the percentage of work done
  1968. self.geo_len = 0
  1969. try:
  1970. self.geo_len = len(self.tools[tool]['solid_geometry'])
  1971. except TypeError:
  1972. self.geo_len = 1
  1973. self.old_disp_number = 0
  1974. self.el_count = 0
  1975. self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
  1976. else:
  1977. # variables to display the percentage of work done
  1978. self.geo_len = 0
  1979. try:
  1980. self.geo_len = len(self.solid_geometry)
  1981. except TypeError:
  1982. self.geo_len = 1
  1983. self.old_disp_number = 0
  1984. self.el_count = 0
  1985. self.solid_geometry = mirror_geom(self.solid_geometry)
  1986. self.app.inform.emit('[success] %s...' % _('Object was mirrored'))
  1987. except AttributeError:
  1988. self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected.")))
  1989. self.app.proc_container.new_text = ''
  1990. def rotate(self, angle, point):
  1991. """
  1992. Rotate an object by an angle (in degrees) around the provided coordinates.
  1993. :param angle:
  1994. The angle of rotation are specified in degrees (default). Positive angles are
  1995. counter-clockwise and negative are clockwise rotations.
  1996. :param point:
  1997. The point of origin can be a keyword 'center' for the bounding box
  1998. center (default), 'centroid' for the geometry's centroid, a Point object
  1999. or a coordinate tuple (x0, y0).
  2000. See shapely manual for more information: http://toblerity.org/shapely/manual.html#affine-transformations
  2001. """
  2002. log.debug("camlib.Geometry.rotate()")
  2003. px, py = point
  2004. def rotate_geom(obj):
  2005. try:
  2006. new_obj = []
  2007. for g in obj:
  2008. new_obj.append(rotate_geom(g))
  2009. return new_obj
  2010. except TypeError:
  2011. try:
  2012. self.el_count += 1
  2013. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  2014. if self.old_disp_number < disp_number <= 100:
  2015. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  2016. self.old_disp_number = disp_number
  2017. return affinity.rotate(obj, angle, origin=(px, py))
  2018. except AttributeError:
  2019. return obj
  2020. try:
  2021. if self.multigeo is True:
  2022. for tool in self.tools:
  2023. # variables to display the percentage of work done
  2024. self.geo_len = 0
  2025. try:
  2026. self.geo_len = len(self.tools[tool]['solid_geometry'])
  2027. except TypeError:
  2028. self.geo_len = 1
  2029. self.old_disp_number = 0
  2030. self.el_count = 0
  2031. self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
  2032. else:
  2033. # variables to display the percentage of work done
  2034. self.geo_len = 0
  2035. try:
  2036. self.geo_len = len(self.solid_geometry)
  2037. except TypeError:
  2038. self.geo_len = 1
  2039. self.old_disp_number = 0
  2040. self.el_count = 0
  2041. self.solid_geometry = rotate_geom(self.solid_geometry)
  2042. self.app.inform.emit('[success] %s...' % _('Object was rotated'))
  2043. except AttributeError:
  2044. self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected.")))
  2045. self.app.proc_container.new_text = ''
  2046. def skew(self, angle_x, angle_y, point):
  2047. """
  2048. Shear/Skew the geometries of an object by angles along x and y dimensions.
  2049. :param angle_x:
  2050. :param angle_y:
  2051. angle_x, angle_y : float, float
  2052. The shear angle(s) for the x and y axes respectively. These can be
  2053. specified in either degrees (default) or radians by setting
  2054. use_radians=True.
  2055. :param point: Origin point for Skew
  2056. point: tuple of coordinates (x,y)
  2057. See shapely manual for more information: http://toblerity.org/shapely/manual.html#affine-transformations
  2058. """
  2059. log.debug("camlib.Geometry.skew()")
  2060. px, py = point
  2061. def skew_geom(obj):
  2062. try:
  2063. new_obj = []
  2064. for g in obj:
  2065. new_obj.append(skew_geom(g))
  2066. return new_obj
  2067. except TypeError:
  2068. try:
  2069. self.el_count += 1
  2070. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  2071. if self.old_disp_number < disp_number <= 100:
  2072. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  2073. self.old_disp_number = disp_number
  2074. return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
  2075. except AttributeError:
  2076. return obj
  2077. try:
  2078. if self.multigeo is True:
  2079. for tool in self.tools:
  2080. # variables to display the percentage of work done
  2081. self.geo_len = 0
  2082. try:
  2083. self.geo_len = len(self.tools[tool]['solid_geometry'])
  2084. except TypeError:
  2085. self.geo_len = 1
  2086. self.old_disp_number = 0
  2087. self.el_count = 0
  2088. self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
  2089. else:
  2090. # variables to display the percentage of work done
  2091. self.geo_len = 0
  2092. try:
  2093. self.geo_len = len(self.solid_geometry)
  2094. except TypeError:
  2095. self.geo_len = 1
  2096. self.old_disp_number = 0
  2097. self.el_count = 0
  2098. self.solid_geometry = skew_geom(self.solid_geometry)
  2099. self.app.inform.emit('[success] %s...' % _('Object was skewed'))
  2100. except AttributeError:
  2101. self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected.")))
  2102. self.app.proc_container.new_text = ''
  2103. # if type(self.solid_geometry) == list:
  2104. # self.solid_geometry = [affinity.skew(g, angle_x, angle_y, origin=(px, py))
  2105. # for g in self.solid_geometry]
  2106. # else:
  2107. # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
  2108. # origin=(px, py))
  2109. def buffer(self, distance, join, factor):
  2110. """
  2111. :param distance: if 'factor' is True then distance is the factor
  2112. :param join: The kind of join used by the shapely buffer method: round, square or bevel
  2113. :param factor: True or False (None)
  2114. :return:
  2115. """
  2116. log.debug("camlib.Geometry.buffer()")
  2117. if distance == 0:
  2118. return
  2119. def buffer_geom(obj):
  2120. if type(obj) is list:
  2121. new_obj = []
  2122. for g in obj:
  2123. new_obj.append(buffer_geom(g))
  2124. return new_obj
  2125. else:
  2126. try:
  2127. self.el_count += 1
  2128. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  2129. if self.old_disp_number < disp_number <= 100:
  2130. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  2131. self.old_disp_number = disp_number
  2132. if factor is None:
  2133. return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
  2134. else:
  2135. return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
  2136. except AttributeError:
  2137. return obj
  2138. try:
  2139. if self.multigeo is True:
  2140. for tool in self.tools:
  2141. # variables to display the percentage of work done
  2142. self.geo_len = 0
  2143. try:
  2144. self.geo_len += len(self.tools[tool]['solid_geometry'])
  2145. except TypeError:
  2146. self.geo_len += 1
  2147. self.old_disp_number = 0
  2148. self.el_count = 0
  2149. res = buffer_geom(self.tools[tool]['solid_geometry'])
  2150. try:
  2151. __ = iter(res)
  2152. self.tools[tool]['solid_geometry'] = res
  2153. except TypeError:
  2154. self.tools[tool]['solid_geometry'] = [res]
  2155. # variables to display the percentage of work done
  2156. self.geo_len = 0
  2157. try:
  2158. self.geo_len = len(self.solid_geometry)
  2159. except TypeError:
  2160. self.geo_len = 1
  2161. self.old_disp_number = 0
  2162. self.el_count = 0
  2163. self.solid_geometry = buffer_geom(self.solid_geometry)
  2164. self.app.inform.emit('[success] %s...' % _('Object was buffered'))
  2165. except AttributeError:
  2166. self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected.")))
  2167. self.app.proc_container.new_text = ''
  2168. class AttrDict(dict):
  2169. def __init__(self, *args, **kwargs):
  2170. super(AttrDict, self).__init__(*args, **kwargs)
  2171. self.__dict__ = self
  2172. class CNCjob(Geometry):
  2173. """
  2174. Represents work to be done by a CNC machine.
  2175. *ATTRIBUTES*
  2176. * ``gcode_parsed`` (list): Each is a dictionary:
  2177. ===================== =========================================
  2178. Key Value
  2179. ===================== =========================================
  2180. geom (Shapely.LineString) Tool path (XY plane)
  2181. kind (string) "AB", A is "T" (travel) or
  2182. "C" (cut). B is "F" (fast) or "S" (slow).
  2183. ===================== =========================================
  2184. """
  2185. defaults = {
  2186. "global_zdownrate": None,
  2187. "pp_geometry_name": 'default',
  2188. "pp_excellon_name": 'default',
  2189. "excellon_optimization_type": "B",
  2190. }
  2191. settings = QtCore.QSettings("Open Source", "FlatCAM")
  2192. if settings.contains("machinist"):
  2193. machinist_setting = settings.value('machinist', type=int)
  2194. else:
  2195. machinist_setting = 0
  2196. def __init__(self,
  2197. units="in", kind="generic", tooldia=0.0,
  2198. z_cut=-0.002, z_move=0.1,
  2199. feedrate=3.0, feedrate_z=3.0, feedrate_rapid=3.0, feedrate_probe=3.0,
  2200. pp_geometry_name='default', pp_excellon_name='default',
  2201. depthpercut=0.1, z_pdepth=-0.02,
  2202. spindlespeed=None, spindledir='CW', dwell=True, dwelltime=1000,
  2203. toolchangez=0.787402, toolchange_xy='0.0,0.0',
  2204. endz=2.0, endxy='',
  2205. segx=None,
  2206. segy=None,
  2207. steps_per_circle=None):
  2208. self.decimals = self.app.decimals
  2209. # Used when parsing G-code arcs
  2210. self.steps_per_circle = steps_per_circle if steps_per_circle is not None else \
  2211. int(self.app.defaults['cncjob_steps_per_circle'])
  2212. Geometry.__init__(self, geo_steps_per_circle=self.steps_per_circle)
  2213. self.kind = kind
  2214. self.units = units
  2215. self.z_cut = z_cut
  2216. self.multidepth = False
  2217. self.z_depthpercut = depthpercut
  2218. self.z_move = z_move
  2219. self.feedrate = feedrate
  2220. self.z_feedrate = feedrate_z
  2221. self.feedrate_rapid = feedrate_rapid
  2222. self.tooldia = tooldia
  2223. self.toolC = tooldia
  2224. self.toolchange = False
  2225. self.z_toolchange = toolchangez
  2226. self.xy_toolchange = toolchange_xy
  2227. self.toolchange_xy_type = None
  2228. self.startz = None
  2229. self.z_end = endz
  2230. self.xy_end = endxy
  2231. self.extracut = False
  2232. self.extracut_length = None
  2233. self.tolerance = self.drawing_tolerance
  2234. # used by the self.generate_from_excellon_by_tool() method
  2235. # but set directly before the actual usage of the method with obj.excellon_optimization_type = value
  2236. self.excellon_optimization_type = 'No'
  2237. # if set True then the GCode generation will use UI; used in Excellon GVode for now
  2238. self.use_ui = False
  2239. self.unitcode = {"IN": "G20", "MM": "G21"}
  2240. self.feedminutecode = "G94"
  2241. # self.absolutecode = "G90"
  2242. # self.incrementalcode = "G91"
  2243. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  2244. self.gcode = ""
  2245. self.gcode_parsed = None
  2246. self.pp_geometry_name = pp_geometry_name
  2247. self.pp_geometry = self.app.preprocessors[self.pp_geometry_name]
  2248. self.pp_excellon_name = pp_excellon_name
  2249. self.pp_excellon = self.app.preprocessors[self.pp_excellon_name]
  2250. self.pp_solderpaste_name = None
  2251. # Controls if the move from Z_Toolchange to Z_Move is done fast with G0 or normally with G1
  2252. self.f_plunge = None
  2253. # Controls if the move from Z_Cutto Z_Move is done fast with G0 or G1 until zero and then G0 to Z_move
  2254. self.f_retract = None
  2255. # how much depth the probe can probe before error
  2256. self.z_pdepth = z_pdepth if z_pdepth else None
  2257. # the feedrate(speed) with which the probel travel while probing
  2258. self.feedrate_probe = feedrate_probe if feedrate_probe else None
  2259. self.spindlespeed = spindlespeed
  2260. self.spindledir = spindledir
  2261. self.dwell = dwell
  2262. self.dwelltime = dwelltime
  2263. self.segx = float(segx) if segx is not None else 0.0
  2264. self.segy = float(segy) if segy is not None else 0.0
  2265. self.input_geometry_bounds = None
  2266. self.oldx = None
  2267. self.oldy = None
  2268. self.tool = 0.0
  2269. self.measured_distance = 0.0
  2270. self.measured_down_distance = 0.0
  2271. self.measured_up_to_zero_distance = 0.0
  2272. self.measured_lift_distance = 0.0
  2273. # here store the travelled distance
  2274. self.travel_distance = 0.0
  2275. # here store the routing time
  2276. self.routing_time = 0.0
  2277. # store here the Excellon source object tools to be accessible locally
  2278. self.exc_tools = None
  2279. # search for toolchange parameters in the Toolchange Custom Code
  2280. self.re_toolchange_custom = re.compile(r'(%[a-zA-Z0-9\-_]+%)')
  2281. # search for toolchange code: M6
  2282. self.re_toolchange = re.compile(r'^\s*(M6)$')
  2283. # Attributes to be included in serialization
  2284. # Always append to it because it carries contents
  2285. # from Geometry.
  2286. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'z_toolchange', 'feedrate', 'z_feedrate', 'feedrate_rapid',
  2287. 'tooldia', 'gcode', 'input_geometry_bounds', 'gcode_parsed', 'steps_per_circle',
  2288. 'z_depthpercut', 'spindlespeed', 'dwell', 'dwelltime']
  2289. @property
  2290. def postdata(self):
  2291. """
  2292. This will return all the attributes of the class in the form of a dictionary
  2293. :return: Class attributes
  2294. :rtype: dict
  2295. """
  2296. return self.__dict__
  2297. def convert_units(self, units):
  2298. """
  2299. Will convert the parameters in the class that are relevant, from metric to imperial and reverse
  2300. :param units: FlatCAM units
  2301. :type units: str
  2302. :return: conversion factor
  2303. :rtype: float
  2304. """
  2305. log.debug("camlib.CNCJob.convert_units()")
  2306. factor = Geometry.convert_units(self, units)
  2307. self.z_cut = float(self.z_cut) * factor
  2308. self.z_move *= factor
  2309. self.feedrate *= factor
  2310. self.z_feedrate *= factor
  2311. self.feedrate_rapid *= factor
  2312. self.tooldia *= factor
  2313. self.z_toolchange *= factor
  2314. self.z_end *= factor
  2315. self.z_depthpercut = float(self.z_depthpercut) * factor
  2316. return factor
  2317. def doformat(self, fun, **kwargs):
  2318. return self.doformat2(fun, **kwargs) + "\n"
  2319. def doformat2(self, fun, **kwargs):
  2320. """
  2321. This method will call one of the current preprocessor methods having as parameters all the attributes of
  2322. current class to which will add the kwargs parameters
  2323. :param fun: One of the methods inside the preprocessor classes which get loaded here in the 'p' object
  2324. :type fun: class 'function'
  2325. :param kwargs: keyword args which will update attributes of the current class
  2326. :type kwargs: dict
  2327. :return: Gcode line
  2328. :rtype: str
  2329. """
  2330. attributes = AttrDict()
  2331. attributes.update(self.postdata)
  2332. attributes.update(kwargs)
  2333. try:
  2334. returnvalue = fun(attributes)
  2335. return returnvalue
  2336. except Exception:
  2337. self.app.log.error('Exception occurred within a preprocessor: ' + traceback.format_exc())
  2338. return ''
  2339. def parse_custom_toolchange_code(self, data):
  2340. """
  2341. Will parse a text and get a toolchange sequence in text format suitable to be included in a Gcode file.
  2342. The '%' symbol is used to surround class variables name and must be removed in the returned string.
  2343. After that, the class variables (attributes) are replaced with the current values. The result is returned.
  2344. :param data: Toolchange sequence
  2345. :type data: str
  2346. :return: Processed toolchange sequence
  2347. :rtype: str
  2348. """
  2349. text = data
  2350. match_list = self.re_toolchange_custom.findall(text)
  2351. if match_list:
  2352. for match in match_list:
  2353. command = match.strip('%')
  2354. try:
  2355. value = getattr(self, command)
  2356. except AttributeError:
  2357. self.app.inform.emit('[ERROR] %s: %s' %
  2358. (_("There is no such parameter"), str(match)))
  2359. log.debug("CNCJob.parse_custom_toolchange_code() --> AttributeError ")
  2360. return 'fail'
  2361. text = text.replace(match, str(value))
  2362. return text
  2363. # Distance callback
  2364. class CreateDistanceCallback(object):
  2365. """Create callback to calculate distances between points."""
  2366. def __init__(self, locs, manager):
  2367. self.manager = manager
  2368. self.matrix = {}
  2369. if locs:
  2370. size = len(locs)
  2371. for from_node in range(size):
  2372. self.matrix[from_node] = {}
  2373. for to_node in range(size):
  2374. if from_node == to_node:
  2375. self.matrix[from_node][to_node] = 0
  2376. else:
  2377. x1 = locs[from_node][0]
  2378. y1 = locs[from_node][1]
  2379. x2 = locs[to_node][0]
  2380. y2 = locs[to_node][1]
  2381. self.matrix[from_node][to_node] = distance_euclidian(x1, y1, x2, y2)
  2382. # def Distance(self, from_node, to_node):
  2383. # return int(self.matrix[from_node][to_node])
  2384. def Distance(self, from_index, to_index):
  2385. # Convert from routing variable Index to distance matrix NodeIndex.
  2386. from_node = self.manager.IndexToNode(from_index)
  2387. to_node = self.manager.IndexToNode(to_index)
  2388. return self.matrix[from_node][to_node]
  2389. @staticmethod
  2390. def create_tool_data_array(points):
  2391. # Create the data.
  2392. return [(pt.coords.xy[0][0], pt.coords.xy[1][0]) for pt in points]
  2393. def optimized_ortools_meta(self, locations, start=None, opt_time=0):
  2394. optimized_path = []
  2395. tsp_size = len(locations)
  2396. num_routes = 1 # The number of routes, which is 1 in the TSP.
  2397. # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
  2398. depot = 0 if start is None else start
  2399. # Create routing model.
  2400. if tsp_size == 0:
  2401. log.warning('OR-tools metaheuristics - Specify an instance greater than 0.')
  2402. return optimized_path
  2403. manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
  2404. routing = pywrapcp.RoutingModel(manager)
  2405. search_parameters = pywrapcp.DefaultRoutingSearchParameters()
  2406. search_parameters.local_search_metaheuristic = (
  2407. routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
  2408. # Set search time limit in milliseconds.
  2409. if float(opt_time) != 0:
  2410. search_parameters.time_limit.seconds = int(
  2411. float(opt_time))
  2412. else:
  2413. search_parameters.time_limit.seconds = 3
  2414. # Callback to the distance function. The callback takes two
  2415. # arguments (the from and to node indices) and returns the distance between them.
  2416. dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager)
  2417. # if there are no distances then go to the next tool
  2418. if not dist_between_locations:
  2419. return
  2420. dist_callback = dist_between_locations.Distance
  2421. transit_callback_index = routing.RegisterTransitCallback(dist_callback)
  2422. routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
  2423. # Solve, returns a solution if any.
  2424. assignment = routing.SolveWithParameters(search_parameters)
  2425. if assignment:
  2426. # Solution cost.
  2427. log.info("OR-tools metaheuristics - Total distance: " + str(assignment.ObjectiveValue()))
  2428. # Inspect solution.
  2429. # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
  2430. route_number = 0
  2431. node = routing.Start(route_number)
  2432. start_node = node
  2433. while not routing.IsEnd(node):
  2434. if self.app.abort_flag:
  2435. # graceful abort requested by the user
  2436. raise grace
  2437. optimized_path.append(node)
  2438. node = assignment.Value(routing.NextVar(node))
  2439. else:
  2440. log.warning('OR-tools metaheuristics - No solution found.')
  2441. return optimized_path
  2442. # ############################################# ##
  2443. def optimized_ortools_basic(self, locations, start=None):
  2444. optimized_path = []
  2445. tsp_size = len(locations)
  2446. num_routes = 1 # The number of routes, which is 1 in the TSP.
  2447. # Nodes are indexed from 0 to tsp_size - 1. The depot is the starting node of the route.
  2448. depot = 0 if start is None else start
  2449. # Create routing model.
  2450. if tsp_size == 0:
  2451. log.warning('Specify an instance greater than 0.')
  2452. return optimized_path
  2453. manager = pywrapcp.RoutingIndexManager(tsp_size, num_routes, depot)
  2454. routing = pywrapcp.RoutingModel(manager)
  2455. search_parameters = pywrapcp.DefaultRoutingSearchParameters()
  2456. # Callback to the distance function. The callback takes two
  2457. # arguments (the from and to node indices) and returns the distance between them.
  2458. dist_between_locations = self.CreateDistanceCallback(locs=locations, manager=manager)
  2459. # if there are no distances then go to the next tool
  2460. if not dist_between_locations:
  2461. return
  2462. dist_callback = dist_between_locations.Distance
  2463. transit_callback_index = routing.RegisterTransitCallback(dist_callback)
  2464. routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
  2465. # Solve, returns a solution if any.
  2466. assignment = routing.SolveWithParameters(search_parameters)
  2467. if assignment:
  2468. # Solution cost.
  2469. log.info("Total distance: " + str(assignment.ObjectiveValue()))
  2470. # Inspect solution.
  2471. # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1.
  2472. route_number = 0
  2473. node = routing.Start(route_number)
  2474. start_node = node
  2475. while not routing.IsEnd(node):
  2476. optimized_path.append(node)
  2477. node = assignment.Value(routing.NextVar(node))
  2478. else:
  2479. log.warning('No solution found.')
  2480. return optimized_path
  2481. # ############################################# ##
  2482. def optimized_travelling_salesman(self, points, start=None):
  2483. """
  2484. As solving the problem in the brute force way is too slow,
  2485. this function implements a simple heuristic: always
  2486. go to the nearest city.
  2487. Even if this algorithm is extremely simple, it works pretty well
  2488. giving a solution only about 25%% longer than the optimal one (cit. Wikipedia),
  2489. and runs very fast in O(N^2) time complexity.
  2490. >>> optimized_travelling_salesman([[i,j] for i in range(5) for j in range(5)])
  2491. [[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],
  2492. [2, 3], [2, 4], [3, 4], [3, 3], [3, 2], [3, 1], [3, 0], [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]]
  2493. >>> optimized_travelling_salesman([[0,0],[10,0],[6,0]])
  2494. [[0, 0], [6, 0], [10, 0]]
  2495. :param points: List of tuples with x, y coordinates
  2496. :type points: list
  2497. :param start: a tuple with a x,y coordinates of the start point
  2498. :type start: tuple
  2499. :return: List of points ordered in a optimized way
  2500. :rtype: list
  2501. """
  2502. if start is None:
  2503. start = points[0]
  2504. must_visit = points
  2505. path = [start]
  2506. # must_visit.remove(start)
  2507. while must_visit:
  2508. nearest = min(must_visit, key=lambda x: distance(path[-1], x))
  2509. path.append(nearest)
  2510. must_visit.remove(nearest)
  2511. return path
  2512. def geo_optimized_rtree(self, geometry):
  2513. locations = []
  2514. # ## Index first and last points in paths. What points to index.
  2515. def get_pts(o):
  2516. return [o.coords[0], o.coords[-1]]
  2517. # Create the indexed storage.
  2518. storage = FlatCAMRTreeStorage()
  2519. storage.get_points = get_pts
  2520. # Store the geometry
  2521. log.debug("Indexing geometry before generating G-Code...")
  2522. self.app.inform.emit(_("Indexing geometry before generating G-Code..."))
  2523. for geo_shape in geometry:
  2524. if self.app.abort_flag:
  2525. # graceful abort requested by the user
  2526. raise grace
  2527. if geo_shape is not None:
  2528. storage.insert(geo_shape)
  2529. current_pt = (0, 0)
  2530. pt, geo = storage.nearest(current_pt)
  2531. try:
  2532. while True:
  2533. storage.remove(geo)
  2534. locations.append((pt, geo))
  2535. current_pt = geo.coords[-1]
  2536. pt, geo = storage.nearest(current_pt)
  2537. except StopIteration:
  2538. pass
  2539. # if there are no locations then go to the next tool
  2540. if not locations:
  2541. return 'fail'
  2542. return locations
  2543. def check_zcut(self, zcut):
  2544. if zcut > 0:
  2545. self.app.inform.emit('[WARNING] %s' %
  2546. _("The Cut Z parameter has positive value. "
  2547. "It is the depth value to drill into material.\n"
  2548. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  2549. "therefore the app will convert the value to negative. "
  2550. "Check the resulting CNC code (Gcode etc)."))
  2551. return -zcut
  2552. elif zcut == 0:
  2553. self.app.inform.emit('[WARNING] %s.' % _("The Cut Z parameter is zero. There will be no cut, aborting"))
  2554. return 'fail'
  2555. else:
  2556. return zcut
  2557. # used in Tool Drilling
  2558. def excellon_tool_gcode_gen(self, tool, points, tools, first_pt, is_first=False, is_last=False, opt_type='T',
  2559. toolchange=False):
  2560. """
  2561. Creates Gcode for this object from an Excellon object
  2562. for the specified tools.
  2563. :return: A tuple made from tool_gcode, another tuple holding the coordinates of the last point
  2564. and the start gcode
  2565. :rtype: tuple
  2566. """
  2567. log.debug("Creating CNC Job from Excellon for tool: %s" % str(tool))
  2568. self.exc_tools = deepcopy(tools)
  2569. t_gcode = ''
  2570. # holds the temporary coordinates of the processed drill point
  2571. locx, locy = first_pt
  2572. temp_locx, temp_locy = first_pt
  2573. # #############################################################################################################
  2574. # #############################################################################################################
  2575. # ################################## DRILLING !!! #########################################################
  2576. # #############################################################################################################
  2577. # #############################################################################################################
  2578. if opt_type == 'M':
  2579. log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
  2580. elif opt_type == 'B':
  2581. log.debug("Using OR-Tools Basic drill path optimization.")
  2582. elif opt_type == 'T':
  2583. log.debug("Using Travelling Salesman drill path optimization.")
  2584. else:
  2585. log.debug("Using no path optimization.")
  2586. tool_dict = tools[tool]['data']
  2587. # check if it has drills
  2588. if not points:
  2589. log.debug("Failed. No drills for tool: %s" % str(tool))
  2590. return 'fail'
  2591. if self.app.abort_flag:
  2592. # graceful abort requested by the user
  2593. raise grace
  2594. # #########################################################################################################
  2595. # #########################################################################################################
  2596. # ############# PARAMETERS used in PREPROCESSORS so they need to be updated ###############################
  2597. # #########################################################################################################
  2598. # #########################################################################################################
  2599. self.tool = str(tool)
  2600. # Preprocessor
  2601. p = self.pp_excellon
  2602. # Z_cut parameter
  2603. if self.machinist_setting == 0:
  2604. self.z_cut = self.check_zcut(zcut=tool_dict["tools_drill_cutz"])
  2605. if self.z_cut == 'fail':
  2606. return 'fail'
  2607. # Depth parameters
  2608. self.z_cut = tool_dict['tools_drill_cutz']
  2609. old_zcut = deepcopy(tool_dict["tools_drill_cutz"]) # multidepth use this
  2610. self.multidepth = tool_dict['tools_drill_multidepth']
  2611. self.z_depthpercut = tool_dict['tools_drill_depthperpass']
  2612. self.z_move = tool_dict['tools_drill_travelz']
  2613. self.f_plunge = tool_dict["tools_drill_f_plunge"] # used directly in the preprocessor Toolchange method
  2614. self.f_retract = tool_dict["tools_drill_f_retract"] # used in the current method
  2615. # Feedrate parameters
  2616. self.z_feedrate = tool_dict['tools_drill_feedrate_z']
  2617. self.feedrate = tool_dict['tools_drill_feedrate_z']
  2618. self.feedrate_rapid = tool_dict['tools_drill_feedrate_rapid']
  2619. # Spindle parameters
  2620. self.spindlespeed = tool_dict['tools_drill_spindlespeed']
  2621. self.dwell = tool_dict['tools_drill_dwell']
  2622. self.dwelltime = tool_dict['tools_drill_dwelltime']
  2623. self.spindledir = tool_dict['tools_drill_spindledir']
  2624. self.tooldia = tools[tool]["tooldia"]
  2625. self.postdata['toolC'] = tools[tool]["tooldia"]
  2626. self.toolchange = toolchange
  2627. # Z_toolchange parameter
  2628. self.z_toolchange = tool_dict['tools_drill_toolchangez']
  2629. # XY_toolchange parameter
  2630. self.xy_toolchange = tool_dict["tools_drill_toolchangexy"]
  2631. try:
  2632. if self.xy_toolchange == '':
  2633. self.xy_toolchange = None
  2634. else:
  2635. # either originally it was a string or not, xy_toolchange will be made string
  2636. self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange)) if self.xy_toolchange else None
  2637. # and now, xy_toolchange is made into a list of floats in format [x, y]
  2638. if self.xy_toolchange:
  2639. self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
  2640. if self.xy_toolchange and len(self.xy_toolchange) != 2:
  2641. self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y)."))
  2642. return 'fail'
  2643. except Exception as e:
  2644. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() xy_toolchange --> %s" % str(e))
  2645. self.xy_toolchange = [0, 0]
  2646. # End position parameters
  2647. self.startz = tool_dict["tools_drill_startz"]
  2648. if self.startz == '':
  2649. self.startz = None
  2650. self.z_end = tool_dict["tools_drill_endz"]
  2651. self.xy_end = tool_dict["tools_drill_endxy"]
  2652. try:
  2653. if self.xy_end == '':
  2654. self.xy_end = None
  2655. else:
  2656. # either originally it was a string or not, xy_end will be made string
  2657. self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
  2658. # and now, xy_end is made into a list of floats in format [x, y]
  2659. if self.xy_end:
  2660. self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
  2661. if self.xy_end and len(self.xy_end) != 2:
  2662. self.app.inform.emit('[ERROR] %s' % _("The End X,Y format has to be (x, y)."))
  2663. return 'fail'
  2664. except Exception as e:
  2665. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() xy_end --> %s" % str(e))
  2666. self.xy_end = [0, 0]
  2667. # Probe parameters
  2668. self.z_pdepth = tool_dict["tools_drill_z_pdepth"]
  2669. self.feedrate_probe = tool_dict["tools_drill_feedrate_probe"]
  2670. # #########################################################################################################
  2671. # #########################################################################################################
  2672. # #########################################################################################################
  2673. # ############ Create the data. ###########################################################################
  2674. # #########################################################################################################
  2675. locations = []
  2676. optimized_path = []
  2677. if opt_type == 'M':
  2678. locations = self.create_tool_data_array(points=points)
  2679. # if there are no locations then go to the next tool
  2680. if not locations:
  2681. return 'fail'
  2682. opt_time = self.app.defaults["excellon_search_time"]
  2683. optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time)
  2684. elif opt_type == 'B':
  2685. locations = self.create_tool_data_array(points=points)
  2686. # if there are no locations then go to the next tool
  2687. if not locations:
  2688. return 'fail'
  2689. optimized_path = self.optimized_ortools_basic(locations=locations)
  2690. elif opt_type == 'T':
  2691. locations = self.create_tool_data_array(points=points)
  2692. # if there are no locations then go to the next tool
  2693. if not locations:
  2694. return 'fail'
  2695. optimized_path = self.optimized_travelling_salesman(locations)
  2696. else:
  2697. # it's actually not optimized path but here we build a list of (x,y) coordinates
  2698. # out of the tool's drills
  2699. for drill in tools[tool]['drills']:
  2700. unoptimized_coords = (
  2701. drill.x,
  2702. drill.y
  2703. )
  2704. optimized_path.append(unoptimized_coords)
  2705. # #########################################################################################################
  2706. # #########################################################################################################
  2707. # Only if there are locations to drill
  2708. if not optimized_path:
  2709. log.debug("CNCJob.excellon_tool_gcode_gen() -> Optimized path is empty.")
  2710. return 'fail'
  2711. if self.app.abort_flag:
  2712. # graceful abort requested by the user
  2713. raise grace
  2714. start_gcode = ''
  2715. if is_first:
  2716. start_gcode = self.doformat(p.start_code)
  2717. # t_gcode += start_gcode
  2718. # do the ToolChange event
  2719. t_gcode += self.doformat(p.z_feedrate_code)
  2720. t_gcode += self.doformat(p.toolchange_code, toolchangexy=(temp_locx, temp_locy))
  2721. t_gcode += self.doformat(p.z_feedrate_code)
  2722. # Spindle start
  2723. t_gcode += self.doformat(p.spindle_code)
  2724. # Dwell time
  2725. if self.dwell is True:
  2726. t_gcode += self.doformat(p.dwell_code)
  2727. current_tooldia = self.app.dec_format(float(tools[tool]["tooldia"]), self.decimals)
  2728. self.app.inform.emit(
  2729. '%s: %s%s.' % (_("Starting G-Code for tool with diameter"), str(current_tooldia), str(self.units))
  2730. )
  2731. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  2732. # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  2733. # because the values for Z offset are created in build_tool_ui()
  2734. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  2735. try:
  2736. z_offset = float(tool_dict['tools_drill_offset']) * (-1)
  2737. except KeyError:
  2738. z_offset = 0
  2739. self.z_cut = z_offset + old_zcut
  2740. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  2741. if self.coordinates_type == "G90":
  2742. # Drillling! for Absolute coordinates type G90
  2743. # variables to display the percentage of work done
  2744. geo_len = len(optimized_path)
  2745. old_disp_number = 0
  2746. log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  2747. loc_nr = 0
  2748. for point in optimized_path:
  2749. if self.app.abort_flag:
  2750. # graceful abort requested by the user
  2751. raise grace
  2752. # if we use Traveling Salesman Algorithm as an optimization
  2753. if opt_type == 'T':
  2754. locx = point[0]
  2755. locy = point[1]
  2756. else:
  2757. locx = locations[point][0]
  2758. locy = locations[point][1]
  2759. travels = self.app.exc_areas.travel_coordinates(start_point=(temp_locx, temp_locy),
  2760. end_point=(locx, locy),
  2761. tooldia=current_tooldia)
  2762. prev_z = None
  2763. for travel in travels:
  2764. locx = travel[1][0]
  2765. locy = travel[1][1]
  2766. if travel[0] is not None:
  2767. # move to next point
  2768. t_gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  2769. # raise to safe Z (travel[0]) each time because safe Z may be different
  2770. self.z_move = travel[0]
  2771. t_gcode += self.doformat(p.lift_code, x=locx, y=locy)
  2772. # restore z_move
  2773. self.z_move = tool_dict['tools_drill_travelz']
  2774. else:
  2775. if prev_z is not None:
  2776. # move to next point
  2777. t_gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  2778. # we assume that previously the z_move was altered therefore raise to
  2779. # the travel_z (z_move)
  2780. self.z_move = tool_dict['tools_drill_travelz']
  2781. t_gcode += self.doformat(p.lift_code, x=locx, y=locy)
  2782. else:
  2783. # move to next point
  2784. t_gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  2785. # store prev_z
  2786. prev_z = travel[0]
  2787. # t_gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  2788. if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  2789. doc = deepcopy(self.z_cut)
  2790. self.z_cut = 0.0
  2791. while abs(self.z_cut) < abs(doc):
  2792. self.z_cut -= self.z_depthpercut
  2793. if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  2794. self.z_cut = doc
  2795. # Move down the drill bit
  2796. t_gcode += self.doformat(p.down_code, x=locx, y=locy)
  2797. # Update the distance travelled down with the current one
  2798. self.measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  2799. if self.f_retract is False:
  2800. t_gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  2801. self.measured_up_to_zero_distance += abs(self.z_cut)
  2802. self.measured_lift_distance += abs(self.z_move)
  2803. else:
  2804. self.measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  2805. t_gcode += self.doformat(p.lift_code, x=locx, y=locy)
  2806. else:
  2807. t_gcode += self.doformat(p.down_code, x=locx, y=locy)
  2808. self.measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  2809. if self.f_retract is False:
  2810. t_gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  2811. self.measured_up_to_zero_distance += abs(self.z_cut)
  2812. self.measured_lift_distance += abs(self.z_move)
  2813. else:
  2814. self.measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  2815. t_gcode += self.doformat(p.lift_code, x=locx, y=locy)
  2816. self.measured_distance += abs(distance_euclidian(locx, locy, temp_locx, temp_locy))
  2817. temp_locx = locx
  2818. temp_locy = locy
  2819. self.oldx = locx
  2820. self.oldy = locy
  2821. loc_nr += 1
  2822. disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  2823. if old_disp_number < disp_number <= 100:
  2824. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  2825. old_disp_number = disp_number
  2826. else:
  2827. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  2828. return 'fail'
  2829. self.z_cut = deepcopy(old_zcut)
  2830. if is_last:
  2831. t_gcode += self.doformat(p.spindle_stop_code)
  2832. # Move to End position
  2833. t_gcode += self.doformat(p.end_code, x=0, y=0)
  2834. self.app.inform.emit('%s %s' % (_("Finished G-Code generation for tool:"), str(tool)))
  2835. return t_gcode, (locx, locy), start_gcode
  2836. # used in Geometry (and soon in Tool Milling)
  2837. def geometry_tool_gcode_gen(self, tool, tools, first_pt, tolerance, is_first=False, is_last=False,
  2838. toolchange=False):
  2839. """
  2840. Algorithm to generate GCode from multitool Geometry.
  2841. :param tool: tool number for which to generate GCode
  2842. :type tool: int
  2843. :param tools: a dictionary holding all the tools and data
  2844. :type tools: dict
  2845. :param first_pt: a tuple of coordinates for the first point of the current tool
  2846. :type first_pt: tuple
  2847. :param tolerance: geometry tolerance
  2848. :type tolerance:
  2849. :param is_first: if the current tool is the first tool (for this we need to add start GCode)
  2850. :type is_first: bool
  2851. :param is_last: if the current tool is the last tool (for this we need to add the end GCode)
  2852. :type is_last: bool
  2853. :param toolchange: add toolchange event
  2854. :type toolchange: bool
  2855. :return: GCode
  2856. :rtype: str
  2857. """
  2858. log.debug("geometry_tool_gcode_gen()")
  2859. t_gcode = ''
  2860. temp_solid_geometry = []
  2861. # The Geometry from which we create GCode
  2862. geometry = tools[tool]['solid_geometry']
  2863. # ## Flatten the geometry. Only linear elements (no polygons) remain.
  2864. flat_geometry = self.flatten(geometry, reset=True, pathonly=True)
  2865. log.debug("%d paths" % len(flat_geometry))
  2866. # #########################################################################################################
  2867. # #########################################################################################################
  2868. # ############# PARAMETERS used in PREPROCESSORS so they need to be updated ###############################
  2869. # #########################################################################################################
  2870. # #########################################################################################################
  2871. self.tool = str(tool)
  2872. tool_dict = tools[tool]['data']
  2873. # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter
  2874. # given under the name 'toolC'
  2875. self.postdata['toolC'] = float(tools[tool]['tooldia'])
  2876. self.tooldia = float(tools[tool]['tooldia'])
  2877. self.use_ui = True
  2878. self.tolerance = tolerance
  2879. # Optimization type. Can be: 'M', 'B', 'T', 'R', 'No'
  2880. opt_type = tool_dict['optimization_type']
  2881. opt_time = tool_dict['search_time'] if 'search_time' in tool_dict else 'R'
  2882. if opt_type == 'M':
  2883. log.debug("Using OR-Tools Metaheuristic Guided Local Search path optimization.")
  2884. elif opt_type == 'B':
  2885. log.debug("Using OR-Tools Basic path optimization.")
  2886. elif opt_type == 'T':
  2887. log.debug("Using Travelling Salesman path optimization.")
  2888. elif opt_type == 'R':
  2889. log.debug("Using RTree path optimization.")
  2890. else:
  2891. log.debug("Using no path optimization.")
  2892. # Preprocessor
  2893. self.pp_geometry_name = tool_dict['ppname_g']
  2894. self.pp_geometry = self.app.preprocessors[self.pp_geometry_name]
  2895. p = self.pp_geometry
  2896. # Offset the Geometry if it is the case
  2897. if tools[tool]['offset'].lower() == 'in':
  2898. tool_offset = -float(tools[tool]['tooldia']) / 2.0
  2899. elif tools[tool]['offset'].lower() == 'out':
  2900. tool_offset = float(tools[tool]['tooldia']) / 2.0
  2901. elif tools[tool]['offset'].lower() == 'custom':
  2902. tool_offset = tools[tool]['offset_value']
  2903. else:
  2904. tool_offset = 0.0
  2905. if tool_offset != 0.0:
  2906. for it in flat_geometry:
  2907. # if the geometry is a closed shape then create a Polygon out of it
  2908. if isinstance(it, LineString):
  2909. if it.is_ring:
  2910. it = Polygon(it)
  2911. temp_solid_geometry.append(it.buffer(tool_offset, join_style=2))
  2912. temp_solid_geometry = self.flatten(temp_solid_geometry, reset=True, pathonly=True)
  2913. else:
  2914. temp_solid_geometry = flat_geometry
  2915. if self.z_cut is None:
  2916. if 'laser' not in self.pp_geometry_name:
  2917. self.app.inform.emit(
  2918. '[ERROR_NOTCL] %s' % _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
  2919. "other parameters."))
  2920. return 'fail'
  2921. else:
  2922. self.z_cut = 0
  2923. if self.machinist_setting == 0:
  2924. if self.z_cut > 0:
  2925. self.app.inform.emit('[WARNING] %s' %
  2926. _("The Cut Z parameter has positive value. "
  2927. "It is the depth value to cut into material.\n"
  2928. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  2929. "therefore the app will convert the value to negative."
  2930. "Check the resulting CNC code (Gcode etc)."))
  2931. self.z_cut = -self.z_cut
  2932. elif self.z_cut == 0 and 'laser' not in self.pp_geometry_name:
  2933. self.app.inform.emit('[WARNING] %s: %s' %
  2934. (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
  2935. self.options['name']))
  2936. return 'fail'
  2937. if self.z_move is None:
  2938. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Travel Z parameter is None or zero."))
  2939. return 'fail'
  2940. if self.z_move < 0:
  2941. self.app.inform.emit('[WARNING] %s' %
  2942. _("The Travel Z parameter has negative value. "
  2943. "It is the height value to travel between cuts.\n"
  2944. "The Z Travel parameter needs to have a positive value, assuming it is a typo "
  2945. "therefore the app will convert the value to positive."
  2946. "Check the resulting CNC code (Gcode etc)."))
  2947. self.z_move = -self.z_move
  2948. elif self.z_move == 0:
  2949. self.app.inform.emit('[WARNING] %s: %s' %
  2950. (_("The Z Travel parameter is zero. This is dangerous, skipping file"),
  2951. self.options['name']))
  2952. return 'fail'
  2953. # made sure that depth_per_cut is no more then the z_cut
  2954. if abs(self.z_cut) < self.z_depthpercut:
  2955. self.z_depthpercut = abs(self.z_cut)
  2956. # Depth parameters
  2957. self.z_cut = float(tool_dict['cutz'])
  2958. self.multidepth = tool_dict['multidepth']
  2959. self.z_depthpercut = float(tool_dict['depthperpass'])
  2960. self.z_move = float(tool_dict['travelz'])
  2961. self.f_plunge = self.app.defaults["geometry_f_plunge"]
  2962. self.feedrate = float(tool_dict['feedrate'])
  2963. self.z_feedrate = float(tool_dict['feedrate_z'])
  2964. self.feedrate_rapid = float(tool_dict['feedrate_rapid'])
  2965. self.spindlespeed = float(tool_dict['spindlespeed'])
  2966. try:
  2967. self.spindledir = tool_dict['spindledir']
  2968. except KeyError:
  2969. self.spindledir = self.app.defaults["geometry_spindledir"]
  2970. self.dwell = tool_dict['dwell']
  2971. self.dwelltime = float(tool_dict['dwelltime'])
  2972. self.startz = float(tool_dict['startz']) if tool_dict['startz'] else None
  2973. if self.startz == '':
  2974. self.startz = None
  2975. self.z_end = float(tool_dict['endz'])
  2976. self.xy_end = tool_dict['endxy']
  2977. try:
  2978. if self.xy_end == '' or self.xy_end is None:
  2979. self.xy_end = None
  2980. else:
  2981. # either originally it was a string or not, xy_end will be made string
  2982. self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
  2983. # and now, xy_end is made into a list of floats in format [x, y]
  2984. if self.xy_end:
  2985. self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
  2986. if self.xy_end and len(self.xy_end) != 2:
  2987. self.app.inform.emit('[ERROR]%s' % _("The End X,Y format has to be (x, y)."))
  2988. return 'fail'
  2989. except Exception as e:
  2990. log.debug("camlib.CNCJob.geometry_from_excellon_by_tool() xy_end --> %s" % str(e))
  2991. self.xy_end = [0, 0]
  2992. self.z_toolchange = tool_dict['toolchangez']
  2993. self.xy_toolchange = tool_dict["toolchangexy"]
  2994. try:
  2995. if self.xy_toolchange == '':
  2996. self.xy_toolchange = None
  2997. else:
  2998. # either originally it was a string or not, xy_toolchange will be made string
  2999. self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange)) if self.xy_toolchange else None
  3000. # and now, xy_toolchange is made into a list of floats in format [x, y]
  3001. if self.xy_toolchange:
  3002. self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
  3003. if self.xy_toolchange and len(self.xy_toolchange) != 2:
  3004. self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y)."))
  3005. return 'fail'
  3006. except Exception as e:
  3007. log.debug("camlib.CNCJob.geometry_from_excellon_by_tool() --> %s" % str(e))
  3008. pass
  3009. self.extracut = tool_dict['extracut']
  3010. self.extracut_length = tool_dict['extracut_length']
  3011. # Probe parameters
  3012. # self.z_pdepth = tool_dict["tools_drill_z_pdepth"]
  3013. # self.feedrate_probe = tool_dict["tools_drill_feedrate_probe"]
  3014. # #########################################################################################################
  3015. # ############ Create the data. ###########################################################################
  3016. # #########################################################################################################
  3017. optimized_path = []
  3018. geo_storage = {}
  3019. for geo in temp_solid_geometry:
  3020. if not geo is None:
  3021. geo_storage[geo.coords[0]] = geo
  3022. locations = list(geo_storage.keys())
  3023. if opt_type == 'M':
  3024. # if there are no locations then go to the next tool
  3025. if not locations:
  3026. return 'fail'
  3027. optimized_locations = self.optimized_ortools_meta(locations=locations, opt_time=opt_time)
  3028. optimized_path = [(locations[loc], geo_storage[locations[loc]]) for loc in optimized_locations]
  3029. elif opt_type == 'B':
  3030. # if there are no locations then go to the next tool
  3031. if not locations:
  3032. return 'fail'
  3033. optimized_locations = self.optimized_ortools_basic(locations=locations)
  3034. optimized_path = [(locations[loc], geo_storage[locations[loc]]) for loc in optimized_locations]
  3035. elif opt_type == 'T':
  3036. # if there are no locations then go to the next tool
  3037. if not locations:
  3038. return 'fail'
  3039. optimized_locations = self.optimized_travelling_salesman(locations)
  3040. optimized_path = [(loc, geo_storage[loc]) for loc in optimized_locations]
  3041. elif opt_type == 'R':
  3042. optimized_path = self.geo_optimized_rtree(temp_solid_geometry)
  3043. if optimized_path == 'fail':
  3044. return 'fail'
  3045. else:
  3046. # it's actually not optimized path but here we build a list of (x,y) coordinates
  3047. # out of the tool's drills
  3048. for geo in temp_solid_geometry:
  3049. optimized_path.append(geo.coords[0])
  3050. # #########################################################################################################
  3051. # #########################################################################################################
  3052. # Only if there are locations to mill
  3053. if not optimized_path:
  3054. log.debug("camlib.CNCJob.geometry_tool_gcode_gen() -> Optimized path is empty.")
  3055. return 'fail'
  3056. if self.app.abort_flag:
  3057. # graceful abort requested by the user
  3058. raise grace
  3059. # #############################################################################################################
  3060. # #############################################################################################################
  3061. # ################# MILLING !!! ##############################################################################
  3062. # #############################################################################################################
  3063. # #############################################################################################################
  3064. log.debug("Starting G-Code...")
  3065. current_tooldia = float('%.*f' % (self.decimals, float(self.tooldia)))
  3066. self.app.inform.emit('%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  3067. str(current_tooldia),
  3068. str(self.units)))
  3069. # Measurements
  3070. total_travel = 0.0
  3071. total_cut = 0.0
  3072. # Start GCode
  3073. start_gcode = ''
  3074. if is_first:
  3075. start_gcode = self.doformat(p.start_code)
  3076. # t_gcode += start_gcode
  3077. # Toolchange code
  3078. t_gcode += self.doformat(p.feedrate_code) # sets the feed rate
  3079. if toolchange:
  3080. t_gcode += self.doformat(p.toolchange_code)
  3081. if 'laser' not in self.pp_geometry_name.lower():
  3082. t_gcode += self.doformat(p.spindle_code) # Spindle start
  3083. else:
  3084. # for laser this will disable the laser
  3085. t_gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy) # Move (up) to travel height
  3086. if self.dwell:
  3087. t_gcode += self.doformat(p.dwell_code) # Dwell time
  3088. else:
  3089. t_gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height
  3090. t_gcode += self.doformat(p.startz_code, x=0, y=0)
  3091. if 'laser' not in self.pp_geometry_name.lower():
  3092. t_gcode += self.doformat(p.spindle_code) # Spindle start
  3093. if self.dwell is True:
  3094. t_gcode += self.doformat(p.dwell_code) # Dwell time
  3095. t_gcode += self.doformat(p.feedrate_code) # sets the feed rate
  3096. # ## Iterate over geometry paths getting the nearest each time.
  3097. path_count = 0
  3098. # variables to display the percentage of work done
  3099. geo_len = len(flat_geometry)
  3100. log.warning("Number of paths for which to generate GCode: %s" % str(geo_len))
  3101. old_disp_number = 0
  3102. current_pt = (0, 0)
  3103. for pt, geo in optimized_path:
  3104. if self.app.abort_flag:
  3105. # graceful abort requested by the user
  3106. raise grace
  3107. path_count += 1
  3108. # If last point in geometry is the nearest but prefer the first one if last point == first point
  3109. # then reverse coordinates.
  3110. if pt != geo.coords[0] and pt == geo.coords[-1]:
  3111. geo = LineString(list(geo.coords)[::-1])
  3112. # ---------- Single depth/pass --------
  3113. if not self.multidepth:
  3114. # calculate the cut distance
  3115. total_cut = total_cut + geo.length
  3116. t_gcode += self.create_gcode_single_pass(geo, current_tooldia, self.extracut,
  3117. self.extracut_length, self.tolerance,
  3118. z_move=self.z_move, old_point=current_pt)
  3119. # --------- Multi-pass ---------
  3120. else:
  3121. # calculate the cut distance
  3122. # due of the number of cuts (multi depth) it has to multiplied by the number of cuts
  3123. nr_cuts = 0
  3124. depth = abs(self.z_cut)
  3125. while depth > 0:
  3126. nr_cuts += 1
  3127. depth -= float(self.z_depthpercut)
  3128. total_cut += (geo.length * nr_cuts)
  3129. gc, geo = self.create_gcode_multi_pass(geo, current_tooldia, self.extracut,
  3130. self.extracut_length, self.tolerance,
  3131. z_move=self.z_move, postproc=p, old_point=current_pt)
  3132. t_gcode += gc
  3133. # calculate the total distance
  3134. total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
  3135. current_pt = geo.coords[-1]
  3136. disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
  3137. if old_disp_number < disp_number <= 100:
  3138. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  3139. old_disp_number = disp_number
  3140. log.debug("Finished G-Code... %s paths traced." % path_count)
  3141. # add move to end position
  3142. total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0))
  3143. self.travel_distance += total_travel + total_cut
  3144. self.routing_time += total_cut / self.feedrate
  3145. # Finish
  3146. if is_last:
  3147. t_gcode += self.doformat(p.spindle_stop_code)
  3148. t_gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
  3149. t_gcode += self.doformat(p.end_code, x=0, y=0)
  3150. self.app.inform.emit(
  3151. '%s... %s %s.' % (_("Finished G-Code generation"), str(path_count), _("paths traced"))
  3152. )
  3153. self.gcode = t_gcode
  3154. return self.gcode, start_gcode
  3155. # used by the Tcl command Drillcncjob
  3156. def generate_from_excellon_by_tool(self, exobj, tools="all", order='fwd', is_first=False, use_ui=False):
  3157. """
  3158. Creates Gcode for this object from an Excellon object
  3159. for the specified tools.
  3160. :param exobj: Excellon object to process
  3161. :type exobj: Excellon
  3162. :param tools: Comma separated tool names
  3163. :type tools: str
  3164. :param order: order of tools processing: "fwd", "rev" or "no"
  3165. :type order: str
  3166. :param is_first: if the tool is the first one should generate the start gcode (not that it matter much
  3167. which is the one doing it)
  3168. :type is_first: bool
  3169. :param use_ui: if True the method will use parameters set in UI
  3170. :type use_ui: bool
  3171. :return: None
  3172. :rtype: None
  3173. """
  3174. # #############################################################################################################
  3175. # #############################################################################################################
  3176. # create a local copy of the exobj.tools so it can be used for creating drill CCode geometry
  3177. # #############################################################################################################
  3178. # #############################################################################################################
  3179. self.exc_tools = deepcopy(exobj.tools)
  3180. # the Excellon GCode preprocessor will use this info in the start_code() method
  3181. self.use_ui = True if use_ui else False
  3182. # Z_cut parameter
  3183. if self.machinist_setting == 0:
  3184. self.z_cut = self.check_zcut(zcut=self.z_cut)
  3185. if self.z_cut == 'fail':
  3186. return 'fail'
  3187. # multidepth use this
  3188. old_zcut = deepcopy(self.z_cut)
  3189. # XY_toolchange parameter
  3190. try:
  3191. if self.xy_toolchange == '':
  3192. self.xy_toolchange = None
  3193. else:
  3194. self.xy_toolchange = re.sub('[()\[\]]', '', str(self.xy_toolchange)) if self.xy_toolchange else None
  3195. if self.xy_toolchange:
  3196. self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
  3197. if self.xy_toolchange and len(self.xy_toolchange) != 2:
  3198. self.app.inform.emit('[ERROR]%s' %
  3199. _("The Toolchange X,Y field in Edit -> Preferences has to be "
  3200. "in the format (x, y) \nbut now there is only one value, not two. "))
  3201. return 'fail'
  3202. except Exception as e:
  3203. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> %s" % str(e))
  3204. pass
  3205. # XY_end parameter
  3206. self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
  3207. if self.xy_end and self.xy_end != '':
  3208. self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
  3209. if self.xy_end and len(self.xy_end) < 2:
  3210. self.app.inform.emit('[ERROR] %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
  3211. "in the format (x, y) but now there is only one value, not two."))
  3212. return 'fail'
  3213. # Prepprocessor
  3214. self.pp_excellon = self.app.preprocessors[self.pp_excellon_name]
  3215. p = self.pp_excellon
  3216. log.debug("Creating CNC Job from Excellon...")
  3217. # #############################################################################################################
  3218. # #############################################################################################################
  3219. # TOOLS
  3220. # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool)
  3221. # so we actually are sorting the tools by diameter
  3222. # #############################################################################################################
  3223. # #############################################################################################################
  3224. all_tools = []
  3225. for tool_as_key, v in list(self.exc_tools.items()):
  3226. all_tools.append((int(tool_as_key), float(v['tooldia'])))
  3227. if order == 'fwd':
  3228. sorted_tools = sorted(all_tools, key=lambda t1: t1[1])
  3229. elif order == 'rev':
  3230. sorted_tools = sorted(all_tools, key=lambda t1: t1[1], reverse=True)
  3231. else:
  3232. sorted_tools = all_tools
  3233. if tools == "all":
  3234. selected_tools = [i[0] for i in all_tools] # we get a array of ordered tools
  3235. else:
  3236. selected_tools = eval(tools)
  3237. # Create a sorted list of selected tools from the sorted_tools list
  3238. tools = [i for i, j in sorted_tools for k in selected_tools if i == k]
  3239. log.debug("Tools sorted are: %s" % str(tools))
  3240. # #############################################################################################################
  3241. # #############################################################################################################
  3242. # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case of
  3243. # running this method from a Tcl Command
  3244. # #############################################################################################################
  3245. # #############################################################################################################
  3246. build_tools_in_use_list = False
  3247. if 'Tools_in_use' not in self.options:
  3248. self.options['Tools_in_use'] = []
  3249. # if the list is empty (either we just added the key or it was already there but empty) signal to build it
  3250. if not self.options['Tools_in_use']:
  3251. build_tools_in_use_list = True
  3252. # #############################################################################################################
  3253. # #############################################################################################################
  3254. # fill the data into the self.exc_cnc_tools dictionary
  3255. # #############################################################################################################
  3256. # #############################################################################################################
  3257. for it in all_tools:
  3258. for to_ol in tools:
  3259. if to_ol == it[0]:
  3260. sol_geo = []
  3261. drill_no = 0
  3262. if 'drills' in exobj.tools[to_ol]:
  3263. drill_no = len(exobj.tools[to_ol]['drills'])
  3264. for drill in exobj.tools[to_ol]['drills']:
  3265. sol_geo.append(drill.buffer((it[1] / 2.0), resolution=self.geo_steps_per_circle))
  3266. slot_no = 0
  3267. if 'slots' in exobj.tools[to_ol]:
  3268. slot_no = len(exobj.tools[to_ol]['slots'])
  3269. for slot in exobj.tools[to_ol]['slots']:
  3270. start = (slot[0].x, slot[0].y)
  3271. stop = (slot[1].x, slot[1].y)
  3272. sol_geo.append(
  3273. LineString([start, stop]).buffer((it[1] / 2.0), resolution=self.geo_steps_per_circle)
  3274. )
  3275. if self.use_ui:
  3276. try:
  3277. z_off = float(exobj.tools[it[0]]['data']['tools_drill_offset']) * (-1)
  3278. except KeyError:
  3279. z_off = 0
  3280. else:
  3281. z_off = 0
  3282. default_data = {}
  3283. for k, v in list(self.options.items()):
  3284. default_data[k] = deepcopy(v)
  3285. # it[1] is the tool diameter
  3286. self.exc_cnc_tools[it[1]] = {}
  3287. self.exc_cnc_tools[it[1]]['tool'] = it[0]
  3288. self.exc_cnc_tools[it[1]]['nr_drills'] = drill_no
  3289. self.exc_cnc_tools[it[1]]['nr_slots'] = slot_no
  3290. self.exc_cnc_tools[it[1]]['offset_z'] = z_off
  3291. self.exc_cnc_tools[it[1]]['data'] = default_data
  3292. self.exc_cnc_tools[it[1]]['solid_geometry'] = deepcopy(sol_geo)
  3293. # build a self.options['Tools_in_use'] list from scratch if we don't have one like in the case of
  3294. # running this method from a Tcl Command
  3295. if build_tools_in_use_list is True:
  3296. self.options['Tools_in_use'].append(
  3297. [it[0], it[1], drill_no, slot_no]
  3298. )
  3299. self.app.inform.emit(_("Creating a list of points to drill..."))
  3300. # #############################################################################################################
  3301. # #############################################################################################################
  3302. # Points (Group by tool): a dictionary of shapely Point geo elements grouped by tool number
  3303. # #############################################################################################################
  3304. # #############################################################################################################
  3305. points = {}
  3306. for tool, tool_dict in self.exc_tools.items():
  3307. if tool in tools:
  3308. if self.app.abort_flag:
  3309. # graceful abort requested by the user
  3310. raise grace
  3311. if 'drills' in tool_dict and tool_dict['drills']:
  3312. for drill_pt in tool_dict['drills']:
  3313. try:
  3314. points[tool].append(drill_pt)
  3315. except KeyError:
  3316. points[tool] = [drill_pt]
  3317. log.debug("Found %d TOOLS with drills." % len(points))
  3318. # check if there are drill points in the exclusion areas.
  3319. # If we find any within the exclusion areas return 'fail'
  3320. for tool in points:
  3321. for pt in points[tool]:
  3322. for area in self.app.exc_areas.exclusion_areas_storage:
  3323. pt_buf = pt.buffer(self.exc_tools[tool]['tooldia'] / 2.0)
  3324. if pt_buf.within(area['shape']) or pt_buf.intersects(area['shape']):
  3325. self.app.inform.emit("[ERROR_NOTCL] %s" % _("Failed. Drill points inside the exclusion zones."))
  3326. return 'fail'
  3327. # this holds the resulting GCode
  3328. self.gcode = []
  3329. # #############################################################################################################
  3330. # #############################################################################################################
  3331. # Initialization
  3332. # #############################################################################################################
  3333. # #############################################################################################################
  3334. gcode = ''
  3335. start_gcode = ''
  3336. if is_first:
  3337. start_gcode = self.doformat(p.start_code)
  3338. if use_ui is False:
  3339. gcode += self.doformat(p.z_feedrate_code)
  3340. if self.toolchange is False:
  3341. if self.xy_toolchange is not None:
  3342. gcode += self.doformat(p.lift_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  3343. gcode += self.doformat(p.startz_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  3344. else:
  3345. gcode += self.doformat(p.lift_code, x=0.0, y=0.0)
  3346. gcode += self.doformat(p.startz_code, x=0.0, y=0.0)
  3347. if self.xy_toolchange is not None:
  3348. self.oldx = self.xy_toolchange[0]
  3349. self.oldy = self.xy_toolchange[1]
  3350. else:
  3351. self.oldx = 0.0
  3352. self.oldy = 0.0
  3353. measured_distance = 0.0
  3354. measured_down_distance = 0.0
  3355. measured_up_to_zero_distance = 0.0
  3356. measured_lift_distance = 0.0
  3357. # #############################################################################################################
  3358. # #############################################################################################################
  3359. # GCODE creation
  3360. # #############################################################################################################
  3361. # #############################################################################################################
  3362. self.app.inform.emit('%s...' % _("Starting G-Code"))
  3363. has_drills = None
  3364. for tool, tool_dict in self.exc_tools.items():
  3365. if 'drills' in tool_dict and tool_dict['drills']:
  3366. has_drills = True
  3367. break
  3368. if not has_drills:
  3369. log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  3370. "The loaded Excellon file has no drills ...")
  3371. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
  3372. return 'fail'
  3373. current_platform = platform.architecture()[0]
  3374. if current_platform == '64bit':
  3375. used_excellon_optimization_type = self.excellon_optimization_type
  3376. else:
  3377. used_excellon_optimization_type = 'T'
  3378. # #############################################################################################################
  3379. # #############################################################################################################
  3380. # ################################## DRILLING !!! #########################################################
  3381. # #############################################################################################################
  3382. # #############################################################################################################
  3383. if used_excellon_optimization_type == 'M':
  3384. log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
  3385. elif used_excellon_optimization_type == 'B':
  3386. log.debug("Using OR-Tools Basic drill path optimization.")
  3387. elif used_excellon_optimization_type == 'T':
  3388. log.debug("Using Travelling Salesman drill path optimization.")
  3389. else:
  3390. log.debug("Using no path optimization.")
  3391. if self.toolchange is True:
  3392. for tool in tools:
  3393. # check if it has drills
  3394. if not self.exc_tools[tool]['drills']:
  3395. continue
  3396. if self.app.abort_flag:
  3397. # graceful abort requested by the user
  3398. raise grace
  3399. self.tool = tool
  3400. self.tooldia = self.exc_tools[tool]["tooldia"]
  3401. self.postdata['toolC'] = self.tooldia
  3402. if self.use_ui:
  3403. self.z_feedrate = self.exc_tools[tool]['data']['tools_drill_feedrate_z']
  3404. self.feedrate = self.exc_tools[tool]['data']['tools_drill_feedrate_z']
  3405. self.z_cut = self.exc_tools[tool]['data']['tools_drill_cutz']
  3406. gcode += self.doformat(p.z_feedrate_code)
  3407. if self.machinist_setting == 0:
  3408. if self.z_cut > 0:
  3409. self.app.inform.emit('[WARNING] %s' %
  3410. _("The Cut Z parameter has positive value. "
  3411. "It is the depth value to drill into material.\n"
  3412. "The Cut Z parameter needs to have a negative value, "
  3413. "assuming it is a typo "
  3414. "therefore the app will convert the value to negative. "
  3415. "Check the resulting CNC code (Gcode etc)."))
  3416. self.z_cut = -self.z_cut
  3417. elif self.z_cut == 0:
  3418. self.app.inform.emit('[WARNING] %s: %s' %
  3419. (_(
  3420. "The Cut Z parameter is zero. There will be no cut, "
  3421. "skipping file"),
  3422. exobj.options['name']))
  3423. return 'fail'
  3424. old_zcut = deepcopy(self.z_cut)
  3425. self.z_move = self.exc_tools[tool]['data']['tools_drill_travelz']
  3426. self.spindlespeed = self.exc_tools[tool]['data']['tools_drill_spindlespeed']
  3427. self.dwell = self.exc_tools[tool]['data']['tools_drill_dwell']
  3428. self.dwelltime = self.exc_tools[tool]['data']['tools_drill_dwelltime']
  3429. self.multidepth = self.exc_tools[tool]['data']['tools_drill_multidepth']
  3430. self.z_depthpercut = self.exc_tools[tool]['data']['tools_drill_depthperpass']
  3431. else:
  3432. old_zcut = deepcopy(self.z_cut)
  3433. # #########################################################################################################
  3434. # ############ Create the data. #################
  3435. # #########################################################################################################
  3436. locations = []
  3437. altPoints = []
  3438. optimized_path = []
  3439. if used_excellon_optimization_type == 'M':
  3440. if tool in points:
  3441. locations = self.create_tool_data_array(points=points[tool])
  3442. # if there are no locations then go to the next tool
  3443. if not locations:
  3444. continue
  3445. opt_time = self.app.defaults["excellon_search_time"]
  3446. optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time)
  3447. elif used_excellon_optimization_type == 'B':
  3448. if tool in points:
  3449. locations = self.create_tool_data_array(points=points[tool])
  3450. # if there are no locations then go to the next tool
  3451. if not locations:
  3452. continue
  3453. optimized_path = self.optimized_ortools_basic(locations=locations)
  3454. elif used_excellon_optimization_type == 'T':
  3455. for point in points[tool]:
  3456. altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
  3457. optimized_path = self.optimized_travelling_salesman(altPoints)
  3458. else:
  3459. # it's actually not optimized path but here we build a list of (x,y) coordinates
  3460. # out of the tool's drills
  3461. for drill in self.exc_tools[tool]['drills']:
  3462. unoptimized_coords = (
  3463. drill.x,
  3464. drill.y
  3465. )
  3466. optimized_path.append(unoptimized_coords)
  3467. # #########################################################################################################
  3468. # #########################################################################################################
  3469. # Only if there are locations to drill
  3470. if not optimized_path:
  3471. continue
  3472. if self.app.abort_flag:
  3473. # graceful abort requested by the user
  3474. raise grace
  3475. # Tool change sequence (optional)
  3476. if self.toolchange:
  3477. gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
  3478. # Spindle start
  3479. gcode += self.doformat(p.spindle_code)
  3480. # Dwell time
  3481. if self.dwell is True:
  3482. gcode += self.doformat(p.dwell_code)
  3483. current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[tool]["tooldia"])))
  3484. self.app.inform.emit(
  3485. '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  3486. str(current_tooldia),
  3487. str(self.units))
  3488. )
  3489. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3490. # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  3491. # because the values for Z offset are created in build_ui()
  3492. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3493. try:
  3494. z_offset = float(self.exc_tools[tool]['data']['tools_drill_offset']) * (-1)
  3495. except KeyError:
  3496. z_offset = 0
  3497. self.z_cut = z_offset + old_zcut
  3498. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  3499. if self.coordinates_type == "G90":
  3500. # Drillling! for Absolute coordinates type G90
  3501. # variables to display the percentage of work done
  3502. geo_len = len(optimized_path)
  3503. old_disp_number = 0
  3504. log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  3505. loc_nr = 0
  3506. for point in optimized_path:
  3507. if self.app.abort_flag:
  3508. # graceful abort requested by the user
  3509. raise grace
  3510. if used_excellon_optimization_type == 'T':
  3511. locx = point[0]
  3512. locy = point[1]
  3513. else:
  3514. locx = locations[point][0]
  3515. locy = locations[point][1]
  3516. travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
  3517. end_point=(locx, locy),
  3518. tooldia=current_tooldia)
  3519. prev_z = None
  3520. for travel in travels:
  3521. locx = travel[1][0]
  3522. locy = travel[1][1]
  3523. if travel[0] is not None:
  3524. # move to next point
  3525. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3526. # raise to safe Z (travel[0]) each time because safe Z may be different
  3527. self.z_move = travel[0]
  3528. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3529. # restore z_move
  3530. self.z_move = self.exc_tools[tool]['data']['tools_drill_travelz']
  3531. else:
  3532. if prev_z is not None:
  3533. # move to next point
  3534. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3535. # we assume that previously the z_move was altered therefore raise to
  3536. # the travel_z (z_move)
  3537. self.z_move = self.exc_tools[tool]['data']['tools_drill_travelz']
  3538. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3539. else:
  3540. # move to next point
  3541. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3542. # store prev_z
  3543. prev_z = travel[0]
  3544. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3545. if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  3546. doc = deepcopy(self.z_cut)
  3547. self.z_cut = 0.0
  3548. while abs(self.z_cut) < abs(doc):
  3549. self.z_cut -= self.z_depthpercut
  3550. if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  3551. self.z_cut = doc
  3552. gcode += self.doformat(p.down_code, x=locx, y=locy)
  3553. measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3554. if self.f_retract is False:
  3555. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3556. measured_up_to_zero_distance += abs(self.z_cut)
  3557. measured_lift_distance += abs(self.z_move)
  3558. else:
  3559. measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3560. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3561. else:
  3562. gcode += self.doformat(p.down_code, x=locx, y=locy)
  3563. measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3564. if self.f_retract is False:
  3565. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3566. measured_up_to_zero_distance += abs(self.z_cut)
  3567. measured_lift_distance += abs(self.z_move)
  3568. else:
  3569. measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3570. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3571. measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  3572. self.oldx = locx
  3573. self.oldy = locy
  3574. loc_nr += 1
  3575. disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  3576. if old_disp_number < disp_number <= 100:
  3577. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  3578. old_disp_number = disp_number
  3579. else:
  3580. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  3581. return 'fail'
  3582. self.z_cut = deepcopy(old_zcut)
  3583. else:
  3584. # We are not using Toolchange therefore we need to decide which tool properties to use
  3585. one_tool = 1
  3586. all_points = []
  3587. for tool in points:
  3588. # check if it has drills
  3589. if not points[tool]:
  3590. continue
  3591. all_points += points[tool]
  3592. if self.app.abort_flag:
  3593. # graceful abort requested by the user
  3594. raise grace
  3595. self.tool = one_tool
  3596. self.tooldia = self.exc_tools[one_tool]["tooldia"]
  3597. self.postdata['toolC'] = self.tooldia
  3598. if self.use_ui:
  3599. self.z_feedrate = self.exc_tools[one_tool]['data']['tools_drill_feedrate_z']
  3600. self.feedrate = self.exc_tools[one_tool]['data']['tools_drill_feedrate_z']
  3601. self.z_cut = self.exc_tools[one_tool]['data']['tools_drill_cutz']
  3602. gcode += self.doformat(p.z_feedrate_code)
  3603. if self.machinist_setting == 0:
  3604. if self.z_cut > 0:
  3605. self.app.inform.emit('[WARNING] %s' %
  3606. _("The Cut Z parameter has positive value. "
  3607. "It is the depth value to drill into material.\n"
  3608. "The Cut Z parameter needs to have a negative value, "
  3609. "assuming it is a typo "
  3610. "therefore the app will convert the value to negative. "
  3611. "Check the resulting CNC code (Gcode etc)."))
  3612. self.z_cut = -self.z_cut
  3613. elif self.z_cut == 0:
  3614. self.app.inform.emit('[WARNING] %s: %s' %
  3615. (_(
  3616. "The Cut Z parameter is zero. There will be no cut, "
  3617. "skipping file"),
  3618. exobj.options['name']))
  3619. return 'fail'
  3620. old_zcut = deepcopy(self.z_cut)
  3621. self.z_move = self.exc_tools[one_tool]['data']['tools_drill_travelz']
  3622. self.spindlespeed = self.exc_tools[one_tool]['data']['tools_drill_spindlespeed']
  3623. self.dwell = self.exc_tools[one_tool]['data']['tools_drill_dwell']
  3624. self.dwelltime = self.exc_tools[one_tool]['data']['tools_drill_dwelltime']
  3625. self.multidepth = self.exc_tools[one_tool]['data']['tools_drill_multidepth']
  3626. self.z_depthpercut = self.exc_tools[one_tool]['data']['tools_drill_depthperpass']
  3627. else:
  3628. old_zcut = deepcopy(self.z_cut)
  3629. # #########################################################################################################
  3630. # ############ Create the data. #################
  3631. # #########################################################################################################
  3632. locations = []
  3633. altPoints = []
  3634. optimized_path = []
  3635. if used_excellon_optimization_type == 'M':
  3636. if all_points:
  3637. locations = self.create_tool_data_array(points=all_points)
  3638. # if there are no locations then go to the next tool
  3639. if not locations:
  3640. return 'fail'
  3641. opt_time = self.app.defaults["excellon_search_time"]
  3642. optimized_path = self.optimized_ortools_meta(locations=locations, opt_time=opt_time)
  3643. elif used_excellon_optimization_type == 'B':
  3644. if all_points:
  3645. locations = self.create_tool_data_array(points=all_points)
  3646. # if there are no locations then go to the next tool
  3647. if not locations:
  3648. return 'fail'
  3649. optimized_path = self.optimized_ortools_basic(locations=locations)
  3650. elif used_excellon_optimization_type == 'T':
  3651. for point in all_points:
  3652. altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
  3653. optimized_path = self.optimized_travelling_salesman(altPoints)
  3654. else:
  3655. # it's actually not optimized path but here we build a list of (x,y) coordinates
  3656. # out of the tool's drills
  3657. for pt in all_points:
  3658. unoptimized_coords = (
  3659. pt.x,
  3660. pt.y
  3661. )
  3662. optimized_path.append(unoptimized_coords)
  3663. # #########################################################################################################
  3664. # #########################################################################################################
  3665. # Only if there are locations to drill
  3666. if not optimized_path:
  3667. return 'fail'
  3668. if self.app.abort_flag:
  3669. # graceful abort requested by the user
  3670. raise grace
  3671. # Spindle start
  3672. gcode += self.doformat(p.spindle_code)
  3673. # Dwell time
  3674. if self.dwell is True:
  3675. gcode += self.doformat(p.dwell_code)
  3676. current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[one_tool]["tooldia"])))
  3677. self.app.inform.emit(
  3678. '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  3679. str(current_tooldia),
  3680. str(self.units))
  3681. )
  3682. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3683. # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  3684. # because the values for Z offset are created in build_ui()
  3685. # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3686. try:
  3687. z_offset = float(self.exc_tools[one_tool]['data']['tools_drill_offset']) * (-1)
  3688. except KeyError:
  3689. z_offset = 0
  3690. self.z_cut = z_offset + old_zcut
  3691. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  3692. if self.coordinates_type == "G90":
  3693. # Drillling! for Absolute coordinates type G90
  3694. # variables to display the percentage of work done
  3695. geo_len = len(optimized_path)
  3696. old_disp_number = 0
  3697. log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  3698. loc_nr = 0
  3699. for point in optimized_path:
  3700. if self.app.abort_flag:
  3701. # graceful abort requested by the user
  3702. raise grace
  3703. if used_excellon_optimization_type == 'T':
  3704. locx = point[0]
  3705. locy = point[1]
  3706. else:
  3707. locx = locations[point][0]
  3708. locy = locations[point][1]
  3709. travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
  3710. end_point=(locx, locy),
  3711. tooldia=current_tooldia)
  3712. prev_z = None
  3713. for travel in travels:
  3714. locx = travel[1][0]
  3715. locy = travel[1][1]
  3716. if travel[0] is not None:
  3717. # move to next point
  3718. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3719. # raise to safe Z (travel[0]) each time because safe Z may be different
  3720. self.z_move = travel[0]
  3721. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3722. # restore z_move
  3723. self.z_move = self.exc_tools[one_tool]['data']['tools_drill_travelz']
  3724. else:
  3725. if prev_z is not None:
  3726. # move to next point
  3727. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3728. # we assume that previously the z_move was altered therefore raise to
  3729. # the travel_z (z_move)
  3730. self.z_move = self.exc_tools[one_tool]['data']['tools_drill_travelz']
  3731. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3732. else:
  3733. # move to next point
  3734. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3735. # store prev_z
  3736. prev_z = travel[0]
  3737. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3738. if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  3739. doc = deepcopy(self.z_cut)
  3740. self.z_cut = 0.0
  3741. while abs(self.z_cut) < abs(doc):
  3742. self.z_cut -= self.z_depthpercut
  3743. if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  3744. self.z_cut = doc
  3745. gcode += self.doformat(p.down_code, x=locx, y=locy)
  3746. measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3747. if self.f_retract is False:
  3748. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3749. measured_up_to_zero_distance += abs(self.z_cut)
  3750. measured_lift_distance += abs(self.z_move)
  3751. else:
  3752. measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3753. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3754. else:
  3755. gcode += self.doformat(p.down_code, x=locx, y=locy)
  3756. measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3757. if self.f_retract is False:
  3758. gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3759. measured_up_to_zero_distance += abs(self.z_cut)
  3760. measured_lift_distance += abs(self.z_move)
  3761. else:
  3762. measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3763. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3764. measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  3765. self.oldx = locx
  3766. self.oldy = locy
  3767. loc_nr += 1
  3768. disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  3769. if old_disp_number < disp_number <= 100:
  3770. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  3771. old_disp_number = disp_number
  3772. else:
  3773. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  3774. return 'fail'
  3775. self.z_cut = deepcopy(old_zcut)
  3776. if used_excellon_optimization_type == 'M':
  3777. log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
  3778. elif used_excellon_optimization_type == 'B':
  3779. log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
  3780. elif used_excellon_optimization_type == 'T':
  3781. log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
  3782. else:
  3783. log.debug("The total travel distance with with no optimization is: %s" % str(measured_distance))
  3784. # if used_excellon_optimization_type == 'M':
  3785. # log.debug("Using OR-Tools Metaheuristic Guided Local Search drill path optimization.")
  3786. #
  3787. # if has_drills:
  3788. # for tool in tools:
  3789. # if self.app.abort_flag:
  3790. # # graceful abort requested by the user
  3791. # raise grace
  3792. #
  3793. # self.tool = tool
  3794. # self.tooldia = self.exc_tools[tool]["tooldia"]
  3795. # self.postdata['toolC'] = self.tooldia
  3796. #
  3797. # if self.use_ui:
  3798. # self.z_feedrate = self.exc_tools[tool]['data']['feedrate_z']
  3799. # self.feedrate = self.exc_tools[tool]['data']['feedrate']
  3800. # gcode += self.doformat(p.z_feedrate_code)
  3801. # self.z_cut = self.exc_tools[tool]['data']['cutz']
  3802. #
  3803. # if self.machinist_setting == 0:
  3804. # if self.z_cut > 0:
  3805. # self.app.inform.emit('[WARNING] %s' %
  3806. # _("The Cut Z parameter has positive value. "
  3807. # "It is the depth value to drill into material.\n"
  3808. # "The Cut Z parameter needs to have a negative value, "
  3809. # "assuming it is a typo "
  3810. # "therefore the app will convert the value to negative. "
  3811. # "Check the resulting CNC code (Gcode etc)."))
  3812. # self.z_cut = -self.z_cut
  3813. # elif self.z_cut == 0:
  3814. # self.app.inform.emit('[WARNING] %s: %s' %
  3815. # (_(
  3816. # "The Cut Z parameter is zero. There will be no cut, "
  3817. # "skipping file"),
  3818. # exobj.options['name']))
  3819. # return 'fail'
  3820. #
  3821. # old_zcut = deepcopy(self.z_cut)
  3822. #
  3823. # self.z_move = self.exc_tools[tool]['data']['travelz']
  3824. # self.spindlespeed = self.exc_tools[tool]['data']['spindlespeed']
  3825. # self.dwell = self.exc_tools[tool]['data']['dwell']
  3826. # self.dwelltime = self.exc_tools[tool]['data']['dwelltime']
  3827. # self.multidepth = self.exc_tools[tool]['data']['multidepth']
  3828. # self.z_depthpercut = self.exc_tools[tool]['data']['depthperpass']
  3829. # else:
  3830. # old_zcut = deepcopy(self.z_cut)
  3831. #
  3832. # # ###############################################
  3833. # # ############ Create the data. #################
  3834. # # ###############################################
  3835. # locations = self.create_tool_data_array(tool=tool, points=points)
  3836. # # if there are no locations then go to the next tool
  3837. # if not locations:
  3838. # continue
  3839. # optimized_path = self.optimized_ortools_meta(locations=locations)
  3840. #
  3841. # # Only if tool has points.
  3842. # if tool in points:
  3843. # if self.app.abort_flag:
  3844. # # graceful abort requested by the user
  3845. # raise grace
  3846. #
  3847. # # Tool change sequence (optional)
  3848. # if self.toolchange:
  3849. # gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
  3850. # # Spindle start
  3851. # gcode += self.doformat(p.spindle_code)
  3852. # # Dwell time
  3853. # if self.dwell is True:
  3854. # gcode += self.doformat(p.dwell_code)
  3855. #
  3856. # current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[tool]["tooldia"])))
  3857. #
  3858. # self.app.inform.emit(
  3859. # '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  3860. # str(current_tooldia),
  3861. # str(self.units))
  3862. # )
  3863. #
  3864. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3865. # # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  3866. # # because the values for Z offset are created in build_ui()
  3867. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3868. # try:
  3869. # z_offset = float(self.exc_tools[tool]['data']['offset']) * (-1)
  3870. # except KeyError:
  3871. # z_offset = 0
  3872. # self.z_cut = z_offset + old_zcut
  3873. #
  3874. # self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  3875. # if self.coordinates_type == "G90":
  3876. # # Drillling! for Absolute coordinates type G90
  3877. # # variables to display the percentage of work done
  3878. # geo_len = len(optimized_path)
  3879. #
  3880. # old_disp_number = 0
  3881. # log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  3882. #
  3883. # loc_nr = 0
  3884. # for k in optimized_path:
  3885. # if self.app.abort_flag:
  3886. # # graceful abort requested by the user
  3887. # raise grace
  3888. #
  3889. # locx = locations[k][0]
  3890. # locy = locations[k][1]
  3891. #
  3892. # travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
  3893. # end_point=(locx, locy),
  3894. # tooldia=current_tooldia)
  3895. # prev_z = None
  3896. # for travel in travels:
  3897. # locx = travel[1][0]
  3898. # locy = travel[1][1]
  3899. #
  3900. # if travel[0] is not None:
  3901. # # move to next point
  3902. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3903. #
  3904. # # raise to safe Z (travel[0]) each time because safe Z may be different
  3905. # self.z_move = travel[0]
  3906. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3907. #
  3908. # # restore z_move
  3909. # self.z_move = self.exc_tools[tool]['data']['travelz']
  3910. # else:
  3911. # if prev_z is not None:
  3912. # # move to next point
  3913. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3914. #
  3915. # # we assume that previously the z_move was altered therefore raise to
  3916. # # the travel_z (z_move)
  3917. # self.z_move = self.exc_tools[tool]['data']['travelz']
  3918. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3919. # else:
  3920. # # move to next point
  3921. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3922. #
  3923. # # store prev_z
  3924. # prev_z = travel[0]
  3925. #
  3926. # # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  3927. #
  3928. # if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  3929. # doc = deepcopy(self.z_cut)
  3930. # self.z_cut = 0.0
  3931. #
  3932. # while abs(self.z_cut) < abs(doc):
  3933. #
  3934. # self.z_cut -= self.z_depthpercut
  3935. # if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  3936. # self.z_cut = doc
  3937. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  3938. #
  3939. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3940. #
  3941. # if self.f_retract is False:
  3942. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3943. # measured_up_to_zero_distance += abs(self.z_cut)
  3944. # measured_lift_distance += abs(self.z_move)
  3945. # else:
  3946. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3947. #
  3948. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3949. #
  3950. # else:
  3951. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  3952. #
  3953. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  3954. #
  3955. # if self.f_retract is False:
  3956. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  3957. # measured_up_to_zero_distance += abs(self.z_cut)
  3958. # measured_lift_distance += abs(self.z_move)
  3959. # else:
  3960. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  3961. #
  3962. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  3963. #
  3964. # measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  3965. # self.oldx = locx
  3966. # self.oldy = locy
  3967. #
  3968. # loc_nr += 1
  3969. # disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  3970. #
  3971. # if old_disp_number < disp_number <= 100:
  3972. # self.app.proc_container.update_view_text(' %d%%' % disp_number)
  3973. # old_disp_number = disp_number
  3974. #
  3975. # else:
  3976. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  3977. # return 'fail'
  3978. # self.z_cut = deepcopy(old_zcut)
  3979. # else:
  3980. # log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  3981. # "The loaded Excellon file has no drills ...")
  3982. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
  3983. # return 'fail'
  3984. #
  3985. # log.debug("The total travel distance with OR-TOOLS Metaheuristics is: %s" % str(measured_distance))
  3986. #
  3987. # elif used_excellon_optimization_type == 'B':
  3988. # log.debug("Using OR-Tools Basic drill path optimization.")
  3989. #
  3990. # if has_drills:
  3991. # for tool in tools:
  3992. # if self.app.abort_flag:
  3993. # # graceful abort requested by the user
  3994. # raise grace
  3995. #
  3996. # self.tool = tool
  3997. # self.tooldia = self.exc_tools[tool]["tooldia"]
  3998. # self.postdata['toolC'] = self.tooldia
  3999. #
  4000. # if self.use_ui:
  4001. # self.z_feedrate = self.exc_tools[tool]['data']['feedrate_z']
  4002. # self.feedrate = self.exc_tools[tool]['data']['feedrate']
  4003. # gcode += self.doformat(p.z_feedrate_code)
  4004. # self.z_cut = self.exc_tools[tool]['data']['cutz']
  4005. #
  4006. # if self.machinist_setting == 0:
  4007. # if self.z_cut > 0:
  4008. # self.app.inform.emit('[WARNING] %s' %
  4009. # _("The Cut Z parameter has positive value. "
  4010. # "It is the depth value to drill into material.\n"
  4011. # "The Cut Z parameter needs to have a negative value, "
  4012. # "assuming it is a typo "
  4013. # "therefore the app will convert the value to negative. "
  4014. # "Check the resulting CNC code (Gcode etc)."))
  4015. # self.z_cut = -self.z_cut
  4016. # elif self.z_cut == 0:
  4017. # self.app.inform.emit('[WARNING] %s: %s' %
  4018. # (_(
  4019. # "The Cut Z parameter is zero. There will be no cut, "
  4020. # "skipping file"),
  4021. # exobj.options['name']))
  4022. # return 'fail'
  4023. #
  4024. # old_zcut = deepcopy(self.z_cut)
  4025. #
  4026. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4027. #
  4028. # self.spindlespeed = self.exc_tools[tool]['data']['spindlespeed']
  4029. # self.dwell = self.exc_tools[tool]['data']['dwell']
  4030. # self.dwelltime = self.exc_tools[tool]['data']['dwelltime']
  4031. # self.multidepth = self.exc_tools[tool]['data']['multidepth']
  4032. # self.z_depthpercut = self.exc_tools[tool]['data']['depthperpass']
  4033. # else:
  4034. # old_zcut = deepcopy(self.z_cut)
  4035. #
  4036. # # ###############################################
  4037. # # ############ Create the data. #################
  4038. # # ###############################################
  4039. # locations = self.create_tool_data_array(tool=tool, points=points)
  4040. # # if there are no locations then go to the next tool
  4041. # if not locations:
  4042. # continue
  4043. # optimized_path = self.optimized_ortools_basic(locations=locations)
  4044. #
  4045. # # Only if tool has points.
  4046. # if tool in points:
  4047. # if self.app.abort_flag:
  4048. # # graceful abort requested by the user
  4049. # raise grace
  4050. #
  4051. # # Tool change sequence (optional)
  4052. # if self.toolchange:
  4053. # gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
  4054. # gcode += self.doformat(p.spindle_code) # Spindle start)
  4055. # if self.dwell is True:
  4056. # gcode += self.doformat(p.dwell_code) # Dwell time
  4057. # else:
  4058. # gcode += self.doformat(p.spindle_code)
  4059. # if self.dwell is True:
  4060. # gcode += self.doformat(p.dwell_code) # Dwell time
  4061. #
  4062. # current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[tool]["tooldia"])))
  4063. #
  4064. # self.app.inform.emit(
  4065. # '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  4066. # str(current_tooldia),
  4067. # str(self.units))
  4068. # )
  4069. #
  4070. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  4071. # # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  4072. # # because the values for Z offset are created in build_ui()
  4073. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  4074. # try:
  4075. # z_offset = float(self.exc_tools[tool]['data']['offset']) * (-1)
  4076. # except KeyError:
  4077. # z_offset = 0
  4078. # self.z_cut = z_offset + old_zcut
  4079. #
  4080. # self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  4081. # if self.coordinates_type == "G90":
  4082. # # Drillling! for Absolute coordinates type G90
  4083. # # variables to display the percentage of work done
  4084. # geo_len = len(optimized_path)
  4085. # old_disp_number = 0
  4086. # log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  4087. #
  4088. # loc_nr = 0
  4089. # for k in optimized_path:
  4090. # if self.app.abort_flag:
  4091. # # graceful abort requested by the user
  4092. # raise grace
  4093. #
  4094. # locx = locations[k][0]
  4095. # locy = locations[k][1]
  4096. #
  4097. # travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
  4098. # end_point=(locx, locy),
  4099. # tooldia=current_tooldia)
  4100. # prev_z = None
  4101. # for travel in travels:
  4102. # locx = travel[1][0]
  4103. # locy = travel[1][1]
  4104. #
  4105. # if travel[0] is not None:
  4106. # # move to next point
  4107. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4108. #
  4109. # # raise to safe Z (travel[0]) each time because safe Z may be different
  4110. # self.z_move = travel[0]
  4111. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4112. #
  4113. # # restore z_move
  4114. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4115. # else:
  4116. # if prev_z is not None:
  4117. # # move to next point
  4118. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4119. #
  4120. # # we assume that previously the z_move was altered therefore raise to
  4121. # # the travel_z (z_move)
  4122. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4123. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4124. # else:
  4125. # # move to next point
  4126. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4127. #
  4128. # # store prev_z
  4129. # prev_z = travel[0]
  4130. #
  4131. # # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4132. #
  4133. # if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  4134. # doc = deepcopy(self.z_cut)
  4135. # self.z_cut = 0.0
  4136. #
  4137. # while abs(self.z_cut) < abs(doc):
  4138. #
  4139. # self.z_cut -= self.z_depthpercut
  4140. # if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  4141. # self.z_cut = doc
  4142. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  4143. #
  4144. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  4145. #
  4146. # if self.f_retract is False:
  4147. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4148. # measured_up_to_zero_distance += abs(self.z_cut)
  4149. # measured_lift_distance += abs(self.z_move)
  4150. # else:
  4151. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  4152. #
  4153. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4154. #
  4155. # else:
  4156. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  4157. #
  4158. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  4159. #
  4160. # if self.f_retract is False:
  4161. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4162. # measured_up_to_zero_distance += abs(self.z_cut)
  4163. # measured_lift_distance += abs(self.z_move)
  4164. # else:
  4165. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  4166. #
  4167. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4168. #
  4169. # measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  4170. # self.oldx = locx
  4171. # self.oldy = locy
  4172. #
  4173. # loc_nr += 1
  4174. # disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  4175. #
  4176. # if old_disp_number < disp_number <= 100:
  4177. # self.app.proc_container.update_view_text(' %d%%' % disp_number)
  4178. # old_disp_number = disp_number
  4179. #
  4180. # else:
  4181. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  4182. # return 'fail'
  4183. # self.z_cut = deepcopy(old_zcut)
  4184. # else:
  4185. # log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  4186. # "The loaded Excellon file has no drills ...")
  4187. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
  4188. # return 'fail'
  4189. #
  4190. # log.debug("The total travel distance with OR-TOOLS Basic Algorithm is: %s" % str(measured_distance))
  4191. #
  4192. # elif used_excellon_optimization_type == 'T':
  4193. # log.debug("Using Travelling Salesman drill path optimization.")
  4194. #
  4195. # for tool in tools:
  4196. # if self.app.abort_flag:
  4197. # # graceful abort requested by the user
  4198. # raise grace
  4199. #
  4200. # if has_drills:
  4201. # self.tool = tool
  4202. # self.tooldia = self.exc_tools[tool]["tooldia"]
  4203. # self.postdata['toolC'] = self.tooldia
  4204. #
  4205. # if self.use_ui:
  4206. # self.z_feedrate = self.exc_tools[tool]['data']['feedrate_z']
  4207. # self.feedrate = self.exc_tools[tool]['data']['feedrate']
  4208. # gcode += self.doformat(p.z_feedrate_code)
  4209. #
  4210. # self.z_cut = self.exc_tools[tool]['data']['cutz']
  4211. #
  4212. # if self.machinist_setting == 0:
  4213. # if self.z_cut > 0:
  4214. # self.app.inform.emit('[WARNING] %s' %
  4215. # _("The Cut Z parameter has positive value. "
  4216. # "It is the depth value to drill into material.\n"
  4217. # "The Cut Z parameter needs to have a negative value, "
  4218. # "assuming it is a typo "
  4219. # "therefore the app will convert the value to negative. "
  4220. # "Check the resulting CNC code (Gcode etc)."))
  4221. # self.z_cut = -self.z_cut
  4222. # elif self.z_cut == 0:
  4223. # self.app.inform.emit('[WARNING] %s: %s' %
  4224. # (_(
  4225. # "The Cut Z parameter is zero. There will be no cut, "
  4226. # "skipping file"),
  4227. # exobj.options['name']))
  4228. # return 'fail'
  4229. #
  4230. # old_zcut = deepcopy(self.z_cut)
  4231. #
  4232. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4233. # self.spindlespeed = self.exc_tools[tool]['data']['spindlespeed']
  4234. # self.dwell = self.exc_tools[tool]['data']['dwell']
  4235. # self.dwelltime = self.exc_tools[tool]['data']['dwelltime']
  4236. # self.multidepth = self.exc_tools[tool]['data']['multidepth']
  4237. # self.z_depthpercut = self.exc_tools[tool]['data']['depthperpass']
  4238. # else:
  4239. # old_zcut = deepcopy(self.z_cut)
  4240. #
  4241. # # ###############################################
  4242. # # ############ Create the data. #################
  4243. # # ###############################################
  4244. # altPoints = []
  4245. # for point in points[tool]:
  4246. # altPoints.append((point.coords.xy[0][0], point.coords.xy[1][0]))
  4247. # optimized_path = self.optimized_travelling_salesman(altPoints)
  4248. #
  4249. # # Only if tool has points.
  4250. # if tool in points:
  4251. # if self.app.abort_flag:
  4252. # # graceful abort requested by the user
  4253. # raise grace
  4254. #
  4255. # # Tool change sequence (optional)
  4256. # if self.toolchange:
  4257. # gcode += self.doformat(p.toolchange_code, toolchangexy=(self.oldx, self.oldy))
  4258. # gcode += self.doformat(p.spindle_code) # Spindle start)
  4259. # if self.dwell is True:
  4260. # gcode += self.doformat(p.dwell_code) # Dwell time
  4261. # else:
  4262. # gcode += self.doformat(p.spindle_code)
  4263. # if self.dwell is True:
  4264. # gcode += self.doformat(p.dwell_code) # Dwell time
  4265. #
  4266. # current_tooldia = float('%.*f' % (self.decimals, float(self.exc_tools[tool]["tooldia"])))
  4267. #
  4268. # self.app.inform.emit(
  4269. # '%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  4270. # str(current_tooldia),
  4271. # str(self.units))
  4272. # )
  4273. #
  4274. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  4275. # # APPLY Offset only when using the appGUI, for TclCommand this will create an error
  4276. # # because the values for Z offset are created in build_ui()
  4277. # # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  4278. # try:
  4279. # z_offset = float(self.exc_tools[tool]['data']['offset']) * (-1)
  4280. # except KeyError:
  4281. # z_offset = 0
  4282. # self.z_cut = z_offset + old_zcut
  4283. #
  4284. # self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  4285. # if self.coordinates_type == "G90":
  4286. # # Drillling! for Absolute coordinates type G90
  4287. # # variables to display the percentage of work done
  4288. # geo_len = len(optimized_path)
  4289. # old_disp_number = 0
  4290. # log.warning("Number of drills for which to generate GCode: %s" % str(geo_len))
  4291. #
  4292. # loc_nr = 0
  4293. # for point in optimized_path:
  4294. # if self.app.abort_flag:
  4295. # # graceful abort requested by the user
  4296. # raise grace
  4297. #
  4298. # locx = point[0]
  4299. # locy = point[1]
  4300. #
  4301. # travels = self.app.exc_areas.travel_coordinates(start_point=(self.oldx, self.oldy),
  4302. # end_point=(locx, locy),
  4303. # tooldia=current_tooldia)
  4304. # prev_z = None
  4305. # for travel in travels:
  4306. # locx = travel[1][0]
  4307. # locy = travel[1][1]
  4308. #
  4309. # if travel[0] is not None:
  4310. # # move to next point
  4311. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4312. #
  4313. # # raise to safe Z (travel[0]) each time because safe Z may be different
  4314. # self.z_move = travel[0]
  4315. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4316. #
  4317. # # restore z_move
  4318. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4319. # else:
  4320. # if prev_z is not None:
  4321. # # move to next point
  4322. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4323. #
  4324. # # we assume that previously the z_move was altered therefore raise to
  4325. # # the travel_z (z_move)
  4326. # self.z_move = self.exc_tools[tool]['data']['travelz']
  4327. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4328. # else:
  4329. # # move to next point
  4330. # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4331. #
  4332. # # store prev_z
  4333. # prev_z = travel[0]
  4334. #
  4335. # # gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  4336. #
  4337. # if self.multidepth and abs(self.z_cut) > abs(self.z_depthpercut):
  4338. # doc = deepcopy(self.z_cut)
  4339. # self.z_cut = 0.0
  4340. #
  4341. # while abs(self.z_cut) < abs(doc):
  4342. #
  4343. # self.z_cut -= self.z_depthpercut
  4344. # if abs(doc) < abs(self.z_cut) < (abs(doc) + self.z_depthpercut):
  4345. # self.z_cut = doc
  4346. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  4347. #
  4348. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  4349. #
  4350. # if self.f_retract is False:
  4351. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4352. # measured_up_to_zero_distance += abs(self.z_cut)
  4353. # measured_lift_distance += abs(self.z_move)
  4354. # else:
  4355. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  4356. #
  4357. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4358. #
  4359. # else:
  4360. # gcode += self.doformat(p.down_code, x=locx, y=locy)
  4361. #
  4362. # measured_down_distance += abs(self.z_cut) + abs(self.z_move)
  4363. #
  4364. # if self.f_retract is False:
  4365. # gcode += self.doformat(p.up_to_zero_code, x=locx, y=locy)
  4366. # measured_up_to_zero_distance += abs(self.z_cut)
  4367. # measured_lift_distance += abs(self.z_move)
  4368. # else:
  4369. # measured_lift_distance += abs(self.z_cut) + abs(self.z_move)
  4370. #
  4371. # gcode += self.doformat(p.lift_code, x=locx, y=locy)
  4372. #
  4373. # measured_distance += abs(distance_euclidian(locx, locy, self.oldx, self.oldy))
  4374. # self.oldx = locx
  4375. # self.oldy = locy
  4376. #
  4377. # loc_nr += 1
  4378. # disp_number = int(np.interp(loc_nr, [0, geo_len], [0, 100]))
  4379. #
  4380. # if old_disp_number < disp_number <= 100:
  4381. # self.app.proc_container.update_view_text(' %d%%' % disp_number)
  4382. # old_disp_number = disp_number
  4383. # else:
  4384. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  4385. # return 'fail'
  4386. # else:
  4387. # log.debug("camlib.CNCJob.generate_from_excellon_by_tool() --> "
  4388. # "The loaded Excellon file has no drills ...")
  4389. # self.app.inform.emit('[ERROR_NOTCL] %s...' % _('The loaded Excellon file has no drills'))
  4390. # return 'fail'
  4391. # self.z_cut = deepcopy(old_zcut)
  4392. # log.debug("The total travel distance with Travelling Salesman Algorithm is: %s" % str(measured_distance))
  4393. #
  4394. # else:
  4395. # log.debug("camlib.CNCJob.generate_from_excellon_by_tool(): Chosen drill optimization doesn't exist.")
  4396. # return 'fail'
  4397. # Spindle stop
  4398. gcode += self.doformat(p.spindle_stop_code)
  4399. # Move to End position
  4400. gcode += self.doformat(p.end_code, x=0, y=0)
  4401. # #############################################################################################################
  4402. # ############################# Calculate DISTANCE and ESTIMATED TIME #########################################
  4403. # #############################################################################################################
  4404. measured_distance += abs(distance_euclidian(self.oldx, self.oldy, 0, 0))
  4405. log.debug("The total travel distance including travel to end position is: %s" %
  4406. str(measured_distance) + '\n')
  4407. self.travel_distance = measured_distance
  4408. # I use the value of self.feedrate_rapid for the feadrate in case of the measure_lift_distance and for
  4409. # traveled_time because it is not always possible to determine the feedrate that the CNC machine uses
  4410. # for G0 move (the fastest speed available to the CNC router). Although self.feedrate_rapids is used only with
  4411. # Marlin preprocessor and derivatives.
  4412. self.routing_time = (measured_down_distance + measured_up_to_zero_distance) / self.feedrate
  4413. lift_time = measured_lift_distance / self.feedrate_rapid
  4414. traveled_time = measured_distance / self.feedrate_rapid
  4415. self.routing_time += lift_time + traveled_time
  4416. # #############################################################################################################
  4417. # ############################# Store the GCODE for further usage ############################################
  4418. # #############################################################################################################
  4419. self.gcode = gcode
  4420. self.app.inform.emit('%s ...' % _("Finished G-Code generation"))
  4421. return gcode, start_gcode
  4422. # no longer used
  4423. def generate_from_multitool_geometry(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=1.0,
  4424. z_move=2.0, feedrate=2.0, feedrate_z=2.0, feedrate_rapid=30,
  4425. spindlespeed=None, spindledir='CW', dwell=False, dwelltime=1.0,
  4426. multidepth=False, depthpercut=None, toolchange=False, toolchangez=1.0,
  4427. toolchangexy="0.0, 0.0", extracut=False, extracut_length=0.2,
  4428. startz=None, endz=2.0, endxy='', pp_geometry_name=None, tool_no=1):
  4429. """
  4430. Algorithm to generate from multitool Geometry.
  4431. Algorithm description:
  4432. ----------------------
  4433. Uses RTree to find the nearest path to follow.
  4434. :param geometry:
  4435. :param append:
  4436. :param tooldia:
  4437. :param offset:
  4438. :param tolerance:
  4439. :param z_cut:
  4440. :param z_move:
  4441. :param feedrate:
  4442. :param feedrate_z:
  4443. :param feedrate_rapid:
  4444. :param spindlespeed:
  4445. :param spindledir: Direction of rotation for the spindle. If using GRBL laser mode will
  4446. adjust the laser mode
  4447. :param dwell:
  4448. :param dwelltime:
  4449. :param multidepth: If True, use multiple passes to reach the desired depth.
  4450. :param depthpercut: Maximum depth in each pass.
  4451. :param toolchange:
  4452. :param toolchangez:
  4453. :param toolchangexy:
  4454. :param extracut: Adds (or not) an extra cut at the end of each path overlapping the
  4455. first point in path to ensure complete copper removal
  4456. :param extracut_length: Extra cut legth at the end of the path
  4457. :param startz:
  4458. :param endz:
  4459. :param endxy:
  4460. :param pp_geometry_name:
  4461. :param tool_no:
  4462. :return: GCode - string
  4463. """
  4464. log.debug("generate_from_multitool_geometry()")
  4465. temp_solid_geometry = []
  4466. if offset != 0.0:
  4467. for it in geometry:
  4468. # if the geometry is a closed shape then create a Polygon out of it
  4469. if isinstance(it, LineString):
  4470. c = it.coords
  4471. if c[0] == c[-1]:
  4472. it = Polygon(it)
  4473. temp_solid_geometry.append(it.buffer(offset, join_style=2))
  4474. else:
  4475. temp_solid_geometry = geometry
  4476. # ## Flatten the geometry. Only linear elements (no polygons) remain.
  4477. flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
  4478. log.debug("%d paths" % len(flat_geometry))
  4479. try:
  4480. self.tooldia = float(tooldia)
  4481. except Exception as e:
  4482. self.app.inform.emit('[ERROR] %s\n%s' % (_("Failed."), str(e)))
  4483. return 'fail'
  4484. self.z_cut = float(z_cut) if z_cut else None
  4485. self.z_move = float(z_move) if z_move is not None else None
  4486. self.feedrate = float(feedrate) if feedrate else self.app.defaults["geometry_feedrate"]
  4487. self.z_feedrate = float(feedrate_z) if feedrate_z is not None else self.app.defaults["geometry_feedrate_z"]
  4488. self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid else self.app.defaults["geometry_feedrate_rapid"]
  4489. self.spindlespeed = int(spindlespeed) if spindlespeed != 0 else None
  4490. self.spindledir = spindledir
  4491. self.dwell = dwell
  4492. self.dwelltime = float(dwelltime) if dwelltime else self.app.defaults["geometry_dwelltime"]
  4493. self.startz = float(startz) if startz is not None else self.app.defaults["geometry_startz"]
  4494. self.z_end = float(endz) if endz is not None else self.app.defaults["geometry_endz"]
  4495. self.xy_end = re.sub('[()\[\]]', '', str(endxy)) if endxy else self.app.defaults["geometry_endxy"]
  4496. if self.xy_end and self.xy_end != '':
  4497. self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
  4498. if self.xy_end and len(self.xy_end) < 2:
  4499. self.app.inform.emit('[ERROR] %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
  4500. "in the format (x, y) but now there is only one value, not two."))
  4501. return 'fail'
  4502. self.z_depthpercut = float(depthpercut) if depthpercut else self.app.defaults["geometry_depthperpass"]
  4503. self.multidepth = multidepth
  4504. self.z_toolchange = float(toolchangez) if toolchangez is not None else self.app.defaults["geometry_toolchangez"]
  4505. # it servers in the preprocessor file
  4506. self.tool = tool_no
  4507. try:
  4508. if toolchangexy == '':
  4509. self.xy_toolchange = None
  4510. else:
  4511. self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy)) \
  4512. if toolchangexy else self.app.defaults["geometry_toolchangexy"]
  4513. if self.xy_toolchange and self.xy_toolchange != '':
  4514. self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
  4515. if len(self.xy_toolchange) < 2:
  4516. self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be "
  4517. "in the format (x, y) \n"
  4518. "but now there is only one value, not two."))
  4519. return 'fail'
  4520. except Exception as e:
  4521. log.debug("camlib.CNCJob.generate_from_multitool_geometry() --> %s" % str(e))
  4522. pass
  4523. self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
  4524. self.f_plunge = self.app.defaults["geometry_f_plunge"]
  4525. if self.z_cut is None:
  4526. if 'laser' not in self.pp_geometry_name:
  4527. self.app.inform.emit(
  4528. '[ERROR_NOTCL] %s' % _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
  4529. "other parameters."))
  4530. return 'fail'
  4531. else:
  4532. self.z_cut = 0
  4533. if self.machinist_setting == 0:
  4534. if self.z_cut > 0:
  4535. self.app.inform.emit('[WARNING] %s' %
  4536. _("The Cut Z parameter has positive value. "
  4537. "It is the depth value to cut into material.\n"
  4538. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  4539. "therefore the app will convert the value to negative."
  4540. "Check the resulting CNC code (Gcode etc)."))
  4541. self.z_cut = -self.z_cut
  4542. elif self.z_cut == 0 and 'laser' not in self.pp_geometry_name:
  4543. self.app.inform.emit('[WARNING] %s: %s' %
  4544. (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
  4545. self.options['name']))
  4546. return 'fail'
  4547. if self.z_move is None:
  4548. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Travel Z parameter is None or zero."))
  4549. return 'fail'
  4550. if self.z_move < 0:
  4551. self.app.inform.emit('[WARNING] %s' %
  4552. _("The Travel Z parameter has negative value. "
  4553. "It is the height value to travel between cuts.\n"
  4554. "The Z Travel parameter needs to have a positive value, assuming it is a typo "
  4555. "therefore the app will convert the value to positive."
  4556. "Check the resulting CNC code (Gcode etc)."))
  4557. self.z_move = -self.z_move
  4558. elif self.z_move == 0:
  4559. self.app.inform.emit('[WARNING] %s: %s' %
  4560. (_("The Z Travel parameter is zero. This is dangerous, skipping file"),
  4561. self.options['name']))
  4562. return 'fail'
  4563. # made sure that depth_per_cut is no more then the z_cut
  4564. if abs(self.z_cut) < self.z_depthpercut:
  4565. self.z_depthpercut = abs(self.z_cut)
  4566. # ## Index first and last points in paths
  4567. # What points to index.
  4568. def get_pts(o):
  4569. return [o.coords[0], o.coords[-1]]
  4570. # Create the indexed storage.
  4571. storage = FlatCAMRTreeStorage()
  4572. storage.get_points = get_pts
  4573. # Store the geometry
  4574. log.debug("Indexing geometry before generating G-Code...")
  4575. self.app.inform.emit(_("Indexing geometry before generating G-Code..."))
  4576. for geo_shape in flat_geometry:
  4577. if self.app.abort_flag:
  4578. # graceful abort requested by the user
  4579. raise grace
  4580. if geo_shape is not None:
  4581. storage.insert(geo_shape)
  4582. # self.input_geometry_bounds = geometry.bounds()
  4583. if not append:
  4584. self.gcode = ""
  4585. # tell preprocessor the number of tool (for toolchange)
  4586. self.tool = tool_no
  4587. # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter
  4588. # given under the name 'toolC'
  4589. self.postdata['toolC'] = self.tooldia
  4590. # Initial G-Code
  4591. self.pp_geometry = self.app.preprocessors[self.pp_geometry_name]
  4592. p = self.pp_geometry
  4593. self.gcode = self.doformat(p.start_code)
  4594. self.gcode += self.doformat(p.feedrate_code) # sets the feed rate
  4595. if toolchange is False:
  4596. self.gcode += self.doformat(p.lift_code, x=0, y=0) # Move (up) to travel height
  4597. self.gcode += self.doformat(p.startz_code, x=0, y=0)
  4598. if toolchange:
  4599. # if "line_xyz" in self.pp_geometry_name:
  4600. # self.gcode += self.doformat(p.toolchange_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4601. # else:
  4602. # self.gcode += self.doformat(p.toolchange_code)
  4603. self.gcode += self.doformat(p.toolchange_code)
  4604. if 'laser' not in self.pp_geometry_name:
  4605. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4606. else:
  4607. # for laser this will disable the laser
  4608. self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy) # Move (up) to travel height
  4609. if self.dwell is True:
  4610. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4611. else:
  4612. if 'laser' not in self.pp_geometry_name:
  4613. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4614. if self.dwell is True:
  4615. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4616. total_travel = 0.0
  4617. total_cut = 0.0
  4618. # ## Iterate over geometry paths getting the nearest each time.
  4619. log.debug("Starting G-Code...")
  4620. self.app.inform.emit('%s...' % _("Starting G-Code"))
  4621. path_count = 0
  4622. current_pt = (0, 0)
  4623. # variables to display the percentage of work done
  4624. geo_len = len(flat_geometry)
  4625. old_disp_number = 0
  4626. log.warning("Number of paths for which to generate GCode: %s" % str(geo_len))
  4627. current_tooldia = float('%.*f' % (self.decimals, float(self.tooldia)))
  4628. self.app.inform.emit('%s: %s%s.' % (_("Starting G-Code for tool with diameter"),
  4629. str(current_tooldia),
  4630. str(self.units)))
  4631. pt, geo = storage.nearest(current_pt)
  4632. try:
  4633. while True:
  4634. if self.app.abort_flag:
  4635. # graceful abort requested by the user
  4636. raise grace
  4637. path_count += 1
  4638. # Remove before modifying, otherwise deletion will fail.
  4639. storage.remove(geo)
  4640. # If last point in geometry is the nearest but prefer the first one if last point == first point
  4641. # then reverse coordinates.
  4642. if pt != geo.coords[0] and pt == geo.coords[-1]:
  4643. # geo.coords = list(geo.coords)[::-1] # Shapley 2.0
  4644. geo = LineString(list(geo.coords)[::-1])
  4645. # ---------- Single depth/pass --------
  4646. if not multidepth:
  4647. # calculate the cut distance
  4648. total_cut = total_cut + geo.length
  4649. self.gcode += self.create_gcode_single_pass(geo, current_tooldia, extracut, extracut_length,
  4650. tolerance, z_move=z_move, old_point=current_pt)
  4651. # --------- Multi-pass ---------
  4652. else:
  4653. # calculate the cut distance
  4654. # due of the number of cuts (multi depth) it has to multiplied by the number of cuts
  4655. nr_cuts = 0
  4656. depth = abs(self.z_cut)
  4657. while depth > 0:
  4658. nr_cuts += 1
  4659. depth -= float(self.z_depthpercut)
  4660. total_cut += (geo.length * nr_cuts)
  4661. gc, geo = self.create_gcode_multi_pass(geo, current_tooldia, extracut, extracut_length,
  4662. tolerance, z_move=z_move, postproc=p,
  4663. old_point=current_pt)
  4664. self.gcode += gc
  4665. # calculate the total distance
  4666. total_travel = total_travel + abs(distance(pt1=current_pt, pt2=pt))
  4667. current_pt = geo.coords[-1]
  4668. pt, geo = storage.nearest(current_pt) # Next
  4669. disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
  4670. if old_disp_number < disp_number <= 100:
  4671. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  4672. old_disp_number = disp_number
  4673. except StopIteration: # Nothing found in storage.
  4674. pass
  4675. log.debug("Finished G-Code... %s paths traced." % path_count)
  4676. # add move to end position
  4677. total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0))
  4678. self.travel_distance += total_travel + total_cut
  4679. self.routing_time += total_cut / self.feedrate
  4680. # Finish
  4681. self.gcode += self.doformat(p.spindle_stop_code)
  4682. self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
  4683. self.gcode += self.doformat(p.end_code, x=0, y=0)
  4684. self.app.inform.emit(
  4685. '%s... %s %s.' % (_("Finished G-Code generation"), str(path_count), _("paths traced"))
  4686. )
  4687. return self.gcode
  4688. def generate_from_geometry_2(self, geometry, append=True, tooldia=None, offset=0.0, tolerance=0, z_cut=None,
  4689. z_move=None, feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None,
  4690. spindledir='CW', dwell=False, dwelltime=None, multidepth=False, depthpercut=None,
  4691. toolchange=False, toolchangez=None, toolchangexy="0.0, 0.0", extracut=False,
  4692. extracut_length=None, startz=None, endz=None, endxy='', pp_geometry_name=None,
  4693. tool_no=1, is_first=False):
  4694. """
  4695. Second algorithm to generate from Geometry.
  4696. Algorithm description:
  4697. ----------------------
  4698. Uses RTree to find the nearest path to follow.
  4699. :param geometry:
  4700. :param append:
  4701. :param tooldia:
  4702. :param offset:
  4703. :param tolerance:
  4704. :param z_cut:
  4705. :param z_move:
  4706. :param feedrate:
  4707. :param feedrate_z:
  4708. :param feedrate_rapid:
  4709. :param spindlespeed:
  4710. :param spindledir:
  4711. :param dwell:
  4712. :param dwelltime:
  4713. :param multidepth: If True, use multiple passes to reach the desired depth.
  4714. :param depthpercut: Maximum depth in each pass.
  4715. :param toolchange:
  4716. :param toolchangez:
  4717. :param toolchangexy:
  4718. :param extracut: Adds (or not) an extra cut at the end of each path overlapping the first point in
  4719. path to ensure complete copper removal
  4720. :param extracut_length: The extra cut length
  4721. :param startz:
  4722. :param endz:
  4723. :param endxy:
  4724. :param pp_geometry_name:
  4725. :param tool_no:
  4726. :param is_first: if the processed tool is the first one and if we should process the start gcode
  4727. :return: None
  4728. """
  4729. log.debug("Executing camlib.CNCJob.generate_from_geometry_2()")
  4730. # if solid_geometry is empty raise an exception
  4731. if not geometry.solid_geometry:
  4732. self.app.inform.emit(
  4733. '[ERROR_NOTCL] %s' % _("Trying to generate a CNC Job from a Geometry object without solid_geometry.")
  4734. )
  4735. return 'fail'
  4736. def bounds_rec(obj):
  4737. if type(obj) is list:
  4738. minx = np.inf
  4739. miny = np.inf
  4740. maxx = -np.inf
  4741. maxy = -np.inf
  4742. for k in obj:
  4743. if type(k) is dict:
  4744. for key in k:
  4745. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  4746. minx = min(minx, minx_)
  4747. miny = min(miny, miny_)
  4748. maxx = max(maxx, maxx_)
  4749. maxy = max(maxy, maxy_)
  4750. else:
  4751. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  4752. minx = min(minx, minx_)
  4753. miny = min(miny, miny_)
  4754. maxx = max(maxx, maxx_)
  4755. maxy = max(maxy, maxy_)
  4756. return minx, miny, maxx, maxy
  4757. else:
  4758. # it's a Shapely object, return it's bounds
  4759. return obj.bounds
  4760. # Create the solid geometry which will be used to generate GCode
  4761. temp_solid_geometry = []
  4762. if offset != 0.0:
  4763. offset_for_use = offset
  4764. if offset < 0:
  4765. a, b, c, d = bounds_rec(geometry.solid_geometry)
  4766. # if the offset is less than half of the total length or less than half of the total width of the
  4767. # solid geometry it's obvious we can't do the offset
  4768. if -offset > ((c - a) / 2) or -offset > ((d - b) / 2):
  4769. self.app.inform.emit(
  4770. '[ERROR_NOTCL] %s' %
  4771. _("The Tool Offset value is too negative to use for the current_geometry.\n"
  4772. "Raise the value (in module) and try again.")
  4773. )
  4774. return 'fail'
  4775. # hack: make offset smaller by 0.0000000001 which is insignificant difference but allow the job
  4776. # to continue
  4777. elif -offset == ((c - a) / 2) or -offset == ((d - b) / 2):
  4778. offset_for_use = offset - 0.0000000001
  4779. for it in geometry.solid_geometry:
  4780. # if the geometry is a closed shape then create a Polygon out of it
  4781. if isinstance(it, LineString):
  4782. c = it.coords
  4783. if c[0] == c[-1]:
  4784. it = Polygon(it)
  4785. temp_solid_geometry.append(it.buffer(offset_for_use, join_style=2))
  4786. else:
  4787. temp_solid_geometry = geometry.solid_geometry
  4788. # ## Flatten the geometry. Only linear elements (no polygons) remain.
  4789. flat_geometry = self.flatten(temp_solid_geometry, pathonly=True)
  4790. log.debug("%d paths" % len(flat_geometry))
  4791. default_dia = None
  4792. if isinstance(self.app.defaults["geometry_cnctooldia"], float):
  4793. default_dia = self.app.defaults["geometry_cnctooldia"]
  4794. else:
  4795. try:
  4796. tools_string = self.app.defaults["geometry_cnctooldia"].split(",")
  4797. tools_diameters = [eval(a) for a in tools_string if a != '']
  4798. default_dia = tools_diameters[0] if tools_diameters else 0.0
  4799. except Exception as e:
  4800. self.app.log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))
  4801. try:
  4802. self.tooldia = float(tooldia) if tooldia else default_dia
  4803. except ValueError:
  4804. self.tooldia = [float(el) for el in tooldia.split(',') if el != ''] if tooldia is not None else default_dia
  4805. if self.tooldia is None:
  4806. self.app.inform.emit('[ERROR] %s' % _("Failed."))
  4807. return 'fail'
  4808. self.z_cut = float(z_cut) if z_cut is not None else self.app.defaults["geometry_cutz"]
  4809. self.z_move = float(z_move) if z_move is not None else self.app.defaults["geometry_travelz"]
  4810. self.feedrate = float(feedrate) if feedrate is not None else self.app.defaults["geometry_feedrate"]
  4811. self.z_feedrate = float(feedrate_z) if feedrate_z is not None else self.app.defaults["geometry_feedrate_z"]
  4812. self.feedrate_rapid = float(feedrate_rapid) if feedrate_rapid is not None else \
  4813. self.app.defaults["geometry_feedrate_rapid"]
  4814. self.spindlespeed = int(spindlespeed) if spindlespeed != 0 and spindlespeed is not None else None
  4815. self.spindledir = spindledir
  4816. self.dwell = dwell
  4817. self.dwelltime = float(dwelltime) if dwelltime is not None else self.app.defaults["geometry_dwelltime"]
  4818. self.startz = float(startz) if startz is not None and startz != '' else self.app.defaults["geometry_startz"]
  4819. self.z_end = float(endz) if endz is not None else self.app.defaults["geometry_endz"]
  4820. self.xy_end = endxy if endxy != '' and endxy else self.app.defaults["geometry_endxy"]
  4821. self.xy_end = re.sub('[()\[\]]', '', str(self.xy_end)) if self.xy_end else None
  4822. if self.xy_end is not None and self.xy_end != '':
  4823. self.xy_end = [float(eval(a)) for a in self.xy_end.split(",")]
  4824. if self.xy_end and len(self.xy_end) < 2:
  4825. self.app.inform.emit('[ERROR] %s' % _("The End Move X,Y field in Edit -> Preferences has to be "
  4826. "in the format (x, y) but now there is only one value, not two."))
  4827. return 'fail'
  4828. self.z_depthpercut = float(depthpercut) if depthpercut is not None and depthpercut != 0 else abs(self.z_cut)
  4829. self.multidepth = multidepth
  4830. self.z_toolchange = float(toolchangez) if toolchangez is not None else self.app.defaults["geometry_toolchangez"]
  4831. self.extracut_length = float(extracut_length) if extracut_length is not None else \
  4832. self.app.defaults["geometry_extracut_length"]
  4833. try:
  4834. if toolchangexy == '':
  4835. self.xy_toolchange = None
  4836. else:
  4837. self.xy_toolchange = re.sub('[()\[\]]', '', str(toolchangexy)) if self.xy_toolchange else None
  4838. if self.xy_toolchange and self.xy_toolchange != '':
  4839. self.xy_toolchange = [float(eval(a)) for a in self.xy_toolchange.split(",")]
  4840. if len(self.xy_toolchange) < 2:
  4841. self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y format has to be (x, y)."))
  4842. return 'fail'
  4843. except Exception as e:
  4844. log.debug("camlib.CNCJob.generate_from_geometry_2() --> %s" % str(e))
  4845. pass
  4846. self.pp_geometry_name = pp_geometry_name if pp_geometry_name else 'default'
  4847. self.f_plunge = self.app.defaults["geometry_f_plunge"]
  4848. if self.machinist_setting == 0:
  4849. if self.z_cut is None:
  4850. if 'laser' not in self.pp_geometry_name:
  4851. self.app.inform.emit(
  4852. '[ERROR_NOTCL] %s' % _("Cut_Z parameter is None or zero. Most likely a bad combinations of "
  4853. "other parameters.")
  4854. )
  4855. return 'fail'
  4856. else:
  4857. self.z_cut = 0.0
  4858. if self.z_cut > 0:
  4859. self.app.inform.emit('[WARNING] %s' %
  4860. _("The Cut Z parameter has positive value. "
  4861. "It is the depth value to cut into material.\n"
  4862. "The Cut Z parameter needs to have a negative value, assuming it is a typo "
  4863. "therefore the app will convert the value to negative."
  4864. "Check the resulting CNC code (Gcode etc)."))
  4865. self.z_cut = -self.z_cut
  4866. elif self.z_cut == 0 and 'laser' not in self.pp_geometry_name:
  4867. self.app.inform.emit(
  4868. '[WARNING] %s: %s' % (_("The Cut Z parameter is zero. There will be no cut, skipping file"),
  4869. geometry.options['name'])
  4870. )
  4871. return 'fail'
  4872. if self.z_move is None:
  4873. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Travel Z parameter is None or zero."))
  4874. return 'fail'
  4875. if self.z_move < 0:
  4876. self.app.inform.emit('[WARNING] %s' %
  4877. _("The Travel Z parameter has negative value. "
  4878. "It is the height value to travel between cuts.\n"
  4879. "The Z Travel parameter needs to have a positive value, assuming it is a typo "
  4880. "therefore the app will convert the value to positive."
  4881. "Check the resulting CNC code (Gcode etc)."))
  4882. self.z_move = -self.z_move
  4883. elif self.z_move == 0:
  4884. self.app.inform.emit(
  4885. '[WARNING] %s: %s' % (_("The Z Travel parameter is zero. This is dangerous, skipping file"),
  4886. self.options['name'])
  4887. )
  4888. return 'fail'
  4889. # made sure that depth_per_cut is no more then the z_cut
  4890. try:
  4891. if abs(self.z_cut) < self.z_depthpercut:
  4892. self.z_depthpercut = abs(self.z_cut)
  4893. except TypeError:
  4894. self.z_depthpercut = abs(self.z_cut)
  4895. # ## Index first and last points in paths
  4896. # What points to index.
  4897. def get_pts(o):
  4898. return [o.coords[0], o.coords[-1]]
  4899. # Create the indexed storage.
  4900. storage = FlatCAMRTreeStorage()
  4901. storage.get_points = get_pts
  4902. # Store the geometry
  4903. log.debug("Indexing geometry before generating G-Code...")
  4904. self.app.inform.emit(_("Indexing geometry before generating G-Code..."))
  4905. for geo_shape in flat_geometry:
  4906. if self.app.abort_flag:
  4907. # graceful abort requested by the user
  4908. raise grace
  4909. if geo_shape is not None:
  4910. storage.insert(geo_shape)
  4911. if not append:
  4912. self.gcode = ""
  4913. # tell preprocessor the number of tool (for toolchange)
  4914. self.tool = tool_no
  4915. # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter
  4916. # given under the name 'toolC'
  4917. # this is a fancy way of adding a class attribute (which should be added in the __init__ method) without doing
  4918. # it there :)
  4919. self.postdata['toolC'] = self.tooldia
  4920. # Initial G-Code
  4921. self.pp_geometry = self.app.preprocessors[self.pp_geometry_name]
  4922. # the 'p' local attribute is a reference to the current preprocessor class
  4923. p = self.pp_geometry
  4924. self.oldx = 0.0
  4925. self.oldy = 0.0
  4926. start_gcode = ''
  4927. if is_first:
  4928. start_gcode = self.doformat(p.start_code)
  4929. # self.gcode = self.doformat(p.start_code)
  4930. self.gcode += self.doformat(p.feedrate_code) # sets the feed rate
  4931. if toolchange is False:
  4932. # all the x and y parameters in self.doformat() are used only by some preprocessors not by all
  4933. self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy) # Move (up) to travel height
  4934. self.gcode += self.doformat(p.startz_code, x=self.oldx, y=self.oldy)
  4935. if toolchange:
  4936. # if "line_xyz" in self.pp_geometry_name:
  4937. # self.gcode += self.doformat(p.toolchange_code, x=self.xy_toolchange[0], y=self.xy_toolchange[1])
  4938. # else:
  4939. # self.gcode += self.doformat(p.toolchange_code)
  4940. self.gcode += self.doformat(p.toolchange_code)
  4941. if 'laser' not in self.pp_geometry_name:
  4942. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4943. else:
  4944. # for laser this will disable the laser
  4945. self.gcode += self.doformat(p.lift_code, x=self.oldx, y=self.oldy) # Move (up) to travel height
  4946. if self.dwell is True:
  4947. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4948. else:
  4949. if 'laser' not in self.pp_geometry_name:
  4950. self.gcode += self.doformat(p.spindle_code) # Spindle start
  4951. if self.dwell is True:
  4952. self.gcode += self.doformat(p.dwell_code) # Dwell time
  4953. total_travel = 0.0
  4954. total_cut = 0.0
  4955. # Iterate over geometry paths getting the nearest each time.
  4956. log.debug("Starting G-Code...")
  4957. self.app.inform.emit('%s...' % _("Starting G-Code"))
  4958. # variables to display the percentage of work done
  4959. geo_len = len(flat_geometry)
  4960. old_disp_number = 0
  4961. log.warning("Number of paths for which to generate GCode: %s" % str(geo_len))
  4962. current_tooldia = float('%.*f' % (self.decimals, float(self.tooldia)))
  4963. self.app.inform.emit(
  4964. '%s: %s%s.' % (_("Starting G-Code for tool with diameter"), str(current_tooldia), str(self.units))
  4965. )
  4966. path_count = 0
  4967. current_pt = (0, 0)
  4968. pt, geo = storage.nearest(current_pt)
  4969. # when nothing is left in the storage a StopIteration exception will be raised therefore stopping
  4970. # the whole process including the infinite loop while True below.
  4971. try:
  4972. while True:
  4973. if self.app.abort_flag:
  4974. # graceful abort requested by the user
  4975. raise grace
  4976. path_count += 1
  4977. # Remove before modifying, otherwise deletion will fail.
  4978. storage.remove(geo)
  4979. # If last point in geometry is the nearest but prefer the first one if last point == first point
  4980. # then reverse coordinates.
  4981. if pt != geo.coords[0] and pt == geo.coords[-1]:
  4982. # geo.coords = list(geo.coords)[::-1] # Shapely 2.0
  4983. geo = LineString(list(geo.coords)[::-1])
  4984. # ---------- Single depth/pass --------
  4985. if not multidepth:
  4986. # calculate the cut distance
  4987. total_cut += geo.length
  4988. self.gcode += self.create_gcode_single_pass(geo, current_tooldia, extracut, self.extracut_length,
  4989. tolerance, z_move=z_move, old_point=current_pt)
  4990. # --------- Multi-pass ---------
  4991. else:
  4992. # calculate the cut distance
  4993. # due of the number of cuts (multi depth) it has to multiplied by the number of cuts
  4994. nr_cuts = 0
  4995. depth = abs(self.z_cut)
  4996. while depth > 0:
  4997. nr_cuts += 1
  4998. depth -= float(self.z_depthpercut)
  4999. total_cut += (geo.length * nr_cuts)
  5000. gc, geo = self.create_gcode_multi_pass(geo, current_tooldia, extracut, self.extracut_length,
  5001. tolerance, z_move=z_move, postproc=p,
  5002. old_point=current_pt)
  5003. self.gcode += gc
  5004. # calculate the travel distance
  5005. total_travel += abs(distance(pt1=current_pt, pt2=pt))
  5006. current_pt = geo.coords[-1]
  5007. pt, geo = storage.nearest(current_pt) # Next
  5008. # update the activity counter (lower left side of the app, status bar)
  5009. disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
  5010. if old_disp_number < disp_number <= 100:
  5011. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  5012. old_disp_number = disp_number
  5013. except StopIteration: # Nothing found in storage.
  5014. pass
  5015. log.debug("Finishing G-Code... %s paths traced." % path_count)
  5016. # add move to end position
  5017. total_travel += abs(distance_euclidian(current_pt[0], current_pt[1], 0, 0))
  5018. self.travel_distance += total_travel + total_cut
  5019. self.routing_time += total_cut / self.feedrate
  5020. # Finish
  5021. self.gcode += self.doformat(p.spindle_stop_code)
  5022. self.gcode += self.doformat(p.lift_code, x=current_pt[0], y=current_pt[1])
  5023. self.gcode += self.doformat(p.end_code, x=0, y=0)
  5024. self.app.inform.emit(
  5025. '%s... %s %s.' % (_("Finished G-Code generation"), str(path_count), _("paths traced"))
  5026. )
  5027. return self.gcode, start_gcode
  5028. def generate_gcode_from_solderpaste_geo(self, **kwargs):
  5029. """
  5030. Algorithm to generate from multitool Geometry.
  5031. Algorithm description:
  5032. ----------------------
  5033. Uses RTree to find the nearest path to follow.
  5034. :return: Gcode string
  5035. """
  5036. log.debug("Generate_from_solderpaste_geometry()")
  5037. # ## Index first and last points in paths
  5038. # What points to index.
  5039. def get_pts(o):
  5040. return [o.coords[0], o.coords[-1]]
  5041. self.gcode = ""
  5042. if not kwargs:
  5043. log.debug("camlib.generate_from_solderpaste_geo() --> No tool in the solderpaste geometry.")
  5044. self.app.inform.emit('[ERROR_NOTCL] %s' %
  5045. _("There is no tool data in the SolderPaste geometry."))
  5046. # this is the tool diameter, it is used as such to accommodate the preprocessor who need the tool diameter
  5047. # given under the name 'toolC'
  5048. self.postdata['z_start'] = kwargs['data']['tools_solderpaste_z_start']
  5049. self.postdata['z_dispense'] = kwargs['data']['tools_solderpaste_z_dispense']
  5050. self.postdata['z_stop'] = kwargs['data']['tools_solderpaste_z_stop']
  5051. self.postdata['z_travel'] = kwargs['data']['tools_solderpaste_z_travel']
  5052. self.postdata['z_toolchange'] = kwargs['data']['tools_solderpaste_z_toolchange']
  5053. self.postdata['xy_toolchange'] = kwargs['data']['tools_solderpaste_xy_toolchange']
  5054. self.postdata['frxy'] = kwargs['data']['tools_solderpaste_frxy']
  5055. self.postdata['frz'] = kwargs['data']['tools_solderpaste_frz']
  5056. self.postdata['frz_dispense'] = kwargs['data']['tools_solderpaste_frz_dispense']
  5057. self.postdata['speedfwd'] = kwargs['data']['tools_solderpaste_speedfwd']
  5058. self.postdata['dwellfwd'] = kwargs['data']['tools_solderpaste_dwellfwd']
  5059. self.postdata['speedrev'] = kwargs['data']['tools_solderpaste_speedrev']
  5060. self.postdata['dwellrev'] = kwargs['data']['tools_solderpaste_dwellrev']
  5061. self.postdata['pp_solderpaste_name'] = kwargs['data']['tools_solderpaste_pp']
  5062. self.postdata['toolC'] = kwargs['tooldia']
  5063. self.pp_solderpaste_name = kwargs['data']['tools_solderpaste_pp'] if kwargs['data']['tools_solderpaste_pp'] \
  5064. else self.app.defaults['tools_solderpaste_pp']
  5065. p = self.app.preprocessors[self.pp_solderpaste_name]
  5066. # ## Flatten the geometry. Only linear elements (no polygons) remain.
  5067. flat_geometry = self.flatten(kwargs['solid_geometry'], pathonly=True)
  5068. log.debug("%d paths" % len(flat_geometry))
  5069. # Create the indexed storage.
  5070. storage = FlatCAMRTreeStorage()
  5071. storage.get_points = get_pts
  5072. # Store the geometry
  5073. log.debug("Indexing geometry before generating G-Code...")
  5074. for geo_shape in flat_geometry:
  5075. if self.app.abort_flag:
  5076. # graceful abort requested by the user
  5077. raise grace
  5078. if geo_shape is not None:
  5079. storage.insert(geo_shape)
  5080. # Initial G-Code
  5081. self.gcode = self.doformat(p.start_code)
  5082. self.gcode += self.doformat(p.spindle_off_code)
  5083. self.gcode += self.doformat(p.toolchange_code)
  5084. # ## Iterate over geometry paths getting the nearest each time.
  5085. log.debug("Starting SolderPaste G-Code...")
  5086. path_count = 0
  5087. current_pt = (0, 0)
  5088. # variables to display the percentage of work done
  5089. geo_len = len(flat_geometry)
  5090. old_disp_number = 0
  5091. pt, geo = storage.nearest(current_pt)
  5092. try:
  5093. while True:
  5094. if self.app.abort_flag:
  5095. # graceful abort requested by the user
  5096. raise grace
  5097. path_count += 1
  5098. # Remove before modifying, otherwise deletion will fail.
  5099. storage.remove(geo)
  5100. # If last point in geometry is the nearest but prefer the first one if last point == first point
  5101. # then reverse coordinates.
  5102. if pt != geo.coords[0] and pt == geo.coords[-1]:
  5103. # geo.coords = list(geo.coords)[::-1] # Shapely 2.0
  5104. geo = LineString(list(geo.coords)[::-1])
  5105. self.gcode += self.create_soldepaste_gcode(geo, p=p, old_point=current_pt)
  5106. current_pt = geo.coords[-1]
  5107. pt, geo = storage.nearest(current_pt) # Next
  5108. disp_number = int(np.interp(path_count, [0, geo_len], [0, 100]))
  5109. if old_disp_number < disp_number <= 100:
  5110. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  5111. old_disp_number = disp_number
  5112. except StopIteration: # Nothing found in storage.
  5113. pass
  5114. log.debug("Finishing SolderPste G-Code... %s paths traced." % path_count)
  5115. self.app.inform.emit(
  5116. '%s... %s %s.' % (_("Finished SolderPaste G-Code generation"), str(path_count), _("paths traced"))
  5117. )
  5118. # Finish
  5119. self.gcode += self.doformat(p.lift_code)
  5120. self.gcode += self.doformat(p.end_code)
  5121. return self.gcode
  5122. def create_soldepaste_gcode(self, geometry, p, old_point=(0, 0)):
  5123. gcode = ''
  5124. path = geometry.coords
  5125. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  5126. if self.coordinates_type == "G90":
  5127. # For Absolute coordinates type G90
  5128. first_x = path[0][0]
  5129. first_y = path[0][1]
  5130. else:
  5131. # For Incremental coordinates type G91
  5132. first_x = path[0][0] - old_point[0]
  5133. first_y = path[0][1] - old_point[1]
  5134. if type(geometry) == LineString or type(geometry) == LinearRing:
  5135. # Move fast to 1st point
  5136. gcode += self.doformat(p.rapid_code, x=first_x, y=first_y) # Move to first point
  5137. # Move down to cutting depth
  5138. gcode += self.doformat(p.z_feedrate_code)
  5139. gcode += self.doformat(p.down_z_start_code)
  5140. gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
  5141. gcode += self.doformat(p.dwell_fwd_code)
  5142. gcode += self.doformat(p.feedrate_z_dispense_code)
  5143. gcode += self.doformat(p.lift_z_dispense_code)
  5144. gcode += self.doformat(p.feedrate_xy_code)
  5145. # Cutting...
  5146. prev_x = first_x
  5147. prev_y = first_y
  5148. for pt in path[1:]:
  5149. if self.coordinates_type == "G90":
  5150. # For Absolute coordinates type G90
  5151. next_x = pt[0]
  5152. next_y = pt[1]
  5153. else:
  5154. # For Incremental coordinates type G91
  5155. next_x = pt[0] - prev_x
  5156. next_y = pt[1] - prev_y
  5157. gcode += self.doformat(p.linear_code, x=next_x, y=next_y) # Linear motion to point
  5158. prev_x = next_x
  5159. prev_y = next_y
  5160. # Up to travelling height.
  5161. gcode += self.doformat(p.spindle_off_code) # Stop dispensing
  5162. gcode += self.doformat(p.spindle_rev_code)
  5163. gcode += self.doformat(p.down_z_stop_code)
  5164. gcode += self.doformat(p.spindle_off_code)
  5165. gcode += self.doformat(p.dwell_rev_code)
  5166. gcode += self.doformat(p.z_feedrate_code)
  5167. gcode += self.doformat(p.lift_code)
  5168. elif type(geometry) == Point:
  5169. gcode += self.doformat(p.linear_code, x=first_x, y=first_y) # Move to first point
  5170. gcode += self.doformat(p.feedrate_z_dispense_code)
  5171. gcode += self.doformat(p.down_z_start_code)
  5172. gcode += self.doformat(p.spindle_fwd_code) # Start dispensing
  5173. gcode += self.doformat(p.dwell_fwd_code)
  5174. gcode += self.doformat(p.lift_z_dispense_code)
  5175. gcode += self.doformat(p.spindle_off_code) # Stop dispensing
  5176. gcode += self.doformat(p.spindle_rev_code)
  5177. gcode += self.doformat(p.spindle_off_code)
  5178. gcode += self.doformat(p.down_z_stop_code)
  5179. gcode += self.doformat(p.dwell_rev_code)
  5180. gcode += self.doformat(p.z_feedrate_code)
  5181. gcode += self.doformat(p.lift_code)
  5182. return gcode
  5183. def create_gcode_single_pass(self, geometry, cdia, extracut, extracut_length, tolerance, z_move, old_point=(0, 0)):
  5184. """
  5185. # G-code. Note: self.linear2gcode() and self.point2gcode() will lower and raise the tool every time.
  5186. :param geometry: A Shapely Geometry (LineString or LinearRing) which is the path to be cut
  5187. :type geometry: LineString, LinearRing
  5188. :param cdia: Tool diameter
  5189. :type cdia: float
  5190. :param extracut: Will add an extra cut over the point where start of the cut is met with the end cut
  5191. :type extracut: bool
  5192. :param extracut_length: The length of the extra cut: half before the meeting point, half after
  5193. :type extracut_length: float
  5194. :param tolerance: Tolerance used to simplify the paths (making them mre rough)
  5195. :type tolerance: float
  5196. :param z_move: Travel Z
  5197. :type z_move: float
  5198. :param old_point: Previous point
  5199. :type old_point: tuple
  5200. :return: Gcode
  5201. :rtype: str
  5202. """
  5203. # p = postproc
  5204. if type(geometry) == LineString or type(geometry) == LinearRing:
  5205. if extracut is False or not geometry.is_ring:
  5206. gcode_single_pass = self.linear2gcode(geometry, cdia, z_move=z_move, tolerance=tolerance,
  5207. old_point=old_point)
  5208. else:
  5209. gcode_single_pass = self.linear2gcode_extra(geometry, cdia, extracut_length, tolerance=tolerance,
  5210. z_move=z_move, old_point=old_point)
  5211. elif type(geometry) == Point:
  5212. gcode_single_pass = self.point2gcode(geometry, cdia, z_move=z_move, old_point=old_point)
  5213. else:
  5214. log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
  5215. return
  5216. return gcode_single_pass
  5217. def create_gcode_multi_pass(self, geometry, cdia, extracut, extracut_length, tolerance, postproc, z_move,
  5218. old_point=(0, 0)):
  5219. """
  5220. :param geometry: A Shapely Geometry (LineString or LinearRing) which is the path to be cut
  5221. :type geometry: LineString, LinearRing
  5222. :param cdia: Tool diameter
  5223. :type cdia: float
  5224. :param extracut: Will add an extra cut over the point where start of the cut is met with the end cut
  5225. :type extracut: bool
  5226. :param extracut_length: The length of the extra cut: half before the meeting point, half after
  5227. :type extracut_length: float
  5228. :param tolerance: Tolerance used to simplify the paths (making them mre rough)
  5229. :type tolerance: float
  5230. :param postproc: Preprocessor class
  5231. :type postproc: class
  5232. :param z_move: Travel Z
  5233. :type z_move: float
  5234. :param old_point: Previous point
  5235. :type old_point: tuple
  5236. :return: Gcode
  5237. :rtype: str
  5238. """
  5239. p = postproc
  5240. gcode_multi_pass = ''
  5241. if isinstance(self.z_cut, Decimal):
  5242. z_cut = self.z_cut
  5243. else:
  5244. z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001'))
  5245. if self.z_depthpercut is None:
  5246. self.z_depthpercut = z_cut
  5247. elif not isinstance(self.z_depthpercut, Decimal):
  5248. self.z_depthpercut = Decimal(self.z_depthpercut).quantize(Decimal('0.000000001'))
  5249. depth = 0
  5250. reverse = False
  5251. while depth > z_cut:
  5252. # Increase depth. Limit to z_cut.
  5253. depth -= self.z_depthpercut
  5254. if depth < z_cut:
  5255. depth = z_cut
  5256. # Cut at specific depth and do not lift the tool.
  5257. # Note: linear2gcode() will use G00 to move to the first point in the path, but it should be already
  5258. # at the first point if the tool is down (in the material). So, an extra G00 should show up but
  5259. # is inconsequential.
  5260. if type(geometry) == LineString or type(geometry) == LinearRing:
  5261. if extracut is False or not geometry.is_ring:
  5262. gcode_multi_pass += self.linear2gcode(geometry, cdia, tolerance=tolerance, z_cut=depth, up=False,
  5263. z_move=z_move, old_point=old_point)
  5264. else:
  5265. gcode_multi_pass += self.linear2gcode_extra(geometry, cdia, extracut_length, tolerance=tolerance,
  5266. z_move=z_move, z_cut=depth, up=False,
  5267. old_point=old_point)
  5268. # Ignore multi-pass for points.
  5269. elif type(geometry) == Point:
  5270. gcode_multi_pass += self.point2gcode(geometry, cdia, z_move=z_move, old_point=old_point)
  5271. break # Ignoring ...
  5272. else:
  5273. log.warning("G-code generation not implemented for %s" % (str(type(geometry))))
  5274. # Reverse coordinates if not a loop so we can continue cutting without returning to the beginning.
  5275. if type(geometry) == LineString:
  5276. geometry = LineString(list(geometry.coords)[::-1])
  5277. reverse = True
  5278. # If geometry is reversed, revert.
  5279. if reverse:
  5280. if type(geometry) == LineString:
  5281. geometry = LineString(list(geometry.coords)[::-1])
  5282. # Lift the tool
  5283. gcode_multi_pass += self.doformat(p.lift_code, x=old_point[0], y=old_point[1])
  5284. return gcode_multi_pass, geometry
  5285. def codes_split(self, gline):
  5286. """
  5287. Parses a line of G-Code such as "G01 X1234 Y987" into
  5288. a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0}
  5289. :param gline: G-Code line string
  5290. :type gline: str
  5291. :return: Dictionary with parsed line.
  5292. :rtype: dict
  5293. """
  5294. command = {}
  5295. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  5296. match_z = re.search(r"^Z(\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?)*;$", gline)
  5297. if match_z:
  5298. command['G'] = 0
  5299. command['X'] = float(match_z.group(1).replace(" ", "")) * 0.025
  5300. command['Y'] = float(match_z.group(2).replace(" ", "")) * 0.025
  5301. command['Z'] = float(match_z.group(3).replace(" ", "")) * 0.025
  5302. elif 'hpgl' in self.pp_excellon_name or 'hpgl' in self.pp_geometry_name:
  5303. match_pa = re.search(r"^PA(\s*-?\d+\.\d+?),(\s*\s*-?\d+\.\d+?)*;$", gline)
  5304. if match_pa:
  5305. command['G'] = 0
  5306. command['X'] = float(match_pa.group(1).replace(" ", "")) / 40
  5307. command['Y'] = float(match_pa.group(2).replace(" ", "")) / 40
  5308. match_pen = re.search(r"^(P[U|D])", gline)
  5309. if match_pen:
  5310. if match_pen.group(1) == 'PU':
  5311. # the value does not matter, only that it is positive so the gcode_parse() know it is > 0,
  5312. # therefore the move is of kind T (travel)
  5313. command['Z'] = 1
  5314. else:
  5315. command['Z'] = 0
  5316. elif 'laser' in self.pp_excellon_name.lower() or 'laser' in self.pp_geometry_name.lower() or \
  5317. (self.pp_solderpaste_name is not None and 'paste' in self.pp_solderpaste_name.lower()):
  5318. match_lsr = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
  5319. if match_lsr:
  5320. command['X'] = float(match_lsr.group(1).replace(" ", ""))
  5321. command['Y'] = float(match_lsr.group(2).replace(" ", ""))
  5322. match_lsr_pos = re.search(r"^(M0?[3-5])", gline)
  5323. if match_lsr_pos:
  5324. if 'M05' in match_lsr_pos.group(1) or 'M5' in match_lsr_pos.group(1):
  5325. # the value does not matter, only that it is positive so the gcode_parse() know it is > 0,
  5326. # therefore the move is of kind T (travel)
  5327. command['Z'] = 1
  5328. else:
  5329. command['Z'] = 0
  5330. match_lsr_pos_2 = re.search(r"^(M10[6|7])", gline)
  5331. if match_lsr_pos_2:
  5332. if 'M107' in match_lsr_pos_2.group(1):
  5333. command['Z'] = 1
  5334. else:
  5335. command['Z'] = 0
  5336. elif self.pp_solderpaste_name is not None:
  5337. if 'Paste' in self.pp_solderpaste_name:
  5338. match_paste = re.search(r"X([\+-]?\d+.[\+-]?\d+)\s*Y([\+-]?\d+.[\+-]?\d+)", gline)
  5339. if match_paste:
  5340. command['X'] = float(match_paste.group(1).replace(" ", ""))
  5341. command['Y'] = float(match_paste.group(2).replace(" ", ""))
  5342. else:
  5343. match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
  5344. while match:
  5345. command[match.group(1)] = float(match.group(2).replace(" ", ""))
  5346. gline = gline[match.end():]
  5347. match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline)
  5348. return command
  5349. def gcode_parse(self, force_parsing=None):
  5350. """
  5351. G-Code parser (from self.gcode). Generates dictionary with
  5352. single-segment LineString's and "kind" indicating cut or travel,
  5353. fast or feedrate speed.
  5354. Will return a list of dict in the format:
  5355. {
  5356. "geom": LineString(path),
  5357. "kind": kind
  5358. }
  5359. where kind can be either ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5360. :param force_parsing:
  5361. :type force_parsing:
  5362. :return:
  5363. :rtype: dict
  5364. """
  5365. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5366. # Results go here
  5367. geometry = []
  5368. # Last known instruction
  5369. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  5370. # Current path: temporary storage until tool is
  5371. # lifted or lowered.
  5372. if self.toolchange_xy_type == "excellon":
  5373. if self.app.defaults["tools_drill_toolchangexy"] == '' or \
  5374. self.app.defaults["tools_drill_toolchangexy"] is None:
  5375. pos_xy = (0, 0)
  5376. else:
  5377. pos_xy = self.app.defaults["tools_drill_toolchangexy"]
  5378. try:
  5379. pos_xy = [float(eval(a)) for a in pos_xy.split(",")]
  5380. except Exception:
  5381. if len(pos_xy) != 2:
  5382. pos_xy = (0, 0)
  5383. else:
  5384. if self.app.defaults["geometry_toolchangexy"] == '' or self.app.defaults["geometry_toolchangexy"] is None:
  5385. pos_xy = (0, 0)
  5386. else:
  5387. pos_xy = self.app.defaults["geometry_toolchangexy"]
  5388. try:
  5389. pos_xy = [float(eval(a)) for a in pos_xy.split(",")]
  5390. except Exception:
  5391. if len(pos_xy) != 2:
  5392. pos_xy = (0, 0)
  5393. path = [pos_xy]
  5394. # path = [(0, 0)]
  5395. gcode_lines_list = self.gcode.splitlines()
  5396. self.app.inform.emit('%s: %d' % (_("Parsing GCode file. Number of lines"), len(gcode_lines_list)))
  5397. # Process every instruction
  5398. for line in gcode_lines_list:
  5399. if force_parsing is False or force_parsing is None:
  5400. if '%MO' in line or '%' in line or 'MOIN' in line or 'MOMM' in line:
  5401. return "fail"
  5402. gobj = self.codes_split(line)
  5403. # ## Units
  5404. if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
  5405. self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
  5406. continue
  5407. # TODO take into consideration the tools and update the travel line thickness
  5408. if 'T' in gobj:
  5409. pass
  5410. # ## Changing height
  5411. if 'Z' in gobj:
  5412. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  5413. pass
  5414. elif 'hpgl' in self.pp_excellon_name or 'hpgl' in self.pp_geometry_name:
  5415. pass
  5416. elif 'laser' in self.pp_excellon_name or 'laser' in self.pp_geometry_name:
  5417. pass
  5418. elif ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  5419. if self.pp_geometry_name == 'line_xyz' or self.pp_excellon_name == 'line_xyz':
  5420. pass
  5421. else:
  5422. log.warning("Non-orthogonal motion: From %s" % str(current))
  5423. log.warning(" To: %s" % str(gobj))
  5424. current['Z'] = gobj['Z']
  5425. # Store the path into geometry and reset path
  5426. if len(path) > 1:
  5427. geometry.append({"geom": LineString(path),
  5428. "kind": kind})
  5429. path = [path[-1]] # Start with the last point of last path.
  5430. # create the geometry for the holes created when drilling Excellon drills
  5431. if self.origin_kind == 'excellon':
  5432. if current['Z'] < 0:
  5433. current_drill_point_coords = (
  5434. float('%.*f' % (self.decimals, current['X'])),
  5435. float('%.*f' % (self.decimals, current['Y']))
  5436. )
  5437. # find the drill diameter knowing the drill coordinates
  5438. break_loop = False
  5439. for tool, tool_dict in self.exc_tools.items():
  5440. if 'drills' in tool_dict:
  5441. for drill_pt in tool_dict['drills']:
  5442. point_in_dict_coords = (
  5443. float('%.*f' % (self.decimals, drill_pt.x)),
  5444. float('%.*f' % (self.decimals, drill_pt.y))
  5445. )
  5446. if point_in_dict_coords == current_drill_point_coords:
  5447. dia = self.exc_tools[tool]['tooldia']
  5448. kind = ['C', 'F']
  5449. geometry.append(
  5450. {
  5451. "geom": Point(current_drill_point_coords).buffer(dia / 2.0).exterior,
  5452. "kind": kind
  5453. }
  5454. )
  5455. break_loop = True
  5456. break
  5457. if break_loop:
  5458. break
  5459. if 'G' in gobj:
  5460. current['G'] = int(gobj['G'])
  5461. if 'X' in gobj or 'Y' in gobj:
  5462. if 'X' in gobj:
  5463. x = gobj['X']
  5464. # current['X'] = x
  5465. else:
  5466. x = current['X']
  5467. if 'Y' in gobj:
  5468. y = gobj['Y']
  5469. else:
  5470. y = current['Y']
  5471. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5472. if current['Z'] > 0:
  5473. kind[0] = 'T'
  5474. if current['G'] > 0:
  5475. kind[1] = 'S'
  5476. if current['G'] in [0, 1]: # line
  5477. path.append((x, y))
  5478. arcdir = [None, None, "cw", "ccw"]
  5479. if current['G'] in [2, 3]: # arc
  5480. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  5481. radius = np.sqrt(gobj['I'] ** 2 + gobj['J'] ** 2)
  5482. start = np.arctan2(-gobj['J'], -gobj['I'])
  5483. stop = np.arctan2(-center[1] + y, -center[0] + x)
  5484. path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
  5485. current['X'] = x
  5486. current['Y'] = y
  5487. # Update current instruction
  5488. for code in gobj:
  5489. current[code] = gobj[code]
  5490. self.app.inform.emit('%s...' % _("Creating Geometry from the parsed GCode file. "))
  5491. # There might not be a change in height at the
  5492. # end, therefore, see here too if there is
  5493. # a final path.
  5494. if len(path) > 1:
  5495. geometry.append(
  5496. {
  5497. "geom": LineString(path),
  5498. "kind": kind
  5499. }
  5500. )
  5501. self.gcode_parsed = geometry
  5502. return geometry
  5503. def excellon_tool_gcode_parse(self, dia, gcode, start_pt=(0, 0), force_parsing=None):
  5504. """
  5505. G-Code parser (from self.exc_cnc_tools['tooldia']['gcode']). Generates dictionary with
  5506. single-segment LineString's and "kind" indicating cut or travel,
  5507. fast or feedrate speed.
  5508. Will return the Geometry as a list of dict in the format:
  5509. {
  5510. "geom": LineString(path),
  5511. "kind": kind
  5512. }
  5513. where kind can be either ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5514. :param dia: the dia is a tool diameter which is the key in self.exc_cnc_tools dict
  5515. :type dia: float
  5516. :param gcode: Gcode to parse
  5517. :type gcode: str
  5518. :param start_pt: the point coordinates from where to start the parsing
  5519. :type start_pt: tuple
  5520. :param force_parsing:
  5521. :type force_parsing: bool
  5522. :return: Geometry as a list of dictionaries
  5523. :rtype: list
  5524. """
  5525. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5526. # Results go here
  5527. geometry = []
  5528. # Last known instruction
  5529. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  5530. # Current path: temporary storage until tool is
  5531. # lifted or lowered.
  5532. pos_xy = start_pt
  5533. path = [pos_xy]
  5534. # path = [(0, 0)]
  5535. gcode_lines_list = gcode.splitlines()
  5536. self.app.inform.emit(
  5537. '%s: %s. %s: %d' % (_("Parsing GCode file for tool diameter"),
  5538. str(dia), _("Number of lines"),
  5539. len(gcode_lines_list))
  5540. )
  5541. # Process every instruction
  5542. for line in gcode_lines_list:
  5543. if force_parsing is False or force_parsing is None:
  5544. if '%MO' in line or '%' in line or 'MOIN' in line or 'MOMM' in line:
  5545. return "fail"
  5546. gobj = self.codes_split(line)
  5547. # ## Units
  5548. if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0):
  5549. self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']]
  5550. continue
  5551. # TODO take into consideration the tools and update the travel line thickness
  5552. if 'T' in gobj:
  5553. pass
  5554. # ## Changing height
  5555. if 'Z' in gobj:
  5556. if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
  5557. pass
  5558. elif 'hpgl' in self.pp_excellon_name or 'hpgl' in self.pp_geometry_name:
  5559. pass
  5560. elif 'laser' in self.pp_excellon_name or 'laser' in self.pp_geometry_name:
  5561. pass
  5562. elif ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  5563. if self.pp_geometry_name == 'line_xyz' or self.pp_excellon_name == 'line_xyz':
  5564. pass
  5565. else:
  5566. log.warning("Non-orthogonal motion: From %s" % str(current))
  5567. log.warning(" To: %s" % str(gobj))
  5568. current['Z'] = gobj['Z']
  5569. # Store the path into geometry and reset path
  5570. if len(path) > 1:
  5571. geometry.append({"geom": LineString(path),
  5572. "kind": kind})
  5573. path = [path[-1]] # Start with the last point of last path.
  5574. # create the geometry for the holes created when drilling Excellon drills
  5575. if current['Z'] < 0:
  5576. current_drill_point_coords = (
  5577. float('%.*f' % (self.decimals, current['X'])),
  5578. float('%.*f' % (self.decimals, current['Y']))
  5579. )
  5580. kind = ['C', 'F']
  5581. geometry.append(
  5582. {
  5583. "geom": Point(current_drill_point_coords).buffer(dia/2.0).exterior,
  5584. "kind": kind
  5585. }
  5586. )
  5587. if 'G' in gobj:
  5588. current['G'] = int(gobj['G'])
  5589. if 'X' in gobj or 'Y' in gobj:
  5590. x = gobj['X'] if 'X' in gobj else current['X']
  5591. y = gobj['Y'] if 'Y' in gobj else current['Y']
  5592. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  5593. if current['Z'] > 0:
  5594. kind[0] = 'T'
  5595. if current['G'] > 0:
  5596. kind[1] = 'S'
  5597. if current['G'] in [0, 1]: # line
  5598. path.append((x, y))
  5599. arcdir = [None, None, "cw", "ccw"]
  5600. if current['G'] in [2, 3]: # arc
  5601. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  5602. radius = np.sqrt(gobj['I'] ** 2 + gobj['J'] ** 2)
  5603. start = np.arctan2(-gobj['J'], -gobj['I'])
  5604. stop = np.arctan2(-center[1] + y, -center[0] + x)
  5605. path += arc(center, radius, start, stop, arcdir[current['G']], int(self.steps_per_circle))
  5606. current['X'] = x
  5607. current['Y'] = y
  5608. # Update current instruction
  5609. for code in gobj:
  5610. current[code] = gobj[code]
  5611. self.app.inform.emit('%s: %s' % (_("Creating Geometry from the parsed GCode file for tool diameter"), str(dia)))
  5612. # There might not be a change in height at the end, therefore, see here too if there is a final path.
  5613. if len(path) > 1:
  5614. geometry.append(
  5615. {
  5616. "geom": LineString(path),
  5617. "kind": kind
  5618. }
  5619. )
  5620. return geometry
  5621. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  5622. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  5623. # alpha={"T": 0.3, "C": 1.0}):
  5624. # """
  5625. # Creates a Matplotlib figure with a plot of the
  5626. # G-code job.
  5627. # """
  5628. # if tooldia is None:
  5629. # tooldia = self.tooldia
  5630. #
  5631. # fig = Figure(dpi=dpi)
  5632. # ax = fig.add_subplot(111)
  5633. # ax.set_aspect(1)
  5634. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  5635. # ax.set_xlim(xmin-margin, xmax+margin)
  5636. # ax.set_ylim(ymin-margin, ymax+margin)
  5637. #
  5638. # if tooldia == 0:
  5639. # for geo in self.gcode_parsed:
  5640. # linespec = '--'
  5641. # linecolor = color[geo['kind'][0]][1]
  5642. # if geo['kind'][0] == 'C':
  5643. # linespec = 'k-'
  5644. # x, y = geo['geom'].coords.xy
  5645. # ax.plot(x, y, linespec, color=linecolor)
  5646. # else:
  5647. # for geo in self.gcode_parsed:
  5648. # poly = geo['geom'].buffer(tooldia/2.0)
  5649. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  5650. # edgecolor=color[geo['kind'][0]][1],
  5651. # alpha=alpha[geo['kind'][0]], zorder=2)
  5652. # ax.add_patch(patch)
  5653. #
  5654. # return fig
  5655. def plot2(self, tooldia=None, dpi=75, margin=0.1, gcode_parsed=None,
  5656. color=None, alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005, obj=None, visible=False, kind='all'):
  5657. """
  5658. Plots the G-code job onto the given axes.
  5659. :param tooldia: Tool diameter.
  5660. :type tooldia: float
  5661. :param dpi: Not used!
  5662. :type dpi: float
  5663. :param margin: Not used!
  5664. :type margin: float
  5665. :param gcode_parsed: Parsed Gcode
  5666. :type gcode_parsed: str
  5667. :param color: Color specification.
  5668. :type color: str
  5669. :param alpha: Transparency specification.
  5670. :type alpha: dict
  5671. :param tool_tolerance: Tolerance when drawing the toolshape.
  5672. :type tool_tolerance: float
  5673. :param obj: The object for whih to plot
  5674. :type obj: class
  5675. :param visible: Visibility status
  5676. :type visible: bool
  5677. :param kind: Can be: "travel", "cut", "all"
  5678. :type kind: str
  5679. :return: None
  5680. :rtype:
  5681. """
  5682. # units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
  5683. if color is None:
  5684. color = {
  5685. "T": [self.app.defaults["cncjob_travel_fill"], self.app.defaults["cncjob_travel_line"]],
  5686. "C": [self.app.defaults["cncjob_plot_fill"], self.app.defaults["cncjob_plot_line"]]
  5687. }
  5688. gcode_parsed = gcode_parsed if gcode_parsed else self.gcode_parsed
  5689. if tooldia is None:
  5690. tooldia = self.tooldia
  5691. # this should be unlikely unless when upstream the tooldia is a tuple made by one dia and a comma like (2.4,)
  5692. if isinstance(tooldia, list):
  5693. tooldia = tooldia[0] if tooldia[0] is not None else self.tooldia
  5694. if tooldia == 0:
  5695. for geo in gcode_parsed:
  5696. if kind == 'all':
  5697. obj.add_shape(shape=geo['geom'], color=color[geo['kind'][0]][1], visible=visible)
  5698. elif kind == 'travel':
  5699. if geo['kind'][0] == 'T':
  5700. obj.add_shape(shape=geo['geom'], color=color['T'][1], visible=visible)
  5701. elif kind == 'cut':
  5702. if geo['kind'][0] == 'C':
  5703. obj.add_shape(shape=geo['geom'], color=color['C'][1], visible=visible)
  5704. else:
  5705. path_num = 0
  5706. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  5707. if self.coordinates_type == "G90":
  5708. # For Absolute coordinates type G90
  5709. for geo in gcode_parsed:
  5710. if geo['kind'][0] == 'T':
  5711. start_position = geo['geom'].coords[0]
  5712. if tooldia not in obj.annotations_dict:
  5713. obj.annotations_dict[tooldia] = {
  5714. 'pos': [],
  5715. 'text': []
  5716. }
  5717. if start_position not in obj.annotations_dict[tooldia]['pos']:
  5718. path_num += 1
  5719. obj.annotations_dict[tooldia]['pos'].append(start_position)
  5720. obj.annotations_dict[tooldia]['text'].append(str(path_num))
  5721. end_position = geo['geom'].coords[-1]
  5722. if tooldia not in obj.annotations_dict:
  5723. obj.annotations_dict[tooldia] = {
  5724. 'pos': [],
  5725. 'text': []
  5726. }
  5727. if end_position not in obj.annotations_dict[tooldia]['pos']:
  5728. path_num += 1
  5729. obj.annotations_dict[tooldia]['pos'].append(end_position)
  5730. obj.annotations_dict[tooldia]['text'].append(str(path_num))
  5731. # plot the geometry of Excellon objects
  5732. if self.origin_kind == 'excellon':
  5733. try:
  5734. # if the geos are travel lines
  5735. if geo['kind'][0] == 'T':
  5736. poly = geo['geom'].buffer(distance=(tooldia / 1.99999999),
  5737. resolution=self.steps_per_circle)
  5738. else:
  5739. poly = Polygon(geo['geom'])
  5740. poly = poly.simplify(tool_tolerance)
  5741. except Exception:
  5742. # deal here with unexpected plot errors due of LineStrings not valid
  5743. continue
  5744. else:
  5745. # plot the geometry of any objects other than Excellon
  5746. poly = geo['geom'].buffer(distance=(tooldia / 1.99999999), resolution=self.steps_per_circle)
  5747. poly = poly.simplify(tool_tolerance)
  5748. if kind == 'all':
  5749. obj.add_shape(shape=poly, color=color[geo['kind'][0]][1], face_color=color[geo['kind'][0]][0],
  5750. visible=visible, layer=1 if geo['kind'][0] == 'C' else 2)
  5751. elif kind == 'travel':
  5752. if geo['kind'][0] == 'T':
  5753. obj.add_shape(shape=poly, color=color['T'][1], face_color=color['T'][0],
  5754. visible=visible, layer=2)
  5755. elif kind == 'cut':
  5756. if geo['kind'][0] == 'C':
  5757. obj.add_shape(shape=poly, color=color['C'][1], face_color=color['C'][0],
  5758. visible=visible, layer=1)
  5759. else:
  5760. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  5761. return 'fail'
  5762. def plot_annotations(self, obj, visible=True):
  5763. """
  5764. Plot annotations.
  5765. :param obj: FlatCAM CNCJob object for which to plot the annotations
  5766. :type obj:
  5767. :param visible: annotations visibility
  5768. :type visible: bool
  5769. :return: Nothing
  5770. :rtype:
  5771. """
  5772. if not obj.annotations_dict:
  5773. return
  5774. if visible is True:
  5775. if self.app.is_legacy is False:
  5776. obj.annotation.clear(update=True)
  5777. obj.text_col.visible = True
  5778. else:
  5779. obj.text_col.visible = False
  5780. return
  5781. text = []
  5782. pos = []
  5783. for tooldia in obj.annotations_dict:
  5784. pos += obj.annotations_dict[tooldia]['pos']
  5785. text += obj.annotations_dict[tooldia]['text']
  5786. if not text or not pos:
  5787. return
  5788. try:
  5789. if self.app.defaults['global_theme'] == 'white':
  5790. obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
  5791. font_size=self.app.defaults["cncjob_annotation_fontsize"],
  5792. color=self.app.defaults["cncjob_annotation_fontcolor"])
  5793. else:
  5794. # invert the color
  5795. old_color = self.app.defaults["cncjob_annotation_fontcolor"].lower()
  5796. new_color = ''
  5797. code = {}
  5798. l1 = "#;0123456789abcdef"
  5799. l2 = "#;fedcba9876543210"
  5800. for i in range(len(l1)):
  5801. code[l1[i]] = l2[i]
  5802. for x in range(len(old_color)):
  5803. new_color += code[old_color[x]]
  5804. obj.annotation.set(text=text, pos=pos, visible=obj.options['plot'],
  5805. font_size=self.app.defaults["cncjob_annotation_fontsize"],
  5806. color=new_color)
  5807. except Exception as e:
  5808. log.debug("CNCJob.plot2() --> annotations --> %s" % str(e))
  5809. if self.app.is_legacy is False:
  5810. obj.annotation.clear(update=True)
  5811. obj.annotation.redraw()
  5812. def create_geometry(self):
  5813. """
  5814. It is used by the Excellon objects. Will create the solid_geometry which will be an attribute of the
  5815. Excellon object class.
  5816. :return: List of Shapely geometry elements
  5817. :rtype: list
  5818. """
  5819. # This takes forever. Too much data?
  5820. # self.app.inform.emit('%s: %s' % (_("Unifying Geometry from parsed Geometry segments"),
  5821. # str(len(self.gcode_parsed))))
  5822. # self.solid_geometry = unary_union([geo['geom'] for geo in self.gcode_parsed])
  5823. # This is much faster but not so nice to look at as you can see different segments of the geometry
  5824. self.solid_geometry = [geo['geom'] for geo in self.gcode_parsed]
  5825. return self.solid_geometry
  5826. def segment(self, coords):
  5827. """
  5828. Break long linear lines to make it more auto level friendly.
  5829. Code snippet added by Lei Zheng in a rejected pull request on FlatCAM https://bitbucket.org/realthunder/
  5830. :param coords: List of coordinates tuples
  5831. :type coords: list
  5832. :return: A path; list with the multiple coordinates breaking a line.
  5833. :rtype: list
  5834. """
  5835. if len(coords) < 2 or self.segx <= 0 and self.segy <= 0:
  5836. return list(coords)
  5837. path = [coords[0]]
  5838. # break the line in either x or y dimension only
  5839. def linebreak_single(line, dim, dmax):
  5840. if dmax <= 0:
  5841. return None
  5842. if line[1][dim] > line[0][dim]:
  5843. sign = 1.0
  5844. d = line[1][dim] - line[0][dim]
  5845. else:
  5846. sign = -1.0
  5847. d = line[0][dim] - line[1][dim]
  5848. if d > dmax:
  5849. # make sure we don't make any new lines too short
  5850. if d > dmax * 2:
  5851. dd = dmax
  5852. else:
  5853. dd = d / 2
  5854. other = dim ^ 1
  5855. return (line[0][dim] + dd * sign, line[0][other] + \
  5856. dd * (line[1][other] - line[0][other]) / d)
  5857. return None
  5858. # recursively breaks down a given line until it is within the
  5859. # required step size
  5860. def linebreak(line):
  5861. pt_new = linebreak_single(line, 0, self.segx)
  5862. if pt_new is None:
  5863. pt_new2 = linebreak_single(line, 1, self.segy)
  5864. else:
  5865. pt_new2 = linebreak_single((line[0], pt_new), 1, self.segy)
  5866. if pt_new2 is not None:
  5867. pt_new = pt_new2[::-1]
  5868. if pt_new is None:
  5869. path.append(line[1])
  5870. else:
  5871. path.append(pt_new)
  5872. linebreak((pt_new, line[1]))
  5873. for pt in coords[1:]:
  5874. linebreak((path[-1], pt))
  5875. return path
  5876. def linear2gcode(self, linear, dia, tolerance=0, down=True, up=True, z_cut=None, z_move=None, zdownrate=None,
  5877. feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False, old_point=(0, 0)):
  5878. """
  5879. Generates G-code to cut along the linear feature.
  5880. :param linear: The path to cut along.
  5881. :type: Shapely.LinearRing or Shapely.Linear String
  5882. :param dia: The tool diameter that is going on the path
  5883. :type dia: float
  5884. :param tolerance: All points in the simplified object will be within the
  5885. tolerance distance of the original geometry.
  5886. :type tolerance: float
  5887. :param down:
  5888. :param up:
  5889. :param z_cut:
  5890. :param z_move:
  5891. :param zdownrate:
  5892. :param feedrate: speed for cut on X - Y plane
  5893. :param feedrate_z: speed for cut on Z plane
  5894. :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
  5895. :param cont:
  5896. :param old_point:
  5897. :return: G-code to cut along the linear feature.
  5898. """
  5899. if z_cut is None:
  5900. z_cut = self.z_cut
  5901. if z_move is None:
  5902. z_move = self.z_move
  5903. #
  5904. # if zdownrate is None:
  5905. # zdownrate = self.zdownrate
  5906. if feedrate is None:
  5907. feedrate = self.feedrate
  5908. if feedrate_z is None:
  5909. feedrate_z = self.z_feedrate
  5910. if feedrate_rapid is None:
  5911. feedrate_rapid = self.feedrate_rapid
  5912. # Simplify paths?
  5913. if tolerance > 0:
  5914. target_linear = linear.simplify(tolerance)
  5915. else:
  5916. target_linear = linear
  5917. gcode = ""
  5918. # path = list(target_linear.coords)
  5919. path = self.segment(target_linear.coords)
  5920. p = self.pp_geometry
  5921. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  5922. if self.coordinates_type == "G90":
  5923. # For Absolute coordinates type G90
  5924. first_x = path[0][0]
  5925. first_y = path[0][1]
  5926. else:
  5927. # For Incremental coordinates type G91
  5928. first_x = path[0][0] - old_point[0]
  5929. first_y = path[0][1] - old_point[1]
  5930. # Move fast to 1st point
  5931. if not cont:
  5932. current_tooldia = dia
  5933. travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
  5934. end_point=(first_x, first_y),
  5935. tooldia=current_tooldia)
  5936. prev_z = None
  5937. for travel in travels:
  5938. locx = travel[1][0]
  5939. locy = travel[1][1]
  5940. if travel[0] is not None:
  5941. # move to next point
  5942. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  5943. # raise to safe Z (travel[0]) each time because safe Z may be different
  5944. self.z_move = travel[0]
  5945. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  5946. # restore z_move
  5947. self.z_move = z_move
  5948. else:
  5949. if prev_z is not None:
  5950. # move to next point
  5951. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  5952. # we assume that previously the z_move was altered therefore raise to
  5953. # the travel_z (z_move)
  5954. self.z_move = z_move
  5955. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  5956. else:
  5957. # move to next point
  5958. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  5959. # store prev_z
  5960. prev_z = travel[0]
  5961. # gcode += self.doformat(p.rapid_code, x=first_x, y=first_y) # Move to first point
  5962. # Move down to cutting depth
  5963. if down:
  5964. # Different feedrate for vertical cut?
  5965. gcode += self.doformat(p.z_feedrate_code)
  5966. # gcode += self.doformat(p.feedrate_code)
  5967. gcode += self.doformat(p.down_code, x=first_x, y=first_y, z_cut=z_cut)
  5968. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  5969. # Cutting...
  5970. prev_x = first_x
  5971. prev_y = first_y
  5972. for pt in path[1:]:
  5973. if self.app.abort_flag:
  5974. # graceful abort requested by the user
  5975. raise grace
  5976. if self.coordinates_type == "G90":
  5977. # For Absolute coordinates type G90
  5978. next_x = pt[0]
  5979. next_y = pt[1]
  5980. else:
  5981. # For Incremental coordinates type G91
  5982. # next_x = pt[0] - prev_x
  5983. # next_y = pt[1] - prev_y
  5984. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  5985. next_x = pt[0]
  5986. next_y = pt[1]
  5987. gcode += self.doformat(p.linear_code, x=next_x, y=next_y, z=z_cut) # Linear motion to point
  5988. prev_x = pt[0]
  5989. prev_y = pt[1]
  5990. # Up to travelling height.
  5991. if up:
  5992. gcode += self.doformat(p.lift_code, x=prev_x, y=prev_y, z_move=z_move) # Stop cutting
  5993. return gcode
  5994. def linear2gcode_extra(self, linear, dia, extracut_length, tolerance=0, down=True, up=True,
  5995. z_cut=None, z_move=None, zdownrate=None,
  5996. feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False, old_point=(0, 0)):
  5997. """
  5998. Generates G-code to cut along the linear feature.
  5999. :param linear: The path to cut along.
  6000. :type: Shapely.LinearRing or Shapely.Linear String
  6001. :param dia: The tool diameter that is going on the path
  6002. :type dia: float
  6003. :param extracut_length: how much to cut extra over the first point at the end of the path
  6004. :param tolerance: All points in the simplified object will be within the
  6005. tolerance distance of the original geometry.
  6006. :type tolerance: float
  6007. :param down:
  6008. :param up:
  6009. :param z_cut:
  6010. :param z_move:
  6011. :param zdownrate:
  6012. :param feedrate: speed for cut on X - Y plane
  6013. :param feedrate_z: speed for cut on Z plane
  6014. :param feedrate_rapid: speed to move between cuts; usually is G0 but some CNC require to specify it
  6015. :param cont:
  6016. :param old_point:
  6017. :return: G-code to cut along the linear feature.
  6018. :rtype: str
  6019. """
  6020. if z_cut is None:
  6021. z_cut = self.z_cut
  6022. if z_move is None:
  6023. z_move = self.z_move
  6024. #
  6025. # if zdownrate is None:
  6026. # zdownrate = self.zdownrate
  6027. if feedrate is None:
  6028. feedrate = self.feedrate
  6029. if feedrate_z is None:
  6030. feedrate_z = self.z_feedrate
  6031. if feedrate_rapid is None:
  6032. feedrate_rapid = self.feedrate_rapid
  6033. # Simplify paths?
  6034. if tolerance > 0:
  6035. target_linear = linear.simplify(tolerance)
  6036. else:
  6037. target_linear = linear
  6038. gcode = ""
  6039. path = list(target_linear.coords)
  6040. p = self.pp_geometry
  6041. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  6042. if self.coordinates_type == "G90":
  6043. # For Absolute coordinates type G90
  6044. first_x = path[0][0]
  6045. first_y = path[0][1]
  6046. else:
  6047. # For Incremental coordinates type G91
  6048. first_x = path[0][0] - old_point[0]
  6049. first_y = path[0][1] - old_point[1]
  6050. # Move fast to 1st point
  6051. if not cont:
  6052. current_tooldia = dia
  6053. travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
  6054. end_point=(first_x, first_y),
  6055. tooldia=current_tooldia)
  6056. prev_z = None
  6057. for travel in travels:
  6058. locx = travel[1][0]
  6059. locy = travel[1][1]
  6060. if travel[0] is not None:
  6061. # move to next point
  6062. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6063. # raise to safe Z (travel[0]) each time because safe Z may be different
  6064. self.z_move = travel[0]
  6065. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  6066. # restore z_move
  6067. self.z_move = z_move
  6068. else:
  6069. if prev_z is not None:
  6070. # move to next point
  6071. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6072. # we assume that previously the z_move was altered therefore raise to
  6073. # the travel_z (z_move)
  6074. self.z_move = z_move
  6075. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  6076. else:
  6077. # move to next point
  6078. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6079. # store prev_z
  6080. prev_z = travel[0]
  6081. # gcode += self.doformat(p.rapid_code, x=first_x, y=first_y) # Move to first point
  6082. # Move down to cutting depth
  6083. if down:
  6084. # Different feedrate for vertical cut?
  6085. if self.z_feedrate is not None:
  6086. gcode += self.doformat(p.z_feedrate_code)
  6087. # gcode += self.doformat(p.feedrate_code)
  6088. gcode += self.doformat(p.down_code, x=first_x, y=first_y, z_cut=z_cut)
  6089. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  6090. else:
  6091. gcode += self.doformat(p.down_code, x=first_x, y=first_y, z_cut=z_cut) # Start cutting
  6092. # Cutting...
  6093. prev_x = first_x
  6094. prev_y = first_y
  6095. for pt in path[1:]:
  6096. if self.app.abort_flag:
  6097. # graceful abort requested by the user
  6098. raise grace
  6099. if self.coordinates_type == "G90":
  6100. # For Absolute coordinates type G90
  6101. next_x = pt[0]
  6102. next_y = pt[1]
  6103. else:
  6104. # For Incremental coordinates type G91
  6105. # For Incremental coordinates type G91
  6106. # next_x = pt[0] - prev_x
  6107. # next_y = pt[1] - prev_y
  6108. self.app.inform.emit('[ERROR_NOTCL] %s...' % _('G91 coordinates not implemented'))
  6109. next_x = pt[0]
  6110. next_y = pt[1]
  6111. gcode += self.doformat(p.linear_code, x=next_x, y=next_y, z=z_cut) # Linear motion to point
  6112. prev_x = next_x
  6113. prev_y = next_y
  6114. # this line is added to create an extra cut over the first point in patch
  6115. # to make sure that we remove the copper leftovers
  6116. # Linear motion to the 1st point in the cut path
  6117. # if self.coordinates_type == "G90":
  6118. # # For Absolute coordinates type G90
  6119. # last_x = path[1][0]
  6120. # last_y = path[1][1]
  6121. # else:
  6122. # # For Incremental coordinates type G91
  6123. # last_x = path[1][0] - first_x
  6124. # last_y = path[1][1] - first_y
  6125. # gcode += self.doformat(p.linear_code, x=last_x, y=last_y)
  6126. # the first point for extracut is always mandatory if the extracut is enabled. But if the length of distance
  6127. # between point 0 and point 1 is more than the distance we set for the extra cut then make an interpolation
  6128. # along the path and find the point at the distance extracut_length
  6129. if extracut_length == 0.0:
  6130. extra_path = [path[-1], path[0], path[1]]
  6131. new_x = extra_path[0][0]
  6132. new_y = extra_path[0][1]
  6133. # this is an extra line therefore lift the milling bit
  6134. gcode += self.doformat(p.lift_code, x=prev_x, y=prev_y, z_move=z_move) # lift
  6135. # move fast to the new first point
  6136. gcode += self.doformat(p.rapid_code, x=new_x, y=new_y)
  6137. # lower the milling bit
  6138. # Different feedrate for vertical cut?
  6139. if self.z_feedrate is not None:
  6140. gcode += self.doformat(p.z_feedrate_code)
  6141. gcode += self.doformat(p.down_code, x=new_x, y=new_y, z_cut=z_cut)
  6142. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  6143. else:
  6144. gcode += self.doformat(p.down_code, x=new_x, y=new_y, z_cut=z_cut) # Start cutting
  6145. # start cutting the extra line
  6146. last_pt = extra_path[0]
  6147. for pt in extra_path[1:]:
  6148. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
  6149. last_pt = pt
  6150. # go back to the original point
  6151. gcode += self.doformat(p.linear_code, x=path[0][0], y=path[0][1])
  6152. last_pt = path[0]
  6153. else:
  6154. # go to the point that is 5% in length before the end (therefore 95% length from start of the line),
  6155. # along the line to be cut
  6156. if extracut_length >= target_linear.length:
  6157. extracut_length = target_linear.length
  6158. # ---------------------------------------------
  6159. # first half
  6160. # ---------------------------------------------
  6161. start_length = target_linear.length - (extracut_length * 0.5)
  6162. extra_line = substring(target_linear, start_length, target_linear.length)
  6163. extra_path = list(extra_line.coords)
  6164. new_x = extra_path[0][0]
  6165. new_y = extra_path[0][1]
  6166. # this is an extra line therefore lift the milling bit
  6167. gcode += self.doformat(p.lift_code, x=prev_x, y=prev_y, z_move=z_move) # lift
  6168. # move fast to the new first point
  6169. gcode += self.doformat(p.rapid_code, x=new_x, y=new_y)
  6170. # lower the milling bit
  6171. # Different feedrate for vertical cut?
  6172. if self.z_feedrate is not None:
  6173. gcode += self.doformat(p.z_feedrate_code)
  6174. gcode += self.doformat(p.down_code, x=new_x, y=new_y, z_cut=z_cut)
  6175. gcode += self.doformat(p.feedrate_code, feedrate=feedrate)
  6176. else:
  6177. gcode += self.doformat(p.down_code, x=new_x, y=new_y, z_cut=z_cut) # Start cutting
  6178. # start cutting the extra line
  6179. for pt in extra_path[1:]:
  6180. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
  6181. # ---------------------------------------------
  6182. # second half
  6183. # ---------------------------------------------
  6184. extra_line = substring(target_linear, 0, (extracut_length * 0.5))
  6185. extra_path = list(extra_line.coords)
  6186. # start cutting the extra line
  6187. last_pt = extra_path[0]
  6188. for pt in extra_path[1:]:
  6189. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
  6190. last_pt = pt
  6191. # ---------------------------------------------
  6192. # back to original start point, cutting
  6193. # ---------------------------------------------
  6194. extra_line = substring(target_linear, 0, (extracut_length * 0.5))
  6195. extra_path = list(extra_line.coords)[::-1]
  6196. # start cutting the extra line
  6197. last_pt = extra_path[0]
  6198. for pt in extra_path[1:]:
  6199. gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
  6200. last_pt = pt
  6201. # if extracut_length == 0.0:
  6202. # gcode += self.doformat(p.linear_code, x=path[1][0], y=path[1][1])
  6203. # last_pt = path[1]
  6204. # else:
  6205. # if abs(distance(path[1], path[0])) > extracut_length:
  6206. # i_point = LineString([path[0], path[1]]).interpolate(extracut_length)
  6207. # gcode += self.doformat(p.linear_code, x=i_point.x, y=i_point.y)
  6208. # last_pt = (i_point.x, i_point.y)
  6209. # else:
  6210. # last_pt = path[0]
  6211. # for pt in path[1:]:
  6212. # extracut_distance = abs(distance(pt, last_pt))
  6213. # if extracut_distance <= extracut_length:
  6214. # gcode += self.doformat(p.linear_code, x=pt[0], y=pt[1])
  6215. # last_pt = pt
  6216. # else:
  6217. # break
  6218. # Up to travelling height.
  6219. if up:
  6220. gcode += self.doformat(p.lift_code, x=last_pt[0], y=last_pt[1], z_move=z_move) # Stop cutting
  6221. return gcode
  6222. def point2gcode(self, point, dia, z_move=None, old_point=(0, 0)):
  6223. """
  6224. :param point: A Shapely Point
  6225. :type point: Point
  6226. :param dia: The tool diameter that is going on the path
  6227. :type dia: float
  6228. :param z_move: Travel Z
  6229. :type z_move: float
  6230. :param old_point: Old point coordinates from which we moved to the 'point'
  6231. :type old_point: tuple
  6232. :return: G-code to cut on the Point feature.
  6233. :rtype: str
  6234. """
  6235. gcode = ""
  6236. if self.app.abort_flag:
  6237. # graceful abort requested by the user
  6238. raise grace
  6239. path = list(point.coords)
  6240. p = self.pp_geometry
  6241. self.coordinates_type = self.app.defaults["cncjob_coords_type"]
  6242. if self.coordinates_type == "G90":
  6243. # For Absolute coordinates type G90
  6244. first_x = path[0][0]
  6245. first_y = path[0][1]
  6246. else:
  6247. # For Incremental coordinates type G91
  6248. # first_x = path[0][0] - old_point[0]
  6249. # first_y = path[0][1] - old_point[1]
  6250. self.app.inform.emit('[ERROR_NOTCL] %s' %
  6251. _('G91 coordinates not implemented ...'))
  6252. first_x = path[0][0]
  6253. first_y = path[0][1]
  6254. current_tooldia = dia
  6255. travels = self.app.exc_areas.travel_coordinates(start_point=(old_point[0], old_point[1]),
  6256. end_point=(first_x, first_y),
  6257. tooldia=current_tooldia)
  6258. prev_z = None
  6259. for travel in travels:
  6260. locx = travel[1][0]
  6261. locy = travel[1][1]
  6262. if travel[0] is not None:
  6263. # move to next point
  6264. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6265. # raise to safe Z (travel[0]) each time because safe Z may be different
  6266. self.z_move = travel[0]
  6267. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  6268. # restore z_move
  6269. self.z_move = z_move
  6270. else:
  6271. if prev_z is not None:
  6272. # move to next point
  6273. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6274. # we assume that previously the z_move was altered therefore raise to
  6275. # the travel_z (z_move)
  6276. self.z_move = z_move
  6277. gcode += self.doformat(p.lift_code, x=locx, y=locy)
  6278. else:
  6279. # move to next point
  6280. gcode += self.doformat(p.rapid_code, x=locx, y=locy)
  6281. # store prev_z
  6282. prev_z = travel[0]
  6283. # gcode += self.doformat(p.linear_code, x=first_x, y=first_y) # Move to first point
  6284. if self.z_feedrate is not None:
  6285. gcode += self.doformat(p.z_feedrate_code)
  6286. gcode += self.doformat(p.down_code, x=first_x, y=first_y, z_cut=self.z_cut)
  6287. gcode += self.doformat(p.feedrate_code)
  6288. else:
  6289. gcode += self.doformat(p.down_code, x=first_x, y=first_y, z_cut=self.z_cut) # Start cutting
  6290. gcode += self.doformat(p.lift_code, x=first_x, y=first_y) # Stop cutting
  6291. return gcode
  6292. def export_svg(self, scale_stroke_factor=0.00):
  6293. """
  6294. Exports the CNC Job as a SVG Element
  6295. :param scale_stroke_factor: A factor to scale the SVG geometry
  6296. :type scale_stroke_factor: float
  6297. :return: SVG Element string
  6298. :rtype: str
  6299. """
  6300. # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export
  6301. # If not specified then try and use the tool diameter
  6302. # This way what is on screen will match what is outputed for the svg
  6303. # This is quite a useful feature for svg's used with visicut
  6304. if scale_stroke_factor <= 0:
  6305. scale_stroke_factor = self.options['tooldia'] / 2
  6306. # If still 0 then default to 0.05
  6307. # This value appears to work for zooming, and getting the output svg line width
  6308. # to match that viewed on screen with FlatCam
  6309. if scale_stroke_factor == 0:
  6310. scale_stroke_factor = 0.01
  6311. # Separate the list of cuts and travels into 2 distinct lists
  6312. # This way we can add different formatting / colors to both
  6313. cuts = []
  6314. travels = []
  6315. cutsgeom = ''
  6316. travelsgeom = ''
  6317. for g in self.gcode_parsed:
  6318. if self.app.abort_flag:
  6319. # graceful abort requested by the user
  6320. raise grace
  6321. if g['kind'][0] == 'C':
  6322. cuts.append(g)
  6323. if g['kind'][0] == 'T':
  6324. travels.append(g)
  6325. # Used to determine the overall board size
  6326. self.solid_geometry = unary_union([geo['geom'] for geo in self.gcode_parsed])
  6327. # Convert the cuts and travels into single geometry objects we can render as svg xml
  6328. if travels:
  6329. travelsgeom = unary_union([geo['geom'] for geo in travels])
  6330. if self.app.abort_flag:
  6331. # graceful abort requested by the user
  6332. raise grace
  6333. if cuts:
  6334. cutsgeom = unary_union([geo['geom'] for geo in cuts])
  6335. # Render the SVG Xml
  6336. # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set
  6337. # It's better to have the travels sitting underneath the cuts for visicut
  6338. svg_elem = ""
  6339. if travels:
  6340. svg_elem = travelsgeom.svg(scale_factor=scale_stroke_factor, stroke_color="#F0E24D")
  6341. if cuts:
  6342. svg_elem += cutsgeom.svg(scale_factor=scale_stroke_factor, stroke_color="#5E6CFF")
  6343. return svg_elem
  6344. def bounds(self, flatten=None):
  6345. """
  6346. Returns coordinates of rectangular bounds of geometry: (xmin, ymin, xmax, ymax).
  6347. :param flatten: Not used, it is here for compatibility with base class method
  6348. :type flatten: bool
  6349. :return: Bounding values in format (xmin, ymin, xmax, ymax)
  6350. :rtype: tuple
  6351. """
  6352. log.debug("camlib.CNCJob.bounds()")
  6353. def bounds_rec(obj):
  6354. if type(obj) is list:
  6355. cminx = np.inf
  6356. cminy = np.inf
  6357. cmaxx = -np.inf
  6358. cmaxy = -np.inf
  6359. for k in obj:
  6360. if type(k) is dict:
  6361. for key in k:
  6362. minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
  6363. cminx = min(cminx, minx_)
  6364. cminy = min(cminy, miny_)
  6365. cmaxx = max(cmaxx, maxx_)
  6366. cmaxy = max(cmaxy, maxy_)
  6367. else:
  6368. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  6369. cminx = min(cminx, minx_)
  6370. cminy = min(cminy, miny_)
  6371. cmaxx = max(cmaxx, maxx_)
  6372. cmaxy = max(cmaxy, maxy_)
  6373. return cminx, cminy, cmaxx, cmaxy
  6374. else:
  6375. # it's a Shapely object, return it's bounds
  6376. return obj.bounds
  6377. if self.multitool is False:
  6378. log.debug("CNCJob->bounds()")
  6379. if self.solid_geometry is None:
  6380. log.debug("solid_geometry is None")
  6381. return 0, 0, 0, 0
  6382. bounds_coords = bounds_rec(self.solid_geometry)
  6383. else:
  6384. minx = np.inf
  6385. miny = np.inf
  6386. maxx = -np.inf
  6387. maxy = -np.inf
  6388. if self.cnc_tools:
  6389. for k, v in self.cnc_tools.items():
  6390. minx = np.inf
  6391. miny = np.inf
  6392. maxx = -np.inf
  6393. maxy = -np.inf
  6394. try:
  6395. for k in v['solid_geometry']:
  6396. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  6397. minx = min(minx, minx_)
  6398. miny = min(miny, miny_)
  6399. maxx = max(maxx, maxx_)
  6400. maxy = max(maxy, maxy_)
  6401. except TypeError:
  6402. minx_, miny_, maxx_, maxy_ = bounds_rec(v['solid_geometry'])
  6403. minx = min(minx, minx_)
  6404. miny = min(miny, miny_)
  6405. maxx = max(maxx, maxx_)
  6406. maxy = max(maxy, maxy_)
  6407. if self.exc_cnc_tools:
  6408. for k, v in self.exc_cnc_tools.items():
  6409. minx = np.inf
  6410. miny = np.inf
  6411. maxx = -np.inf
  6412. maxy = -np.inf
  6413. try:
  6414. for k in v['solid_geometry']:
  6415. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  6416. minx = min(minx, minx_)
  6417. miny = min(miny, miny_)
  6418. maxx = max(maxx, maxx_)
  6419. maxy = max(maxy, maxy_)
  6420. except TypeError:
  6421. minx_, miny_, maxx_, maxy_ = bounds_rec(v['solid_geometry'])
  6422. minx = min(minx, minx_)
  6423. miny = min(miny, miny_)
  6424. maxx = max(maxx, maxx_)
  6425. maxy = max(maxy, maxy_)
  6426. bounds_coords = minx, miny, maxx, maxy
  6427. return bounds_coords
  6428. # TODO This function should be replaced at some point with a "real" function. Until then it's an ugly hack ...
  6429. def scale(self, xfactor, yfactor=None, point=None):
  6430. """
  6431. Scales all the geometry on the XY plane in the object by the
  6432. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  6433. not altered.
  6434. :param factor: Number by which to scale the object.
  6435. :type factor: float
  6436. :param point: the (x,y) coords for the point of origin of scale
  6437. :type tuple of floats
  6438. :return: None
  6439. :rtype: None
  6440. """
  6441. log.debug("camlib.CNCJob.scale()")
  6442. if yfactor is None:
  6443. yfactor = xfactor
  6444. if point is None:
  6445. px = 0
  6446. py = 0
  6447. else:
  6448. px, py = point
  6449. def scale_g(g):
  6450. """
  6451. :param g: 'g' parameter it's a gcode string
  6452. :return: scaled gcode string
  6453. """
  6454. temp_gcode = ''
  6455. header_start = False
  6456. header_stop = False
  6457. units = self.app.defaults['units'].upper()
  6458. lines = StringIO(g)
  6459. for line in lines:
  6460. # this changes the GCODE header ---- UGLY HACK
  6461. if "TOOL DIAMETER" in line or "Feedrate:" in line:
  6462. header_start = True
  6463. if "G20" in line or "G21" in line:
  6464. header_start = False
  6465. header_stop = True
  6466. if header_start is True:
  6467. header_stop = False
  6468. if "in" in line:
  6469. if units == 'MM':
  6470. line = line.replace("in", "mm")
  6471. if "mm" in line:
  6472. if units == 'IN':
  6473. line = line.replace("mm", "in")
  6474. # find any float number in header (even multiple on the same line) and convert it
  6475. numbers_in_header = re.findall(self.g_nr_re, line)
  6476. if numbers_in_header:
  6477. for nr in numbers_in_header:
  6478. new_nr = float(nr) * xfactor
  6479. # replace the updated string
  6480. line = line.replace(nr, ('%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_nr))
  6481. )
  6482. # this scales all the X and Y and Z and F values and also the Tool Dia in the toolchange message
  6483. if header_stop is True:
  6484. if "G20" in line:
  6485. if units == 'MM':
  6486. line = line.replace("G20", "G21")
  6487. if "G21" in line:
  6488. if units == 'IN':
  6489. line = line.replace("G21", "G20")
  6490. # find the X group
  6491. match_x = self.g_x_re.search(line)
  6492. if match_x:
  6493. if match_x.group(1) is not None:
  6494. new_x = float(match_x.group(1)[1:]) * xfactor
  6495. # replace the updated string
  6496. line = line.replace(
  6497. match_x.group(1),
  6498. 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
  6499. )
  6500. # find the Y group
  6501. match_y = self.g_y_re.search(line)
  6502. if match_y:
  6503. if match_y.group(1) is not None:
  6504. new_y = float(match_y.group(1)[1:]) * yfactor
  6505. line = line.replace(
  6506. match_y.group(1),
  6507. 'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
  6508. )
  6509. # find the Z group
  6510. match_z = self.g_z_re.search(line)
  6511. if match_z:
  6512. if match_z.group(1) is not None:
  6513. new_z = float(match_z.group(1)[1:]) * xfactor
  6514. line = line.replace(
  6515. match_z.group(1),
  6516. 'Z%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_z)
  6517. )
  6518. # find the F group
  6519. match_f = self.g_f_re.search(line)
  6520. if match_f:
  6521. if match_f.group(1) is not None:
  6522. new_f = float(match_f.group(1)[1:]) * xfactor
  6523. line = line.replace(
  6524. match_f.group(1),
  6525. 'F%.*f' % (self.app.defaults["cncjob_fr_decimals"], new_f)
  6526. )
  6527. # find the T group (tool dia on toolchange)
  6528. match_t = self.g_t_re.search(line)
  6529. if match_t:
  6530. if match_t.group(1) is not None:
  6531. new_t = float(match_t.group(1)[1:]) * xfactor
  6532. line = line.replace(
  6533. match_t.group(1),
  6534. '= %.*f' % (self.app.defaults["cncjob_coords_decimals"], new_t)
  6535. )
  6536. temp_gcode += line
  6537. lines.close()
  6538. header_stop = False
  6539. return temp_gcode
  6540. if self.multitool is False:
  6541. # offset Gcode
  6542. self.gcode = scale_g(self.gcode)
  6543. # variables to display the percentage of work done
  6544. self.geo_len = 0
  6545. try:
  6546. self.geo_len = len(self.gcode_parsed)
  6547. except TypeError:
  6548. self.geo_len = 1
  6549. self.old_disp_number = 0
  6550. self.el_count = 0
  6551. # scale geometry
  6552. for g in self.gcode_parsed:
  6553. try:
  6554. g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
  6555. except AttributeError:
  6556. return g['geom']
  6557. self.el_count += 1
  6558. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6559. if self.old_disp_number < disp_number <= 100:
  6560. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6561. self.old_disp_number = disp_number
  6562. self.create_geometry()
  6563. else:
  6564. for k, v in self.cnc_tools.items():
  6565. # scale Gcode
  6566. v['gcode'] = scale_g(v['gcode'])
  6567. # variables to display the percentage of work done
  6568. self.geo_len = 0
  6569. try:
  6570. self.geo_len = len(v['gcode_parsed'])
  6571. except TypeError:
  6572. self.geo_len = 1
  6573. self.old_disp_number = 0
  6574. self.el_count = 0
  6575. # scale gcode_parsed
  6576. for g in v['gcode_parsed']:
  6577. try:
  6578. g['geom'] = affinity.scale(g['geom'], xfactor, yfactor, origin=(px, py))
  6579. except AttributeError:
  6580. return g['geom']
  6581. self.el_count += 1
  6582. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6583. if self.old_disp_number < disp_number <= 100:
  6584. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6585. self.old_disp_number = disp_number
  6586. v['solid_geometry'] = unary_union([geo['geom'] for geo in v['gcode_parsed']])
  6587. self.create_geometry()
  6588. self.app.proc_container.new_text = ''
  6589. def offset(self, vect):
  6590. """
  6591. Offsets all the geometry on the XY plane in the object by the
  6592. given vector.
  6593. Offsets all the GCODE on the XY plane in the object by the
  6594. given vector.
  6595. g_offsetx_re, g_offsety_re, multitool, cnnc_tools are attributes of FlatCAMCNCJob class in camlib
  6596. :param vect: (x, y) offset vector.
  6597. :type vect: tuple
  6598. :return: None
  6599. """
  6600. log.debug("camlib.CNCJob.offset()")
  6601. dx, dy = vect
  6602. def offset_g(g):
  6603. """
  6604. :param g: 'g' parameter it's a gcode string
  6605. :return: offseted gcode string
  6606. """
  6607. temp_gcode = ''
  6608. lines = StringIO(g)
  6609. for line in lines:
  6610. # find the X group
  6611. match_x = self.g_x_re.search(line)
  6612. if match_x:
  6613. if match_x.group(1) is not None:
  6614. # get the coordinate and add X offset
  6615. new_x = float(match_x.group(1)[1:]) + dx
  6616. # replace the updated string
  6617. line = line.replace(
  6618. match_x.group(1),
  6619. 'X%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_x)
  6620. )
  6621. match_y = self.g_y_re.search(line)
  6622. if match_y:
  6623. if match_y.group(1) is not None:
  6624. new_y = float(match_y.group(1)[1:]) + dy
  6625. line = line.replace(
  6626. match_y.group(1),
  6627. 'Y%.*f' % (self.app.defaults["cncjob_coords_decimals"], new_y)
  6628. )
  6629. temp_gcode += line
  6630. lines.close()
  6631. return temp_gcode
  6632. if self.multitool is False:
  6633. # offset Gcode
  6634. self.gcode = offset_g(self.gcode)
  6635. # variables to display the percentage of work done
  6636. self.geo_len = 0
  6637. try:
  6638. self.geo_len = len(self.gcode_parsed)
  6639. except TypeError:
  6640. self.geo_len = 1
  6641. self.old_disp_number = 0
  6642. self.el_count = 0
  6643. # offset geometry
  6644. for g in self.gcode_parsed:
  6645. try:
  6646. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  6647. except AttributeError:
  6648. return g['geom']
  6649. self.el_count += 1
  6650. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6651. if self.old_disp_number < disp_number <= 100:
  6652. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6653. self.old_disp_number = disp_number
  6654. self.create_geometry()
  6655. else:
  6656. for k, v in self.cnc_tools.items():
  6657. # offset Gcode
  6658. v['gcode'] = offset_g(v['gcode'])
  6659. # variables to display the percentage of work done
  6660. self.geo_len = 0
  6661. try:
  6662. self.geo_len = len(v['gcode_parsed'])
  6663. except TypeError:
  6664. self.geo_len = 1
  6665. self.old_disp_number = 0
  6666. self.el_count = 0
  6667. # offset gcode_parsed
  6668. for g in v['gcode_parsed']:
  6669. try:
  6670. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  6671. except AttributeError:
  6672. return g['geom']
  6673. self.el_count += 1
  6674. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6675. if self.old_disp_number < disp_number <= 100:
  6676. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6677. self.old_disp_number = disp_number
  6678. # for the bounding box
  6679. v['solid_geometry'] = unary_union([geo['geom'] for geo in v['gcode_parsed']])
  6680. self.app.proc_container.new_text = ''
  6681. def mirror(self, axis, point):
  6682. """
  6683. Mirror the geometry of an object by an given axis around the coordinates of the 'point'
  6684. :param axis: Axis for Mirror
  6685. :param point: tuple of coordinates (x,y). Point of origin for Mirror
  6686. :return:
  6687. """
  6688. log.debug("camlib.CNCJob.mirror()")
  6689. px, py = point
  6690. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  6691. # variables to display the percentage of work done
  6692. self.geo_len = 0
  6693. try:
  6694. self.geo_len = len(self.gcode_parsed)
  6695. except TypeError:
  6696. self.geo_len = 1
  6697. self.old_disp_number = 0
  6698. self.el_count = 0
  6699. for g in self.gcode_parsed:
  6700. try:
  6701. g['geom'] = affinity.scale(g['geom'], xscale, yscale, origin=(px, py))
  6702. except AttributeError:
  6703. return g['geom']
  6704. self.el_count += 1
  6705. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6706. if self.old_disp_number < disp_number <= 100:
  6707. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6708. self.old_disp_number = disp_number
  6709. self.create_geometry()
  6710. self.app.proc_container.new_text = ''
  6711. def skew(self, angle_x, angle_y, point):
  6712. """
  6713. Shear/Skew the geometries of an object by angles along x and y dimensions.
  6714. :param angle_x:
  6715. :param angle_y:
  6716. angle_x, angle_y : float, float
  6717. The shear angle(s) for the x and y axes respectively. These can be
  6718. specified in either degrees (default) or radians by setting
  6719. use_radians=True.
  6720. :param point: tupple of coordinates (x,y)
  6721. See shapely manual for more information: http://toblerity.org/shapely/manual.html#affine-transformations
  6722. """
  6723. log.debug("camlib.CNCJob.skew()")
  6724. px, py = point
  6725. # variables to display the percentage of work done
  6726. self.geo_len = 0
  6727. try:
  6728. self.geo_len = len(self.gcode_parsed)
  6729. except TypeError:
  6730. self.geo_len = 1
  6731. self.old_disp_number = 0
  6732. self.el_count = 0
  6733. for g in self.gcode_parsed:
  6734. try:
  6735. g['geom'] = affinity.skew(g['geom'], angle_x, angle_y, origin=(px, py))
  6736. except AttributeError:
  6737. return g['geom']
  6738. self.el_count += 1
  6739. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6740. if self.old_disp_number < disp_number <= 100:
  6741. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6742. self.old_disp_number = disp_number
  6743. self.create_geometry()
  6744. self.app.proc_container.new_text = ''
  6745. def rotate(self, angle, point):
  6746. """
  6747. Rotate the geometry of an object by an given angle around the coordinates of the 'point'
  6748. :param angle: Angle of Rotation
  6749. :param point: tuple of coordinates (x,y). Origin point for Rotation
  6750. :return:
  6751. """
  6752. log.debug("camlib.CNCJob.rotate()")
  6753. px, py = point
  6754. # variables to display the percentage of work done
  6755. self.geo_len = 0
  6756. try:
  6757. self.geo_len = len(self.gcode_parsed)
  6758. except TypeError:
  6759. self.geo_len = 1
  6760. self.old_disp_number = 0
  6761. self.el_count = 0
  6762. for g in self.gcode_parsed:
  6763. try:
  6764. g['geom'] = affinity.rotate(g['geom'], angle, origin=(px, py))
  6765. except AttributeError:
  6766. return g['geom']
  6767. self.el_count += 1
  6768. disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
  6769. if self.old_disp_number < disp_number <= 100:
  6770. self.app.proc_container.update_view_text(' %d%%' % disp_number)
  6771. self.old_disp_number = disp_number
  6772. self.create_geometry()
  6773. self.app.proc_container.new_text = ''
  6774. def get_bounds(geometry_list):
  6775. """
  6776. Will return limit values for a list of geometries
  6777. :param geometry_list: List of geometries for which to calculate the bounds limits
  6778. :return:
  6779. """
  6780. xmin = np.inf
  6781. ymin = np.inf
  6782. xmax = -np.inf
  6783. ymax = -np.inf
  6784. for gs in geometry_list:
  6785. try:
  6786. gxmin, gymin, gxmax, gymax = gs.bounds()
  6787. xmin = min([xmin, gxmin])
  6788. ymin = min([ymin, gymin])
  6789. xmax = max([xmax, gxmax])
  6790. ymax = max([ymax, gymax])
  6791. except Exception:
  6792. log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.")
  6793. return [xmin, ymin, xmax, ymax]
  6794. def arc(center, radius, start, stop, direction, steps_per_circ):
  6795. """
  6796. Creates a list of point along the specified arc.
  6797. :param center: Coordinates of the center [x, y]
  6798. :type center: list
  6799. :param radius: Radius of the arc.
  6800. :type radius: float
  6801. :param start: Starting angle in radians
  6802. :type start: float
  6803. :param stop: End angle in radians
  6804. :type stop: float
  6805. :param direction: Orientation of the arc, "CW" or "CCW"
  6806. :type direction: string
  6807. :param steps_per_circ: Number of straight line segments to
  6808. represent a circle.
  6809. :type steps_per_circ: int
  6810. :return: The desired arc, as list of tuples
  6811. :rtype: list
  6812. """
  6813. # TODO: Resolution should be established by maximum error from the exact arc.
  6814. da_sign = {"cw": -1.0, "ccw": 1.0}
  6815. points = []
  6816. if direction == "ccw" and stop <= start:
  6817. stop += 2 * np.pi
  6818. if direction == "cw" and stop >= start:
  6819. stop -= 2 * np.pi
  6820. angle = abs(stop - start)
  6821. # angle = stop-start
  6822. steps = max([int(np.ceil(angle / (2 * np.pi) * steps_per_circ)), 2])
  6823. delta_angle = da_sign[direction] * angle * 1.0 / steps
  6824. for i in range(steps + 1):
  6825. theta = start + delta_angle * i
  6826. points.append((center[0] + radius * np.cos(theta), center[1] + radius * np.sin(theta)))
  6827. return points
  6828. def arc2(p1, p2, center, direction, steps_per_circ):
  6829. r = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
  6830. start = np.arctan2(p1[1] - center[1], p1[0] - center[0])
  6831. stop = np.arctan2(p2[1] - center[1], p2[0] - center[0])
  6832. return arc(center, r, start, stop, direction, steps_per_circ)
  6833. def arc_angle(start, stop, direction):
  6834. if direction == "ccw" and stop <= start:
  6835. stop += 2 * np.pi
  6836. if direction == "cw" and stop >= start:
  6837. stop -= 2 * np.pi
  6838. angle = abs(stop - start)
  6839. return angle
  6840. # def find_polygon(poly, point):
  6841. # """
  6842. # Find an object that object.contains(Point(point)) in
  6843. # poly, which can can be iterable, contain iterable of, or
  6844. # be itself an implementer of .contains().
  6845. #
  6846. # :param poly: See description
  6847. # :return: Polygon containing point or None.
  6848. # """
  6849. #
  6850. # if poly is None:
  6851. # return None
  6852. #
  6853. # try:
  6854. # for sub_poly in poly:
  6855. # p = find_polygon(sub_poly, point)
  6856. # if p is not None:
  6857. # return p
  6858. # except TypeError:
  6859. # try:
  6860. # if poly.contains(Point(point)):
  6861. # return poly
  6862. # except AttributeError:
  6863. # return None
  6864. #
  6865. # return None
  6866. def to_dict(obj):
  6867. """
  6868. Makes the following types into serializable form:
  6869. * ApertureMacro
  6870. * BaseGeometry
  6871. :param obj: Shapely geometry.
  6872. :type obj: BaseGeometry
  6873. :return: Dictionary with serializable form if ``obj`` was
  6874. BaseGeometry or ApertureMacro, otherwise returns ``obj``.
  6875. """
  6876. if isinstance(obj, ApertureMacro):
  6877. return {
  6878. "__class__": "ApertureMacro",
  6879. "__inst__": obj.to_dict()
  6880. }
  6881. if isinstance(obj, BaseGeometry):
  6882. return {
  6883. "__class__": "Shply",
  6884. "__inst__": sdumps(obj)
  6885. }
  6886. return obj
  6887. def dict2obj(d):
  6888. """
  6889. Default deserializer.
  6890. :param d: Serializable dictionary representation of an object
  6891. to be reconstructed.
  6892. :return: Reconstructed object.
  6893. """
  6894. if '__class__' in d and '__inst__' in d:
  6895. if d['__class__'] == "Shply":
  6896. return sloads(d['__inst__'])
  6897. if d['__class__'] == "ApertureMacro":
  6898. am = ApertureMacro()
  6899. am.from_dict(d['__inst__'])
  6900. return am
  6901. return d
  6902. else:
  6903. return d
  6904. # def plotg(geo, solid_poly=False, color="black"):
  6905. # try:
  6906. # __ = iter(geo)
  6907. # except:
  6908. # geo = [geo]
  6909. #
  6910. # for g in geo:
  6911. # if type(g) == Polygon:
  6912. # if solid_poly:
  6913. # patch = PolygonPatch(g,
  6914. # facecolor="#BBF268",
  6915. # edgecolor="#006E20",
  6916. # alpha=0.75,
  6917. # zorder=2)
  6918. # ax = subplot(111)
  6919. # ax.add_patch(patch)
  6920. # else:
  6921. # x, y = g.exterior.coords.xy
  6922. # plot(x, y, color=color)
  6923. # for ints in g.interiors:
  6924. # x, y = ints.coords.xy
  6925. # plot(x, y, color=color)
  6926. # continue
  6927. #
  6928. # if type(g) == LineString or type(g) == LinearRing:
  6929. # x, y = g.coords.xy
  6930. # plot(x, y, color=color)
  6931. # continue
  6932. #
  6933. # if type(g) == Point:
  6934. # x, y = g.coords.xy
  6935. # plot(x, y, 'o')
  6936. # continue
  6937. #
  6938. # try:
  6939. # __ = iter(g)
  6940. # plotg(g, color=color)
  6941. # except:
  6942. # log.error("Cannot plot: " + str(type(g)))
  6943. # continue
  6944. # def alpha_shape(points, alpha):
  6945. # """
  6946. # Compute the alpha shape (concave hull) of a set of points.
  6947. #
  6948. # @param points: Iterable container of points.
  6949. # @param alpha: alpha value to influence the gooeyness of the border. Smaller
  6950. # numbers don't fall inward as much as larger numbers. Too large,
  6951. # and you lose everything!
  6952. # """
  6953. # if len(points) < 4:
  6954. # # When you have a triangle, there is no sense in computing an alpha
  6955. # # shape.
  6956. # return MultiPoint(list(points)).convex_hull
  6957. #
  6958. # def add_edge(edges, edge_points, coords, i, j):
  6959. # """Add a line between the i-th and j-th points, if not in the list already"""
  6960. # if (i, j) in edges or (j, i) in edges:
  6961. # # already added
  6962. # return
  6963. # edges.add( (i, j) )
  6964. # edge_points.append(coords[ [i, j] ])
  6965. #
  6966. # coords = np.array([point.coords[0] for point in points])
  6967. #
  6968. # tri = Delaunay(coords)
  6969. # edges = set()
  6970. # edge_points = []
  6971. # # loop over triangles:
  6972. # # ia, ib, ic = indices of corner points of the triangle
  6973. # for ia, ib, ic in tri.vertices:
  6974. # pa = coords[ia]
  6975. # pb = coords[ib]
  6976. # pc = coords[ic]
  6977. #
  6978. # # Lengths of sides of triangle
  6979. # a = math.sqrt((pa[0]-pb[0])**2 + (pa[1]-pb[1])**2)
  6980. # b = math.sqrt((pb[0]-pc[0])**2 + (pb[1]-pc[1])**2)
  6981. # c = math.sqrt((pc[0]-pa[0])**2 + (pc[1]-pa[1])**2)
  6982. #
  6983. # # Semiperimeter of triangle
  6984. # s = (a + b + c)/2.0
  6985. #
  6986. # # Area of triangle by Heron's formula
  6987. # area = math.sqrt(s*(s-a)*(s-b)*(s-c))
  6988. # circum_r = a*b*c/(4.0*area)
  6989. #
  6990. # # Here's the radius filter.
  6991. # #print circum_r
  6992. # if circum_r < 1.0/alpha:
  6993. # add_edge(edges, edge_points, coords, ia, ib)
  6994. # add_edge(edges, edge_points, coords, ib, ic)
  6995. # add_edge(edges, edge_points, coords, ic, ia)
  6996. #
  6997. # m = MultiLineString(edge_points)
  6998. # triangles = list(polygonize(m))
  6999. # return unary_union(triangles), edge_points
  7000. # def voronoi(P):
  7001. # """
  7002. # Returns a list of all edges of the voronoi diagram for the given input points.
  7003. # """
  7004. # delauny = Delaunay(P)
  7005. # triangles = delauny.points[delauny.vertices]
  7006. #
  7007. # circum_centers = np.array([triangle_csc(tri) for tri in triangles])
  7008. # long_lines_endpoints = []
  7009. #
  7010. # lineIndices = []
  7011. # for i, triangle in enumerate(triangles):
  7012. # circum_center = circum_centers[i]
  7013. # for j, neighbor in enumerate(delauny.neighbors[i]):
  7014. # if neighbor != -1:
  7015. # lineIndices.append((i, neighbor))
  7016. # else:
  7017. # ps = triangle[(j+1)%3] - triangle[(j-1)%3]
  7018. # ps = np.array((ps[1], -ps[0]))
  7019. #
  7020. # middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
  7021. # di = middle - triangle[j]
  7022. #
  7023. # ps /= np.linalg.norm(ps)
  7024. # di /= np.linalg.norm(di)
  7025. #
  7026. # if np.dot(di, ps) < 0.0:
  7027. # ps *= -1000.0
  7028. # else:
  7029. # ps *= 1000.0
  7030. #
  7031. # long_lines_endpoints.append(circum_center + ps)
  7032. # lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
  7033. #
  7034. # vertices = np.vstack((circum_centers, long_lines_endpoints))
  7035. #
  7036. # # filter out any duplicate lines
  7037. # lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
  7038. # lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
  7039. # lineIndicesUnique = np.unique(lineIndicesTupled)
  7040. #
  7041. # return vertices, lineIndicesUnique
  7042. #
  7043. #
  7044. # def triangle_csc(pts):
  7045. # rows, cols = pts.shape
  7046. #
  7047. # A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
  7048. # [np.ones((1, rows)), np.zeros((1, 1))]])
  7049. #
  7050. # b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
  7051. # x = np.linalg.solve(A,b)
  7052. # bary_coords = x[:-1]
  7053. # return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
  7054. #
  7055. #
  7056. # def voronoi_cell_lines(points, vertices, lineIndices):
  7057. # """
  7058. # Returns a mapping from a voronoi cell to its edges.
  7059. #
  7060. # :param points: shape (m,2)
  7061. # :param vertices: shape (n,2)
  7062. # :param lineIndices: shape (o,2)
  7063. # :rtype: dict point index -> list of shape (n,2) with vertex indices
  7064. # """
  7065. # kd = KDTree(points)
  7066. #
  7067. # cells = collections.defaultdict(list)
  7068. # for i1, i2 in lineIndices:
  7069. # v1, v2 = vertices[i1], vertices[i2]
  7070. # mid = (v1+v2)/2
  7071. # _, (p1Idx, p2Idx) = kd.query(mid, 2)
  7072. # cells[p1Idx].append((i1, i2))
  7073. # cells[p2Idx].append((i1, i2))
  7074. #
  7075. # return cells
  7076. #
  7077. #
  7078. # def voronoi_edges2polygons(cells):
  7079. # """
  7080. # Transforms cell edges into polygons.
  7081. #
  7082. # :param cells: as returned from voronoi_cell_lines
  7083. # :rtype: dict point index -> list of vertex indices which form a polygon
  7084. # """
  7085. #
  7086. # # first, close the outer cells
  7087. # for pIdx, lineIndices_ in cells.items():
  7088. # dangling_lines = []
  7089. # for i1, i2 in lineIndices_:
  7090. # p = (i1, i2)
  7091. # connections = filter(lambda k: p != k and
  7092. # (p[0] == k[0] or p[0] == k[1] or p[1] == k[0] or p[1] == k[1]), lineIndices_)
  7093. # # connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and
  7094. # (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
  7095. # assert 1 <= len(connections) <= 2
  7096. # if len(connections) == 1:
  7097. # dangling_lines.append((i1, i2))
  7098. # assert len(dangling_lines) in [0, 2]
  7099. # if len(dangling_lines) == 2:
  7100. # (i11, i12), (i21, i22) = dangling_lines
  7101. # s = (i11, i12)
  7102. # t = (i21, i22)
  7103. #
  7104. # # determine which line ends are unconnected
  7105. # connected = filter(lambda k: k != s and (k[0] == s[0] or k[1] == s[0]), lineIndices_)
  7106. # # connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
  7107. # i11Unconnected = len(connected) == 0
  7108. #
  7109. # connected = filter(lambda k: k != t and (k[0] == t[0] or k[1] == t[0]), lineIndices_)
  7110. # # connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
  7111. # i21Unconnected = len(connected) == 0
  7112. #
  7113. # startIdx = i11 if i11Unconnected else i12
  7114. # endIdx = i21 if i21Unconnected else i22
  7115. #
  7116. # cells[pIdx].append((startIdx, endIdx))
  7117. #
  7118. # # then, form polygons by storing vertex indices in (counter-)clockwise order
  7119. # polys = {}
  7120. # for pIdx, lineIndices_ in cells.items():
  7121. # # get a directed graph which contains both directions and arbitrarily follow one of both
  7122. # directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
  7123. # directedGraphMap = collections.defaultdict(list)
  7124. # for (i1, i2) in directedGraph:
  7125. # directedGraphMap[i1].append(i2)
  7126. # orderedEdges = []
  7127. # currentEdge = directedGraph[0]
  7128. # while len(orderedEdges) < len(lineIndices_):
  7129. # i1 = currentEdge[1]
  7130. # i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
  7131. # nextEdge = (i1, i2)
  7132. # orderedEdges.append(nextEdge)
  7133. # currentEdge = nextEdge
  7134. #
  7135. # polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
  7136. #
  7137. # return polys
  7138. #
  7139. #
  7140. # def voronoi_polygons(points):
  7141. # """
  7142. # Returns the voronoi polygon for each input point.
  7143. #
  7144. # :param points: shape (n,2)
  7145. # :rtype: list of n polygons where each polygon is an array of vertices
  7146. # """
  7147. # vertices, lineIndices = voronoi(points)
  7148. # cells = voronoi_cell_lines(points, vertices, lineIndices)
  7149. # polys = voronoi_edges2polygons(cells)
  7150. # polylist = []
  7151. # for i in range(len(points)):
  7152. # poly = vertices[np.asarray(polys[i])]
  7153. # polylist.append(poly)
  7154. # return polylist
  7155. #
  7156. #
  7157. # class Zprofile:
  7158. # def __init__(self):
  7159. #
  7160. # # data contains lists of [x, y, z]
  7161. # self.data = []
  7162. #
  7163. # # Computed voronoi polygons (shapely)
  7164. # self.polygons = []
  7165. # pass
  7166. #
  7167. # # def plot_polygons(self):
  7168. # # axes = plt.subplot(1, 1, 1)
  7169. # #
  7170. # # plt.axis([-0.05, 1.05, -0.05, 1.05])
  7171. # #
  7172. # # for poly in self.polygons:
  7173. # # p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
  7174. # # axes.add_patch(p)
  7175. #
  7176. # def init_from_csv(self, filename):
  7177. # pass
  7178. #
  7179. # def init_from_string(self, zpstring):
  7180. # pass
  7181. #
  7182. # def init_from_list(self, zplist):
  7183. # self.data = zplist
  7184. #
  7185. # def generate_polygons(self):
  7186. # self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
  7187. #
  7188. # def normalize(self, origin):
  7189. # pass
  7190. #
  7191. # def paste(self, path):
  7192. # """
  7193. # Return a list of dictionaries containing the parts of the original
  7194. # path and their z-axis offset.
  7195. # """
  7196. #
  7197. # # At most one region/polygon will contain the path
  7198. # containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
  7199. #
  7200. # if len(containing) > 0:
  7201. # return [{"path": path, "z": self.data[containing[0]][2]}]
  7202. #
  7203. # # All region indexes that intersect with the path
  7204. # crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
  7205. #
  7206. # return [{"path": path.intersection(self.polygons[i]),
  7207. # "z": self.data[i][2]} for i in crossing]
  7208. def autolist(obj):
  7209. try:
  7210. __ = iter(obj)
  7211. return obj
  7212. except TypeError:
  7213. return [obj]
  7214. def three_point_circle(p1, p2, p3):
  7215. """
  7216. Computes the center and radius of a circle from
  7217. 3 points on its circumference.
  7218. :param p1: Point 1
  7219. :param p2: Point 2
  7220. :param p3: Point 3
  7221. :return: center, radius
  7222. """
  7223. # Midpoints
  7224. a1 = (p1 + p2) / 2.0
  7225. a2 = (p2 + p3) / 2.0
  7226. # Normals
  7227. b1 = np.dot((p2 - p1), np.array([[0, -1], [1, 0]], dtype=np.float32))
  7228. b2 = np.dot((p3 - p2), np.array([[0, 1], [-1, 0]], dtype=np.float32))
  7229. # Params
  7230. try:
  7231. T = solve(np.transpose(np.array([-b1, b2])), a1 - a2)
  7232. except Exception as e:
  7233. log.debug("camlib.three_point_circle() --> %s" % str(e))
  7234. return
  7235. # Center
  7236. center = a1 + b1 * T[0]
  7237. # Radius
  7238. radius = np.linalg.norm(center - p1)
  7239. return center, radius, T[0]
  7240. def distance(pt1, pt2):
  7241. return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
  7242. def distance_euclidian(x1, y1, x2, y2):
  7243. return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
  7244. class FlatCAMRTree(object):
  7245. """
  7246. Indexes geometry (Any object with "cooords" property containing
  7247. a list of tuples with x, y values). Objects are indexed by
  7248. all their points by default. To index by arbitrary points,
  7249. override self.points2obj.
  7250. """
  7251. def __init__(self):
  7252. # Python RTree Index
  7253. self.rti = rtindex.Index()
  7254. # ## Track object-point relationship
  7255. # Each is list of points in object.
  7256. self.obj2points = []
  7257. # Index is index in rtree, value is index of
  7258. # object in obj2points.
  7259. self.points2obj = []
  7260. self.get_points = lambda go: go.coords
  7261. def grow_obj2points(self, idx):
  7262. """
  7263. Increases the size of self.obj2points to fit
  7264. idx + 1 items.
  7265. :param idx: Index to fit into list.
  7266. :return: None
  7267. """
  7268. if len(self.obj2points) > idx:
  7269. # len == 2, idx == 1, ok.
  7270. return
  7271. else:
  7272. # len == 2, idx == 2, need 1 more.
  7273. # range(2, 3)
  7274. for i in range(len(self.obj2points), idx + 1):
  7275. self.obj2points.append([])
  7276. def insert(self, objid, obj):
  7277. self.grow_obj2points(objid)
  7278. self.obj2points[objid] = []
  7279. for pt in self.get_points(obj):
  7280. self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
  7281. self.obj2points[objid].append(len(self.points2obj))
  7282. self.points2obj.append(objid)
  7283. def remove_obj(self, objid, obj):
  7284. # Use all ptids to delete from index
  7285. for i, pt in enumerate(self.get_points(obj)):
  7286. try:
  7287. self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
  7288. except IndexError:
  7289. pass
  7290. def nearest(self, pt):
  7291. """
  7292. Will raise StopIteration if no items are found.
  7293. :param pt:
  7294. :return:
  7295. """
  7296. return next(self.rti.nearest(pt, objects=True))
  7297. def intersection(self, pt):
  7298. """
  7299. Will raise StopIteration if no items are found.
  7300. :param pt:
  7301. :return:
  7302. """
  7303. return next(self.rti.intersection(pt, objects=True))
  7304. class FlatCAMRTreeStorage(FlatCAMRTree):
  7305. """
  7306. Just like FlatCAMRTree it indexes geometry, but also serves
  7307. as storage for the geometry.
  7308. """
  7309. def __init__(self):
  7310. # super(FlatCAMRTreeStorage, self).__init__()
  7311. super().__init__()
  7312. self.objects = []
  7313. # Optimization attempt!
  7314. self.indexes = {}
  7315. def insert(self, obj):
  7316. self.objects.append(obj)
  7317. idx = len(self.objects) - 1
  7318. # Note: Shapely objects are not hashable any more, although
  7319. # there seem to be plans to re-introduce the feature in
  7320. # version 2.0. For now, we will index using the object's id,
  7321. # but it's important to remember that shapely geometry is
  7322. # mutable, ie. it can be modified to a totally different shape
  7323. # and continue to have the same id.
  7324. # self.indexes[obj] = idx
  7325. self.indexes[id(obj)] = idx
  7326. # super(FlatCAMRTreeStorage, self).insert(idx, obj)
  7327. super().insert(idx, obj)
  7328. # @profile
  7329. def remove(self, obj):
  7330. # See note about self.indexes in insert().
  7331. # objidx = self.indexes[obj]
  7332. objidx = self.indexes[id(obj)]
  7333. # Remove from list
  7334. self.objects[objidx] = None
  7335. # Remove from index
  7336. self.remove_obj(objidx, obj)
  7337. def get_objects(self):
  7338. return (o for o in self.objects if o is not None)
  7339. def nearest(self, pt):
  7340. """
  7341. Returns the nearest matching points and the object
  7342. it belongs to.
  7343. :param pt: Query point.
  7344. :return: (match_x, match_y), Object owner of
  7345. matching point.
  7346. :rtype: tuple
  7347. """
  7348. tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
  7349. return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
  7350. # class myO:
  7351. # def __init__(self, coords):
  7352. # self.coords = coords
  7353. #
  7354. #
  7355. # def test_rti():
  7356. #
  7357. # o1 = myO([(0, 0), (0, 1), (1, 1)])
  7358. # o2 = myO([(2, 0), (2, 1), (2, 1)])
  7359. # o3 = myO([(2, 0), (2, 1), (3, 1)])
  7360. #
  7361. # os = [o1, o2]
  7362. #
  7363. # idx = FlatCAMRTree()
  7364. #
  7365. # for o in range(len(os)):
  7366. # idx.insert(o, os[o])
  7367. #
  7368. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  7369. #
  7370. # idx.remove_obj(0, o1)
  7371. #
  7372. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  7373. #
  7374. # idx.remove_obj(1, o2)
  7375. #
  7376. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  7377. #
  7378. #
  7379. # def test_rtis():
  7380. #
  7381. # o1 = myO([(0, 0), (0, 1), (1, 1)])
  7382. # o2 = myO([(2, 0), (2, 1), (2, 1)])
  7383. # o3 = myO([(2, 0), (2, 1), (3, 1)])
  7384. #
  7385. # os = [o1, o2]
  7386. #
  7387. # idx = FlatCAMRTreeStorage()
  7388. #
  7389. # for o in range(len(os)):
  7390. # idx.insert(os[o])
  7391. #
  7392. # #os = None
  7393. # #o1 = None
  7394. # #o2 = None
  7395. #
  7396. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  7397. #
  7398. # idx.remove(idx.nearest((2,0))[1])
  7399. #
  7400. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  7401. #
  7402. # idx.remove(idx.nearest((0,0))[1])
  7403. #
  7404. # print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]