| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103 |
- ############################################################
- # FlatCAM: 2D Post-processing for Manufacturing #
- # http://caram.cl/software/flatcam #
- # Author: Juan Pablo Caram (c) #
- # Date: 2/5/2014 #
- # MIT Licence #
- ############################################################
- import threading
- # TODO: Bundle together. This is just for debugging.
- from gi.repository import Gtk
- from gi.repository import Gdk
- from gi.repository import GdkPixbuf
- from gi.repository import GLib
- from gi.repository import GObject
- import simplejson as json
- from matplotlib.figure import Figure
- from numpy import arange, sin, pi
- from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
- #from mpl_toolkits.axes_grid.anchored_artists import AnchoredText
- from camlib import *
- import sys
- import urllib
- import copy
- import random
- ########################################
- ## FlatCAMObj ##
- ########################################
- class FlatCAMObj:
- """
- 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):
- self.options = {"name": name}
- self.form_kinds = {"name": "entry_text"} # Kind of form element for each option
- self.radios = {} # Name value pairs for radio sets
- self.radios_inv = {} # Inverse of self.radios
- self.axes = None # Matplotlib axes
- self.kind = None # Override with proper name
- def setup_axes(self, figure):
- """
- 1) Creates axes if they don't exist. 2) Clears axes. 3) Attaches
- them to figure if not part of the figure. 4) Sets transparent
- background. 5) Sets 1:1 scale aspect ratio.
- :param figure: A Matplotlib.Figure on which to add/configure axes.
- :type figure: matplotlib.figure.Figure
- :return: None
- :rtype: None
- """
- if self.axes is None:
- print "New axes"
- self.axes = figure.add_axes([0.05, 0.05, 0.9, 0.9],
- label=self.options["name"])
- elif self.axes not in figure.axes:
- print "Clearing and attaching axes"
- self.axes.cla()
- figure.add_axes(self.axes)
- else:
- print "Clearing Axes"
- self.axes.cla()
- # Remove all decoration. The app's axes will have
- # the ticks and grid.
- self.axes.set_frame_on(False) # No frame
- self.axes.set_xticks([]) # No tick
- self.axes.set_yticks([]) # No ticks
- self.axes.patch.set_visible(False) # No background
- self.axes.set_aspect(1)
- def to_form(self):
- """
- Copies options to the UI form.
- :return: None
- """
- for option in self.options:
- self.set_form_item(option)
- def read_form(self):
- """
- Reads form into ``self.options``.
- :return: None
- :rtype: None
- """
- for option in self.options:
- self.read_form_item(option)
- def build_ui(self):
- """
- Sets up the UI/form for this object.
- :return: None
- :rtype: None
- """
- # Where the UI for this object is drawn
- box_selected = self.app.builder.get_object("box_selected")
- # Remove anything else in the box
- box_children = box_selected.get_children()
- for child in box_children:
- box_selected.remove(child)
- osw = self.app.builder.get_object("offscrwindow_" + self.kind) # offscreenwindow
- sw = self.app.builder.get_object("sw_" + self.kind) # scrollwindows
- osw.remove(sw) # TODO: Is this needed ?
- vp = self.app.builder.get_object("vp_" + self.kind) # Viewport
- vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
- # Put in the UI
- box_selected.pack_start(sw, True, True, 0)
- entry_name = self.app.builder.get_object("entry_text_" + self.kind + "_name")
- entry_name.connect("activate", self.app.on_activate_name)
- self.to_form()
- sw.show()
- 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
- """
- fkind = self.form_kinds[option]
- fname = fkind + "_" + self.kind + "_" + option
- if fkind == 'entry_eval' or fkind == 'entry_text':
- self.app.builder.get_object(fname).set_text(str(self.options[option]))
- return
- if fkind == 'cb':
- self.app.builder.get_object(fname).set_active(self.options[option])
- return
- if fkind == 'radio':
- self.app.builder.get_object(self.radios_inv[option][self.options[option]]).set_active(True)
- return
- print "Unknown kind of form item:", fkind
- 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
- """
- fkind = self.form_kinds[option]
- fname = fkind + "_" + self.kind + "_" + option
- if fkind == 'entry_text':
- self.options[option] = self.app.builder.get_object(fname).get_text()
- return
- if fkind == 'entry_eval':
- self.options[option] = self.app.get_eval(fname)
- return
- if fkind == 'cb':
- self.options[option] = self.app.builder.get_object(fname).get_active()
- return
- if fkind == 'radio':
- self.options[option] = self.app.get_radio_value(self.radios[option])
- return
- print "Unknown kind of form item:", fkind
- def plot(self):
- """
- Plot this object (Extend this method to implement the actual plotting).
- Axes get created, appended to canvas and cleared before plotting.
- Call this in descendants before doing the plotting.
- :return: Whether to continue plotting or not depending on the "plot" option.
- :rtype: bool
- """
- # Axes must exist and be attached to canvas.
- if self.axes is None or self.axes not in self.app.plotcanvas.figure.axes:
- self.axes = self.app.plotcanvas.new_axes(self.options['name'])
- if not self.options["plot"]:
- self.axes.cla()
- self.app.plotcanvas.auto_adjust_axes()
- return False
- # Clear axes or we will plot on top of them.
- self.axes.cla()
- # GLib.idle_add(self.axes.cla)
- 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
- class FlatCAMGerber(FlatCAMObj, Gerber):
- """
- Represents Gerber code.
- """
- def __init__(self, name):
- Gerber.__init__(self)
- FlatCAMObj.__init__(self, name)
- self.kind = "gerber"
- # The 'name' is already in self.options from FlatCAMObj
- self.options.update({
- "plot": True,
- "mergepolys": True,
- "multicolored": False,
- "solid": False,
- "isotooldia": 0.016,
- "isopasses": 1,
- "isooverlap": 0.15,
- "cutouttooldia": 0.07,
- "cutoutmargin": 0.2,
- "cutoutgapsize": 0.15,
- "gaps": "tb",
- "noncoppermargin": 0.0,
- "noncopperrounded": False,
- "bboxmargin": 0.0,
- "bboxrounded": False
- })
- # The 'name' is already in self.form_kinds from FlatCAMObj
- self.form_kinds.update({
- "plot": "cb",
- "mergepolys": "cb",
- "multicolored": "cb",
- "solid": "cb",
- "isotooldia": "entry_eval",
- "isopasses": "entry_eval",
- "isooverlap": "entry_eval",
- "cutouttooldia": "entry_eval",
- "cutoutmargin": "entry_eval",
- "cutoutgapsize": "entry_eval",
- "gaps": "radio",
- "noncoppermargin": "entry_eval",
- "noncopperrounded": "cb",
- "bboxmargin": "entry_eval",
- "bboxrounded": "cb"
- })
- self.radios = {"gaps": {"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"}}
- self.radios_inv = {"gaps": {"tb": "rb_2tb", "lr": "rb_2lr", "4": "rb_4"}}
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind']
- 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['cutoutmargin'] *= factor
- self.options['cutoutgapsize'] *= factor
- self.options['noncoppermargin'] *= factor
- self.options['bboxmargin'] *= 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
- if self.options["mergepolys"]:
- geometry = self.solid_geometry
- else:
- geometry = self.buffered_paths + \
- [poly['polygon'] for poly in self.regions] + \
- self.flash_geometry
- if self.options["multicolored"]:
- linespec = '-'
- else:
- linespec = 'k-'
- if self.options["solid"]:
- for poly in geometry:
- # TODO: Too many things hardcoded.
- patch = PolygonPatch(poly,
- facecolor="#BBF268",
- edgecolor="#006E20",
- alpha=0.75,
- zorder=2)
- self.axes.add_patch(patch)
- else:
- for poly in geometry:
- x, y = poly.exterior.xy
- self.axes.plot(x, y, linespec)
- for ints in poly.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, linespec)
- # self.app.plotcanvas.auto_adjust_axes()
- GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
- def serialize(self):
- return {
- "options": self.options,
- "kind": self.kind
- }
- class FlatCAMExcellon(FlatCAMObj, Excellon):
- """
- Represents Excellon/Drill code.
- """
- def __init__(self, name):
- Excellon.__init__(self)
- FlatCAMObj.__init__(self, name)
- self.kind = "excellon"
- self.options.update({
- "plot": True,
- "solid": False,
- "drillz": -0.1,
- "travelz": 0.1,
- "feedrate": 5.0,
- "toolselection": ""
- })
- self.form_kinds.update({
- "plot": "cb",
- "solid": "cb",
- "drillz": "entry_eval",
- "travelz": "entry_eval",
- "feedrate": "entry_eval",
- "toolselection": "entry_text"
- })
- # 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']
- def convert_units(self, units):
- factor = Excellon.convert_units(self, units)
- self.options['drillz'] *= factor
- self.options['travelz'] *= factor
- self.options['feedrate'] *= 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]
- # Plot excellon (All polygons?)
- if self.options["solid"]:
- for geo in self.solid_geometry:
- patch = PolygonPatch(geo,
- facecolor="#C40000",
- edgecolor="#750000",
- alpha=0.75,
- zorder=3)
- self.axes.add_patch(patch)
- else:
- for geo in self.solid_geometry:
- x, y = geo.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in geo.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'g-')
- #self.app.plotcanvas.auto_adjust_axes()
- GLib.idle_add(self.app.plotcanvas.auto_adjust_axes)
- def show_tool_chooser(self):
- win = Gtk.Window()
- box = Gtk.Box(spacing=2)
- box.set_orientation(Gtk.Orientation(1))
- win.add(box)
- for tool in self.tools:
- self.tool_cbs[tool] = Gtk.CheckButton(label=tool + ": " + str(self.tools[tool]))
- box.pack_start(self.tool_cbs[tool], False, False, 1)
- button = Gtk.Button(label="Accept")
- box.pack_start(button, False, False, 1)
- win.show_all()
- def on_accept(widget):
- win.destroy()
- tool_list = []
- for toolx in self.tool_cbs:
- if self.tool_cbs[toolx].get_active():
- tool_list.append(toolx)
- self.options["toolselection"] = ", ".join(tool_list)
- self.to_form()
- button.connect("activate", on_accept)
- button.connect("clicked", on_accept)
- class FlatCAMCNCjob(FlatCAMObj, CNCjob):
- """
- Represents G-Code.
- """
- def __init__(self, name, units="in", kind="generic", z_move=0.1,
- feedrate=3.0, z_cut=-0.002, tooldia=0.0):
- CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
- feedrate=feedrate, z_cut=z_cut, tooldia=tooldia)
- FlatCAMObj.__init__(self, name)
- self.kind = "cncjob"
- self.options.update({
- "plot": True,
- "tooldia": 0.4 / 25.4 # 0.4mm in inches
- })
- self.form_kinds.update({
- "plot": "cb",
- "tooldia": "entry_eval"
- })
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind']
- 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
- self.plot2(self.axes, tooldia=self.options["tooldia"])
- self.app.plotcanvas.auto_adjust_axes()
- def convert_units(self, units):
- factor = CNCjob.convert_units(self, units)
- print "FlatCAMCNCjob.convert_units()"
- self.options["tooldia"] *= factor
- class FlatCAMGeometry(FlatCAMObj, Geometry):
- """
- Geometric object not associated with a specific
- format.
- """
- def __init__(self, name):
- FlatCAMObj.__init__(self, name)
- Geometry.__init__(self)
- self.kind = "geometry"
- self.options.update({
- "plot": True,
- "solid": False,
- "multicolored": False,
- "cutz": -0.002,
- "travelz": 0.1,
- "feedrate": 5.0,
- "cnctooldia": 0.4 / 25.4,
- "painttooldia": 0.0625,
- "paintoverlap": 0.15,
- "paintmargin": 0.01
- })
- self.form_kinds.update({
- "plot": "cb",
- "solid": "cb",
- "multicolored": "cb",
- "cutz": "entry_eval",
- "travelz": "entry_eval",
- "feedrate": "entry_eval",
- "cnctooldia": "entry_eval",
- "painttooldia": "entry_eval",
- "paintoverlap": "entry_eval",
- "paintmargin": "entry_eval"
- })
- # Attributes to be included in serialization
- # Always append to it because it carries contents
- # from predecessors.
- self.ser_attrs += ['options', 'kind']
- def scale(self, factor):
- """
- Scales all geometry by a given factor.
- :param factor: Factor by which to scale the object's geometry/
- :type factor: float
- :return: None
- :rtype: None
- """
- if type(self.solid_geometry) == list:
- self.solid_geometry = [affinity.scale(g, factor, factor, origin=(0, 0))
- for g in self.solid_geometry]
- else:
- self.solid_geometry = affinity.scale(self.solid_geometry, factor, factor,
- origin=(0, 0))
- 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
- """
- dx, dy = vect
- if type(self.solid_geometry) == list:
- self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy)
- for g in self.solid_geometry]
- else:
- self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
- def convert_units(self, units):
- factor = Geometry.convert_units(self, units)
- self.options['cutz'] *= factor
- self.options['travelz'] *= factor
- self.options['feedrate'] *= factor
- self.options['cnctooldia'] *= factor
- self.options['painttooldia'] *= factor
- self.options['paintmargin'] *= factor
- return factor
- def plot(self):
- """
- Plots the object into its axes. If None, of if the axes
- are not part of the app's figure, it fetches new ones.
- :return: None
- """
- # Does all the required setup and returns False
- # if the 'ptint' option is set to False.
- if not FlatCAMObj.plot(self):
- return
- # Make sure solid_geometry is iterable.
- try:
- _ = iter(self.solid_geometry)
- except TypeError:
- self.solid_geometry = [self.solid_geometry]
- for geo in self.solid_geometry:
- if type(geo) == Polygon:
- x, y = geo.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in geo.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
- if type(geo) == LineString or type(geo) == LinearRing:
- x, y = geo.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
- if type(geo) == MultiPolygon:
- for poly in geo:
- x, y = poly.exterior.coords.xy
- self.axes.plot(x, y, 'r-')
- for ints in poly.interiors:
- x, y = ints.coords.xy
- self.axes.plot(x, y, 'r-')
- continue
- print "WARNING: Did not plot:", str(type(geo))
- self.app.plotcanvas.auto_adjust_axes()
- ########################################
- ## App ##
- ########################################
- class App:
- """
- The main application class. The constructor starts the GUI.
- """
- def __init__(self):
- """
- Starts the application. Takes no parameters.
- :return: app
- :rtype: App
- """
- # Needed to interact with the GUI from other threads.
- GObject.threads_init()
- # GLib.log_set_handler()
- #### GUI ####
- self.gladefile = "FlatCAM.ui"
- self.builder = Gtk.Builder()
- self.builder.add_from_file(self.gladefile)
- self.window = self.builder.get_object("window1")
- self.position_label = self.builder.get_object("label3")
- self.grid = self.builder.get_object("grid1")
- self.notebook = self.builder.get_object("notebook1")
- self.info_label = self.builder.get_object("label_status")
- self.progress_bar = self.builder.get_object("progressbar")
- self.progress_bar.set_show_text(True)
- self.units_label = self.builder.get_object("label_units")
- self.toolbar = self.builder.get_object("toolbar_main")
- # White (transparent) background on the "Options" tab.
- self.builder.get_object("vp_options").override_background_color(Gtk.StateType.NORMAL,
- Gdk.RGBA(1, 1, 1, 1))
- # Combo box to choose between project and application options.
- self.combo_options = self.builder.get_object("combo_options")
- self.combo_options.set_active(1)
- self.setup_project_list() # The "Project" tab
- self.setup_component_editor() # The "Selected" tab
- self.setup_toolbar()
- #### Event handling ####
- self.builder.connect_signals(self)
- #### Make plot area ####
- self.plotcanvas = PlotCanvas(self.grid)
- self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot)
- self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot)
- self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot)
- self.setup_tooltips()
- #### DATA ####
- self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
- self.setup_obj_classes()
- self.stuff = {} # FlatCAMObj's by name
- self.mouse = None # Mouse coordinates over plot
- # What is selected by the user. It is
- # a key if self.stuff
- self.selected_item_name = None
- # Used to inhibit the on_options_update callback when
- # the options are being changed by the program and not the user.
- self.options_update_ignore = False
- self.toggle_units_ignore = False
- self.defaults = {
- "units": "in"
- } # Application defaults
- ## Current Project ##
- self.options = {} # Project options
- self.project_filename = None
- self.form_kinds = {
- "units": "radio"
- }
- self.radios = {"units": {"rb_inch": "IN", "rb_mm": "MM"},
- "gerber_gaps": {"rb_app_2tb": "tb", "rb_app_2lr": "lr", "rb_app_4": "4"}}
- self.radios_inv = {"units": {"IN": "rb_inch", "MM": "rb_mm"},
- "gerber_gaps": {"tb": "rb_app_2tb", "lr": "rb_app_2lr", "4": "rb_app_4"}}
- # Options for each kind of FlatCAMObj.
- # Example: 'gerber_plot': 'cb'. The widget name would be: 'cb_app_gerber_plot'
- for FlatCAMClass in [FlatCAMExcellon, FlatCAMGeometry, FlatCAMGerber, FlatCAMCNCjob]:
- obj = FlatCAMClass("no_name")
- for option in obj.form_kinds:
- self.form_kinds[obj.kind + "_" + option] = obj.form_kinds[option]
- # if obj.form_kinds[option] == "radio":
- # self.radios.update({obj.kind + "_" + option: obj.radios[option]})
- # self.radios_inv.update({obj.kind + "_" + option: obj.radios_inv[option]})
- ## Event subscriptions ##
- self.plot_click_subscribers = {}
- self.plot_mousemove_subscribers = {}
- ## Tools ##
- self.measure = Measurement(self.builder.get_object("box39"), self.plotcanvas.axes,
- self.plot_click_subscribers, self.plot_mousemove_subscribers)
- # Toolbar icon
- # TODO: Where should I put this? Tool should have a method to add to toolbar?
- meas_ico = Gtk.Image.new_from_file('share/measure32.png')
- measure = Gtk.ToolButton.new(meas_ico, "")
- measure.connect("clicked", self.measure.toggle_active)
- measure.set_tooltip_markup("<b>Measure Tool:</b> Enable/disable tool.\n" +
- "Click on point to set reference.\n" +
- "(Click on plot and hit <b>m</b>)")
- self.toolbar.insert(measure, -1)
- #### Initialization ####
- self.load_defaults()
- self.options.update(self.defaults) # Copy app defaults to project options
- self.options2form() # Populate the app defaults form
- self.units_label.set_text("[" + self.options["units"] + "]")
- #### Check for updates ####
- self.version = 2
- t1 = threading.Thread(target=self.versionCheck)
- t1.daemon = True
- t1.start()
- #### For debugging only ###
- def somethreadfunc(app_obj):
- print "Hello World!"
- t = threading.Thread(target=somethreadfunc, args=(self,))
- t.daemon = True
- t.start()
- ########################################
- ## START ##
- ########################################
- self.icon256 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon256.png')
- self.icon48 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon48.png')
- self.icon16 = GdkPixbuf.Pixbuf.new_from_file('share/flatcam_icon16.png')
- Gtk.Window.set_default_icon_list([self.icon16, self.icon48, self.icon256])
- self.window.set_title("FlatCAM - Alpha 3 UNSTABLE - Check for updates!")
- self.window.set_default_size(900, 600)
- self.window.show_all()
- def setup_toolbar(self):
- # Zoom fit
- zf_ico = Gtk.Image.new_from_file('share/zoom_fit32.png')
- zoom_fit = Gtk.ToolButton.new(zf_ico, "")
- zoom_fit.connect("clicked", self.on_zoom_fit)
- zoom_fit.set_tooltip_markup("Zoom Fit.\n(Click on plot and hit <b>1</b>)")
- self.toolbar.insert(zoom_fit, -1)
- # Zoom out
- zo_ico = Gtk.Image.new_from_file('share/zoom_out32.png')
- zoom_out = Gtk.ToolButton.new(zo_ico, "")
- zoom_out.connect("clicked", self.on_zoom_out)
- zoom_out.set_tooltip_markup("Zoom Out.\n(Click on plot and hit <b>2</b>)")
- self.toolbar.insert(zoom_out, -1)
- # Zoom in
- zi_ico = Gtk.Image.new_from_file('share/zoom_in32.png')
- zoom_in = Gtk.ToolButton.new(zi_ico, "")
- zoom_in.connect("clicked", self.on_zoom_in)
- zoom_in.set_tooltip_markup("Zoom In.\n(Click on plot and hit <b>3</b>)")
- self.toolbar.insert(zoom_in, -1)
- # Clear plot
- cp_ico = Gtk.Image.new_from_file('share/clear_plot32.png')
- clear_plot = Gtk.ToolButton.new(cp_ico, "")
- clear_plot.connect("clicked", self.on_clear_plots)
- clear_plot.set_tooltip_markup("Clear Plot")
- self.toolbar.insert(clear_plot, -1)
- # Replot
- rp_ico = Gtk.Image.new_from_file('share/replot32.png')
- replot = Gtk.ToolButton.new(rp_ico, "")
- replot.connect("clicked", self.on_toolbar_replot)
- replot.set_tooltip_markup("Re-plot all")
- self.toolbar.insert(replot, -1)
- # Delete item
- del_ico = Gtk.Image.new_from_file('share/delete32.png')
- delete = Gtk.ToolButton.new(del_ico, "")
- delete.connect("clicked", self.on_delete)
- delete.set_tooltip_markup("Delete selected\nobject.")
- self.toolbar.insert(delete, -1)
- def setup_obj_classes(self):
- """
- Sets up application specifics on the FlatCAMObj class.
- :return: None
- """
- FlatCAMObj.app = self
- def setup_project_list(self):
- """
- Sets up list or Tree where whatever has been loaded or created is
- displayed.
- :return: None
- """
- self.store = Gtk.ListStore(str)
- self.tree = Gtk.TreeView(self.store)
- #self.list = Gtk.ListBox()
- self.tree.connect("row_activated", self.on_row_activated)
- self.tree_select = self.tree.get_selection()
- self.signal_id = self.tree_select.connect("changed", self.on_tree_selection_changed)
- renderer = Gtk.CellRendererText()
- column = Gtk.TreeViewColumn("Objects", renderer, text=0)
- self.tree.append_column(column)
- self.builder.get_object("box_project").pack_start(self.tree, False, False, 1)
- def setup_component_editor(self):
- """
- Initial configuration of the component editor. Creates
- a page titled "Selection" on the notebook on the left
- side of the main window.
- :return: None
- """
- box_selected = self.builder.get_object("box_selected")
- # Remove anything else in the box
- box_children = box_selected.get_children()
- for child in box_children:
- box_selected.remove(child)
- box1 = Gtk.Box(Gtk.Orientation.VERTICAL)
- label1 = Gtk.Label("Choose an item from Project")
- box1.pack_start(label1, True, False, 1)
- box_selected.pack_start(box1, True, True, 0)
- #box_selected.show()
- box1.show()
- label1.show()
- def info(self, text):
- """
- Show text on the status bar.
- :param text: Text to display.
- :type text: str
- :return: None
- """
- self.info_label.set_text(text)
- def build_list(self):
- """
- Clears and re-populates the list of objects in currently
- in the project.
- :return: None
- """
- print "build_list(): clearing"
- self.tree_select.unselect_all()
- self.store.clear()
- print "repopulating...",
- for key in self.stuff:
- print key,
- self.store.append([key])
- print
- def get_radio_value(self, radio_set):
- """
- Returns the radio_set[key] of the radiobutton
- whose name is key is active.
- :param radio_set: A dictionary containing widget_name: value pairs.
- :type radio_set: dict
- :return: radio_set[key]
- """
- for name in radio_set:
- if self.builder.get_object(name).get_active():
- return radio_set[name]
- def plot_all(self):
- """
- Re-generates all plots from all objects.
- :return: None
- """
- self.plotcanvas.clear()
- self.set_progress_bar(0.1, "Re-plotting...")
- def thread_func(app_obj):
- percentage = 0.1
- try:
- delta = 0.9 / len(self.stuff)
- except ZeroDivisionError:
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
- return
- for i in self.stuff:
- self.stuff[i].plot()
- percentage += delta
- GLib.idle_add(lambda: app_obj.set_progress_bar(percentage, "Re-plotting..."))
- #app_obj.plotcanvas.auto_adjust_axes()
- #self.on_zoom_fit(None)
- GLib.idle_add(app_obj.plotcanvas.auto_adjust_axes)
- GLib.idle_add(lambda: self.on_zoom_fit(None))
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, ""))
- t = threading.Thread(target=thread_func, args=(self,))
- t.daemon = True
- t.start()
- def get_eval(self, widget_name):
- """
- Runs eval() on the on the text entry of name 'widget_name'
- and returns the results.
- :param widget_name: Name of Gtk.Entry
- :type widget_name: str
- :return: Depends on contents of the entry text.
- """
- value = self.builder.get_object(widget_name).get_text()
- if value == "":
- value = "None"
- try:
- evald = eval(value)
- return evald
- except:
- self.info("Could not evaluate: " + value)
- return None
- def set_list_selection(self, name):
- """
- Marks a given object as selected in the list ob objects
- in the GUI. This selection will in turn trigger
- ``self.on_tree_selection_changed()``.
- :param name: Name of the object.
- :type name: str
- :return: None
- """
- iter = self.store.get_iter_first()
- while iter is not None and self.store[iter][0] != name:
- iter = self.store.iter_next(iter)
- self.tree_select.unselect_all()
- self.tree_select.select_iter(iter)
- # Need to return False such that GLib.idle_add
- # or .timeout_add do not repeat.
- return False
- def new_object(self, kind, name, initialize):
- """
- Creates a new specalized FlatCAMObj and attaches it to the application,
- this is, updates the GUI accordingly, any other records and plots it.
- :param kind: The kind of object to create. One of 'gerber',
- 'excellon', 'cncjob' and 'geometry'.
- :type kind: str
- :param name: Name for the object.
- :type name: str
- :param initialize: Function to run after creation of the object
- but before it is attached to the application. The function is
- called with 2 parameters: the new object and the App instance.
- :type initialize: function
- :return: None
- :rtype: None
- """
- ### Check for existing name
- if name in self.stuff:
- ## Create a new name
- # Ends with number?
- match = re.search(r'(.*[^\d])?(\d+)$', name)
- if match: # Yes: Increment the number!
- base = match.group(1) or ''
- num = int(match.group(2))
- name = base + str(num + 1)
- else: # No: add a number!
- name += "_1"
- # Create object
- classdict = {
- "gerber": FlatCAMGerber,
- "excellon": FlatCAMExcellon,
- "cncjob": FlatCAMCNCjob,
- "geometry": FlatCAMGeometry
- }
- obj = classdict[kind](name)
- obj.units = self.options["units"] # TODO: The constructor should look at defaults.
- # Initialize as per user request
- # User must take care to implement initialize
- # in a thread-safe way as is is likely that we
- # have been invoked in a separate thread.
- #initialize(obj, self)
- # Set default options from self.options
- for option in self.options:
- if option.find(kind + "_") == 0:
- oname = option[len(kind)+1:]
- obj.options[oname] = self.options[option]
- # Initialize as per user request
- # User must take care to implement initialize
- # in a thread-safe way as is is likely that we
- # have been invoked in a separate thread.
- initialize(obj, self)
- # Check units and convert if necessary
- if self.options["units"].upper() != obj.units.upper():
- GLib.idle_add(lambda: self.info("Converting units to " + self.options["units"] + "."))
- obj.convert_units(self.options["units"])
- # Add to our records
- self.stuff[name] = obj
- # Update GUI list and select it (Thread-safe?)
- self.store.append([name])
- #self.build_list()
- GLib.idle_add(lambda: self.set_list_selection(name))
- # TODO: Gtk.notebook.set_current_page is not known to
- # TODO: return False. Fix this??
- GLib.timeout_add(100, lambda: self.notebook.set_current_page(1))
- # Plot
- # TODO: (Thread-safe?)
- obj.plot()
- # TODO: Threading dissaster!
- GLib.idle_add(lambda: self.on_zoom_fit(None))
- return obj
- def set_progress_bar(self, percentage, text=""):
- """
- Sets the application's progress bar to a given frac_digits and text.
- :param percentage: The frac_digits (0.0-1.0) of the progress.
- :type percentage: float
- :param text: Text to display on the progress bar.
- :type text: str
- :return: None
- """
- self.progress_bar.set_text(text)
- self.progress_bar.set_fraction(percentage)
- return False
- def get_current(self):
- """
- Returns the currently selected FlatCAMObj in the application.
- :return: Currently selected FlatCAMObj in the application.
- :rtype: FlatCAMObj or None
- """
- # TODO: Could possibly read the form into the object here.
- # But there are some cases when the form for the object
- # is not up yet. See on_tree_selection_changed.
- try:
- return self.stuff[self.selected_item_name]
- except:
- return None
- def load_defaults(self):
- """
- Loads the aplication's default settings from defaults.json into
- ``self.defaults``.
- :return: None
- """
- try:
- f = open("defaults.json")
- options = f.read()
- f.close()
- except:
- self.info("ERROR: Could not load defaults file.")
- return
- try:
- defaults = json.loads(options)
- except:
- e = sys.exc_info()[0]
- print e
- self.info("ERROR: Failed to parse defaults file.")
- return
- self.defaults.update(defaults)
- def read_form(self):
- """
- Reads the options form into self.defaults/self.options.
- :return: None
- :rtype: None
- """
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
- for option in options_set:
- self.read_form_item(option, options_set)
- def read_form_item(self, name, dest):
- """
- Reads the value of a form item in the defaults/options form and
- saves it to the corresponding dictionary.
- :param name: Name of the form item. A key in ``self.defaults`` or
- ``self.options``.
- :type name: str
- :param dest: Dictionary to which to save the value.
- :type dest: dict
- :return: None
- """
- fkind = self.form_kinds[name]
- fname = fkind + "_" + "app" + "_" + name
- if fkind == 'entry_text':
- dest[name] = self.builder.get_object(fname).get_text()
- return
- if fkind == 'entry_eval':
- dest[name] = self.get_eval(fname)
- return
- if fkind == 'cb':
- dest[name] = self.builder.get_object(fname).get_active()
- return
- if fkind == 'radio':
- dest[name] = self.get_radio_value(self.radios[name])
- return
- print "Unknown kind of form item:", fkind
- def options2form(self):
- """
- Sets the 'Project Options' or 'Application Defaults' form with values from
- ``self.options`` or ``self.defaults``.
- :return: None
- :rtype: None
- """
- # Set the on-change callback to do nothing while we do the changes.
- self.options_update_ignore = True
- self.toggle_units_ignore = True
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
- for option in options_set:
- self.set_form_item(option, options_set[option])
- self.options_update_ignore = False
- self.toggle_units_ignore = False
- def set_form_item(self, name, value):
- """
- Sets a form item 'name' in the GUI with the given 'value'. The syntax of
- form names in the GUI is <kind>_app_<name>, where kind is one of: rb (radio button),
- cb (check button), entry_eval or entry_text (entry), combo (combo box). name is
- whatever name it's been given. For self.defaults, name is a key in the dictionary.
- :param name: Name of the form field.
- :type name: str
- :param value: The value to set the form field to.
- :type value: Depends on field kind.
- :return: None
- """
- if name not in self.form_kinds:
- print "WARNING: Tried to set unknown option/form item:", name
- return
- fkind = self.form_kinds[name]
- fname = fkind + "_" + "app" + "_" + name
- if fkind == 'entry_eval' or fkind == 'entry_text':
- try:
- self.builder.get_object(fname).set_text(str(value))
- except:
- print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
- return
- if fkind == 'cb':
- try:
- self.builder.get_object(fname).set_active(value)
- except:
- print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
- return
- if fkind == 'radio':
- try:
- self.builder.get_object(self.radios_inv[name][value]).set_active(True)
- except:
- print "ERROR: Failed to set value of %s to %s" % (fname, str(value))
- return
- print "Unknown kind of form item:", fkind
- def save_project(self, filename):
- """
- Saves the current project to the specified file.
- :param filename: Name of the file in which to save.
- :type filename: str
- :return: None
- """
- # Capture the latest changes
- try:
- self.get_current().read_form()
- except:
- pass
- d = {"objs": [self.stuff[o].to_dict() for o in self.stuff],
- "options": self.options}
- try:
- f = open(filename, 'w')
- except:
- print "ERROR: Failed to open file for saving:", filename
- return
- try:
- json.dump(d, f, default=to_dict)
- except:
- print "ERROR: File open but failed to write:", filename
- f.close()
- return
- f.close()
- def open_project(self, filename):
- """
- Loads a project from the specified file.
- :param filename: Name of the file from which to load.
- :type filename: str
- :return: None
- """
- try:
- f = open(filename, 'r')
- except:
- print "WARNING: Failed to open project file:", filename
- return
- try:
- d = json.load(f, object_hook=dict2obj)
- except:
- print "WARNING: Failed to parse project file:", filename
- f.close()
- return
- # Clear the current project
- self.on_file_new(None)
- # Project options
- self.options.update(d['options'])
- self.project_filename = filename
- self.units_label.set_text(self.options["units"])
- # Re create objects
- for obj in d['objs']:
- def obj_init(obj_inst, app_inst):
- obj_inst.from_dict(obj)
- self.new_object(obj['kind'], obj['options']['name'], obj_init)
- self.info("Project loaded from: " + filename)
- def populate_objects_combo(self, combo):
- """
- Populates a Gtk.Comboboxtext with the list of the object in the project.
- :param combo: Name or instance of the comboboxtext.
- :type combo: str or Gtk.ComboBoxText
- :return: None
- """
- print "Populating combo!"
- if type(combo) == str:
- combo = self.builder.get_object(combo)
- combo.remove_all()
- for obj in self.stuff:
- combo.append_text(obj)
- def versionCheck(self, *args):
- """
- Checks for the latest version of the program. Alerts the
- user if theirs is outdated. This method is meant to be run
- in a saeparate thread.
- :return: None
- """
- try:
- f = urllib.urlopen("http://caram.cl/flatcam/VERSION") # TODO: Hardcoded.
- except:
- GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
- return
- try:
- data = json.load(f)
- except:
- GLib.idle_add(lambda: self.info("ERROR trying to check for latest version."))
- f.close()
- return
- f.close()
- if self.version >= data["version"]:
- GLib.idle_add(lambda: self.info("FlatCAM is up to date!"))
- return
- label = Gtk.Label("There is a newer version of FlatCAM\n" +
- "available for download:\n\n" +
- data["name"] + "\n\n" + data["message"])
- dialog = Gtk.Dialog("Newer Version Available", self.window, 0,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK))
- dialog.set_default_size(150, 100)
- dialog.set_modal(True)
- box = dialog.get_content_area()
- box.set_border_width(10)
- box.add(label)
- def do_dialog():
- dialog.show_all()
- response = dialog.run()
- dialog.destroy()
- GLib.idle_add(lambda: do_dialog())
- return
- def setup_tooltips(self):
- tooltips = {
- "cb_gerber_plot": "Plot this object on the main window.",
- "cb_gerber_mergepolys": "Show overlapping polygons as single.",
- "cb_gerber_solid": "Paint inside polygons.",
- "cb_gerber_multicolored": "Draw polygons with different colors."
- }
- for widget in tooltips:
- self.builder.get_object(widget).set_tooltip_markup(tooltips[widget])
- def do_nothing(self, param):
- return
- ########################################
- ## EVENT HANDLERS ##
- ########################################
- def on_offset_object(self, widget):
- """
- Offsets the object's geometry by the vector specified
- in the form. Re-plots.
- :param widget: Ignored
- :return: None
- """
- obj = self.get_current()
- obj.read_form()
- assert isinstance(obj, FlatCAMObj)
- try:
- vect = self.get_eval("entry_eval_" + obj.kind + "_offset")
- except:
- self.info("ERROR: Vector is not in (x, y) format.")
- return
- assert isinstance(obj, Geometry)
- obj.offset(vect)
- obj.plot()
- return
- def on_cb_plot_toggled(self, widget):
- """
- Callback for toggling the "Plot" checkbox. Re-plots.
- :param widget: Ignored.
- :return: None
- """
- self.get_current().read_form()
- self.get_current().plot()
- def on_about(self, widget):
- """
- Opens the 'About' dialog box.
- :param widget: Ignored.
- :return: None
- """
- about = self.builder.get_object("aboutdialog")
- response = about.run()
- #about.destroy()
- about.hide()
- def on_create_mirror(self, widget):
- """
- Creates a mirror image of an object to be used as a bottom layer.
- :param widget: Ignored.
- :return: None
- """
- # TODO: Move (some of) this to camlib!
- # Object to mirror
- try:
- obj_name = self.builder.get_object("comboboxtext_bottomlayer").get_active_text()
- fcobj = self.stuff[obj_name]
- except KeyError:
- self.info("WARNING: Cannot mirror that object.")
- return
- # For now, lets limit to Gerbers and Excellons.
- # assert isinstance(gerb, FlatCAMGerber)
- if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon):
- self.info("ERROR: Only Gerber and Excellon objects can be mirrored.")
- return
- # Mirror axis "X" or "Y
- axis = self.get_radio_value({"rb_mirror_x": "X",
- "rb_mirror_y": "Y"})
- mode = self.get_radio_value({"rb_mirror_box": "box",
- "rb_mirror_point": "point"})
- if mode == "point": # A single point defines the mirror axis
- # TODO: Error handling
- px, py = eval(self.point_entry.get_text())
- else: # The axis is the line dividing the box in the middle
- name = self.box_combo.get_active_text()
- bb_obj = self.stuff[name]
- xmin, ymin, xmax, ymax = bb_obj.bounds()
- px = 0.5*(xmin+xmax)
- py = 0.5*(ymin+ymax)
- # Do the mirroring
- # xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
- # mirrored = affinity.scale(fcobj.solid_geometry, xscale, yscale, origin=(px, py))
- #
- # def obj_init(obj_inst, app_inst):
- # obj_inst.solid_geometry = mirrored
- #
- # self.new_object("gerber", fcobj.options["name"] + "_mirror", obj_init)
- fcobj.mirror(axis, [px, py])
- fcobj.plot()
- #self.on_update_plot(None)
- def on_create_aligndrill(self, widget):
- """
- Creates alignment holes Excellon object. Creates mirror duplicates
- of the specified holes around the specified axis.
- :param widget: Ignored.
- :return: None
- """
- # Mirror axis. Same as in on_create_mirror.
- axis = self.get_radio_value({"rb_mirror_x": "X",
- "rb_mirror_y": "Y"})
- # TODO: Error handling
- mode = self.get_radio_value({"rb_mirror_box": "box",
- "rb_mirror_point": "point"})
- if mode == "point":
- px, py = eval(self.point_entry.get_text())
- else:
- name = self.box_combo.get_active_text()
- bb_obj = self.stuff[name]
- xmin, ymin, xmax, ymax = bb_obj.bounds()
- px = 0.5*(xmin+xmax)
- py = 0.5*(ymin+ymax)
- xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
- # Tools
- dia = self.get_eval("entry_dblsided_alignholediam")
- tools = {"1": {"C": dia}}
- # Parse hole list
- # TODO: Better parsing
- holes = self.builder.get_object("entry_dblsided_alignholes").get_text()
- holes = eval("[" + holes + "]")
- drills = []
- for hole in holes:
- point = Point(hole)
- point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
- drills.append({"point": point, "tool": "1"})
- drills.append({"point": point_mirror, "tool": "1"})
- def obj_init(obj_inst, app_inst):
- obj_inst.tools = tools
- obj_inst.drills = drills
- obj_inst.create_geometry()
- self.new_object("excellon", "Alignment Drills", obj_init)
- def on_toggle_pointbox(self, widget):
- """
- Callback for radio selection change between point and box in the
- Double-sided PCB tool. Updates the UI accordingly.
- :param widget: Ignored.
- :return: None
- """
- # Where the entry or combo go
- box = self.builder.get_object("box_pointbox")
- # Clear contents
- children = box.get_children()
- for child in children:
- box.remove(child)
- choice = self.get_radio_value({"rb_mirror_point": "point",
- "rb_mirror_box": "box"})
- if choice == "point":
- self.point_entry = Gtk.Entry()
- self.builder.get_object("box_pointbox").pack_start(self.point_entry,
- False, False, 1)
- self.point_entry.show()
- else:
- self.box_combo = Gtk.ComboBoxText()
- self.builder.get_object("box_pointbox").pack_start(self.box_combo,
- False, False, 1)
- self.populate_objects_combo(self.box_combo)
- self.box_combo.show()
- def on_tools_doublesided(self, param):
- """
- Callback for menu item Tools->Double Sided PCB Tool. Launches the
- tool placing its UI in the "Tool" tab in the notebook.
- :param param: Ignored.
- :return: None
- """
- # Were are we drawing the UI
- box_tool = self.builder.get_object("box_tool")
- # Remove anything else in the box
- box_children = box_tool.get_children()
- for child in box_children:
- box_tool.remove(child)
- # Get the UI
- osw = self.builder.get_object("offscreenwindow_dblsided")
- sw = self.builder.get_object("sw_dblsided")
- osw.remove(sw)
- vp = self.builder.get_object("vp_dblsided")
- vp.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(1, 1, 1, 1))
- # Put in the UI
- box_tool.pack_start(sw, True, True, 0)
- # INITIALIZATION
- # Populate combo box
- self.populate_objects_combo("comboboxtext_bottomlayer")
- # Point entry
- self.point_entry = Gtk.Entry()
- box = self.builder.get_object("box_pointbox")
- for child in box.get_children():
- box.remove(child)
- box.pack_start(self.point_entry, False, False, 1)
- # Show the "Tool" tab
- self.notebook.set_current_page(3)
- sw.show_all()
- def on_toggle_units(self, widget):
- """
- Callback for the Units radio-button change in the Options tab.
- Changes the application's default units or the current project's units.
- If changing the project's units, the change propagates to all of
- the objects in the project.
- :param widget: Ignored.
- :return: None
- """
- if self.toggle_units_ignore:
- return
- combo_sel = self.combo_options.get_active()
- options_set = [self.options, self.defaults][combo_sel]
- # Options to scale
- dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize',
- 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz',
- 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia',
- 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate',
- 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap',
- 'geometry_paintmargin']
- def scale_options(factor):
- for dim in dimensions:
- options_set[dim] *= factor
- # The scaling factor depending on choice of units.
- factor = 1/25.4
- if self.builder.get_object('rb_mm').get_active():
- factor = 25.4
- # App units. Convert without warning.
- if combo_sel == 1:
- self.read_form()
- scale_options(factor)
- self.options2form()
- return
- # Changing project units. Warn user.
- label = Gtk.Label("Changing the units of the project causes all geometrical \n" + \
- "properties of all objects to be scaled accordingly. Continue?")
- dialog = Gtk.Dialog("Changing Project Units", self.window, 0,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK))
- dialog.set_default_size(150, 100)
- dialog.set_modal(True)
- box = dialog.get_content_area()
- box.set_border_width(10)
- box.add(label)
- dialog.show_all()
- response = dialog.run()
- dialog.destroy()
- if response == Gtk.ResponseType.OK:
- #print "Converting units..."
- #print "Converting options..."
- self.read_form()
- scale_options(factor)
- self.options2form()
- for obj in self.stuff:
- units = self.get_radio_value({"rb_mm": "MM", "rb_inch": "IN"})
- #print "Converting ", obj, " to ", units
- self.stuff[obj].convert_units(units)
- current = self.get_current()
- if current is not None:
- current.to_form()
- self.plot_all()
- else:
- # Undo toggling
- self.toggle_units_ignore = True
- if self.builder.get_object('rb_mm').get_active():
- self.builder.get_object('rb_inch').set_active(True)
- else:
- self.builder.get_object('rb_mm').set_active(True)
- self.toggle_units_ignore = False
- self.read_form()
- self.info("Converted units to %s" % self.options["units"])
- self.units_label.set_text("[" + self.options["units"] + "]")
- def on_file_openproject(self, param):
- """
- Callback for menu item File->Open Project. Opens a file chooser and calls
- ``self.open_project()`` after successful selection of a filename.
- :param param: Ignored.
- :return: None
- """
- def on_success(app_obj, filename):
- app_obj.open_project(filename)
- self.file_chooser_action(on_success)
- def on_file_saveproject(self, param):
- """
- Callback for menu item File->Save Project. Saves the project to
- ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
- if set to None. The project is saved by calling ``self.save_project()``.
- :param param: Ignored.
- :return: None
- """
- if self.project_filename is None:
- self.on_file_saveprojectas(None)
- else:
- self.save_project(self.project_filename)
- self.info("Project saved to: " + self.project_filename)
- def on_file_saveprojectas(self, param):
- """
- Callback for menu item File->Save Project As... Opens a file
- chooser and saves the project to the given file via
- ``self.save_project()``.
- :param param: Ignored.
- :return: None
- """
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
- app_obj.save_project(filename)
- self.project_filename = filename
- app_obj.info("Project saved to: " + filename)
- self.file_chooser_save_action(on_success)
- def on_file_saveprojectcopy(self, param):
- """
- Callback for menu item File->Save Project Copy... Opens a file
- chooser and saves the project to the given file via
- ``self.save_project``. It does not update ``self.project_filename`` so
- subsequent save requests are done on the previous known filename.
- :param param: Ignore.
- :return: None
- """
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
- app_obj.save_project(filename)
- app_obj.info("Project copy saved to: " + filename)
- self.file_chooser_save_action(on_success)
- def on_options_app2project(self, param):
- """
- Callback for Options->Transfer Options->App=>Project. Copies options
- from application defaults to project defaults.
- :param param: Ignored.
- :return: None
- """
- self.options.update(self.defaults)
- self.options2form() # Update UI
- def on_options_project2app(self, param):
- """
- Callback for Options->Transfer Options->Project=>App. Copies options
- from project defaults to application defaults.
- :param param: Ignored.
- :return: None
- """
- self.defaults.update(self.options)
- self.options2form() # Update UI
- def on_options_project2object(self, param):
- """
- Callback for Options->Transfer Options->Project=>Object. Copies options
- from project defaults to the currently selected object.
- :param param: Ignored.
- :return: None
- """
- obj = self.get_current()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- for option in self.options:
- if option.find(obj.kind + "_") == 0:
- oname = option[len(obj.kind)+1:]
- obj.options[oname] = self.options[option]
- obj.to_form() # Update UI
- def on_options_object2project(self, param):
- """
- Callback for Options->Transfer Options->Object=>Project. Copies options
- from the currently selected object to project defaults.
- :param param: Ignored.
- :return: None
- """
- obj = self.get_current()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- obj.read_form()
- for option in obj.options:
- if option in ['name']: # TODO: Handle this better...
- continue
- self.options[obj.kind + "_" + option] = obj.options[option]
- self.options2form() # Update UI
- def on_options_object2app(self, param):
- """
- Callback for Options->Transfer Options->Object=>App. Copies options
- from the currently selected object to application defaults.
- :param param: Ignored.
- :return: None
- """
- obj = self.get_current()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- obj.read_form()
- for option in obj.options:
- if option in ['name']: # TODO: Handle this better...
- continue
- self.defaults[obj.kind + "_" + option] = obj.options[option]
- self.options2form() # Update UI
- def on_options_app2object(self, param):
- """
- Callback for Options->Transfer Options->App=>Object. Copies options
- from application defaults to the currently selected object.
- :param param: Ignored.
- :return: None
- """
- obj = self.get_current()
- if obj is None:
- self.info("WARNING: No object selected.")
- return
- for option in self.defaults:
- if option.find(obj.kind + "_") == 0:
- oname = option[len(obj.kind)+1:]
- obj.options[oname] = self.defaults[option]
- obj.to_form() # Update UI
- def on_file_savedefaults(self, param):
- """
- Callback for menu item File->Save Defaults. Saves application default options
- ``self.defaults`` to defaults.json.
- :param param: Ignored.
- :return: None
- """
- # Read options from file
- try:
- f = open("defaults.json")
- options = f.read()
- f.close()
- except:
- self.info("ERROR: Could not load defaults file.")
- return
- try:
- defaults = json.loads(options)
- except:
- e = sys.exc_info()[0]
- print e
- self.info("ERROR: Failed to parse defaults file.")
- return
- # Update options
- assert isinstance(defaults, dict)
- defaults.update(self.defaults)
- # Save update options
- try:
- f = open("defaults.json", "w")
- json.dump(defaults, f)
- f.close()
- except:
- self.info("ERROR: Failed to write defaults to file.")
- return
- self.info("Defaults saved.")
- def on_options_combo_change(self, widget):
- """
- Called when the combo box to choose between application defaults and
- project option changes value. The corresponding variables are
- copied to the UI.
- :param widget: The widget from which this was called. Ignore.
- :return: None
- """
- #combo_sel = self.combo_options.get_active()
- #print "Options --> ", combo_sel
- self.options2form()
- def on_options_update(self, widget):
- """
- Called whenever a value in the options/defaults form changes.
- All values are updated. Can be inhibited by setting ``self.options_update_ignore = True``,
- which may be necessary when updating the UI from code and not by the user.
- :param widget: The widget from which this was called. Ignore.
- :return: None
- """
- if self.options_update_ignore:
- return
- self.read_form()
- def on_scale_object(self, widget):
- """
- Callback for request to change an objects geometry scale. The object
- is re-scaled and replotted.
- :param widget: Ignored.
- :return: None
- """
- obj = self.get_current()
- factor = self.get_eval("entry_eval_" + obj.kind + "_scalefactor")
- obj.scale(factor)
- obj.to_form()
- self.on_update_plot(None)
- def on_canvas_configure(self, widget, event):
- """
- Called whenever the canvas changes size. The axes are updated such
- as to use the whole canvas.
- :param widget: Ignored.
- :param event: Ignored.
- :return: None
- """
- self.plotcanvas.auto_adjust_axes()
- def on_row_activated(self, widget, path, col):
- """
- Callback for selection activation (Enter or double-click) on the Project list.
- Switches the notebook page to the object properties form. Calls
- ``self.notebook.set_current_page(1)``.
- :param widget: Ignored.
- :param path: Ignored.
- :param col: Ignored.
- :return: None
- """
- self.notebook.set_current_page(1)
- def on_generate_gerber_bounding_box(self, widget):
- """
- Callback for request from the Gerber form to generate a bounding box for the
- geometry in the object. Creates a FlatCAMGeometry with the bounding box.
- The box can have rounded corners if specified in the form.
- :param widget: Ignored.
- :return: None
- """
- # TODO: Use Gerber.get_bounding_box(...)
- gerber = self.get_current()
- gerber.read_form()
- name = gerber.options["name"] + "_bbox"
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry)
- # Bounding box with rounded corners
- bounding_box = gerber.solid_geometry.envelope.buffer(gerber.options["bboxmargin"])
- if not gerber.options["bboxrounded"]: # Remove rounded corners
- bounding_box = bounding_box.envelope
- geo_obj.solid_geometry = bounding_box
- self.new_object("geometry", name, geo_init)
- def on_update_plot(self, widget):
- """
- Callback for button on form for all kinds of objects.
- Re-plots the current object only.
- :param widget: The widget from which this was called. Ignored.
- :return: None
- """
- obj = self.get_current()
- obj.read_form()
- self.set_progress_bar(0.5, "Plotting...")
- def thread_func(app_obj):
- assert isinstance(app_obj, App)
- #GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Plotting..."))
- #GLib.idle_add(lambda: app_obj.get_current().plot(app_obj.figure))
- obj.plot()
- #GLib.idle_add(lambda: app_obj.on_zoom_fit(None))
- GLib.timeout_add(300, lambda: app_obj.set_progress_bar(0.0, "Idle"))
- t = threading.Thread(target=thread_func, args=(self,))
- t.daemon = True
- t.start()
- def on_generate_excellon_cncjob(self, widget):
- """
- Callback for button active/click on Excellon form to
- create a CNC Job for the Excellon file.
- :param widget: Ignored
- :return: None
- """
- excellon = self.get_current()
- excellon.read_form()
- job_name = excellon.options["name"] + "_cnc"
- # Object initialization function for app.new_object()
- def job_init(job_obj, app_obj):
- # excellon_ = self.get_current()
- # assert isinstance(excellon_, FlatCAMExcellon)
- assert isinstance(job_obj, FlatCAMCNCjob)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
- job_obj.z_cut = excellon.options["drillz"]
- job_obj.z_move = excellon.options["travelz"]
- job_obj.feedrate = excellon.options["feedrate"]
- # There could be more than one drill size...
- # job_obj.tooldia = # TODO: duplicate variable!
- # job_obj.options["tooldia"] =
- job_obj.generate_from_excellon_by_tool(excellon, excellon.options["toolselection"])
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
- job_obj.gcode_parse()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
- job_obj.create_geometry()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
- # To be run in separate thread
- def job_thread(app_obj):
- app_obj.new_object("cncjob", job_name, job_init)
- GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
- # Start the thread
- t = threading.Thread(target=job_thread, args=(self,))
- t.daemon = True
- t.start()
- def on_excellon_tool_choose(self, widget):
- """
- Callback for button on Excellon form to open up a window for
- selecting tools.
- :param widget: The widget from which this was called.
- :return: None
- """
- excellon = self.get_current()
- assert isinstance(excellon, FlatCAMExcellon)
- excellon.show_tool_chooser()
- def on_entry_eval_activate(self, widget):
- """
- Called when an entry is activated (eg. by hitting enter) if
- set to do so. Its text is eval()'d and set to the returned value.
- The current object is updated.
- :param widget:
- :return:
- """
- self.on_eval_update(widget)
- obj = self.get_current()
- assert isinstance(obj, FlatCAMObj)
- obj.read_form()
- def on_gerber_generate_noncopper(self, widget):
- """
- Callback for button on Gerber form to create a geometry object
- with polygons covering the area without copper or negative of the
- Gerber.
- :param widget: The widget from which this was called.
- :return: None
- """
- gerb = self.get_current()
- gerb.read_form()
- name = gerb.options["name"] + "_noncopper"
- def geo_init(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry)
- bounding_box = gerb.solid_geometry.envelope.buffer(gerb.options["noncoppermargin"])
- if not gerb.options["noncopperrounded"]:
- bounding_box = bounding_box.envelope
- non_copper = bounding_box.difference(gerb.solid_geometry)
- geo_obj.solid_geometry = non_copper
- # TODO: Check for None
- self.new_object("geometry", name, geo_init)
- def on_gerber_generate_cutout(self, widget):
- """
- Callback for button on Gerber form to create geometry with lines
- for cutting off the board.
- :param widget: The widget from which this was called.
- :return: None
- """
- gerb = self.get_current()
- gerb.read_form()
- name = gerb.options["name"] + "_cutout"
- def geo_init(geo_obj, app_obj):
- margin = gerb.options["cutoutmargin"] + gerb.options["cutouttooldia"]/2
- gap_size = gerb.options["cutoutgapsize"] + gerb.options["cutouttooldia"]
- minx, miny, maxx, maxy = gerb.bounds()
- minx -= margin
- maxx += margin
- miny -= margin
- maxy += margin
- midx = 0.5 * (minx + maxx)
- midy = 0.5 * (miny + maxy)
- hgap = 0.5 * gap_size
- pts = [[midx - hgap, maxy],
- [minx, maxy],
- [minx, midy + hgap],
- [minx, midy - hgap],
- [minx, miny],
- [midx - hgap, miny],
- [midx + hgap, miny],
- [maxx, miny],
- [maxx, midy - hgap],
- [maxx, midy + hgap],
- [maxx, maxy],
- [midx + hgap, maxy]]
- cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
- [pts[6], pts[7], pts[10], pts[11]]],
- "lr": [[pts[9], pts[10], pts[1], pts[2]],
- [pts[3], pts[4], pts[7], pts[8]]],
- "4": [[pts[0], pts[1], pts[2]],
- [pts[3], pts[4], pts[5]],
- [pts[6], pts[7], pts[8]],
- [pts[9], pts[10], pts[11]]]}
- cuts = cases[app.get_radio_value({"rb_2tb": "tb", "rb_2lr": "lr", "rb_4": "4"})]
- geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
- # TODO: Check for None
- self.new_object("geometry", name, geo_init)
- def on_eval_update(self, widget):
- """
- Modifies the content of a Gtk.Entry by running
- eval() on its contents and puting it back as a
- string.
- :param widget: The widget from which this was called.
- :return: None
- """
- # TODO: error handling here
- widget.set_text(str(eval(widget.get_text())))
- def on_generate_isolation(self, widget):
- """
- Callback for button on Gerber form to create isolation routing geometry.
- :param widget: The widget from which this was called.
- :return: None
- """
- gerb = self.get_current()
- gerb.read_form()
- dia = gerb.options["isotooldia"]
- passes = int(gerb.options["isopasses"])
- overlap = gerb.options["isooverlap"] * dia
- for i in range(passes):
- offset = (2*i + 1)/2.0 * dia - i*overlap
- iso_name = gerb.options["name"] + "_iso%d" % (i+1)
- # 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"] = gerb.options["isotooldia"]
- geo_obj.solid_geometry = gerb.isolation_geometry(offset)
- app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
- # TODO: Do something if this is None. Offer changing name?
- self.new_object("geometry", iso_name, iso_init)
- def on_generate_cncjob(self, widget):
- """
- Callback for button on geometry form to generate CNC job.
- :param widget: The widget from which this was called.
- :return: None
- """
- source_geo = self.get_current()
- source_geo.read_form()
- job_name = source_geo.options["name"] + "_cnc"
- # Object initialization function for app.new_object()
- # RUNNING ON SEPARATE THREAD!
- def job_init(job_obj, app_obj):
- assert isinstance(job_obj, FlatCAMCNCjob)
- # Propagate options
- job_obj.options["tooldia"] = source_geo.options["cnctooldia"]
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Creating CNC Job..."))
- job_obj.z_cut = source_geo.options["cutz"]
- job_obj.z_move = source_geo.options["travelz"]
- job_obj.feedrate = source_geo.options["feedrate"]
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
- # TODO: The tolerance should not be hard coded. Just for testing.
- job_obj.generate_from_geometry(source_geo, tolerance=0.0005)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
- job_obj.gcode_parse()
- # TODO: job_obj.create_geometry creates stuff that is not used.
- #GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Creating New Geometry..."))
- #job_obj.create_geometry()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.8, "Plotting..."))
- # To be run in separate thread
- def job_thread(app_obj):
- app_obj.new_object("cncjob", job_name, job_init)
- GLib.idle_add(lambda: app_obj.info("CNCjob created: %s" % job_name))
- GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
- # Start the thread
- t = threading.Thread(target=job_thread, args=(self,))
- t.daemon = True
- t.start()
- def on_generate_paintarea(self, widget):
- """
- Callback for button on geometry form.
- Subscribes to the "Click on plot" event and continues
- after the click. Finds the polygon containing
- the clicked point and runs clear_poly() on it, resulting
- in a new FlatCAMGeometry object.
- :param widget: The widget from which this was called.
- :return: None
- """
- self.info("Click inside the desired polygon.")
- geo = self.get_current()
- geo.read_form()
- assert isinstance(geo, FlatCAMGeometry)
- tooldia = geo.options["painttooldia"]
- overlap = geo.options["paintoverlap"]
- # To be called after clicking on the plot.
- def doit(event):
- self.plot_click_subscribers.pop("generate_paintarea")
- self.info("")
- point = [event.xdata, event.ydata]
- poly = find_polygon(geo.solid_geometry, point)
- # Initializes the new geometry object
- def gen_paintarea(geo_obj, app_obj):
- assert isinstance(geo_obj, FlatCAMGeometry)
- assert isinstance(app_obj, App)
- cp = clear_poly(poly.buffer(-geo.options["paintmargin"]), tooldia, overlap)
- geo_obj.solid_geometry = cp
- geo_obj.options["cnctooldia"] = tooldia
- name = self.selected_item_name + "_paint"
- self.new_object("geometry", name, gen_paintarea)
- self.plot_click_subscribers["generate_paintarea"] = doit
- def on_cncjob_exportgcode(self, widget):
- """
- Called from button on CNCjob form to save the G-Code from the object.
- :param widget: The widget from which this was called.
- :return: None
- """
- def on_success(app_obj, filename):
- cncjob = app_obj.get_current()
- f = open(filename, 'w')
- f.write(cncjob.gcode)
- f.close()
- app_obj.info("Saved to: " + filename)
- self.file_chooser_save_action(on_success)
- def on_delete(self, widget):
- """
- Delete the currently selected FlatCAMObj.
- :param widget: The widget from which this was called.
- :return: None
- """
- # Keep this for later
- name = copy.copy(self.selected_item_name)
- # Remove plot
- self.plotcanvas.figure.delaxes(self.get_current().axes)
- self.plotcanvas.auto_adjust_axes()
- # Remove from dictionary
- self.stuff.pop(self.selected_item_name)
- # Update UI
- self.build_list() # Update the items list
- self.info("Object deleted: %s" % name)
- def on_toolbar_replot(self, widget):
- """
- Callback for toolbar button. Re-plots all objects.
- :param widget: The widget from which this was called.
- :return: None
- """
- self.get_current().read_form()
- self.plot_all()
- def on_clear_plots(self, widget):
- """
- Callback for toolbar button. Clears all plots.
- :param widget: The widget from which this was called.
- :return: None
- """
- self.plotcanvas.clear()
- def on_activate_name(self, entry):
- """
- Hitting 'Enter' after changing the name of an item
- updates the item dictionary and re-builds the item list.
- :param entry: The widget from which this was called.
- :return: None
- """
- # Disconnect event listener
- self.tree.get_selection().disconnect(self.signal_id)
- new_name = entry.get_text() # Get from form
- self.stuff[new_name] = self.stuff.pop(self.selected_item_name) # Update dictionary
- self.stuff[new_name].options["name"] = new_name # update object
- self.info('Name change: ' + self.selected_item_name + " to " + new_name)
- self.selected_item_name = new_name # Update selection name
- self.build_list() # Update the items list
- # Reconnect event listener
- self.signal_id = self.tree.get_selection().connect(
- "changed", self.on_tree_selection_changed)
- def on_tree_selection_changed(self, selection):
- """
- Callback for selection change in the project list. This changes
- the currently selected FlatCAMObj.
- :param selection: Selection associated to the project tree or list
- :type selection: Gtk.TreeSelection
- :return: None
- """
- print "DEBUG: on_tree_selection_change(): ",
- model, treeiter = selection.get_selected()
- if treeiter is not None:
- # Save data for previous selection
- obj = self.get_current()
- if obj is not None:
- obj.read_form()
- print "DEBUG: You selected", model[treeiter][0]
- self.selected_item_name = model[treeiter][0]
- obj_new = self.get_current()
- if obj_new is not None:
- GLib.idle_add(lambda: obj_new.build_ui())
- else:
- print "DEBUG: Nothing selected"
- self.selected_item_name = None
- self.setup_component_editor()
- def on_file_new(self, param):
- """
- Callback for menu item File->New. Returns the application to its
- startup state.
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
- # Remove everythong from memory
- # Clear plot
- self.plotcanvas.clear()
- # Clear object editor
- #self.setup_component_editor()
- # Clear data
- self.stuff = {}
- # Clear list
- #self.tree_select.unselect_all()
- self.build_list()
- # Clear project filename
- self.project_filename = None
- # Re-fresh project options
- self.on_options_app2project(None)
- def on_filequit(self, param):
- """
- Callback for menu item File->Quit. Closes the application.
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
- self.window.destroy()
- Gtk.main_quit()
- def on_closewindow(self, param):
- """
- Callback for closing the main window.
- :param param: Whatever is passed by the event. Ignore.
- :return: None
- """
- self.window.destroy()
- Gtk.main_quit()
- def file_chooser_action(self, on_success):
- """
- Opens the file chooser and runs on_success on a separate thread
- upon completion of valid file choice.
- :param on_success: A function to run upon completion of a valid file
- selection. Takes 2 parameters: The app instance and the filename.
- Note that it is run on a separate thread, therefore it must take the
- appropriate precautions when accessing shared resources.
- :type on_success: func
- :return: None
- """
- dialog = Gtk.FileChooserDialog("Please choose a file", self.window,
- Gtk.FileChooserAction.OPEN,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
- response = dialog.run()
- if response == Gtk.ResponseType.OK:
- filename = dialog.get_filename()
- dialog.destroy()
- t = threading.Thread(target=on_success, args=(self, filename))
- t.daemon = True
- t.start()
- #on_success(self, filename)
- elif response == Gtk.ResponseType.CANCEL:
- self.info("Open cancelled.") # print("Cancel clicked")
- dialog.destroy()
- def file_chooser_save_action(self, on_success):
- """
- Opens the file chooser and runs on_success upon completion of valid file choice.
- :param on_success: A function to run upon selection of a filename. Takes 2
- parameters: The instance of the application (App) and the chosen filename. This
- gets run immediately in the same thread.
- :return: None
- """
- dialog = Gtk.FileChooserDialog("Save file", self.window,
- Gtk.FileChooserAction.SAVE,
- (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
- dialog.set_current_name("Untitled")
- response = dialog.run()
- if response == Gtk.ResponseType.OK:
- filename = dialog.get_filename()
- dialog.destroy()
- on_success(self, filename)
- elif response == Gtk.ResponseType.CANCEL:
- self.info("Save cancelled.") # print("Cancel clicked")
- dialog.destroy()
- def on_fileopengerber(self, param):
- """
- Callback for menu item File->Open Gerber. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMGerber object
- and updates the progress bar throughout the process.
- :param param: Ignore
- :return: None
- """
- # IMPORTANT: on_success will run on a separate thread. Use
- # GLib.idle_add(function, **kwargs) to launch actions that will
- # updata the GUI.
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Gerber ..."))
- def obj_init(gerber_obj, app_obj):
- assert isinstance(gerber_obj, FlatCAMGerber)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
- gerber_obj.parse_file(filename)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Creating Geometry ..."))
- gerber_obj.create_geometry()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
- name = filename.split('/')[-1].split('\\')[-1]
- app_obj.new_object("gerber", name, obj_init)
- GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
- # on_success gets run on a separate thread
- self.file_chooser_action(on_success)
- def on_fileopenexcellon(self, param):
- """
- Callback for menu item File->Open Excellon. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMExcellon object
- and updates the progress bar throughout the process.
- :param param: Ignore
- :return: None
- """
- # IMPORTANT: on_success will run on a separate thread. Use
- # GLib.idle_add(function, **kwargs) to launch actions that will
- # updata the GUI.
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.1, "Opening Excellon ..."))
- def obj_init(excellon_obj, app_obj):
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.2, "Parsing ..."))
- excellon_obj.parse_file(filename)
- excellon_obj.create_geometry()
- GLib.idle_add(lambda: app_obj.set_progress_bar(0.6, "Plotting ..."))
- name = filename.split('/')[-1].split('\\')[-1]
- app_obj.new_object("excellon", name, obj_init)
- GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
- # on_success gets run on a separate thread
- self.file_chooser_action(on_success)
- def on_fileopengcode(self, param):
- """
- Callback for menu item File->Open G-Code. Defines a function that is then passed
- to ``self.file_chooser_action()``. It requests the creation of a FlatCAMCNCjob object
- and updates the progress bar throughout the process.
- :param param: Ignore
- :return: None
- """
- # IMPORTANT: on_success will run on a separate thread. Use
- # GLib.idle_add(function, **kwargs) to launch actions that will
- # updata the GUI.
- def on_success(app_obj, filename):
- assert isinstance(app_obj, App)
- def obj_init(job_obj, app_obj_):
- """
- :type app_obj_: App
- """
- assert isinstance(app_obj_, App)
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.1, "Opening G-Code ..."))
- f = open(filename)
- gcode = f.read()
- f.close()
- job_obj.gcode = gcode
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.2, "Parsing ..."))
- job_obj.gcode_parse()
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Creating geometry ..."))
- job_obj.create_geometry()
- GLib.idle_add(lambda: app_obj_.set_progress_bar(0.6, "Plotting ..."))
- name = filename.split('/')[-1].split('\\')[-1]
- app_obj.new_object("cncjob", name, obj_init)
- GLib.idle_add(lambda: app_obj.set_progress_bar(1.0, "Done!"))
- GLib.timeout_add_seconds(1, lambda: app_obj.set_progress_bar(0.0, ""))
- # on_success gets run on a separate thread
- self.file_chooser_action(on_success)
- def on_mouse_move_over_plot(self, event):
- """
- Callback for the mouse motion event over the plot. This event is generated
- by the Matplotlib backend and has been registered in ``self.__init__()``.
- For details, see: http://matplotlib.org/users/event_handling.html
- :param event: Contains information about the event.
- :return: None
- """
- try: # May fail in case mouse not within axes
- self.position_label.set_label("X: %.4f Y: %.4f" % (
- event.xdata, event.ydata))
- self.mouse = [event.xdata, event.ydata]
- for subscriber in self.plot_mousemove_subscribers:
- self.plot_mousemove_subscribers[subscriber](event)
- except:
- self.position_label.set_label("")
- self.mouse = None
- def on_click_over_plot(self, event):
- """
- Callback for the mouse click event over the plot. This event is generated
- by the Matplotlib backend and has been registered in ``self.__init__()``.
- For details, see: http://matplotlib.org/users/event_handling.html
- Default actions are:
- * Copy coordinates to clipboard. Ex.: (65.5473, -13.2679)
- :param event: Contains information about the event, like which button
- was clicked, the pixel coordinates and the axes coordinates.
- :return: None
- """
- # For key presses
- self.plotcanvas.canvas.grab_focus()
- try:
- print 'button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % (
- event.button, event.x, event.y, event.xdata, event.ydata)
- # TODO: This custom subscription mechanism is probably not necessary.
- for subscriber in self.plot_click_subscribers:
- self.plot_click_subscribers[subscriber](event)
- self.clipboard.set_text("(%.4f, %.4f)" % (event.xdata, event.ydata), -1)
- except Exception, e:
- print "Outside plot!"
- def on_zoom_in(self, event):
- """
- Callback for zoom-in request. This can be either from the corresponding
- toolbar button or the '3' key when the canvas is focused. Calls ``self.zoom()``.
- :param event: Ignored.
- :return: None
- """
- self.plotcanvas.zoom(1.5)
- return
- def on_zoom_out(self, event):
- """
- Callback for zoom-out request. This can be either from the corresponding
- toolbar button or the '2' key when the canvas is focused. Calls ``self.zoom()``.
- :param event: Ignored.
- :return: None
- """
- self.plotcanvas.zoom(1 / 1.5)
- def on_zoom_fit(self, event):
- """
- Callback for zoom-out request. This can be either from the corresponding
- toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
- with axes limits from the geometry bounds of all objects.
- :param event: Ignored.
- :return: None
- """
- xmin, ymin, xmax, ymax = get_bounds(self.stuff)
- width = xmax - xmin
- height = ymax - ymin
- xmin -= 0.05 * width
- xmax += 0.05 * width
- ymin -= 0.05 * height
- ymax += 0.05 * height
- self.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
- def on_key_over_plot(self, event):
- """
- Callback for the key pressed event when the canvas is focused. Keyboard
- shortcuts are handled here. So far, these are the shortcuts:
- ========== ============================================
- Key Action
- ========== ============================================
- '1' Zoom-fit. Fits the axes limits to the data.
- '2' Zoom-out.
- '3' Zoom-in.
- ========== ============================================
- :param event: Ignored.
- :return: None
- """
- #print 'you pressed', event.key, event.xdata, event.ydata
- if event.key == '1': # 1
- self.on_zoom_fit(None)
- return
- if event.key == '2': # 2
- self.plotcanvas.zoom(1 / 1.5, self.mouse)
- return
- if event.key == '3': # 3
- self.plotcanvas.zoom(1.5, self.mouse)
- return
- if event.key == 'm':
- if self.measure.toggle_active():
- self.info("Measuring tool ON")
- else:
- self.info("Measuring tool OFF")
- return
- class BaseDraw:
- def __init__(self, plotcanvas, name=None):
- """
- :param plotcanvas: The PlotCanvas where the drawing tool will operate.
- :type plotcanvas: PlotCanvas
- """
- self.plotcanvas = plotcanvas
- # Must have unique axes
- charset = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890"
- self.name = name or [random.choice(charset) for i in range(20)]
- self.axes = self.plotcanvas.new_axes(self.name)
- class DrawingObject(BaseDraw):
- def __init__(self, plotcanvas, name=None):
- """
- Possible objects are:
- * Point
- * Line
- * Rectangle
- * Circle
- * Polygon
- """
- BaseDraw.__init__(self, plotcanvas)
- self.properties = {}
- def plot(self):
- return
- def update_plot(self):
- self.axes.cla()
- self.plot()
- self.plotcanvas.auto_adjust_axes()
- class DrawingPoint(DrawingObject):
- def __init__(self, plotcanvas, name=None, coord=None):
- DrawingObject.__init__(self, plotcanvas)
- self.properties.update({
- "coordinate": coord
- })
- def plot(self):
- x, y = self.properties["coordinate"]
- self.axes.plot(x, y, 'o')
- class Measurement:
- def __init__(self, container, axes, click_subscibers, move_subscribers, update=None):
- self.update = update
- self.container = container
- self.frame = None
- self.label = None
- self.click_subscribers = click_subscibers
- self.move_subscribers = move_subscribers
- self.point1 = None
- self.point2 = None
- self.active = False
- def toggle_active(self, *args):
- if self.active: # Deactivate
- self.active = False
- self.move_subscribers.pop("meas")
- self.click_subscribers.pop("meas")
- self.container.remove(self.frame)
- if self.update is not None:
- self.update()
- return False
- else: # Activate
- print "DEBUG: Activating Measurement Tool..."
- self.active = True
- self.click_subscribers["meas"] = self.on_click
- self.move_subscribers["meas"] = self.on_move
- self.frame = Gtk.Frame()
- self.frame.set_margin_right(5)
- self.frame.set_margin_top(3)
- align = Gtk.Alignment()
- align.set(0, 0.5, 0, 0)
- align.set_padding(4, 4, 4, 4)
- self.label = Gtk.Label()
- self.label.set_label("Click on a reference point...")
- abox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 10)
- abox.pack_start(Gtk.Image.new_from_file('share/measure16.png'), False, False, 0)
- abox.pack_start(self.label, False, False, 0)
- align.add(abox)
- self.frame.add(align)
- self.container.pack_end(self.frame, False, True, 1)
- self.frame.show_all()
- return True
- def on_move(self, event):
- if self.point1 is None:
- self.label.set_label("Click on a reference point...")
- else:
- dx = event.xdata - self.point1[0]
- dy = event.ydata - self.point1[1]
- d = sqrt(dx**2 + dy**2)
- self.label.set_label("D = %.4f D(x) = %.4f D(y) = %.4f" % (d, dx, dy))
- if self.update is not None:
- self.update()
- def on_click(self, event):
- if self.point1 is None:
- self.point1 = (event.xdata, event.ydata)
- return
- else:
- self.point2 = copy.copy(self.point1)
- self.point1 = (event.xdata, event.ydata)
- self.on_move(event)
- class PlotCanvas:
- """
- Class handling the plotting area in the application.
- """
- def __init__(self, container):
- """
- The constructor configures the Matplotlib figure that
- will contain all plots, creates the base axes and connects
- events to the plotting area.
- :param container: The parent container in which to draw plots.
- :rtype: PlotCanvas
- """
- # Options
- self.x_margin = 15 # pixels
- self.y_margin = 25 # Pixels
- # Parent container
- self.container = container
- # Plots go onto a single matplotlib.figure
- self.figure = Figure(dpi=50) # TODO: dpi needed?
- self.figure.patch.set_visible(False)
- # These axes show the ticks and grid. No plotting done here.
- # New axes must have a label, otherwise mpl returns an existing one.
- self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
- self.axes.set_aspect(1)
- self.axes.grid(True)
- # The canvas is the top level container (Gtk.DrawingArea)
- self.canvas = FigureCanvas(self.figure)
- self.canvas.set_hexpand(1)
- self.canvas.set_vexpand(1)
- self.canvas.set_can_focus(True) # For key press
- # Attach to parent
- self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
- # Events
- self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
- self.canvas.connect('configure-event', self.auto_adjust_axes)
- self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
- self.canvas.connect("scroll-event", self.on_scroll)
- self.mouse = [0,0]
- def mpl_connect(self, event_name, callback):
- """
- Attach an event handler to the canvas through the Matplotlib interface.
- :param event_name: Name of the event
- :type event_name: str
- :param callback: Function to call
- :type callback: func
- :return: Nothing
- """
- self.canvas.mpl_connect(event_name, callback)
- def connect(self, event_name, callback):
- """
- Attach an event handler to the canvas through the native GTK interface.
- :param event_name: Name of the event
- :type event_name: str
- :param callback: Function to call
- :type callback: function
- :return: Nothing
- """
- self.canvas.connect(event_name, callback)
- def clear(self):
- """
- Clears axes and figure.
- :return: None
- """
- # Clear
- self.axes.cla()
- self.figure.clf()
- # Re-build
- self.figure.add_axes(self.axes)
- self.axes.set_aspect(1)
- self.axes.grid(True)
- # Re-draw
- self.canvas.queue_draw()
- def adjust_axes(self, xmin, ymin, xmax, ymax):
- """
- Adjusts all axes while maintaining the use of the whole canvas
- and an aspect ratio to 1:1 between x and y axes. The parameters are an original
- request that will be modified to fit these restrictions.
- :param xmin: Requested minimum value for the X axis.
- :type xmin: float
- :param ymin: Requested minimum value for the Y axis.
- :type ymin: float
- :param xmax: Requested maximum value for the X axis.
- :type xmax: float
- :param ymax: Requested maximum value for the Y axis.
- :type ymax: float
- :return: None
- """
- width = xmax - xmin
- height = ymax - ymin
- try:
- r = width / height
- except:
- print "ERROR: Height is", height
- return
- canvas_w, canvas_h = self.canvas.get_width_height()
- canvas_r = float(canvas_w) / canvas_h
- x_ratio = float(self.x_margin) / canvas_w
- y_ratio = float(self.y_margin) / canvas_h
- if r > canvas_r:
- ycenter = (ymin + ymax) / 2.0
- newheight = height * r / canvas_r
- ymin = ycenter - newheight / 2.0
- ymax = ycenter + newheight / 2.0
- else:
- xcenter = (xmax + ymin) / 2.0
- newwidth = width * canvas_r / r
- xmin = xcenter - newwidth / 2.0
- xmax = xcenter + newwidth / 2.0
- # Adjust axes
- for ax in self.figure.get_axes():
- if ax._label != 'base':
- ax.set_frame_on(False) # No frame
- ax.set_xticks([]) # No tick
- ax.set_yticks([]) # No ticks
- ax.patch.set_visible(False) # No background
- ax.set_aspect(1)
- ax.set_xlim((xmin, xmax))
- ax.set_ylim((ymin, ymax))
- ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
- # Re-draw
- self.canvas.queue_draw()
- def auto_adjust_axes(self, *args):
- """
- Calls ``adjust_axes()`` using the extents of the base axes.
- :rtype : None
- :return: None
- """
- xmin, xmax = self.axes.get_xlim()
- ymin, ymax = self.axes.get_ylim()
- self.adjust_axes(xmin, ymin, xmax, ymax)
- def zoom(self, factor, center=None):
- """
- Zooms the plot by factor around a given
- center point. Takes care of re-drawing.
- :param factor: Number by which to scale the plot.
- :type factor: float
- :param center: Coordinates [x, y] of the point around which to scale the plot.
- :type center: list
- :return: None
- """
- xmin, xmax = self.axes.get_xlim()
- ymin, ymax = self.axes.get_ylim()
- width = xmax - xmin
- height = ymax - ymin
- if center is None or center == [None, None]:
- center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
- # For keeping the point at the pointer location
- relx = (xmax - center[0]) / width
- rely = (ymax - center[1]) / height
- new_width = width / factor
- new_height = height / factor
- xmin = center[0] - new_width * (1 - relx)
- xmax = center[0] + new_width * relx
- ymin = center[1] - new_height * (1 - rely)
- ymax = center[1] + new_height * rely
- # Adjust axes
- for ax in self.figure.get_axes():
- ax.set_xlim((xmin, xmax))
- ax.set_ylim((ymin, ymax))
- # Re-draw
- self.canvas.queue_draw()
- def new_axes(self, name):
- """
- Creates and returns an Axes object attached to this object's Figure.
- :param name: Unique label for the axes.
- :return: Axes attached to the figure.
- :rtype: Axes
- """
- return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
- # def plot_axes(self, axes):
- #
- # if axes not in self.figure.axes:
- # self.figure.add_axes(axes)
- #
- # # Basic configuration
- # axes.set_frame_on(False) # No frame
- # axes.set_xticks([]) # No tick
- # axes.set_yticks([]) # No ticks
- # axes.patch.set_visible(False) # No background
- # axes.set_aspect(1)
- #
- # # Adjust limits
- # self.auto_adjust_axes()
- def on_scroll(self, canvas, event):
- """
- Scroll event handler.
- :param canvas: The widget generating the event. Ignored.
- :param event: Event object containing the event information.
- :return: None
- """
- z, direction = event.get_scroll_direction()
- if direction is Gdk.ScrollDirection.UP:
- self.zoom(1.5, self.mouse)
- else:
- self.zoom(1/1.5, self.mouse)
- def on_mouse_move(self, event):
- """
- Mouse movement event hadler.
- :param event: Contains information about the event.
- :return: None
- """
- self.mouse = [event.xdata, event.ydata]
- app = App()
- Gtk.main()
|