| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477 |
- ############################################################
- # FlatCAM: 2D Post-processing for Manufacturing #
- # http://flatcam.org #
- # Author: Juan Pablo Caram (c) #
- # Date: 2/5/2014 #
- # MIT Licence #
- ############################################################
- from io import StringIO
- from PyQt5 import QtCore, QtGui
- from PyQt5.QtCore import Qt
- import copy
- import inspect # TODO: For debugging only.
- from shapely.geometry.base import JOIN_STYLE
- from datetime import datetime
- import FlatCAMApp
- from ObjectUI import *
- from FlatCAMCommon import LoudDict
- from FlatCAMEditor import FlatCAMGeoEditor
- from camlib import *
- from VisPyVisuals import ShapeCollectionVisual
- import itertools
- # Interrupts plotting process if FlatCAMObj has been deleted
- class ObjectDeleted(Exception):
- pass
- class ValidationError(Exception):
- def __init__(self, message, errors):
- super().__init__(message)
- self.errors = errors
- ########################################
- ## FlatCAMObj ##
- ########################################
- class FlatCAMObj(QtCore.QObject):
- """
- Base type of objects handled in FlatCAM. These become interactive
- in the GUI, can be plotted, and their options can be modified
- by the user in their respective forms.
- """
- # Instance of the application to which these are related.
- # The app should set this value.
- app = None
- def __init__(self, name):
- """
- Constructor.
- :param name: Name of the object given by the user.
- :return: FlatCAMObj
- """
- QtCore.QObject.__init__(self)
- # View
- self.ui = None
- self.options = LoudDict(name=name)
- self.options.set_change_callback(self.on_options_change)
- self.form_fields = {}
- self.kind = None # Override with proper name
- # self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
- self.shapes = self.app.plotcanvas.new_shape_group()
- self.item = None # Link with project view item
- self.muted_ui = False
- self.deleted = False
- self._drawing_tolerance = 0.01
- # assert isinstance(self.ui, ObjectUI)
- # self.ui.name_entry.returnPressed.connect(self.on_name_activate)
- # self.ui.offset_button.clicked.connect(self.on_offset_button_click)
- # self.ui.scale_button.clicked.connect(self.on_scale_button_click)
- def __del__(self):
- pass
- def __str__(self):
- return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
- def from_dict(self, d):
- """
- This supersedes ``from_dict`` in derived classes. Derived classes
- must inherit from FlatCAMObj first, then from derivatives of Geometry.
- ``self.options`` is only updated, not overwritten. This ensures that
- options set by the app do not vanish when reading the objects
- from a project file.
- :param d: Dictionary with attributes to set.
- :return: None
- """
- for attr in self.ser_attrs:
- if attr == 'options':
- self.options.update(d[attr])
- else:
- setattr(self, attr, d[attr])
- def on_options_change(self, key):
- # Update form on programmatically options change
- self.set_form_item(key)
- # Set object visibility
- if key == 'plot':
- self.visible = self.options['plot']
- # self.emit(QtCore.SIGNAL("optionChanged"), key)
- self.optionChanged.emit(key)
- def set_ui(self, ui):
- self.ui = ui
- self.form_fields = {"name": self.ui.name_entry}
- assert isinstance(self.ui, ObjectUI)
- self.ui.name_entry.returnPressed.connect(self.on_name_activate)
- self.ui.offset_button.clicked.connect(self.on_offset_button_click)
- self.ui.scale_button.clicked.connect(self.on_scale_button_click)
- self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
- self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
- # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
- def build_ui(self):
- """
- Sets up the UI/form for this object. Show the UI
- in the App.
- :return: None
- :rtype: None
- """
- self.muted_ui = True
- FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
- # Remove anything else in the box
- # box_children = self.app.ui.notebook.selected_contents.get_children()
- # for child in box_children:
- # self.app.ui.notebook.selected_contents.remove(child)
- # while self.app.ui.selected_layout.count():
- # self.app.ui.selected_layout.takeAt(0)
- # Put in the UI
- # box_selected.pack_start(sw, True, True, 0)
- # self.app.ui.notebook.selected_contents.add(self.ui)
- # self.app.ui.selected_layout.addWidget(self.ui)
- try:
- self.app.ui.selected_scroll_area.takeWidget()
- except:
- self.app.log.debug("Nothing to remove")
- self.app.ui.selected_scroll_area.setWidget(self.ui)
- self.muted_ui = False
- def on_name_activate(self):
- old_name = copy.copy(self.options["name"])
- new_name = self.ui.name_entry.get_value()
- # update the SHELL auto-completer model data
- try:
- self.app.myKeywords.remove(old_name)
- self.app.myKeywords.append(new_name)
- self.app.shell._edit.set_model_data(self.app.myKeywords)
- except:
- log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
- self.options["name"] = self.ui.name_entry.get_value()
- self.app.inform.emit("[success]Name changed from %s to %s" % (old_name, new_name))
- def on_offset_button_click(self):
- self.app.report_usage("obj_on_offset_button")
- self.read_form()
- vect = self.ui.offsetvector_entry.get_value()
- self.offset(vect)
- self.plot()
- self.app.object_changed.emit(self)
- def on_scale_button_click(self):
- self.app.report_usage("obj_on_scale_button")
- self.read_form()
- factor = self.ui.scale_entry.get_value()
- self.scale(factor)
- self.plot()
- self.app.object_changed.emit(self)
- def on_skew_button_click(self):
- self.app.report_usage("obj_on_skew_button")
- self.read_form()
- xangle = self.ui.xangle_entry.get_value()
- yangle = self.ui.yangle_entry.get_value()
- self.skew(xangle, yangle)
- self.plot()
- self.app.object_changed.emit(self)
- def to_form(self):
- """
- Copies options to the UI form.
- :return: None
- """
- FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
- for option in self.options:
- try:
- self.set_form_item(option)
- except:
- self.app.log.warning("Unexpected error:", sys.exc_info())
- def read_form(self):
- """
- Reads form into ``self.options``.
- :return: None
- :rtype: None
- """
- FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
- for option in self.options:
- try:
- self.read_form_item(option)
- except:
- self.app.log.warning("Unexpected error:", sys.exc_info())
- def set_form_item(self, option):
- """
- Copies the specified option to the UI form.
- :param option: Name of the option (Key in ``self.options``).
- :type option: str
- :return: None
- """
- try:
- self.form_fields[option].set_value(self.options[option])
- except KeyError:
- # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
- pass
- def read_form_item(self, option):
- """
- Reads the specified option from the UI form into ``self.options``.
- :param option: Name of the option.
- :type option: str
- :return: None
- """
- try:
- self.options[option] = self.form_fields[option].get_value()
- except KeyError:
- self.app.log.warning("Failed to read option from field: %s" % option)
- def plot(self):
- """
- Plot this object (Extend this method to implement the actual plotting).
- Call this in descendants before doing the plotting.
- :return: Whether to continue plotting or not depending on the "plot" option.
- :rtype: bool
- """
- FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
- if self.deleted:
- return False
- self.clear()
- return True
- def serialize(self):
- """
- Returns a representation of the object as a dictionary so
- it can be later exported as JSON. Override this method.
- :return: Dictionary representing the object
- :rtype: dict
- """
- return
- def deserialize(self, obj_dict):
- """
- Re-builds an object from its serialized version.
- :param obj_dict: Dictionary representing a FlatCAMObj
- :type obj_dict: dict
- :return: None
- """
- return
- def add_shape(self, **kwargs):
- if self.deleted:
- raise ObjectDeleted()
- else:
- key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
- return key
- @property
- def visible(self):
- return self.shapes.visible
- @visible.setter
- def visible(self, value):
- self.shapes.visible = value
- # Not all object types has annotations
- try:
- self.annotation.visible = value
- except AttributeError:
- pass
- @property
- def drawing_tolerance(self):
- return self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
- @drawing_tolerance.setter
- def drawing_tolerance(self, value):
- self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
- def clear(self, update=False):
- self.shapes.clear(update)
- # Not all object types has annotations
- try:
- self.annotation.clear(update)
- except AttributeError:
- pass
- def delete(self):
- # Free resources
- del self.ui
- del self.options
- # Set flag
- self.deleted = True
- class FlatCAMGerber(FlatCAMObj, Gerber):
- """
- Represents Gerber code.
- """
- optionChanged = QtCore.pyqtSignal(str)
- ui_type = GerberObjectUI
- @staticmethod
- def merge(grb_list, grb_final):
- """
- Merges the geometry of objects in geo_list into
- the geometry of geo_final.
- :param grb_list: List of FlatCAMGerber Objects to join.
- :param grb_final: Destination FlatCAMGeometry object.
- :return: None
- """
- if grb_final.solid_geometry is None:
- grb_final.solid_geometry = []
- if type(grb_final.solid_geometry) is not list:
- grb_final.solid_geometry = [grb_final.solid_geometry]
- for grb in grb_list:
- for option in grb.options:
- if option is not 'name':
- try:
- grb_final.options[option] = grb.options[option]
- except:
- log.warning("Failed to copy option.", option)
- # Expand lists
- if type(grb) is list:
- FlatCAMGerber.merge(grb, grb_final)
- else: # If not list, just append
- for geos in grb.solid_geometry:
- grb_final.solid_geometry.append(geos)
- grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
- def __init__(self, name):
- Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
- FlatCAMObj.__init__(self, name)
- self.kind = "gerber"
- # The 'name' is already in self.options from FlatCAMObj
- # Automatically updates the UI
- self.options.update({
- "plot": True,
- "multicolored": False,
- "solid": False,
- "isotooldia": 0.016,
- "isopasses": 1,
- "isooverlap": 0.15,
- "milling_type": "cl",
- "combine_passes": True,
- "noncoppermargin": 0.0,
- "noncopperrounded": False,
- "bboxmargin": 0.0,
- "bboxrounded": False
- })
- # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
- self.iso_type = 2
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind']
- self.multigeo = False
- # assert isinstance(self.ui, GerberObjectUI)
- # self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- # self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
- # self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
- # self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
- # self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
- # self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
- # self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
- def set_ui(self, ui):
- """
- Maps options with GUI inputs.
- Connects GUI events to methods.
- :param ui: GUI object.
- :type ui: GerberObjectUI
- :return: None
- """
- FlatCAMObj.set_ui(self, ui)
- FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
- self.form_fields.update({
- "plot": self.ui.plot_cb,
- "multicolored": self.ui.multicolored_cb,
- "solid": self.ui.solid_cb,
- "isotooldia": self.ui.iso_tool_dia_entry,
- "isopasses": self.ui.iso_width_entry,
- "isooverlap": self.ui.iso_overlap_entry,
- "milling_type": self.ui.milling_type_radio,
- "combine_passes": self.ui.combine_passes_cb,
- "noncoppermargin": self.ui.noncopper_margin_entry,
- "noncopperrounded": self.ui.noncopper_rounded_cb,
- "bboxmargin": self.ui.bbmargin_entry,
- "bboxrounded": self.ui.bbrounded_cb
- })
- # Fill form fields only on object create
- self.to_form()
- assert isinstance(self.ui, GerberObjectUI)
- self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
- self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
- self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click)
- self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click)
- self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
- self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
- self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
- self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
- self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
- def on_generatenoncopper_button_click(self, *args):
- self.app.report_usage("gerber_on_generatenoncopper_button")
- self.read_form()
- name = self.options["name"] + "_noncopper"
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry)
- bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
- if not self.options["noncopperrounded"]:
- bounding_box = bounding_box.envelope
- non_copper = bounding_box.difference(self.solid_geometry)
- geo_obj.solid_geometry = non_copper
- # TODO: Check for None
- self.app.new_object("geometry", name, geo_init)
- def on_generatebb_button_click(self, *args):
- self.app.report_usage("gerber_on_generatebb_button")
- self.read_form()
- name = self.options["name"] + "_bbox"
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry)
- # Bounding box with rounded corners
- bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
- if not self.options["bboxrounded"]: # Remove rounded corners
- bounding_box = bounding_box.envelope
- geo_obj.solid_geometry = bounding_box
- self.app.new_object("geometry", name, geo_init)
- def on_ext_iso_button_click(self, *args):
- if self.ui.follow_cb.get_value() == True:
- obj = self.app.collection.get_active()
- obj.follow()
- # in the end toggle the visibility of the origin object so we can see the generated Geometry
- obj.ui.plot_cb.toggle()
- else:
- self.app.report_usage("gerber_on_iso_button")
- self.read_form()
- self.isolate(iso_type=0)
- def on_int_iso_button_click(self, *args):
- if self.ui.follow_cb.get_value() == True:
- obj = self.app.collection.get_active()
- obj.follow()
- # in the end toggle the visibility of the origin object so we can see the generated Geometry
- obj.ui.plot_cb.toggle()
- else:
- self.app.report_usage("gerber_on_iso_button")
- self.read_form()
- self.isolate(iso_type=1)
- def on_iso_button_click(self, *args):
- if self.ui.follow_cb.get_value() == True:
- obj = self.app.collection.get_active()
- obj.follow()
- # in the end toggle the visibility of the origin object so we can see the generated Geometry
- obj.ui.plot_cb.toggle()
- else:
- self.app.report_usage("gerber_on_iso_button")
- self.read_form()
- self.isolate()
- def follow(self, outname=None):
- """
- Creates a geometry object "following" the gerber paths.
- :return: None
- """
- # default_name = self.options["name"] + "_follow"
- # follow_name = outname or default_name
- if outname is None:
- follow_name = self.options["name"] + "_follow"
- else:
- follow_name = outname
- def follow_init(follow_obj, app):
- # Propagate options
- follow_obj.options["cnctooldia"] = self.options["isotooldia"]
- follow_obj.solid_geometry = self.solid_geometry
- # TODO: Do something if this is None. Offer changing name?
- try:
- self.app.new_object("geometry", follow_name, follow_init)
- except Exception as e:
- return "Operation failed: %s" % str(e)
- def isolate(self, iso_type=None, dia=None, passes=None, overlap=None,
- outname=None, combine=None, milling_type=None):
- """
- Creates an isolation routing geometry object in the project.
- :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
- :param dia: Tool diameter
- :param passes: Number of tool widths to cut
- :param overlap: Overlap between passes in fraction of tool diameter
- :param outname: Base name of the output object
- :return: None
- """
- if dia is None:
- dia = self.options["isotooldia"]
- if passes is None:
- passes = int(self.options["isopasses"])
- if overlap is None:
- overlap = self.options["isooverlap"]
- if combine is None:
- combine = self.options["combine_passes"]
- else:
- combine = bool(combine)
- if milling_type is None:
- milling_type = self.options["milling_type"]
- if iso_type is None:
- self.iso_type = 2
- else:
- self.iso_type = iso_type
- base_name = self.options["name"] + "_iso"
- base_name = outname or base_name
- def generate_envelope(offset, invert, envelope_iso_type=2):
- # isolation_geometry produces an envelope that is going on the left of the geometry
- # (the copper features). To leave the least amount of burrs on the features
- # the tool needs to travel on the right side of the features (this is called conventional milling)
- # the first pass is the one cutting all of the features, so it needs to be reversed
- # the other passes overlap preceding ones and cut the left over copper. It is better for them
- # to cut on the right side of the left over copper i.e on the left side of the features.
- try:
- geom = self.isolation_geometry(offset, iso_type=envelope_iso_type)
- except Exception as e:
- log.debug(str(e))
- return 'fail'
- if invert:
- try:
- if type(geom) is MultiPolygon:
- pl = []
- for p in geom:
- pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
- geom = MultiPolygon(pl)
- elif type(geom) is Polygon:
- geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
- else:
- log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry")
- except Exception as e:
- log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
- return geom
- if combine:
- if self.iso_type == 0:
- iso_name = self.options["name"] + "_ext_iso"
- elif self.iso_type == 1:
- iso_name = self.options["name"] + "_int_iso"
- else:
- iso_name = base_name
- # TODO: This is ugly. Create way to pass data into init function.
- def iso_init(geo_obj, app_obj):
- # Propagate options
- geo_obj.options["cnctooldia"] = self.options["isotooldia"]
- geo_obj.solid_geometry = []
- for i in range(passes):
- iso_offset = (((2 * i + 1) / 2.0) * dia) - (i * overlap * dia)
- # if milling type is climb then the move is counter-clockwise around features
- if milling_type == 'cl':
- # geom = generate_envelope (offset, i == 0)
- geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type)
- else:
- geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type)
- geo_obj.solid_geometry.append(geom)
- # detect if solid_geometry is empty and this require list flattening which is "heavy"
- # or just looking in the lists (they are one level depth) and if any is not empty
- # proceed with object creation, if there are empty and the number of them is the length
- # of the list then we have an empty solid_geometry which should raise a Custom Exception
- empty_cnt = 0
- if not isinstance(geo_obj.solid_geometry, list):
- geo_obj.solid_geometry = [geo_obj.solid_geometry]
- for g in geo_obj.solid_geometry:
- if g:
- app_obj.inform.emit("[success]Isolation geometry created: %s" % geo_obj.options["name"])
- break
- else:
- empty_cnt += 1
- if empty_cnt == len(geo_obj.solid_geometry):
- raise ValidationError("Empty Geometry", None)
- geo_obj.multigeo = False
- # TODO: Do something if this is None. Offer changing name?
- self.app.new_object("geometry", iso_name, iso_init)
- else:
- for i in range(passes):
- offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
- if passes > 1:
- if self.iso_type == 0:
- iso_name = self.options["name"] + "_ext_iso" + str(i + 1)
- elif self.iso_type == 1:
- iso_name = self.options["name"] + "_int_iso" + str(i + 1)
- else:
- iso_name = base_name + str(i + 1)
- else:
- if self.iso_type == 0:
- iso_name = self.options["name"] + "_ext_iso"
- elif self.iso_type == 1:
- iso_name = self.options["name"] + "_int_iso"
- else:
- iso_name = base_name
- # TODO: This is ugly. Create way to pass data into init function.
- def iso_init(geo_obj, app_obj):
- # Propagate options
- geo_obj.options["cnctooldia"] = self.options["isotooldia"]
- # if milling type is climb then the move is counter-clockwise around features
- if milling_type == 'cl':
- # geo_obj.solid_geometry = generate_envelope(offset, i == 0)
- geo_obj.solid_geometry = generate_envelope(offset, 1, envelope_iso_type=self.iso_type)
- else:
- geo_obj.solid_geometry = generate_envelope(offset, 0, envelope_iso_type=self.iso_type)
- # detect if solid_geometry is empty and this require list flattening which is "heavy"
- # or just looking in the lists (they are one level depth) and if any is not empty
- # proceed with object creation, if there are empty and the number of them is the length
- # of the list then we have an empty solid_geometry which should raise a Custom Exception
- empty_cnt = 0
- if not isinstance(geo_obj.solid_geometry, list):
- geo_obj.solid_geometry = [geo_obj.solid_geometry]
- for g in geo_obj.solid_geometry:
- if g:
- app_obj.inform.emit("[success]Isolation geometry created: %s" % geo_obj.options["name"])
- break
- else:
- empty_cnt += 1
- if empty_cnt == len(geo_obj.solid_geometry):
- raise ValidationError("Empty Geometry", None)
- geo_obj.multigeo = False
- # TODO: Do something if this is None. Offer changing name?
- self.app.new_object("geometry", iso_name, iso_init)
- def on_plot_cb_click(self, *args):
- if self.muted_ui:
- return
- self.read_form_item('plot')
- def on_solid_cb_click(self, *args):
- if self.muted_ui:
- return
- self.read_form_item('solid')
- self.plot()
- def on_multicolored_cb_click(self, *args):
- if self.muted_ui:
- return
- self.read_form_item('multicolored')
- self.plot()
- def convert_units(self, units):
- """
- Converts the units of the object by scaling dimensions in all geometry
- and options.
- :param units: Units to which to convert the object: "IN" or "MM".
- :type units: str
- :return: None
- :rtype: None
- """
- factor = Gerber.convert_units(self, units)
- self.options['isotooldia'] *= factor
- self.options['bboxmargin'] *= factor
- def plot(self, **kwargs):
- """
- :param kwargs: color and face_color
- :return:
- """
- FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
- # Does all the required setup and returns False
- # if the 'ptint' option is set to False.
- if not FlatCAMObj.plot(self):
- return
- if 'color' in kwargs:
- color = kwargs['color']
- else:
- color = self.app.defaults['global_plot_line']
- if 'face_color' in kwargs:
- face_color = kwargs['face_color']
- else:
- face_color = self.app.defaults['global_plot_fill']
- geometry = self.solid_geometry
- # Make sure geometry is iterable.
- try:
- _ = iter(geometry)
- except TypeError:
- geometry = [geometry]
- def random_color():
- color = np.random.rand(4)
- color[3] = 1
- return color
- try:
- if self.options["solid"]:
- for g in geometry:
- if type(g) == Polygon or type(g) == LineString:
- self.add_shape(shape=g, color=color,
- face_color=random_color() if self.options['multicolored']
- else face_color, visible=self.options['plot'])
- else:
- for el in g:
- self.add_shape(shape=el, color=color,
- face_color=random_color() if self.options['multicolored']
- else face_color, visible=self.options['plot'])
- else:
- for g in geometry:
- if type(g) == Polygon or type(g) == LineString:
- self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black',
- visible=self.options['plot'])
- else:
- for el in g:
- self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
- visible=self.options['plot'])
- self.shapes.redraw()
- except (ObjectDeleted, AttributeError):
- self.shapes.clear(update=True)
- def serialize(self):
- return {
- "options": self.options,
- "kind": self.kind
- }
- class FlatCAMExcellon(FlatCAMObj, Excellon):
- """
- Represents Excellon/Drill code.
- """
- ui_type = ExcellonObjectUI
- optionChanged = QtCore.pyqtSignal(str)
- def __init__(self, name):
- Excellon.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
- FlatCAMObj.__init__(self, name)
- self.kind = "excellon"
- self.options.update({
- "plot": True,
- "solid": False,
- "drillz": -0.1,
- "travelz": 0.1,
- "feedrate": 5.0,
- "feedrate_rapid": 5.0,
- "tooldia": 0.1,
- "slot_tooldia": 0.1,
- "toolchange": False,
- "toolchangez": 1.0,
- "toolchangexy": "0.0, 0.0",
- "endz": 2.0,
- "startz": None,
- "spindlespeed": None,
- "dwell": True,
- "dwelltime": 1000,
- "ppname_e": 'defaults',
- "optimization_type": "R",
- "gcode_type": "drills"
- })
- # TODO: Document this.
- self.tool_cbs = {}
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind']
- # variable to store the total amount of drills per job
- self.tot_drill_cnt = 0
- self.tool_row = 0
- # variable to store the total amount of slots per job
- self.tot_slot_cnt = 0
- self.tool_row_slots = 0
- self.multigeo = False
- @staticmethod
- def merge(exc_list, exc_final):
- """
- Merge Excellon objects found in exc_list parameter into exc_final object.
- Options are always copied from source .
- Tools are disregarded, what is taken in consideration is the unique drill diameters found as values in the
- exc_list tools dict's. In the reconstruction section for each unique tool diameter it will be created a
- tool_name to be used in the final Excellon object, exc_final.
- If only one object is in exc_list parameter then this function will copy that object in the exc_final
- :param exc_list: List or one object of FlatCAMExcellon Objects to join.
- :param exc_final: Destination FlatCAMExcellon object.
- :return: None
- """
- # flag to signal that we need to reorder the tools dictionary and drills and slots lists
- flag_order = False
- try:
- flattened_list = list(itertools.chain(*exc_list))
- except TypeError:
- flattened_list = exc_list
- # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
- # values will be list of Shapely Points; for drills
- custom_dict_drills = {}
- # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
- # values will be list of Shapely Points; for slots
- custom_dict_slots = {}
- for exc in flattened_list:
- # copy options of the current excellon obj to the final excellon obj
- for option in exc.options:
- if option is not 'name':
- try:
- exc_final.options[option] = exc.options[option]
- except:
- exc.app.log.warning("Failed to copy option.", option)
- for drill in exc.drills:
- exc_tool_dia = float('%.3f' % exc.tools[drill['tool']]['C'])
- if exc_tool_dia not in custom_dict_drills:
- custom_dict_drills[exc_tool_dia] = [drill['point']]
- else:
- custom_dict_drills[exc_tool_dia].append(drill['point'])
- for slot in exc.slots:
- exc_tool_dia = float('%.3f' % exc.tools[slot['tool']]['C'])
- if exc_tool_dia not in custom_dict_slots:
- custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]]
- else:
- custom_dict_slots[exc_tool_dia].append([slot['start'], slot['stop']])
- # add the zeros and units to the exc_final object
- exc_final.zeros = exc.zeros
- exc_final.units = exc.units
- # variable to make tool_name for the tools
- current_tool = 0
- # Here we add data to the exc_final object
- # the tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points in case of
- # drills
- for tool_dia in custom_dict_drills:
- # we create a tool name for each key in the drill_dia dict (the key is a unique drill diameter)
- current_tool += 1
- tool_name = str(current_tool)
- spec = {"C": float(tool_dia)}
- exc_final.tools[tool_name] = spec
- # rebuild the drills list of dict's that belong to the exc_final object
- for point in custom_dict_drills[tool_dia]:
- exc_final.drills.append(
- {
- "point": point,
- "tool": str(current_tool)
- }
- )
- # Here we add data to the exc_final object
- # the tools diameter are now the keys in the drill_dia dict and the values are a list ([start, stop])
- # of two Shapely Points in case of slots
- for tool_dia in custom_dict_slots:
- # we create a tool name for each key in the slot_dia dict (the key is a unique slot diameter)
- # but only if there are no drills
- if not exc_final.tools:
- current_tool += 1
- tool_name = str(current_tool)
- spec = {"C": float(tool_dia)}
- exc_final.tools[tool_name] = spec
- else:
- dia_list = []
- for v in exc_final.tools.values():
- dia_list.append(float(v["C"]))
- if tool_dia not in dia_list:
- flag_order = True
- current_tool = len(dia_list) + 1
- tool_name = str(current_tool)
- spec = {"C": float(tool_dia)}
- exc_final.tools[tool_name] = spec
- else:
- for k, v in exc_final.tools.items():
- if v["C"] == tool_dia:
- current_tool = int(k)
- break
- # rebuild the slots list of dict's that belong to the exc_final object
- for point in custom_dict_slots[tool_dia]:
- exc_final.slots.append(
- {
- "start": point[0],
- "stop": point[1],
- "tool": str(current_tool)
- }
- )
- # flag_order == True means that there was an slot diameter not in the tools and we also have drills
- # and the new tool was added to self.tools therefore we need to reorder the tools and drills and slots
- current_tool = 0
- if flag_order is True:
- dia_list = []
- temp_drills = []
- temp_slots = []
- temp_tools = {}
- for v in exc_final.tools.values():
- dia_list.append(float(v["C"]))
- dia_list.sort()
- for ordered_dia in dia_list:
- current_tool += 1
- tool_name_temp = str(current_tool)
- spec_temp = {"C": float(ordered_dia)}
- temp_tools[tool_name_temp] = spec_temp
- for drill in exc_final.drills:
- exc_tool_dia = float('%.3f' % exc_final.tools[drill['tool']]['C'])
- if exc_tool_dia == ordered_dia:
- temp_drills.append(
- {
- "point": drill["point"],
- "tool": str(current_tool)
- }
- )
- for slot in exc_final.slots:
- slot_tool_dia = float('%.3f' % exc_final.tools[slot['tool']]['C'])
- if slot_tool_dia == ordered_dia:
- temp_slots.append(
- {
- "start": slot["start"],
- "stop": slot["stop"],
- "tool": str(current_tool)
- }
- )
- # delete the exc_final tools, drills and slots
- exc_final.tools = dict()
- exc_final.drills[:] = []
- exc_final.slots[:] = []
- # update the exc_final tools, drills and slots with the ordered values
- exc_final.tools = temp_tools
- exc_final.drills[:] = temp_drills
- exc_final.slots[:] = temp_slots
- # create the geometry for the exc_final object
- exc_final.create_geometry()
- def build_ui(self):
- FlatCAMObj.build_ui(self)
- n = len(self.tools)
- # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
- self.ui.tools_table.setRowCount(n + 2)
- self.tot_drill_cnt = 0
- self.tot_slot_cnt = 0
- self.tool_row = 0
- sort = []
- for k, v in list(self.tools.items()):
- sort.append((k, v.get('C')))
- sorted_tools = sorted(sort, key=lambda t1: t1[1])
- tools = [i[0] for i in sorted_tools]
- for tool_no in tools:
- drill_cnt = 0 # variable to store the nr of drills per tool
- slot_cnt = 0 # variable to store the nr of slots per tool
- # Find no of drills for the current tool
- for drill in self.drills:
- if drill['tool'] == tool_no:
- drill_cnt += 1
- self.tot_drill_cnt += drill_cnt
- # Find no of slots for the current tool
- for slot in self.slots:
- if slot['tool'] == tool_no:
- slot_cnt += 1
- self.tot_slot_cnt += slot_cnt
- id = QtWidgets.QTableWidgetItem('%d' % int(tool_no))
- id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- self.ui.tools_table.setItem(self.tool_row, 0, id) # Tool name/id
- # Make sure that the drill diameter when in MM is with no more than 2 decimals
- # There are no drill bits in MM with more than 3 decimals diameter
- # For INCH the decimals should be no more than 3. There are no drills under 10mils
- if self.units == 'MM':
- dia = QtWidgets.QTableWidgetItem('%.2f' % (self.tools[tool_no]['C']))
- else:
- dia = QtWidgets.QTableWidgetItem('%.3f' % (self.tools[tool_no]['C']))
- dia.setFlags(QtCore.Qt.ItemIsEnabled)
- drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
- drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
- # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
- if slot_cnt > 0:
- slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
- else:
- slot_count = QtWidgets.QTableWidgetItem('')
- slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
- self.ui.tools_table.setItem(self.tool_row, 1, dia) # Diameter
- self.ui.tools_table.setItem(self.tool_row, 2, drill_count) # Number of drills per tool
- self.ui.tools_table.setItem(self.tool_row, 3, slot_count) # Number of drills per tool
- self.tool_row += 1
- # add a last row with the Total number of drills
- empty = QtWidgets.QTableWidgetItem('')
- empty.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- empty_1 = QtWidgets.QTableWidgetItem('')
- empty_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- label_tot_drill_count = QtWidgets.QTableWidgetItem('Total Drills')
- tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
- label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
- tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
- self.ui.tools_table.setItem(self.tool_row, 0, empty)
- self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count)
- self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count) # Total number of drills
- self.ui.tools_table.setItem(self.tool_row, 3, empty_1) # Total number of drills
- font = QtGui.QFont()
- font.setBold(True)
- font.setWeight(75)
- for k in [1, 2]:
- self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
- self.ui.tools_table.item(self.tool_row, k).setFont(font)
- self.tool_row += 1
- # add a last row with the Total number of slots
- empty_2 = QtWidgets.QTableWidgetItem('')
- empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- empty_3 = QtWidgets.QTableWidgetItem('')
- empty_3.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- label_tot_slot_count = QtWidgets.QTableWidgetItem('Total Slots')
- tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
- label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
- tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
- self.ui.tools_table.setItem(self.tool_row, 0, empty_2)
- self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count)
- self.ui.tools_table.setItem(self.tool_row, 2, empty_3)
- self.ui.tools_table.setItem(self.tool_row, 3, tot_slot_count) # Total number of slots
- for kl in [1, 2, 3]:
- self.ui.tools_table.item(self.tool_row, kl).setFont(font)
- self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
- # sort the tool diameter column
- # self.ui.tools_table.sortItems(1)
- # all the tools are selected by default
- self.ui.tools_table.selectColumn(0)
- #
- self.ui.tools_table.resizeColumnsToContents()
- self.ui.tools_table.resizeRowsToContents()
- vertical_header = self.ui.tools_table.verticalHeader()
- # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
- vertical_header.hide()
- self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- horizontal_header = self.ui.tools_table.horizontalHeader()
- horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
- horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
- horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
- horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
- # horizontal_header.setStretchLastSection(True)
- self.ui.tools_table.setSortingEnabled(False)
- self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight())
- self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight())
- if not self.drills:
- self.ui.tdlabel.hide()
- self.ui.tooldia_entry.hide()
- self.ui.generate_milling_button.hide()
- else:
- self.ui.tdlabel.show()
- self.ui.tooldia_entry.show()
- self.ui.generate_milling_button.show()
- if not self.slots:
- self.ui.stdlabel.hide()
- self.ui.slot_tooldia_entry.hide()
- self.ui.generate_milling_slots_button.hide()
- else:
- self.ui.stdlabel.show()
- self.ui.slot_tooldia_entry.show()
- self.ui.generate_milling_slots_button.show()
- def set_ui(self, ui):
- """
- Configures the user interface for this object.
- Connects options to form fields.
- :param ui: User interface object.
- :type ui: ExcellonObjectUI
- :return: None
- """
- FlatCAMObj.set_ui(self, ui)
- FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
- self.form_fields.update({
- "plot": self.ui.plot_cb,
- "solid": self.ui.solid_cb,
- "drillz": self.ui.cutz_entry,
- "travelz": self.ui.travelz_entry,
- "feedrate": self.ui.feedrate_entry,
- "feedrate_rapid": self.ui.feedrate_rapid_entry,
- "tooldia": self.ui.tooldia_entry,
- "slot_tooldia": self.ui.slot_tooldia_entry,
- "toolchange": self.ui.toolchange_cb,
- "toolchangez": self.ui.toolchangez_entry,
- "spindlespeed": self.ui.spindlespeed_entry,
- "dwell": self.ui.dwell_cb,
- "dwelltime": self.ui.dwelltime_entry,
- "startz": self.ui.estartz_entry,
- "endz": self.ui.eendz_entry,
- "ppname_e": self.ui.pp_excellon_name_cb,
- "gcode_type": self.ui.excellon_gcode_type_radio
- })
- for name in list(self.app.postprocessors.keys()):
- # the HPGL postprocessor is only for Geometry not for Excellon job therefore don't add it
- if name == 'hpgl':
- continue
- self.ui.pp_excellon_name_cb.addItem(name)
- # Fill form fields
- self.to_form()
- assert isinstance(self.ui, ExcellonObjectUI), \
- "Expected a ExcellonObjectUI, got %s" % type(self.ui)
- self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
- self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
- self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
- self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click)
- def get_selected_tools_list(self):
- """
- Returns the keys to the self.tools dictionary corresponding
- to the selections on the tool list in the GUI.
- :return: List of tools.
- :rtype: list
- """
- return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
- def get_selected_tools_table_items(self):
- """
- Returns a list of lists, each list in the list is made out of row elements
- :return: List of table_tools items.
- :rtype: list
- """
- table_tools_items = []
- for x in self.ui.tools_table.selectedItems():
- table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
- for column in range(0, self.ui.tools_table.columnCount())])
- for item in table_tools_items:
- item[0] = str(item[0])
- return table_tools_items
- def export_excellon(self):
- """
- Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
- :return: has_slots and Excellon_code
- """
- excellon_code = ''
- units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
- # store here if the file has slots, return 1 if any slots, 0 if only drills
- has_slots = 0
- # drills processing
- try:
- for tool in self.tools:
- if int(tool) < 10:
- excellon_code += 'T0' + str(tool) + '\n'
- else:
- excellon_code += 'T' + str(tool) + '\n'
- for drill in self.drills:
- if tool == drill['tool']:
- if units == 'MM':
- excellon_code += 'X' + '%.3f' % drill['point'].x + 'Y' + '%.3f' % drill['point'].y + '\n'
- else:
- excellon_code += 'X' + '%.4f' % drill['point'].x + 'Y' + '%.4f' % drill['point'].y + '\n'
- except Exception as e:
- log.debug(str(e))
- # slots processing
- try:
- if self.slots:
- has_slots = 1
- for tool in self.tools:
- if int(tool) < 10:
- excellon_code += 'T0' + str(tool) + '\n'
- else:
- excellon_code += 'T' + str(tool) + '\n'
- for slot in self.slots:
- if tool == slot['tool']:
- if units == 'MM':
- excellon_code += 'G00' + 'X' + '%.3f' % slot['start'].x + 'Y' + \
- '%.3f' % slot['start'].y + '\n'
- excellon_code += 'M15\n'
- excellon_code += 'G01' + 'X' + '%.3f' % slot['stop'].x + 'Y' + \
- '%.3f' % slot['stop'].y + '\n'
- excellon_code += 'M16\n'
- else:
- excellon_code += 'G00' + 'X' + '%.4f' % slot['start'].x + 'Y' + \
- '%.4f' % slot['start'].y + '\n'
- excellon_code += 'M15\n'
- excellon_code += 'G01' + 'X' + '%.4f' % slot['stop'].x + 'Y' + \
- '%.4f' % slot['stop'].y + '\n'
- excellon_code += 'M16\n'
- except Exception as e:
- log.debug(str(e))
- return has_slots, excellon_code
- def export_excellon_altium(self):
- """
- Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
- :return: has_slots and Excellon_code
- """
- excellon_code = ''
- units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
- # store here if the file has slots, return 1 if any slots, 0 if only drills
- has_slots = 0
- # drills processing
- try:
- for tool in self.tools:
- if int(tool) < 10:
- excellon_code += 'T0' + str(tool) + '\n'
- else:
- excellon_code += 'T' + str(tool) + '\n'
- for drill in self.drills:
- if tool == drill['tool']:
- drill_x = drill['point'].x
- drill_y = drill['point'].y
- if units == 'MM':
- drill_x /= 25.4
- drill_y /= 25.4
- exc_x_formatted = ('%.4f' % drill_x).replace('.', '')
- if drill_x < 10:
- exc_x_formatted = '0' + exc_x_formatted
- exc_y_formatted = ('%.4f' % drill_y).replace('.', '')
- if drill_y < 10:
- exc_y_formatted = '0' + exc_y_formatted
- excellon_code += 'X' + exc_x_formatted + 'Y' + exc_y_formatted + '\n'
- except Exception as e:
- log.debug(str(e))
- # slots processing
- try:
- if self.slots:
- has_slots = 1
- for tool in self.tools:
- if int(tool) < 10:
- excellon_code += 'T0' + str(tool) + '\n'
- else:
- excellon_code += 'T' + str(tool) + '\n'
- for slot in self.slots:
- if tool == slot['tool']:
- start_slot_x = slot['start'].x
- start_slot_y = slot['start'].y
- stop_slot_x = slot['stop'].x
- stop_slot_y = slot['stop'].y
- if units == 'MM':
- start_slot_x /= 25.4
- start_slot_y /= 25.4
- stop_slot_x /= 25.4
- stop_slot_y /= 25.4
- start_slot_x_formatted = ('%.4f' % start_slot_x).replace('.', '')
- if start_slot_x < 10:
- start_slot_x_formatted = '0' + start_slot_x_formatted
- start_slot_y_formatted = ('%.4f' % start_slot_y).replace('.', '')
- if start_slot_y < 10:
- start_slot_y_formatted = '0' + start_slot_y_formatted
- stop_slot_x_formatted = ('%.4f' % stop_slot_x).replace('.', '')
- if stop_slot_x < 10:
- stop_slot_x_formatted = '0' + stop_slot_x_formatted
- stop_slot_y_formatted = ('%.4f' % stop_slot_y).replace('.', '')
- if stop_slot_y < 10:
- stop_slot_y_formatted = '0' + stop_slot_y_formatted
- excellon_code += 'G00' + 'X' + start_slot_x_formatted + 'Y' + \
- start_slot_y_formatted + '\n'
- excellon_code += 'M15\n'
- excellon_code += 'G01' + 'X' + stop_slot_x_formatted + 'Y' + \
- stop_slot_y_formatted + '\n'
- excellon_code += 'M16\n'
- except Exception as e:
- log.debug(str(e))
- return has_slots, excellon_code
- def generate_milling_drills(self, tools=None, outname=None, tooldia=None, use_thread=False):
- """
- Note: This method is a good template for generic operations as
- it takes it's options from parameters or otherwise from the
- object's options and returns a (success, msg) tuple as feedback
- for shell operations.
- :return: Success/failure condition tuple (bool, str).
- :rtype: tuple
- """
- # Get the tools from the list. These are keys
- # to self.tools
- if tools is None:
- tools = self.get_selected_tools_list()
- if outname is None:
- outname = self.options["name"] + "_mill"
- if tooldia is None:
- tooldia = self.options["tooldia"]
- # Sort tools by diameter. items() -> [('name', diameter), ...]
- # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
- sort = []
- for k, v in self.tools.items():
- sort.append((k, v.get('C')))
- sorted_tools = sorted(sort, key=lambda t1: t1[1])
- if tools == "all":
- tools = [i[0] for i in sorted_tools] # List if ordered tool names.
- log.debug("Tools 'all' and sorted are: %s" % str(tools))
- if len(tools) == 0:
- self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.")
- return False, "Error: No tools."
- for tool in tools:
- if tooldia > self.tools[tool]["C"]:
- self.app.inform.emit("[ERROR_NOTCL] Milling tool for DRILLS is larger than hole size. Cancelled.")
- return False, "Error: Milling tool is larger than hole."
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry), \
- "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
- app_obj.progress.emit(20)
- ### Add properties to the object
- # get the tool_table items in a list of row items
- tool_table_items = self.get_selected_tools_table_items()
- # insert an information only element in the front
- tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
- geo_obj.options['Tools_in_use'] = tool_table_items
- geo_obj.options['type'] = 'Excellon Geometry'
- geo_obj.solid_geometry = []
- # in case that the tool used has the same diameter with the hole, and since the maximum resolution
- # for FlatCAM is 6 decimals,
- # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
- for hole in self.drills:
- if hole['tool'] in tools:
- buffer_value = self.tools[hole['tool']]["C"] / 2 - tooldia / 2
- if buffer_value == 0:
- geo_obj.solid_geometry.append(
- Point(hole['point']).buffer(0.0000001).exterior)
- else:
- geo_obj.solid_geometry.append(
- Point(hole['point']).buffer(buffer_value).exterior)
- if use_thread:
- def geo_thread(app_obj):
- app_obj.new_object("geometry", outname, geo_init)
- app_obj.progress.emit(100)
- # Create a promise with the new name
- self.app.collection.promise(outname)
- # Send to worker
- self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
- else:
- self.app.new_object("geometry", outname, geo_init)
- return True, ""
- def generate_milling_slots(self, tools=None, outname=None, tooldia=None, use_thread=False):
- """
- Note: This method is a good template for generic operations as
- it takes it's options from parameters or otherwise from the
- object's options and returns a (success, msg) tuple as feedback
- for shell operations.
- :return: Success/failure condition tuple (bool, str).
- :rtype: tuple
- """
- # Get the tools from the list. These are keys
- # to self.tools
- if tools is None:
- tools = self.get_selected_tools_list()
- if outname is None:
- outname = self.options["name"] + "_mill"
- if tooldia is None:
- tooldia = self.options["slot_tooldia"]
- # Sort tools by diameter. items() -> [('name', diameter), ...]
- # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
- sort = []
- for k, v in self.tools.items():
- sort.append((k, v.get('C')))
- sorted_tools = sorted(sort, key=lambda t1: t1[1])
- if tools == "all":
- tools = [i[0] for i in sorted_tools] # List if ordered tool names.
- log.debug("Tools 'all' and sorted are: %s" % str(tools))
- if len(tools) == 0:
- self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.")
- return False, "Error: No tools."
- for tool in tools:
- if tooldia > self.tools[tool]["C"]:
- self.app.inform.emit("[ERROR_NOTCL] Milling tool for SLOTS is larger than hole size. Cancelled.")
- return False, "Error: Milling tool is larger than hole."
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry), \
- "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
- app_obj.progress.emit(20)
- ### Add properties to the object
- # get the tool_table items in a list of row items
- tool_table_items = self.get_selected_tools_table_items()
- # insert an information only element in the front
- tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
- geo_obj.options['Tools_in_use'] = tool_table_items
- geo_obj.options['type'] = 'Excellon Geometry'
- geo_obj.solid_geometry = []
- # in case that the tool used has the same diameter with the hole, and since the maximum resolution
- # for FlatCAM is 6 decimals,
- # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
- for slot in self.slots:
- if slot['tool'] in tools:
- buffer_value = self.tools[slot['tool']]["C"] / 2 - tooldia / 2
- if buffer_value == 0:
- start = slot['start']
- stop = slot['stop']
- lines_string = LineString([start, stop])
- poly = lines_string.buffer(0.0000001, self.geo_steps_per_circle).exterior
- geo_obj.solid_geometry.append(poly)
- else:
- start = slot['start']
- stop = slot['stop']
- lines_string = LineString([start, stop])
- poly = lines_string.buffer(buffer_value, self.geo_steps_per_circle).exterior
- geo_obj.solid_geometry.append(poly)
- if use_thread:
- def geo_thread(app_obj):
- app_obj.new_object("geometry", outname + '_slot', geo_init)
- app_obj.progress.emit(100)
- # Create a promise with the new name
- self.app.collection.promise(outname)
- # Send to worker
- self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
- else:
- self.app.new_object("geometry", outname + '_slot', geo_init)
- return True, ""
- def on_generate_milling_button_click(self, *args):
- self.app.report_usage("excellon_on_create_milling_drills button")
- self.read_form()
- self.generate_milling_drills(use_thread=False)
- def on_generate_milling_slots_button_click(self, *args):
- self.app.report_usage("excellon_on_create_milling_slots_button")
- self.read_form()
- self.generate_milling_slots(use_thread=False)
- def on_create_cncjob_button_click(self, *args):
- self.app.report_usage("excellon_on_create_cncjob_button")
- self.read_form()
- # Get the tools from the list
- tools = self.get_selected_tools_list()
- if len(tools) == 0:
- self.app.inform.emit("[ERROR_NOTCL]Please select one or more tools from the list and try again.")
- return
- xmin = self.options['xmin']
- ymin = self.options['ymin']
- xmax = self.options['xmax']
- ymax = self.options['ymax']
- job_name = self.options["name"] + "_cnc"
- pp_excellon_name = self.options["ppname_e"]
- # Object initialization function for app.new_object()
- def job_init(job_obj, app_obj):
- assert isinstance(job_obj, FlatCAMCNCjob), \
- "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
- # get the tool_table items in a list of row items
- tool_table_items = self.get_selected_tools_table_items()
- # insert an information only element in the front
- tool_table_items.insert(0, ["Tool_nr", "Diameter", "Drills_Nr", "Slots_Nr"])
- ### Add properties to the object
- job_obj.options['Tools_in_use'] = tool_table_items
- job_obj.options['type'] = 'Excellon'
- app_obj.progress.emit(20)
- job_obj.z_cut = self.options["drillz"]
- job_obj.z_move = self.options["travelz"]
- job_obj.feedrate = self.options["feedrate"]
- job_obj.feedrate_rapid = self.options["feedrate_rapid"]
- job_obj.spindlespeed = self.options["spindlespeed"]
- job_obj.dwell = self.options["dwell"]
- job_obj.dwelltime = self.options["dwelltime"]
- job_obj.pp_excellon_name = pp_excellon_name
- job_obj.toolchange_xy = self.app.defaults["excellon_toolchangexy"]
- job_obj.toolchange_xy_type = "excellon"
- job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
- job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
- job_obj.options['xmin'] = xmin
- job_obj.options['ymin'] = ymin
- job_obj.options['xmax'] = xmax
- job_obj.options['ymax'] = ymax
- # There could be more than one drill size...
- # job_obj.tooldia = # TODO: duplicate variable!
- # job_obj.options["tooldia"] =
- tools_csv = ','.join(tools)
- job_obj.generate_from_excellon_by_tool(self, tools_csv,
- drillz=self.options['drillz'],
- toolchange=self.options["toolchange"],
- toolchangez=self.options["toolchangez"],
- startz=self.options["startz"],
- endz=self.options["endz"],
- excellon_optimization_type=self.options["optimization_type"])
- app_obj.progress.emit(50)
- job_obj.gcode_parse()
- app_obj.progress.emit(60)
- job_obj.create_geometry()
- app_obj.progress.emit(80)
- # To be run in separate thread
- def job_thread(app_obj):
- with self.app.proc_container.new("Generating CNC Code"):
- app_obj.new_object("cncjob", job_name, job_init)
- app_obj.progress.emit(100)
- # Create promise for the new name.
- self.app.collection.promise(job_name)
- # Send to worker
- # self.app.worker.add_task(job_thread, [self.app])
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
- def on_plot_cb_click(self, *args):
- if self.muted_ui:
- return
- self.read_form_item('plot')
- def on_solid_cb_click(self, *args):
- if self.muted_ui:
- return
- self.read_form_item('solid')
- self.plot()
- def convert_units(self, units):
- factor = Excellon.convert_units(self, units)
- self.options['drillz'] *= factor
- self.options['travelz'] *= factor
- self.options['feedrate'] *= factor
- self.options['feedrate_rapid'] *= factor
- self.options['toolchangez'] *= factor
- if self.app.defaults["excellon_toolchangexy"] == '':
- self.options['toolchangexy'] = "0.0, 0.0"
- else:
- coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
- if len(coords_xy) < 2:
- self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
- "in the format (x, y) \nbut now there is only one value, not two. ")
- return 'fail'
- coords_xy[0] *= factor
- coords_xy[1] *= factor
- self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
- if self.options['startz'] is not None:
- self.options['startz'] *= factor
- self.options['endz'] *= factor
- def plot(self):
- # Does all the required setup and returns False
- # if the 'ptint' option is set to False.
- if not FlatCAMObj.plot(self):
- return
- try:
- _ = iter(self.solid_geometry)
- except TypeError:
- self.solid_geometry = [self.solid_geometry]
- try:
- # Plot excellon (All polygons?)
- if self.options["solid"]:
- for geo in self.solid_geometry:
- self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF', visible=self.options['plot'],
- layer=2)
- else:
- for geo in self.solid_geometry:
- self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
- for ints in geo.interiors:
- self.add_shape(shape=ints, color='green', visible=self.options['plot'])
- self.shapes.redraw()
- except (ObjectDeleted, AttributeError):
- self.shapes.clear(update=True)
- # try:
- # # Plot excellon (All polygons?)
- # if self.options["solid"]:
- # for geo_type in self.solid_geometry:
- # if geo_type is not None:
- # if type(geo_type) is dict:
- # for tooldia in geo_type:
- # geo_list = geo_type[tooldia]
- # for geo in geo_list:
- # self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF',
- # visible=self.options['plot'],
- # layer=2)
- # else:
- # self.add_shape(shape=geo_type, color='#750000BF', face_color='#C40000BF',
- # visible=self.options['plot'],
- # layer=2)
- # else:
- # for geo_type in self.solid_geometry:
- # if geo_type is not None:
- # if type(geo_type) is dict:
- # for tooldia in geo_type:
- # geo_list = geo_type[tooldia]
- # for geo in geo_list:
- # self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
- # for ints in geo.interiors:
- # self.add_shape(shape=ints, color='green', visible=self.options['plot'])
- # else:
- # self.add_shape(shape=geo_type.exterior, color='red', visible=self.options['plot'])
- # for ints in geo_type.interiors:
- # self.add_shape(shape=ints, color='green', visible=self.options['plot'])
- # self.shapes.redraw()
- # except (ObjectDeleted, AttributeError):
- # self.shapes.clear(update=True)
- class FlatCAMGeometry(FlatCAMObj, Geometry):
- """
- Geometric object not associated with a specific
- format.
- """
- optionChanged = QtCore.pyqtSignal(str)
- ui_type = GeometryObjectUI
- @staticmethod
- def merge(geo_list, geo_final, multigeo=None):
- """
- Merges the geometry of objects in grb_list into
- the geometry of geo_final.
- :param geo_list: List of FlatCAMGerber Objects to join.
- :param geo_final: Destination FlatCAMGerber object.
- :return: None
- """
- if geo_final.solid_geometry is None:
- geo_final.solid_geometry = []
- if type(geo_final.solid_geometry) is not list:
- geo_final.solid_geometry = [geo_final.solid_geometry]
- for geo in geo_list:
- for option in geo.options:
- if option is not 'name':
- try:
- geo_final.options[option] = geo.options[option]
- except:
- log.warning("Failed to copy option.", option)
- # Expand lists
- if type(geo) is list:
- FlatCAMGeometry.merge(geo, geo_final)
- # If not list, just append
- else:
- # merge solid_geometry, useful for singletool geometry, for multitool each is empty
- if multigeo is None or multigeo == False:
- geo_final.multigeo = False
- try:
- geo_final.solid_geometry.append(geo.solid_geometry)
- except Exception as e:
- log.debug("FlatCAMGeometry.merge() --> %s" % str(e))
- else:
- geo_final.multigeo = True
- # if multigeo the solid_geometry is empty in the object attributes because it now lives in the
- # tools object attribute, as a key value
- geo_final.solid_geometry = []
- # find the tool_uid maximum value in the geo_final
- geo_final_uid_list = []
- for key in geo_final.tools:
- geo_final_uid_list.append(int(key))
- try:
- max_uid = max(geo_final_uid_list, key=int)
- except ValueError:
- max_uid = 0
- # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
- # to merge the obj.tools as it is likely there is none to merge.
- if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon):
- for tool_uid in geo.tools:
- max_uid += 1
- geo_final.tools[max_uid] = copy.deepcopy(geo.tools[tool_uid])
- @staticmethod
- def get_pts(o):
- """
- Returns a list of all points in the object, where
- the object can be a MultiPolygon, Polygon, Not a polygon, or a list
- of such. Search is done recursively.
- :param: geometric object
- :return: List of points
- :rtype: list
- """
- pts = []
- ## Iterable: descend into each item.
- try:
- for subo in o:
- pts += FlatCAMGeometry.get_pts(subo)
- ## Non-iterable
- except TypeError:
- if o is not None:
- if type(o) == MultiPolygon:
- for poly in o:
- pts += FlatCAMGeometry.get_pts(poly)
- ## Descend into .exerior and .interiors
- elif type(o) == Polygon:
- pts += FlatCAMGeometry.get_pts(o.exterior)
- for i in o.interiors:
- pts += FlatCAMGeometry.get_pts(i)
- elif type(o) == MultiLineString:
- for line in o:
- pts += FlatCAMGeometry.get_pts(line)
- ## Has .coords: list them.
- else:
- pts += list(o.coords)
- else:
- return
- return pts
- def __init__(self, name):
- FlatCAMObj.__init__(self, name)
- Geometry.__init__(self, geo_steps_per_circle=self.app.defaults["geometry_circle_steps"])
- self.kind = "geometry"
- self.options.update({
- "plot": True,
- "cutz": -0.002,
- "vtipdia": 0.1,
- "vtipangle": 30,
- "travelz": 0.1,
- "feedrate": 5.0,
- "feedrate_z": 5.0,
- "feedrate_rapid": 5.0,
- "spindlespeed": None,
- "dwell": True,
- "dwelltime": 1000,
- "multidepth": False,
- "depthperpass": 0.002,
- "extracut": False,
- "endz": 2.0,
- "toolchange": False,
- "toolchangez": 1.0,
- "toolchangexy": "0.0, 0.0",
- "startz": None,
- "ppname_g": 'default',
- })
- if "cnctooldia" not in self.options:
- self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"]
- self.options["startz"] = self.app.defaults["geometry_startz"]
- # this will hold the tool unique ID that is useful when having multiple tools with same diameter
- self.tooluid = 0
- '''
- self.tools = {}
- This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the
- tool_id of the tools and the value is another dict that will hold the data under the following form:
- {tooluid: {
- 'tooldia': 1,
- 'offset': 'Path',
- 'offset_value': 0.0
- 'type': 'Rough',
- 'tool_type': 'C1',
- 'data': self.default_tool_data
- 'solid_geometry': []
- }
- }
- '''
- self.tools = {}
- # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table
- # those elements are the ones used for generating GCode
- self.sel_tools = {}
- self.offset_item_options = ["Path", "In", "Out", "Custom"]
- self.type_item_options = ["Iso", "Rough", "Finish"]
- self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
- # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
- self.v_tool_type = None
- self.multigeo = False
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind', 'tools', 'multigeo']
- def build_ui(self):
- self.ui_disconnect()
- FlatCAMObj.build_ui(self)
- offset = 0
- tool_idx = 0
- n = len(self.tools)
- self.ui.geo_tools_table.setRowCount(n)
- for tooluid_key, tooluid_value in self.tools.items():
- tool_idx += 1
- row_no = tool_idx - 1
- id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
- id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- self.ui.geo_tools_table.setItem(row_no, 0, id) # Tool name/id
- # Make sure that the tool diameter when in MM is with no more than 2 decimals.
- # There are no tool bits in MM with more than 3 decimals diameter.
- # For INCH the decimals should be no more than 3. There are no tools under 10mils.
- if self.units == 'MM':
- dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(tooluid_value['tooldia']))
- else:
- dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(tooluid_value['tooldia']))
- dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
- offset_item = QtWidgets.QComboBox()
- for item in self.offset_item_options:
- offset_item.addItem(item)
- offset_item.setStyleSheet('background-color: rgb(255,255,255)')
- idx = offset_item.findText(tooluid_value['offset'])
- offset_item.setCurrentIndex(idx)
- type_item = QtWidgets.QComboBox()
- for item in self.type_item_options:
- type_item.addItem(item)
- type_item.setStyleSheet('background-color: rgb(255,255,255)')
- idx = type_item.findText(tooluid_value['type'])
- type_item.setCurrentIndex(idx)
- tool_type_item = QtWidgets.QComboBox()
- for item in self.tool_type_item_options:
- tool_type_item.addItem(item)
- tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
- idx = tool_type_item.findText(tooluid_value['tool_type'])
- tool_type_item.setCurrentIndex(idx)
- tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
- plot_item = FCCheckBox()
- plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
- if self.ui.plot_cb.isChecked():
- plot_item.setChecked(True)
- self.ui.geo_tools_table.setItem(row_no, 1, dia_item) # Diameter
- self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item)
- self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
- self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
- ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
- self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID
- self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
- try:
- self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
- except:
- log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools")
- # make the diameter column editable
- for row in range(tool_idx):
- self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
- QtCore.Qt.ItemIsEditable |
- QtCore.Qt.ItemIsEnabled)
- # sort the tool diameter column
- # self.ui.geo_tools_table.sortItems(1)
- # all the tools are selected by default
- # self.ui.geo_tools_table.selectColumn(0)
- self.ui.geo_tools_table.resizeColumnsToContents()
- self.ui.geo_tools_table.resizeRowsToContents()
- vertical_header = self.ui.geo_tools_table.verticalHeader()
- # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
- vertical_header.hide()
- self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- horizontal_header = self.ui.geo_tools_table.horizontalHeader()
- horizontal_header.setMinimumSectionSize(10)
- horizontal_header.setDefaultSectionSize(70)
- horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(0, 20)
- horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
- # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents)
- horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
- horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(4, 40)
- horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(4, 17)
- # horizontal_header.setStretchLastSection(True)
- self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self.ui.geo_tools_table.setColumnWidth(0, 20)
- self.ui.geo_tools_table.setColumnWidth(4, 40)
- self.ui.geo_tools_table.setColumnWidth(6, 17)
- # self.ui.geo_tools_table.setSortingEnabled(True)
- self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight())
- self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight())
- # update UI for all rows - useful after units conversion but only if there is at least one row
- row_cnt = self.ui.geo_tools_table.rowCount()
- if row_cnt > 0:
- for r in range(row_cnt):
- self.update_ui(r)
- # select only the first tool / row
- selected_row = 0
- try:
- self.select_tools_table_row(selected_row, clearsel=True)
- # update the Geometry UI
- self.update_ui()
- except Exception as e:
- # when the tools table is empty there will be this error but once the table is populated it will go away
- log.debug(str(e))
- # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed
- # and can create some problems
- if self.multigeo is False:
- self.ui.geo_tools_table.setColumnHidden(6, True)
- else:
- self.ui.geo_tools_table.setColumnHidden(6, False)
- self.set_tool_offset_visibility(selected_row)
- self.ui_connect()
- def set_ui(self, ui):
- FlatCAMObj.set_ui(self, ui)
- log.debug("FlatCAMGeometry.set_ui()")
- assert isinstance(self.ui, GeometryObjectUI), \
- "Expected a GeometryObjectUI, got %s" % type(self.ui)
- # populate postprocessor names in the combobox
- for name in list(self.app.postprocessors.keys()):
- self.ui.pp_geometry_name_cb.addItem(name)
- self.form_fields.update({
- "plot": self.ui.plot_cb,
- "cutz": self.ui.cutz_entry,
- "vtipdia": self.ui.tipdia_entry,
- "vtipangle": self.ui.tipangle_entry,
- "travelz": self.ui.travelz_entry,
- "feedrate": self.ui.cncfeedrate_entry,
- "feedrate_z": self.ui.cncplunge_entry,
- "feedrate_rapid": self.ui.cncfeedrate_rapid_entry,
- "spindlespeed": self.ui.cncspindlespeed_entry,
- "dwell": self.ui.dwell_cb,
- "dwelltime": self.ui.dwelltime_entry,
- "multidepth": self.ui.mpass_cb,
- "ppname_g": self.ui.pp_geometry_name_cb,
- "depthperpass": self.ui.maxdepth_entry,
- "extracut": self.ui.extracut_cb,
- "toolchange": self.ui.toolchangeg_cb,
- "toolchangez": self.ui.toolchangez_entry,
- "endz": self.ui.gendz_entry,
- })
- # Fill form fields only on object create
- self.to_form()
- self.ui.tipdialabel.hide()
- self.ui.tipdia_entry.hide()
- self.ui.tipanglelabel.hide()
- self.ui.tipangle_entry.hide()
- self.ui.cutz_entry.setDisabled(False)
- # store here the default data for Geometry Data
- self.default_data = {}
- self.default_data.update({
- "name": None,
- "plot": None,
- "cutz": None,
- "vtipdia": None,
- "vtipangle": None,
- "travelz": None,
- "feedrate": None,
- "feedrate_z": None,
- "feedrate_rapid": None,
- "dwell": None,
- "dwelltime": None,
- "multidepth": None,
- "ppname_g": None,
- "depthperpass": None,
- "extracut": None,
- "toolchange": None,
- "toolchangez": None,
- "endz": None,
- "spindlespeed": None,
- "toolchangexy": None,
- "startz": None
- })
- # fill in self.default_data values from self.options
- for def_key in self.default_data:
- for opt_key, opt_val in self.options.items():
- if def_key == opt_key:
- self.default_data[def_key] = opt_val
- self.tooluid += 1
- if not self.tools:
- self.tools.update({
- self.tooluid: {
- 'tooldia': self.options["cnctooldia"],
- 'offset': 'Path',
- 'offset_value': 0.0,
- 'type': 'Rough',
- 'tool_type': 'C1',
- 'data': self.default_data,
- 'solid_geometry': self.solid_geometry
- }
- })
- else:
- # if self.tools is not empty then it can safely be assumed that it comes from an opened project.
- # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools
- # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are
- # again float type; dict's don't like having keys changed when iterated through therefore the need for the
- # following convoluted way of changing the keys from string to float type
- temp_tools = {}
- new_key = 0.0
- for tooluid_key in self.tools:
- val = copy.deepcopy(self.tools[tooluid_key])
- new_key = copy.deepcopy(int(tooluid_key))
- temp_tools[new_key] = val
- self.tools.clear()
- self.tools = copy.deepcopy(temp_tools)
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- # used to store the state of the mpass_cb if the selected postproc for geometry is hpgl
- self.old_pp_state = self.default_data['multidepth']
- self.old_toolchangeg_state = self.default_data['toolchange']
- if not isinstance(self.ui, GeometryObjectUI):
- log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui))
- return
- self.ui.geo_tools_table.setupContextMenu()
- self.ui.geo_tools_table.addContextMenu(
- "Copy", self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
- self.ui.geo_tools_table.addContextMenu(
- "Delete", lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png"))
- self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
- self.ui.paint_tool_button.clicked.connect(self.app.paint_tool.run)
- self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
- def set_tool_offset_visibility(self, current_row):
- if current_row is None:
- return
- try:
- tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
- if tool_offset is not None:
- tool_offset_txt = tool_offset.currentText()
- if tool_offset_txt == 'Custom':
- self.ui.tool_offset_entry.show()
- self.ui.tool_offset_lbl.show()
- else:
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- except Exception as e:
- log.debug("set_tool_offset_visibility() --> " + str(e))
- return
- def on_offset_value_edited(self):
- '''
- This will save the offset_value into self.tools storage whenever the oofset value is edited
- :return:
- '''
- for current_row in self.ui.geo_tools_table.selectedItems():
- # sometime the header get selected and it has row number -1
- # we don't want to do anything with the header :)
- if current_row.row() < 0:
- continue
- tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
- self.set_tool_offset_visibility(current_row.row())
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == tool_uid:
- try:
- tooluid_value['offset_value'] = float(self.ui.tool_offset_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- tooluid_value['offset_value'] = float(
- self.ui.tool_offset_entry.get_value().replace(',', '.')
- )
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- def ui_connect(self):
- # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
- # changes in geometry UI
- for i in range(self.ui.grid3.count()):
- try:
- # works for CheckBoxes
- self.ui.grid3.itemAt(i).widget().stateChanged.connect(self.gui_form_to_storage)
- except:
- # works for ComboBoxes
- try:
- self.ui.grid3.itemAt(i).widget().currentIndexChanged.connect(self.gui_form_to_storage)
- except:
- # works for Entry
- try:
- self.ui.grid3.itemAt(i).widget().editingFinished.connect(self.gui_form_to_storage)
- except:
- pass
- for row in range(self.ui.geo_tools_table.rowCount()):
- for col in [2, 3, 4]:
- self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect(
- self.on_tooltable_cellwidget_change)
- # I use lambda's because the connected functions have parameters that could be used in certain scenarios
- self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
- self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
- self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
- self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change)
- self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
- self.ui.tool_offset_entry.editingFinished.connect(self.on_offset_value_edited)
- for row in range(self.ui.geo_tools_table.rowCount()):
- self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
- self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- def ui_disconnect(self):
- try:
- # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
- # changes in geometry UI
- for i in range(self.ui.grid3.count()):
- if isinstance(self.ui.grid3.itemAt(i).widget(), FCCheckBox):
- self.ui.grid3.itemAt(i).widget().stateChanged.disconnect()
- if isinstance(self.ui.grid3.itemAt(i).widget(), FCComboBox):
- self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect()
- if isinstance(self.ui.grid3.itemAt(i).widget(), LengthEntry) or \
- isinstance(self.ui.grid3.itemAt(i), IntEntry) or \
- isinstance(self.ui.grid3.itemAt(i), FCEntry):
- self.ui.grid3.itemAt(i).widget().editingFinished.disconnect()
- except:
- pass
- try:
- for row in range(self.ui.geo_tools_table.rowCount()):
- for col in [2, 3, 4]:
- self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect()
- except:
- pass
- # I use lambda's because the connected functions have parameters that could be used in certain scenarios
- try:
- self.ui.addtool_btn.clicked.disconnect()
- except:
- pass
- try:
- self.ui.copytool_btn.clicked.disconnect()
- except:
- pass
- try:
- self.ui.deltool_btn.clicked.disconnect()
- except:
- pass
- try:
- self.ui.geo_tools_table.currentItemChanged.disconnect()
- except:
- pass
- try:
- self.ui.geo_tools_table.itemChanged.disconnect()
- except:
- pass
- try:
- self.ui.tool_offset_entry.editingFinished.disconnect()
- except:
- pass
- for row in range(self.ui.geo_tools_table.rowCount()):
- try:
- self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect()
- except:
- pass
- try:
- self.ui.plot_cb.stateChanged.disconnect()
- except:
- pass
- def on_tool_add(self, dia=None):
- self.ui_disconnect()
- last_offset = None
- last_offset_value = None
- last_type = None
- last_tool_type = None
- last_data = None
- last_solid_geometry = []
- # if a Tool diameter entered is a char instead a number the final message of Tool adding is changed
- # because the Default value for Tool is used.
- change_message = False
- if dia is not None:
- tooldia = dia
- else:
- try:
- tooldia = float(self.ui.addtool_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- tooldia = float(self.ui.addtool_entry.get_value().replace(',', '.'))
- except ValueError:
- change_message = True
- tooldia = float(self.app.defaults["geometry_cnctooldia"])
- if tooldia is None:
- self.build_ui()
- self.app.inform.emit("[ERROR_NOTCL] Please enter the desired tool diameter in Float format.")
- return
- # construct a list of all 'tooluid' in the self.tools
- tool_uid_list = []
- for tooluid_key in self.tools:
- tool_uid_item = int(tooluid_key)
- tool_uid_list.append(tool_uid_item)
- # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
- if not tool_uid_list:
- max_uid = 0
- else:
- max_uid = max(tool_uid_list)
- self.tooluid = max_uid + 1
- if self.units == 'IN':
- tooldia = float('%.4f' % tooldia)
- else:
- tooldia = float('%.2f' % tooldia)
- # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data
- # otherwise we add a tool with data copied from last tool
- if not self.tools:
- self.tools.update({
- self.tooluid: {
- 'tooldia': tooldia,
- 'offset': 'Path',
- 'offset_value': 0.0,
- 'type': 'Rough',
- 'tool_type': 'C1',
- 'data': copy.deepcopy(self.default_data),
- 'solid_geometry': self.solid_geometry
- }
- })
- else:
- # print("LAST", self.tools[maxuid])
- last_data = self.tools[max_uid]['data']
- last_offset = self.tools[max_uid]['offset']
- last_offset_value = self.tools[max_uid]['offset_value']
- last_type = self.tools[max_uid]['type']
- last_tool_type = self.tools[max_uid]['tool_type']
- last_solid_geometry = self.tools[max_uid]['solid_geometry']
- # if previous geometry was empty (it may happen for the first tool added)
- # then copy the object.solid_geometry
- if not last_solid_geometry:
- last_solid_geometry = self.solid_geometry
- self.tools.update({
- self.tooluid: {
- 'tooldia': tooldia,
- 'offset': last_offset,
- 'offset_value': last_offset_value,
- 'type': last_type,
- 'tool_type': last_tool_type,
- 'data': copy.deepcopy(last_data),
- 'solid_geometry': copy.deepcopy(last_solid_geometry)
- }
- })
- # print("CURRENT", self.tools[-1])
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
- try:
- self.ser_attrs.remove('tools')
- except:
- pass
- self.ser_attrs.append('tools')
- if change_message is False:
- self.app.inform.emit("[success] Tool added in Tool Table.")
- else:
- change_message = False
- self.app.inform.emit("[ERROR_NOTCL]Default Tool added. Wrong value format entered.")
- self.build_ui()
- def on_tool_copy(self, all=None):
- self.ui_disconnect()
- # find the tool_uid maximum value in the self.tools
- uid_list = []
- for key in self.tools:
- uid_list.append(int(key))
- try:
- max_uid = max(uid_list, key=int)
- except ValueError:
- max_uid = 0
- if all is None:
- if self.ui.geo_tools_table.selectedItems():
- for current_row in self.ui.geo_tools_table.selectedItems():
- # sometime the header get selected and it has row number -1
- # we don't want to do anything with the header :)
- if current_row.row() < 0:
- continue
- try:
- tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
- self.set_tool_offset_visibility(current_row.row())
- max_uid += 1
- self.tools[int(max_uid)] = copy.deepcopy(self.tools[tooluid_copy])
- except AttributeError:
- self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.")
- self.build_ui()
- return
- except Exception as e:
- log.debug("on_tool_copy() --> " + str(e))
- # deselect the table
- # self.ui.geo_tools_table.clearSelection()
- else:
- self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to copy.")
- self.build_ui()
- return
- else:
- # we copy all tools in geo_tools_table
- try:
- temp_tools = copy.deepcopy(self.tools)
- max_uid += 1
- for tooluid in temp_tools:
- self.tools[int(max_uid)] = copy.deepcopy(temp_tools[tooluid])
- temp_tools.clear()
- except Exception as e:
- log.debug("on_tool_copy() --> " + str(e))
- # if there are no more tools in geo tools table then hide the tool offset
- if not self.tools:
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
- try:
- self.ser_attrs.remove('tools')
- except:
- pass
- self.ser_attrs.append('tools')
- self.build_ui()
- self.app.inform.emit("[success] Tool was copied in Tool Table.")
- def on_tool_edit(self, current_item):
- self.ui_disconnect()
- current_row = current_item.row()
- try:
- d = float(self.ui.geo_tools_table.item(current_row, 1).text())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.'))
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- tool_dia = float('%.4f' % d)
- tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
- self.tools[tooluid]['tooldia'] = tool_dia
- try:
- self.ser_attrs.remove('tools')
- self.ser_attrs.append('tools')
- except:
- pass
- self.app.inform.emit("[success] Tool was edited in Tool Table.")
- self.build_ui()
- def on_tool_delete(self, all=None):
- self.ui_disconnect()
- if all is None:
- if self.ui.geo_tools_table.selectedItems():
- for current_row in self.ui.geo_tools_table.selectedItems():
- # sometime the header get selected and it has row number -1
- # we don't want to do anything with the header :)
- if current_row.row() < 0:
- continue
- try:
- tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
- self.set_tool_offset_visibility(current_row.row())
- temp_tools = copy.deepcopy(self.tools)
- for tooluid_key in self.tools:
- if int(tooluid_key) == tooluid_del:
- temp_tools.pop(tooluid_del, None)
- self.tools = copy.deepcopy(temp_tools)
- temp_tools.clear()
- except AttributeError:
- self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to delete.")
- self.build_ui()
- return
- except Exception as e:
- log.debug("on_tool_delete() --> " + str(e))
- # deselect the table
- # self.ui.geo_tools_table.clearSelection()
- else:
- self.app.inform.emit("[WARNING_NOTCL]Failed. Select a tool to delete.")
- self.build_ui()
- return
- else:
- # we delete all tools in geo_tools_table
- self.tools.clear()
- self.app.plot_all()
- # if there are no more tools in geo tools table then hide the tool offset
- if not self.tools:
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
- try:
- self.ser_attrs.remove('tools')
- except:
- pass
- self.ser_attrs.append('tools')
- self.build_ui()
- self.app.inform.emit("[success] Tool was deleted in Tool Table.")
- obj_active = self.app.collection.get_active()
- # if the object was MultiGeo and now it has no tool at all (therefore no geometry)
- # we make it back SingleGeo
- if self.ui.geo_tools_table.rowCount() <= 0:
- obj_active.multigeo = False
- obj_active.options['xmin'] = 0
- obj_active.options['ymin'] = 0
- obj_active.options['xmax'] = 0
- obj_active.options['ymax'] = 0
- if obj_active.multigeo is True:
- try:
- xmin, ymin, xmax, ymax = obj_active.bounds()
- obj_active.options['xmin'] = xmin
- obj_active.options['ymin'] = ymin
- obj_active.options['xmax'] = xmax
- obj_active.options['ymax'] = ymax
- except:
- obj_active.options['xmin'] = 0
- obj_active.options['ymin'] = 0
- obj_active.options['xmax'] = 0
- obj_active.options['ymax'] = 0
- def on_row_selection_change(self):
- self.update_ui()
- def update_ui(self, row=None):
- self.ui_disconnect()
- if row is None:
- try:
- current_row = self.ui.geo_tools_table.currentRow()
- except:
- current_row = 0
- else:
- current_row = row
- if current_row < 0:
- current_row = 0
- self.set_tool_offset_visibility(current_row)
- # populate the form with the data from the tool associated with the row parameter
- try:
- tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
- except Exception as e:
- log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
- return
- # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
- # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
- try:
- tool_type_txt = self.ui.geo_tools_table.cellWidget(current_row, 4).currentText()
- self.ui_update_v_shape(tool_type_txt=tool_type_txt)
- except Exception as e:
- log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
- return
- try:
- # set the form with data from the newly selected tool
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == tooluid:
- for key, value in tooluid_value.items():
- if key == 'data':
- form_value_storage = tooluid_value[key]
- self.update_form(form_value_storage)
- if key == 'offset_value':
- # update the offset value in the entry even if the entry is hidden
- self.ui.tool_offset_entry.set_value(tooluid_value[key])
- if key == 'tool_type' and value == 'V':
- self.update_cutz()
- except Exception as e:
- log.debug("FlatCAMObj ---> update_ui() " + str(e))
- self.ui_connect()
- def ui_update_v_shape(self, tool_type_txt):
- if tool_type_txt == 'V':
- self.ui.tipdialabel.show()
- self.ui.tipdia_entry.show()
- self.ui.tipanglelabel.show()
- self.ui.tipangle_entry.show()
- self.ui.cutz_entry.setDisabled(True)
- self.update_cutz()
- else:
- self.ui.tipdialabel.hide()
- self.ui.tipdia_entry.hide()
- self.ui.tipanglelabel.hide()
- self.ui.tipangle_entry.hide()
- self.ui.cutz_entry.setDisabled(False)
- def update_cutz(self):
- vdia = float(self.ui.tipdia_entry.get_value())
- half_vangle = float(self.ui.tipangle_entry.get_value()) / 2
- row = self.ui.geo_tools_table.currentRow()
- tool_uid = int(self.ui.geo_tools_table.item(row, 5).text())
- tooldia = float(self.ui.geo_tools_table.item(row, 1).text())
- new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle)))
- new_cutz = float('%.4f' % -new_cutz)
- self.ui.cutz_entry.set_value(new_cutz)
- # store the new CutZ value into storage (self.tools)
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == tool_uid:
- tooluid_value['data']['cutz'] = new_cutz
- def on_tooltable_cellwidget_change(self):
- cw = self.sender()
- cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
- cw_row = cw_index.row()
- cw_col = cw_index.column()
- current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text())
- # store the text of the cellWidget that changed it's index in the self.tools
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == current_uid:
- cb_txt = cw.currentText()
- if cw_col == 2:
- tooluid_value['offset'] = cb_txt
- if cb_txt == 'Custom':
- self.ui.tool_offset_entry.show()
- self.ui.tool_offset_lbl.show()
- else:
- self.ui.tool_offset_entry.hide()
- self.ui.tool_offset_lbl.hide()
- # reset the offset_value in storage self.tools
- tooluid_value['offset_value'] = 0.0
- elif cw_col == 3:
- # force toolpath type as 'Iso' if the tool type is V-Shape
- if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V':
- tooluid_value['type'] = 'Iso'
- idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText('Iso')
- self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
- else:
- tooluid_value['type'] = cb_txt
- elif cw_col == 4:
- tooluid_value['tool_type'] = cb_txt
- # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso
- if cb_txt == 'V':
- idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText('Iso')
- self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
- self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
- def update_form(self, dict_storage):
- for form_key in self.form_fields:
- for storage_key in dict_storage:
- if form_key == storage_key:
- try:
- self.form_fields[form_key].set_value(dict_storage[form_key])
- except Exception as e:
- log.debug(str(e))
- # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled
- # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here
- self.ui.ois_dwell_geo.on_cb_change()
- self.ui.ois_mpass_geo.on_cb_change()
- self.ui.ois_tcz_geo.on_cb_change()
- def gui_form_to_storage(self):
- self.ui_disconnect()
- widget_changed = self.sender()
- try:
- widget_idx = self.ui.grid3.indexOf(widget_changed)
- except:
- return
- # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
- if widget_idx == 1 or widget_idx == 3:
- self.update_cutz()
- # the original connect() function of the OptionalInpuSelection is no longer working because of the
- # ui_diconnect() so I use this 'hack'
- if isinstance(widget_changed, FCCheckBox):
- if widget_changed.text() == 'Multi-Depth:':
- self.ui.ois_mpass_geo.on_cb_change()
- if widget_changed.text() == 'Tool change':
- self.ui.ois_tcz_geo.on_cb_change()
- if widget_changed.text() == 'Dwell:':
- self.ui.ois_dwell_geo.on_cb_change()
- row = self.ui.geo_tools_table.currentRow()
- if row < 0:
- row = 0
- # store all the data associated with the row parameter to the self.tools storage
- tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text())
- offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText()
- type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText()
- tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText()
- tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
- try:
- offset_value_item = float(self.ui.tool_offset_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- offset_value_item = float(self.ui.tool_offset_entry.get_value().replace(',', '.')
- )
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- # this new dict will hold the actual useful data, another dict that is the value of key 'data'
- temp_tools = {}
- temp_dia = {}
- temp_data = {}
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == tooluid_item:
- for key, value in tooluid_value.items():
- if key == 'tooldia':
- temp_dia[key] = tooldia_item
- # update the 'offset', 'type' and 'tool_type' sections
- if key == 'offset':
- temp_dia[key] = offset_item
- if key == 'type':
- temp_dia[key] = type_item
- if key == 'tool_type':
- temp_dia[key] = tool_type_item
- if key == 'offset_value':
- temp_dia[key] = offset_value_item
- if key == 'data':
- # update the 'data' section
- for data_key in tooluid_value[key].keys():
- for form_key, form_value in self.form_fields.items():
- if form_key == data_key:
- temp_data[data_key] = form_value.get_value()
- # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
- # updated from self.app.defaults
- if data_key not in self.form_fields:
- temp_data[data_key] = value[data_key]
- temp_dia[key] = copy.deepcopy(temp_data)
- temp_data.clear()
- if key == 'solid_geometry':
- temp_dia[key] = copy.deepcopy(self.tools[tooluid_key]['solid_geometry'])
- temp_tools[tooluid_key] = copy.deepcopy(temp_dia)
- else:
- temp_tools[tooluid_key] = copy.deepcopy(tooluid_value)
- self.tools.clear()
- self.tools = copy.deepcopy(temp_tools)
- temp_tools.clear()
- self.ui_connect()
- def select_tools_table_row(self, row, clearsel=None):
- if clearsel:
- self.ui.geo_tools_table.clearSelection()
- if self.ui.geo_tools_table.rowCount() > 0:
- # self.ui.geo_tools_table.item(row, 0).setSelected(True)
- self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
- def export_dxf(self):
- units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
- dwg = None
- try:
- dwg = ezdxf.new('R2010')
- msp = dwg.modelspace()
- def g2dxf(dxf_space, geo):
- if isinstance(geo, MultiPolygon):
- for poly in geo:
- ext_points = list(poly.exterior.coords)
- dxf_space.add_lwpolyline(ext_points)
- for interior in poly.interiors:
- dxf_space.add_lwpolyline(list(interior.coords))
- if isinstance(geo, Polygon):
- ext_points = list(geo.exterior.coords)
- dxf_space.add_lwpolyline(ext_points)
- for interior in geo.interiors:
- dxf_space.add_lwpolyline(list(interior.coords))
- if isinstance(geo, MultiLineString):
- for line in geo:
- dxf_space.add_lwpolyline(list(line.coords))
- if isinstance(geo, LineString) or isinstance(geo, LinearRing):
- dxf_space.add_lwpolyline(list(geo.coords))
- multigeo_solid_geometry = []
- if self.multigeo:
- for tool in self.tools:
- multigeo_solid_geometry += self.tools[tool]['solid_geometry']
- else:
- multigeo_solid_geometry = self.solid_geometry
- for geo in multigeo_solid_geometry:
- if type(geo) == list:
- for g in geo:
- g2dxf(msp, g)
- else:
- g2dxf(msp, geo)
- # points = FlatCAMGeometry.get_pts(geo)
- # msp.add_lwpolyline(points)
- except Exception as e:
- log.debug(str(e))
- return dwg
- def get_selected_tools_table_items(self):
- """
- Returns a list of lists, each list in the list is made out of row elements
- :return: List of table_tools items.
- :rtype: list
- """
- table_tools_items = []
- for x in self.ui.geo_tools_table.selectedItems():
- table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text()
- for column in range(0, self.ui.geo_tools_table.columnCount())])
- for item in table_tools_items:
- item[0] = str(item[0])
- return table_tools_items
- def on_pp_changed(self):
- current_pp = self.ui.pp_geometry_name_cb.get_value()
- if current_pp == 'hpgl':
- self.old_pp_state = self.ui.mpass_cb.get_value()
- self.old_toolchangeg_state = self.ui.toolchangeg_cb.get_value()
- self.ui.mpass_cb.set_value(False)
- self.ui.mpass_cb.setDisabled(True)
- self.ui.toolchangeg_cb.set_value(True)
- self.ui.toolchangeg_cb.setDisabled(True)
- else:
- self.ui.mpass_cb.set_value(self.old_pp_state)
- self.ui.mpass_cb.setDisabled(False)
- self.ui.toolchangeg_cb.set_value(self.old_toolchangeg_state)
- self.ui.toolchangeg_cb.setDisabled(False)
- def on_generatecnc_button_click(self, *args):
- self.app.report_usage("geometry_on_generatecnc_button")
- self.read_form()
- # test to see if we have tools available in the tool table
- if self.ui.geo_tools_table.selectedItems():
- for x in self.ui.geo_tools_table.selectedItems():
- try:
- tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.'))
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong Tool Dia value format entered, "
- "use a number.")
- return
- tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
- for tooluid_key, tooluid_value in self.tools.items():
- if int(tooluid_key) == tooluid:
- self.sel_tools.update({
- tooluid: copy.deepcopy(tooluid_value)
- })
- self.mtool_gen_cncjob()
- self.ui.geo_tools_table.clearSelection()
- else:
- self.app.inform.emit("[ERROR_NOTCL] Failed. No tool selected in the tool table ...")
- def mtool_gen_cncjob(self, segx=None, segy=None, use_thread=True):
- """
- Creates a multi-tool CNCJob out of this Geometry object.
- The actual work is done by the target FlatCAMCNCjob object's
- `generate_from_geometry_2()` method.
- :param z_cut: Cut depth (negative)
- :param z_move: Hight of the tool when travelling (not cutting)
- :param feedrate: Feed rate while cutting on X - Y plane
- :param feedrate_z: Feed rate while cutting on Z plane
- :param feedrate_rapid: Feed rate while moving with rapids
- :param tooldia: Tool diameter
- :param outname: Name of the new object
- :param spindlespeed: Spindle speed (RPM)
- :param ppname_g Name of the postprocessor
- :return: None
- """
- offset_str = ''
- multitool_gcode = ''
- # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
- outname = "%s_%s" % (self.options["name"], 'cnc')
- segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
- segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
- xmin = self.options['xmin']
- ymin = self.options['ymin']
- xmax = self.options['xmax']
- ymax = self.options['ymax']
- # Object initialization function for app.new_object()
- # RUNNING ON SEPARATE THREAD!
- def job_init_single_geometry(job_obj, app_obj):
- assert isinstance(job_obj, FlatCAMCNCjob), \
- "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
- # count the tools
- tool_cnt = 0
- dia_cnc_dict = {}
- # this turn on the FlatCAMCNCJob plot for multiple tools
- job_obj.multitool = True
- job_obj.multigeo = False
- job_obj.cnc_tools.clear()
- # job_obj.create_geometry()
- job_obj.options['Tools_in_use'] = self.get_selected_tools_table_items()
- job_obj.segx = segx
- job_obj.segy = segy
- for tooluid_key in self.sel_tools:
- tool_cnt += 1
- app_obj.progress.emit(20)
- for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
- if diadict_key == 'tooldia':
- tooldia_val = float('%.4f' % float(diadict_value))
- dia_cnc_dict.update({
- diadict_key: tooldia_val
- })
- if diadict_key == 'offset':
- o_val = diadict_value.lower()
- dia_cnc_dict.update({
- diadict_key: o_val
- })
- if diadict_key == 'type':
- t_val = diadict_value
- dia_cnc_dict.update({
- diadict_key: t_val
- })
- if diadict_key == 'tool_type':
- tt_val = diadict_value
- dia_cnc_dict.update({
- diadict_key: tt_val
- })
- if diadict_key == 'data':
- for data_key, data_value in diadict_value.items():
- if data_key == "multidepth":
- multidepth = data_value
- if data_key == "depthperpass":
- depthpercut = data_value
- if data_key == "extracut":
- extracut = data_value
- if data_key == "startz":
- startz = data_value
- if data_key == "endz":
- endz = data_value
- if data_key == "toolchangez":
- toolchangez =data_value
- if data_key == "toolchangexy":
- toolchangexy = data_value
- if data_key == "toolchange":
- toolchange = data_value
- if data_key == "cutz":
- z_cut = data_value
- if data_key == "travelz":
- z_move = data_value
- if data_key == "feedrate":
- feedrate = data_value
- if data_key == "feedrate_z":
- feedrate_z = data_value
- if data_key == "feedrate_rapid":
- feedrate_rapid = data_value
- if data_key == "ppname_g":
- pp_geometry_name = data_value
- if data_key == "spindlespeed":
- spindlespeed = data_value
- if data_key == "dwell":
- dwell = data_value
- if data_key == "dwelltime":
- dwelltime = data_value
- datadict = copy.deepcopy(diadict_value)
- dia_cnc_dict.update({
- diadict_key: datadict
- })
- if dia_cnc_dict['offset'] == 'in':
- tool_offset = -dia_cnc_dict['tooldia'] / 2
- offset_str = 'inside'
- elif dia_cnc_dict['offset'].lower() == 'out':
- tool_offset = dia_cnc_dict['tooldia'] / 2
- offset_str = 'outside'
- elif dia_cnc_dict['offset'].lower() == 'path':
- offset_str = 'onpath'
- tool_offset = 0.0
- else:
- offset_str = 'custom'
- try:
- offset_value = float(self.ui.tool_offset_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.')
- )
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- if offset_value:
- tool_offset = float(offset_value)
- else:
- self.app.inform.emit(
- "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n"
- "Add a Tool Offset or change the Offset Type."
- )
- return
- dia_cnc_dict.update({
- 'offset_value': tool_offset
- })
- job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
- job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
- # Propagate options
- job_obj.options["tooldia"] = tooldia_val
- job_obj.options['type'] = 'Geometry'
- job_obj.options['tool_dia'] = tooldia_val
- job_obj.options['xmin'] = xmin
- job_obj.options['ymin'] = ymin
- job_obj.options['xmax'] = xmax
- job_obj.options['ymax'] = ymax
- app_obj.progress.emit(40)
- res = job_obj.generate_from_geometry_2(
- self, tooldia=tooldia_val, offset=tool_offset, tolerance=0.0005,
- z_cut=z_cut, z_move=z_move,
- feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
- spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
- multidepth=multidepth, depthpercut=depthpercut,
- extracut=extracut, startz=startz, endz=endz,
- toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
- pp_geometry_name=pp_geometry_name,
- tool_no=tool_cnt)
- if res == 'fail':
- log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed")
- return 'fail'
- else:
- dia_cnc_dict['gcode'] = res
- app_obj.progress.emit(50)
- # tell gcode_parse from which point to start drawing the lines depending on what kind of
- # object is the source of gcode
- job_obj.toolchange_xy_type = "geometry"
- dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
- # TODO this serve for bounding box creation only; should be optimized
- dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
- app_obj.progress.emit(80)
- job_obj.cnc_tools.update({
- tooluid_key: copy.deepcopy(dia_cnc_dict)
- })
- dia_cnc_dict.clear()
- # Object initialization function for app.new_object()
- # RUNNING ON SEPARATE THREAD!
- def job_init_multi_geometry(job_obj, app_obj):
- assert isinstance(job_obj, FlatCAMCNCjob), \
- "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
- # count the tools
- tool_cnt = 0
- dia_cnc_dict = {}
- current_uid = int(1)
- # this turn on the FlatCAMCNCJob plot for multiple tools
- job_obj.multitool = True
- job_obj.multigeo = True
- job_obj.cnc_tools.clear()
- for tooluid_key in self.sel_tools:
- tool_cnt += 1
- app_obj.progress.emit(20)
- # find the tool_dia associated with the tooluid_key
- sel_tool_dia = self.sel_tools[tooluid_key]['tooldia']
- # search in the self.tools for the sel_tool_dia and when found see what tooluid has
- # on the found tooluid in self.tools we also have the solid_geometry that interest us
- for k, v in self.tools.items():
- if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(sel_tool_dia)):
- current_uid = int(k)
- break
- for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
- if diadict_key == 'tooldia':
- tooldia_val = float('%.4f' % float(diadict_value))
- dia_cnc_dict.update({
- diadict_key: tooldia_val
- })
- if diadict_key == 'offset':
- o_val = diadict_value.lower()
- dia_cnc_dict.update({
- diadict_key: o_val
- })
- if diadict_key == 'type':
- t_val = diadict_value
- dia_cnc_dict.update({
- diadict_key: t_val
- })
- if diadict_key == 'tool_type':
- tt_val = diadict_value
- dia_cnc_dict.update({
- diadict_key: tt_val
- })
- if diadict_key == 'data':
- for data_key, data_value in diadict_value.items():
- if data_key == "multidepth":
- multidepth = data_value
- if data_key == "depthperpass":
- depthpercut = data_value
- if data_key == "extracut":
- extracut = data_value
- if data_key == "startz":
- startz = data_value
- if data_key == "endz":
- endz = data_value
- if data_key == "toolchangez":
- toolchangez =data_value
- if data_key == "toolchangexy":
- toolchangexy = data_value
- if data_key == "toolchange":
- toolchange = data_value
- if data_key == "cutz":
- z_cut = data_value
- if data_key == "travelz":
- z_move = data_value
- if data_key == "feedrate":
- feedrate = data_value
- if data_key == "feedrate_z":
- feedrate_z = data_value
- if data_key == "feedrate_rapid":
- feedrate_rapid = data_value
- if data_key == "ppname_g":
- pp_geometry_name = data_value
- if data_key == "spindlespeed":
- spindlespeed = data_value
- if data_key == "dwell":
- dwell = data_value
- if data_key == "dwelltime":
- dwelltime = data_value
- datadict = copy.deepcopy(diadict_value)
- dia_cnc_dict.update({
- diadict_key: datadict
- })
- if dia_cnc_dict['offset'] == 'in':
- tool_offset = -dia_cnc_dict['tooldia'] / 2
- offset_str = 'inside'
- elif dia_cnc_dict['offset'].lower() == 'out':
- tool_offset = dia_cnc_dict['tooldia'] / 2
- offset_str = 'outside'
- elif dia_cnc_dict['offset'].lower() == 'path':
- offset_str = 'onpath'
- tool_offset = 0.0
- else:
- offset_str = 'custom'
- try:
- offset_value = float(self.ui.tool_offset_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.')
- )
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- if offset_value:
- tool_offset = float(offset_value)
- else:
- self.app.inform.emit(
- "[WARNING] Tool Offset is selected in Tool Table but no value is provided.\n"
- "Add a Tool Offset or change the Offset Type."
- )
- return
- dia_cnc_dict.update({
- 'offset_value': tool_offset
- })
- job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
- job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
- # Propagate options
- job_obj.options["tooldia"] = tooldia_val
- job_obj.options['type'] = 'Geometry'
- job_obj.options['tool_dia'] = tooldia_val
- app_obj.progress.emit(40)
- tool_solid_geometry = self.tools[current_uid]['solid_geometry']
- res = job_obj.generate_from_multitool_geometry(
- tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
- tolerance=0.0005, z_cut=z_cut, z_move=z_move,
- feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
- spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
- multidepth=multidepth, depthpercut=depthpercut,
- extracut=extracut, startz=startz, endz=endz,
- toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
- pp_geometry_name=pp_geometry_name,
- tool_no=tool_cnt)
- if res == 'fail':
- log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed")
- return 'fail'
- else:
- dia_cnc_dict['gcode'] = res
- dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
- # TODO this serve for bounding box creation only; should be optimized
- dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
- # tell gcode_parse from which point to start drawing the lines depending on what kind of
- # object is the source of gcode
- job_obj.toolchange_xy_type = "geometry"
- app_obj.progress.emit(80)
- job_obj.cnc_tools.update({
- tooluid_key: copy.deepcopy(dia_cnc_dict)
- })
- dia_cnc_dict.clear()
- if use_thread:
- # To be run in separate thread
- # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no
- # separate solid_geometry in the self.tools dictionary
- def job_thread(app_obj):
- if self.solid_geometry:
- with self.app.proc_container.new("Generating CNC Code"):
- if app_obj.new_object("cncjob", outname, job_init_single_geometry) != 'fail':
- app_obj.inform.emit("[success]CNCjob created: %s" % outname)
- app_obj.progress.emit(100)
- else:
- with self.app.proc_container.new("Generating CNC Code"):
- if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail':
- app_obj.inform.emit("[success]CNCjob created: %s" % outname)
- app_obj.progress.emit(100)
- # Create a promise with the name
- self.app.collection.promise(outname)
- # Send to worker
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
- else:
- if self.solid_geometry:
- self.app.new_object("cncjob", outname, job_init_single_geometry)
- else:
- self.app.new_object("cncjob", outname, job_init_multi_geometry)
- def generatecncjob(self, outname=None,
- tooldia=None, offset=None,
- z_cut=None, z_move=None,
- feedrate=None, feedrate_z=None, feedrate_rapid=None,
- spindlespeed=None, dwell=None, dwelltime=None,
- multidepth=None, depthperpass=None,
- toolchange=None, toolchangez=None, toolchangexy=None,
- extracut=None, startz=None, endz=None,
- ppname_g=None,
- segx=None,
- segy=None,
- use_thread=True):
- """
- Only used for TCL Command.
- Creates a CNCJob out of this Geometry object. The actual
- work is done by the target FlatCAMCNCjob object's
- `generate_from_geometry_2()` method.
- :param z_cut: Cut depth (negative)
- :param z_move: Hight of the tool when travelling (not cutting)
- :param feedrate: Feed rate while cutting on X - Y plane
- :param feedrate_z: Feed rate while cutting on Z plane
- :param feedrate_rapid: Feed rate while moving with rapids
- :param tooldia: Tool diameter
- :param outname: Name of the new object
- :param spindlespeed: Spindle speed (RPM)
- :param ppname_g Name of the postprocessor
- :return: None
- """
- tooldia = tooldia if tooldia else self.options["cnctooldia"]
- outname = outname if outname is not None else self.options["name"]
- z_cut = z_cut if z_cut is not None else self.options["cutz"]
- z_move = z_move if z_move is not None else self.options["travelz"]
- feedrate = feedrate if feedrate is not None else self.options["feedrate"]
- feedrate_z = feedrate_z if feedrate_z is not None else self.options["feedrate_z"]
- feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else self.options["feedrate_rapid"]
- multidepth = multidepth if multidepth is not None else self.options["multidepth"]
- depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"]
- segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
- segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
- extracut = extracut if extracut is not None else self.options["extracut"]
- startz = startz if startz is not None else self.options["startz"]
- endz = endz if endz is not None else self.options["endz"]
- toolchangez = toolchangez if toolchangez else self.options["toolchangez"]
- toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
- toolchange = toolchange if toolchange else self.options["toolchange"]
- offset = offset if offset else 0.0
- # int or None.
- spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed']
- dwell = dwell if dwell else self.options["dwell"]
- dwelltime = dwelltime if dwelltime else self.options["dwelltime"]
- ppname_g = ppname_g if ppname_g else self.options["ppname_g"]
- # Object initialization function for app.new_object()
- # RUNNING ON SEPARATE THREAD!
- def job_init(job_obj, app_obj):
- assert isinstance(job_obj, FlatCAMCNCjob), "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
- # Propagate options
- job_obj.options["tooldia"] = tooldia
- app_obj.progress.emit(20)
- job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
- job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
- app_obj.progress.emit(40)
- job_obj.options['type'] = 'Geometry'
- job_obj.options['tool_dia'] = tooldia
- job_obj.segx = segx
- job_obj.segy = segy
- # TODO: The tolerance should not be hard coded. Just for testing.
- job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=0.0005,
- z_cut=z_cut, z_move=z_move,
- feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
- spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
- multidepth=multidepth, depthpercut=depthperpass,
- toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
- extracut=extracut, startz=startz, endz=endz,
- pp_geometry_name=ppname_g
- )
- app_obj.progress.emit(50)
- # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
- # source of gcode
- job_obj.toolchange_xy_type = "geometry"
- job_obj.gcode_parse()
- app_obj.progress.emit(80)
- if use_thread:
- # To be run in separate thread
- def job_thread(app_obj):
- with self.app.proc_container.new("Generating CNC Code"):
- app_obj.new_object("cncjob", outname, job_init)
- app_obj.inform.emit("[success]CNCjob created: %s" % outname)
- app_obj.progress.emit(100)
- # Create a promise with the name
- self.app.collection.promise(outname)
- # Send to worker
- self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
- else:
- self.app.new_object("cncjob", outname, job_init)
- # def on_plot_cb_click(self, *args): # TODO: args not needed
- # if self.muted_ui:
- # return
- # self.read_form_item('plot')
- def scale(self, xfactor, yfactor=None, point=None):
- """
- Scales all geometry by a given factor.
- :param xfactor: Factor by which to scale the object's geometry/
- :type xfactor: float
- :param yfactor: Factor by which to scale the object's geometry/
- :type yfactor: float
- :return: None
- :rtype: None
- """
- try:
- xfactor = float(xfactor)
- except:
- self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.")
- return
- if yfactor is None:
- yfactor = xfactor
- else:
- try:
- yfactor = float(yfactor)
- except:
- self.app.inform.emit("[ERROR_NOTCL] Scale factor has to be a number: integer or float.")
- return
- if point is None:
- px = 0
- py = 0
- else:
- px, py = point
- # if type(self.solid_geometry) == list:
- # geo_list = self.flatten(self.solid_geometry)
- # self.solid_geometry = []
- # # for g in geo_list:
- # # self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py)))
- # self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py))
- # for g in geo_list]
- # else:
- # self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor,
- # origin=(px, py))
- # self.app.inform.emit("[success]Geometry Scale done.")
- def scale_recursion(geom):
- if type(geom) == list:
- geoms=list()
- for local_geom in geom:
- geoms.append(scale_recursion(local_geom))
- return geoms
- else:
- return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
- if self.multigeo is True:
- for tool in self.tools:
- self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
- else:
- self.solid_geometry=scale_recursion(self.solid_geometry)
- self.app.inform.emit("[success]Geometry Scale done.")
- def offset(self, vect):
- """
- Offsets all geometry by a given vector/
- :param vect: (x, y) vector by which to offset the object's geometry.
- :type vect: tuple
- :return: None
- :rtype: None
- """
- try:
- dx, dy = vect
- except TypeError:
- self.app.inform.emit("[ERROR_NOTCL]An (x,y) pair of values are needed. "
- "Probable you entered only one value in the Offset field.")
- return
- def translate_recursion(geom):
- if type(geom) == list:
- geoms=list()
- for local_geom in geom:
- geoms.append(translate_recursion(local_geom))
- return geoms
- else:
- return affinity.translate(geom, xoff=dx, yoff=dy)
- if self.multigeo is True:
- for tool in self.tools:
- self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
- else:
- self.solid_geometry=translate_recursion(self.solid_geometry)
- self.app.inform.emit("[success]Geometry Offset done.")
- def convert_units(self, units):
- self.ui_disconnect()
- factor = Geometry.convert_units(self, units)
- self.options['cutz'] *= factor
- self.options['depthperpass'] *= factor
- self.options['travelz'] *= factor
- self.options['feedrate'] *= factor
- self.options['feedrate_z'] *= factor
- self.options['feedrate_rapid'] *= factor
- self.options['endz'] *= factor
- # self.options['cnctooldia'] *= factor
- # self.options['painttooldia'] *= factor
- # self.options['paintmargin'] *= factor
- # self.options['paintoverlap'] *= factor
- self.options["toolchangez"] *= factor
- if self.app.defaults["geometry_toolchangexy"] == '':
- self.options['toolchangexy'] = "0.0, 0.0"
- else:
- coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")]
- if len(coords_xy) < 2:
- self.app.inform.emit("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
- "in the format (x, y) \nbut now there is only one value, not two. ")
- return 'fail'
- coords_xy[0] *= factor
- coords_xy[1] *= factor
- self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
- if self.options['startz'] is not None:
- self.options['startz'] *= factor
- param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
- 'endz', 'toolchangez']
- temp_tools_dict = {}
- tool_dia_copy = {}
- data_copy = {}
- for tooluid_key, tooluid_value in self.tools.items():
- for dia_key, dia_value in tooluid_value.items():
- if dia_key == 'tooldia':
- dia_value *= factor
- dia_value = float('%.4f' % dia_value)
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'offset':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'offset_value':
- dia_value *= factor
- tool_dia_copy[dia_key] = dia_value
- # convert the value in the Custom Tool Offset entry in UI
- try:
- custom_offset = float(self.ui.tool_offset_entry.get_value())
- except ValueError:
- # try to convert comma to decimal point. if it's still not working error message and return
- try:
- custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.')
- )
- except ValueError:
- self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
- "use a number.")
- return
- if custom_offset:
- custom_offset *= factor
- self.ui.tool_offset_entry.set_value(custom_offset)
- if dia_key == 'type':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'tool_type':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'data':
- for data_key, data_value in dia_value.items():
- # convert the form fields that are convertible
- for param in param_list:
- if data_key == param and data_value is not None:
- data_copy[data_key] = data_value * factor
- # copy the other dict entries that are not convertible
- if data_key not in param_list:
- data_copy[data_key] = data_value
- tool_dia_copy[dia_key] = copy.deepcopy(data_copy)
- data_copy.clear()
- temp_tools_dict.update({
- tooluid_key: copy.deepcopy(tool_dia_copy)
- })
- tool_dia_copy.clear()
- self.tools.clear()
- self.tools = copy.deepcopy(temp_tools_dict)
- # if there is a value in the new tool field then convert that one too
- tooldia = self.ui.addtool_entry.get_value()
- if tooldia:
- tooldia *= factor
- # limit the decimals to 2 for METRIC and 3 for INCH
- if units.lower() == 'in':
- tooldia = float('%.4f' % tooldia)
- else:
- tooldia = float('%.2f' % tooldia)
- self.ui.addtool_entry.set_value(tooldia)
- return factor
- def plot_element(self, element, color='red', visible=None):
- visible = visible if visible else self.options['plot']
- try:
- for sub_el in element:
- self.plot_element(sub_el)
- except TypeError: # Element is not iterable...
- self.add_shape(shape=element, color=color, visible=visible, layer=0)
- def plot(self, visible=None):
- """
- Adds the object into collection.
- :return: None
- """
- # Does all the required setup and returns False
- # if the 'ptint' option is set to False.
- if not FlatCAMObj.plot(self):
- return
- try:
- # plot solid geometries found as members of self.tools attribute dict
- # for MultiGeo
- if self.multigeo == True: # geo multi tool usage
- for tooluid_key in self.tools:
- solid_geometry = self.tools[tooluid_key]['solid_geometry']
- self.plot_element(solid_geometry, visible=visible)
- # plot solid geometry that may be an direct attribute of the geometry object
- # for SingleGeo
- if self.solid_geometry:
- self.plot_element(self.solid_geometry, visible=visible)
- # self.plot_element(self.solid_geometry, visible=self.options['plot'])
- self.shapes.redraw()
- except (ObjectDeleted, AttributeError):
- self.shapes.clear(update=True)
- def on_plot_cb_click(self, *args):
- if self.muted_ui:
- return
- self.plot()
- self.read_form_item('plot')
- self.ui_disconnect()
- cb_flag = self.ui.plot_cb.isChecked()
- for row in range(self.ui.geo_tools_table.rowCount()):
- table_cb = self.ui.geo_tools_table.cellWidget(row, 6)
- if cb_flag:
- table_cb.setChecked(True)
- else:
- table_cb.setChecked(False)
- self.ui_connect()
- def on_plot_cb_click_table(self):
- # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
- self.ui_disconnect()
- cw = self.sender()
- cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
- cw_row = cw_index.row()
- check_row = 0
- self.shapes.clear(update=True)
- for tooluid_key in self.tools:
- solid_geometry = self.tools[tooluid_key]['solid_geometry']
- # find the geo_tool_table row associated with the tooluid_key
- for row in range(self.ui.geo_tools_table.rowCount()):
- tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
- if tooluid_item == int(tooluid_key):
- check_row = row
- break
- if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
- self.plot_element(element=solid_geometry, visible=True)
- self.shapes.redraw()
- # make sure that the general plot is disabled if one of the row plot's are disabled and
- # if all the row plot's are enabled also enable the general plot checkbox
- cb_cnt = 0
- total_row = self.ui.geo_tools_table.rowCount()
- for row in range(total_row):
- if self.ui.geo_tools_table.cellWidget(row, 6).isChecked():
- cb_cnt += 1
- else:
- cb_cnt -= 1
- if cb_cnt < total_row:
- self.ui.plot_cb.setChecked(False)
- else:
- self.ui.plot_cb.setChecked(True)
- self.ui_connect()
- class FlatCAMCNCjob(FlatCAMObj, CNCjob):
- """
- Represents G-Code.
- """
- optionChanged = QtCore.pyqtSignal(str)
- ui_type = CNCObjectUI
- def __init__(self, name, units="in", kind="generic", z_move=0.1,
- feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
- spindlespeed=None):
- FlatCAMApp.App.log.debug("Creating CNCJob object...")
- CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
- feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
- spindlespeed=spindlespeed, steps_per_circle=self.app.defaults["cncjob_steps_per_circle"])
- FlatCAMObj.__init__(self, name)
- self.kind = "cncjob"
- self.options.update({
- "plot": True,
- "tooldia": 0.03937, # 0.4mm in inches
- "append": "",
- "prepend": "",
- "dwell": False,
- "dwelltime": 1,
- "type": 'Geometry'
- })
- '''
- This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the
- diameter of the tools and the value is another dict that will hold the data under the following form:
- {tooldia: {
- 'tooluid': 1,
- 'offset': 'Path',
- 'type_item': 'Rough',
- 'tool_type': 'C1',
- 'data': {} # a dict to hold the parameters
- 'gcode': "" # a string with the actual GCODE
- 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move)
- 'solid_geometry': []
- },
- ...
- }
- It is populated in the FlatCAMGeometry.mtool_gen_cncjob()
- BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
- '''
- self.cnc_tools = {}
- # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
- # (like the one in the TCL Command), False
- self.multitool = False
- # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
- gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
- self.g_x_re = re.compile(gcodex_re_string)
- gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
- self.g_y_re = re.compile(gcodey_re_string)
- gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
- self.g_z_re = re.compile(gcodez_re_string)
- gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
- self.g_f_re = re.compile(gcodef_re_string)
- gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
- self.g_t_re = re.compile(gcodet_re_string)
- gcodenr_re_string = r'([+-]?\d*\.\d+)'
- self.g_nr_re = re.compile(gcodenr_re_string)
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
- self.annotation = self.app.plotcanvas.new_text_group()
- def build_ui(self):
- self.ui_disconnect()
- FlatCAMObj.build_ui(self)
- # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
- if self.cnc_tools:
- self.ui.cnc_tools_table.show()
- self.ui.plot_options_label.show()
- else:
- self.ui.cnc_tools_table.hide()
- self.ui.plot_options_label.hide()
- offset = 0
- tool_idx = 0
- n = len(self.cnc_tools)
- self.ui.cnc_tools_table.setRowCount(n)
- for dia_key, dia_value in self.cnc_tools.items():
- tool_idx += 1
- row_no = tool_idx - 1
- id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
- # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
- self.ui.cnc_tools_table.setItem(row_no, 0, id) # Tool name/id
- # Make sure that the tool diameter when in MM is with no more than 2 decimals.
- # There are no tool bits in MM with more than 2 decimals diameter.
- # For INCH the decimals should be no more than 4. There are no tools under 10mils.
- if self.units == 'MM':
- dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(dia_value['tooldia']))
- else:
- dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(dia_value['tooldia']))
- offset_txt = list(str(dia_value['offset']))
- offset_txt[0] = offset_txt[0].upper()
- offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
- type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
- tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
- id.setFlags(QtCore.Qt.ItemIsEnabled)
- dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
- offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
- type_item.setFlags(QtCore.Qt.ItemIsEnabled)
- tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
- # hack so the checkbox stay centered in the table cell
- # used this:
- # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
- # plot_item = QtWidgets.QWidget()
- # checkbox = FCCheckBox()
- # checkbox.setCheckState(QtCore.Qt.Checked)
- # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
- # qhboxlayout.addWidget(checkbox)
- # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
- # qhboxlayout.setContentsMargins(0, 0, 0, 0)
- plot_item = FCCheckBox()
- plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
- tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
- if self.ui.plot_cb.isChecked():
- plot_item.setChecked(True)
- self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter
- self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset
- self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type
- self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type
- ### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
- self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID)
- self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
- # make the diameter column editable
- # for row in range(tool_idx):
- # self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
- # QtCore.Qt.ItemIsEnabled)
- for row in range(tool_idx):
- self.ui.cnc_tools_table.item(row, 0).setFlags(
- self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
- self.ui.cnc_tools_table.resizeColumnsToContents()
- self.ui.cnc_tools_table.resizeRowsToContents()
- vertical_header = self.ui.cnc_tools_table.verticalHeader()
- # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
- vertical_header.hide()
- self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
- horizontal_header.setMinimumSectionSize(10)
- horizontal_header.setDefaultSectionSize(70)
- horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(0, 20)
- horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
- horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
- horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(4, 40)
- horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
- horizontal_header.resizeSection(4, 17)
- # horizontal_header.setStretchLastSection(True)
- self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
- self.ui.cnc_tools_table.setColumnWidth(0, 20)
- self.ui.cnc_tools_table.setColumnWidth(4, 40)
- self.ui.cnc_tools_table.setColumnWidth(6, 17)
- # self.ui.geo_tools_table.setSortingEnabled(True)
- self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
- self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
- self.ui_connect()
- def set_ui(self, ui):
- FlatCAMObj.set_ui(self, ui)
- FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
- assert isinstance(self.ui, CNCObjectUI), \
- "Expected a CNCObjectUI, got %s" % type(self.ui)
- self.form_fields.update({
- "plot": self.ui.plot_cb,
- # "tooldia": self.ui.tooldia_entry,
- "append": self.ui.append_text,
- "prepend": self.ui.prepend_text,
- })
- # Fill form fields only on object create
- self.to_form()
- # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
- self.ui.cncplot_method_combo.set_value('all')
- self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
- self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
- self.ui.modify_gcode_button.clicked.connect(self.on_modifygcode_button_click)
- self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
- def ui_connect(self):
- for row in range(self.ui.cnc_tools_table.rowCount()):
- self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
- self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
- def ui_disconnect(self):
- for row in range(self.ui.cnc_tools_table.rowCount()):
- self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
- try:
- self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
- except:
- pass
- def on_updateplot_button_click(self, *args):
- """
- Callback for the "Updata Plot" button. Reads the form for updates
- and plots the object.
- """
- self.read_form()
- self.plot()
- def on_plot_kind_change(self):
- kind = self.ui.cncplot_method_combo.get_value()
- self.plot(kind=kind)
- def on_exportgcode_button_click(self, *args):
- self.app.report_usage("cncjob_on_exportgcode_button")
- self.read_form()
- name = self.app.collection.get_active().options['name']
- if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
- _filter_ = "RML1 Files (*.rol);;" \
- "All Files (*.*)"
- elif 'hpgl' in self.pp_geometry_name:
- _filter_ = "HPGL Files (*.plt);;" \
- "All Files (*.*)"
- else:
- _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
- "G-Code Files (*.g-code);;All Files (*.*)"
- try:
- filename = str(QtWidgets.QFileDialog.getSaveFileName(
- caption="Export Machine Code ...",
- directory=self.app.get_last_save_folder() + '/' + name,
- filter=_filter_
- )[0])
- except TypeError:
- filename = str(QtWidgets.QFileDialog.getSaveFileName(caption="Export Machine Code ...", filter=_filter_)[0])
- if filename == '':
- self.app.inform.emit("[WARNING_NOTCL]Export Machine Code cancelled ...")
- return
- preamble = str(self.ui.prepend_text.get_value())
- postamble = str(self.ui.append_text.get_value())
- self.export_gcode(filename, preamble=preamble, postamble=postamble)
- self.app.file_saved.emit("gcode", filename)
- self.app.inform.emit("[success] Machine Code file saved to: %s" % filename)
- def on_modifygcode_button_click(self, *args):
- # add the tab if it was closed
- self.app.ui.plot_tab_area.addTab(self.app.ui.cncjob_tab, "CNC Code Editor")
- # delete the absolute and relative position and messages in the infobar
- self.app.ui.position_label.setText("")
- self.app.ui.rel_position_label.setText("")
- # Switch plot_area to CNCJob tab
- self.app.ui.plot_tab_area.setCurrentWidget(self.app.ui.cncjob_tab)
- preamble = str(self.ui.prepend_text.get_value())
- postamble = str(self.ui.append_text.get_value())
- self.app.gcode_edited = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
- # print(self.app.gcode_edited)
- # first clear previous text in text editor (if any)
- self.app.ui.code_editor.clear()
- # then append the text from GCode to the text editor
- for line in self.app.gcode_edited:
- proc_line = str(line).strip('\n')
- self.app.ui.code_editor.append(proc_line)
- self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
- self.app.handleTextChanged()
- self.app.ui.show()
- def gcode_header(self):
- log.debug("FlatCAMCNCJob.gcode_header()")
- time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
- marlin = False
- hpgl = False
- try:
- for key in self.cnc_tools:
- if self.cnc_tools[key]['data']['ppname_g'] == 'marlin':
- marlin = True
- break
- if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl':
- hpgl = True
- break
- except Exception as e:
- log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
- try:
- for key in self.cnc_tools:
- if self.cnc_tools[key]['data']['ppname_e'] == 'marlin':
- marlin = True
- break
- except:
- pass
- if marlin is True:
- gcode = ';Marlin G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
- (str(self.app.version), str(self.app.version_date)) + '\n'
- gcode += ';Name: ' + str(self.options['name']) + '\n'
- gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
- # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
- # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
- gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
- gcode += ';Created on ' + time_str + '\n' + '\n'
- elif hpgl is True:
- gcode = 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \
- (str(self.app.version), str(self.app.version_date)) + '";\n'
- gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
- gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
- # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
- # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
- gcode += 'CO "Units: ' + self.units.upper() + '";\n'
- gcode += 'CO "Created on ' + time_str + '";\n'
- else:
- gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
- (str(self.app.version), str(self.app.version_date)) + '\n'
- gcode += '(Name: ' + str(self.options['name']) + ')\n'
- gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
- # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
- # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
- gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
- gcode += '(Created on ' + time_str + ')\n' + '\n'
- return gcode
- def export_gcode(self, filename=None, preamble='', postamble='', to_file=False):
- gcode = ''
- roland = False
- hpgl = False
- # detect if using Roland postprocessor
- try:
- for key in self.cnc_tools:
- if self.cnc_tools[key]['data']['ppname_g'] == 'Roland_MDX_20':
- roland = True
- break
- if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl':
- hpgl = True
- break
- except:
- try:
- for key in self.cnc_tools:
- if self.cnc_tools[key]['data']['ppname_e'] == 'Roland_MDX_20':
- roland = True
- break
- except:
- pass
- # do not add gcode_header when using the Roland postprocessor, add it for every other postprocessor
- if roland is False and hpgl is False:
- gcode = self.gcode_header()
- # detect if using multi-tool and make the Gcode summation correctly for each case
- if self.multitool is True:
- for tooluid_key in self.cnc_tools:
- for key, value in self.cnc_tools[tooluid_key].items():
- if key == 'gcode':
- gcode += value
- break
- else:
- gcode += self.gcode
- if roland is True:
- g = preamble + gcode + postamble
- elif hpgl is True:
- g = self.gcode_header() + preamble + gcode + postamble
- else:
- # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
- g_idx = gcode.rfind('G20')
- # if it did not find 'G20' then search for 'G21'
- if g_idx == -1:
- g_idx = gcode.rfind('G21')
- # if it did not find 'G20' and it did not find 'G21' then there is an error and return
- if g_idx == -1:
- self.app.inform.emit("[ERROR_NOTCL] G-code does not have a units code: either G20 or G21")
- return
- g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble
- # lines = StringIO(self.gcode)
- lines = StringIO(g)
- ## Write
- if filename is not None:
- try:
- with open(filename, 'w') as f:
- for line in lines:
- f.write(line)
- except FileNotFoundError:
- self.app.inform.emit("[WARNING_NOTCL] No such file or directory")
- return
- elif to_file is False:
- # Just for adding it to the recent files list.
- self.app.file_opened.emit("cncjob", filename)
- self.app.inform.emit("[success] Saved to: " + filename)
- else:
- return lines
- def get_gcode(self, preamble='', postamble=''):
- #we need this to be able get_gcode separatelly for shell command export_gcode
- return preamble + '\n' + self.gcode + "\n" + postamble
- def get_svg(self):
- # we need this to be able get_svg separately for shell command export_svg
- pass
- def on_plot_cb_click(self, *args):
- if self.muted_ui:
- return
- self.plot()
- self.read_form_item('plot')
- self.ui_disconnect()
- cb_flag = self.ui.plot_cb.isChecked()
- for row in range(self.ui.cnc_tools_table.rowCount()):
- table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
- if cb_flag:
- table_cb.setChecked(True)
- else:
- table_cb.setChecked(False)
- self.ui_connect()
- def on_plot_cb_click_table(self):
- # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
- self.ui_disconnect()
- cw = self.sender()
- cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
- cw_row = cw_index.row()
- self.shapes.clear(update=True)
- for tooluid_key in self.cnc_tools:
- tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
- gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
- # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
- for r in range(self.ui.cnc_tools_table.rowCount()):
- if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
- if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
- self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed)
- self.shapes.redraw()
- # make sure that the general plot is disabled if one of the row plot's are disabled and
- # if all the row plot's are enabled also enable the general plot checkbox
- cb_cnt = 0
- total_row = self.ui.cnc_tools_table.rowCount()
- for row in range(total_row):
- if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
- cb_cnt += 1
- else:
- cb_cnt -= 1
- if cb_cnt < total_row:
- self.ui.plot_cb.setChecked(False)
- else:
- self.ui.plot_cb.setChecked(True)
- self.ui_connect()
- def plot(self, visible=None, kind='all'):
- # Does all the required setup and returns False
- # if the 'ptint' option is set to False.
- if not FlatCAMObj.plot(self):
- return
- visible = visible if visible else self.options['plot']
- try:
- if self.multitool is False: # single tool usage
- self.plot2(tooldia=self.options["tooldia"], obj=self, visible=visible, kind=kind)
- else:
- # multiple tools usage
- for tooluid_key in self.cnc_tools:
- tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
- gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
- self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
- self.shapes.redraw()
- except (ObjectDeleted, AttributeError):
- self.shapes.clear(update=True)
- self.annotation.clear(update=True)
- def convert_units(self, units):
- factor = CNCjob.convert_units(self, units)
- FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
- self.options["tooldia"] *= factor
- param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
- 'endz', 'toolchangez']
- temp_tools_dict = {}
- tool_dia_copy = {}
- data_copy = {}
- for tooluid_key, tooluid_value in self.cnc_tools.items():
- for dia_key, dia_value in tooluid_value.items():
- if dia_key == 'tooldia':
- dia_value *= factor
- dia_value = float('%.4f' % dia_value)
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'offset':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'offset_value':
- dia_value *= factor
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'type':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'tool_type':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'data':
- for data_key, data_value in dia_value.items():
- # convert the form fields that are convertible
- for param in param_list:
- if data_key == param and data_value is not None:
- data_copy[data_key] = data_value * factor
- # copy the other dict entries that are not convertible
- if data_key not in param_list:
- data_copy[data_key] = data_value
- tool_dia_copy[dia_key] = copy.deepcopy(data_copy)
- data_copy.clear()
- if dia_key == 'gcode':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'gcode_parsed':
- tool_dia_copy[dia_key] = dia_value
- if dia_key == 'solid_geometry':
- tool_dia_copy[dia_key] = dia_value
- # if dia_key == 'solid_geometry':
- # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
- # if dia_key == 'gcode_parsed':
- # for g in dia_value:
- # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
- #
- # tool_dia_copy['gcode_parsed'] = copy.deepcopy(dia_value)
- # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
- temp_tools_dict.update({
- tooluid_key: copy.deepcopy(tool_dia_copy)
- })
- tool_dia_copy.clear()
- self.cnc_tools.clear()
- self.cnc_tools = copy.deepcopy(temp_tools_dict)
- # end of file
|