The tools are ordered by diameter as I found that the tools order in the Excellon file is not always diameter based. There is also a plated / no-plated holes criteria. The tools in the GUI tool-list are selected all by default. If the user wants to select only some tools, he should be carefull when selecting the tools as the order of the selection will be the actual order of the tools in G-code.
1359 lines
47 KiB
Python
1359 lines
47 KiB
Python
from PyQt4 import QtCore
|
|
from copy import copy
|
|
from ObjectUI import *
|
|
import FlatCAMApp
|
|
import inspect # TODO: For debugging only.
|
|
from camlib import *
|
|
from FlatCAMCommon import LoudDict
|
|
from FlatCAMDraw import FlatCAMDraw
|
|
|
|
|
|
########################################
|
|
## FlatCAMObj ##
|
|
########################################
|
|
class FlatCAMObj(QtCore.QObject):
|
|
"""
|
|
Base type of objects handled in FlatCAM. These become interactive
|
|
in the GUI, can be plotted, and their options can be modified
|
|
by the user in their respective forms.
|
|
"""
|
|
|
|
# Instance of the application to which these are related.
|
|
# The app should set this value.
|
|
app = None
|
|
|
|
def __init__(self, name):
|
|
"""
|
|
|
|
:param name: Name of the object given by the user.
|
|
:return: FlatCAMObj
|
|
"""
|
|
QtCore.QObject.__init__(self)
|
|
|
|
# View
|
|
self.ui = None
|
|
|
|
self.options = LoudDict(name=name)
|
|
self.options.set_change_callback(self.on_options_change)
|
|
|
|
self.form_fields = {}
|
|
|
|
self.axes = None # Matplotlib axes
|
|
self.kind = None # Override with proper name
|
|
|
|
self.muted_ui = False
|
|
|
|
# assert isinstance(self.ui, ObjectUI)
|
|
# self.ui.name_entry.returnPressed.connect(self.on_name_activate)
|
|
# self.ui.offset_button.clicked.connect(self.on_offset_button_click)
|
|
# self.ui.scale_button.clicked.connect(self.on_scale_button_click)
|
|
|
|
def on_options_change(self, key):
|
|
self.emit(QtCore.SIGNAL("optionChanged"), key)
|
|
|
|
def set_ui(self, ui):
|
|
self.ui = ui
|
|
|
|
self.form_fields = {"name": self.ui.name_entry}
|
|
|
|
assert isinstance(self.ui, ObjectUI)
|
|
self.ui.name_entry.returnPressed.connect(self.on_name_activate)
|
|
self.ui.offset_button.clicked.connect(self.on_offset_button_click)
|
|
self.ui.scale_button.clicked.connect(self.on_scale_button_click)
|
|
|
|
def __str__(self):
|
|
return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
|
|
|
|
def on_name_activate(self):
|
|
old_name = copy(self.options["name"])
|
|
new_name = self.ui.name_entry.get_value()
|
|
self.options["name"] = self.ui.name_entry.get_value()
|
|
self.app.info("Name changed from %s to %s" % (old_name, new_name))
|
|
|
|
def on_offset_button_click(self):
|
|
self.app.report_usage("obj_on_offset_button")
|
|
|
|
self.read_form()
|
|
vect = self.ui.offsetvector_entry.get_value()
|
|
self.offset(vect)
|
|
self.plot()
|
|
|
|
def on_scale_button_click(self):
|
|
self.app.report_usage("obj_on_scale_button")
|
|
self.read_form()
|
|
factor = self.ui.scale_entry.get_value()
|
|
self.scale(factor)
|
|
self.plot()
|
|
|
|
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:
|
|
FlatCAMApp.App.log.debug("setup_axes(): 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:
|
|
FlatCAMApp.App.log.debug("setup_axes(): Clearing and attaching axes")
|
|
self.axes.cla()
|
|
figure.add_axes(self.axes)
|
|
else:
|
|
FlatCAMApp.App.log.debug("setup_axes(): 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
|
|
"""
|
|
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
|
|
for option in self.options:
|
|
self.read_form_item(option)
|
|
|
|
def build_ui(self):
|
|
"""
|
|
Sets up the UI/form for this object. Show the UI
|
|
in the App.
|
|
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
|
|
self.muted_ui = True
|
|
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
|
|
|
|
# Remove anything else in the box
|
|
# box_children = self.app.ui.notebook.selected_contents.get_children()
|
|
# for child in box_children:
|
|
# self.app.ui.notebook.selected_contents.remove(child)
|
|
# while self.app.ui.selected_layout.count():
|
|
# self.app.ui.selected_layout.takeAt(0)
|
|
|
|
# Put in the UI
|
|
# box_selected.pack_start(sw, True, True, 0)
|
|
# self.app.ui.notebook.selected_contents.add(self.ui)
|
|
# self.app.ui.selected_layout.addWidget(self.ui)
|
|
try:
|
|
self.app.ui.selected_scroll_area.takeWidget()
|
|
except:
|
|
self.app.log.debug("Nothing to remove")
|
|
self.app.ui.selected_scroll_area.setWidget(self.ui)
|
|
self.to_form()
|
|
|
|
self.muted_ui = False
|
|
|
|
def set_form_item(self, option):
|
|
"""
|
|
Copies the specified option to the UI form.
|
|
|
|
:param option: Name of the option (Key in ``self.options``).
|
|
:type option: str
|
|
:return: None
|
|
"""
|
|
|
|
try:
|
|
self.form_fields[option].set_value(self.options[option])
|
|
except KeyError:
|
|
self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
|
|
|
|
def read_form_item(self, option):
|
|
"""
|
|
Reads the specified option from the UI form into ``self.options``.
|
|
|
|
:param option: Name of the option.
|
|
:type option: str
|
|
:return: None
|
|
"""
|
|
|
|
try:
|
|
self.options[option] = self.form_fields[option].get_value()
|
|
except KeyError:
|
|
self.app.log.warning("Failed to read option from field: %s" % option)
|
|
|
|
def plot(self):
|
|
"""
|
|
Plot this object (Extend this method to implement the actual plotting).
|
|
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
|
|
"""
|
|
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
|
|
|
|
# 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() # TODO: Thread safe?
|
|
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.
|
|
"""
|
|
|
|
ui_type = GerberObjectUI
|
|
|
|
def __init__(self, name):
|
|
Gerber.__init__(self)
|
|
FlatCAMObj.__init__(self, name)
|
|
|
|
self.kind = "gerber"
|
|
|
|
# The 'name' is already in self.options from FlatCAMObj
|
|
# Automatically updates the UI
|
|
self.options.update({
|
|
"plot": True,
|
|
"multicolored": False,
|
|
"solid": False,
|
|
"isotooldia": 0.016,
|
|
"isopasses": 1,
|
|
"isooverlap": 0.15,
|
|
"combine_passes": True,
|
|
"cutouttooldia": 0.07,
|
|
"cutoutmargin": 0.2,
|
|
"cutoutgapsize": 0.15,
|
|
"gaps": "tb",
|
|
"noncoppermargin": 0.0,
|
|
"noncopperrounded": False,
|
|
"bboxmargin": 0.0,
|
|
"bboxrounded": False
|
|
})
|
|
|
|
# Attributes to be included in serialization
|
|
# Always append to it because it carries contents
|
|
# from predecessors.
|
|
self.ser_attrs += ['options', 'kind']
|
|
|
|
# assert isinstance(self.ui, GerberObjectUI)
|
|
# self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
# self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
|
|
# self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
|
|
# self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
|
|
# self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
|
|
# self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
|
|
# self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
|
|
|
|
def set_ui(self, ui):
|
|
"""
|
|
Maps options with GUI inputs.
|
|
Connects GUI events to methods.
|
|
|
|
:param ui: GUI object.
|
|
:type ui: GerberObjectUI
|
|
:return: None
|
|
"""
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"multicolored": self.ui.multicolored_cb,
|
|
"solid": self.ui.solid_cb,
|
|
"isotooldia": self.ui.iso_tool_dia_entry,
|
|
"isopasses": self.ui.iso_width_entry,
|
|
"isooverlap": self.ui.iso_overlap_entry,
|
|
"combine_passes":self.ui.combine_passes_cb,
|
|
"cutouttooldia": self.ui.cutout_tooldia_entry,
|
|
"cutoutmargin": self.ui.cutout_margin_entry,
|
|
"cutoutgapsize": self.ui.cutout_gap_entry,
|
|
"gaps": self.ui.gaps_radio,
|
|
"noncoppermargin": self.ui.noncopper_margin_entry,
|
|
"noncopperrounded": self.ui.noncopper_rounded_cb,
|
|
"bboxmargin": self.ui.bbmargin_entry,
|
|
"bboxrounded": self.ui.bbrounded_cb
|
|
})
|
|
|
|
assert isinstance(self.ui, GerberObjectUI)
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
|
|
self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
|
|
self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
|
|
self.ui.generate_cutout_button.clicked.connect(self.on_generatecutout_button_click)
|
|
self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
|
|
self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
|
|
|
|
def on_generatenoncopper_button_click(self, *args):
|
|
self.app.report_usage("gerber_on_generatenoncopper_button")
|
|
|
|
self.read_form()
|
|
name = self.options["name"] + "_noncopper"
|
|
|
|
def geo_init(geo_obj, app_obj):
|
|
assert isinstance(geo_obj, FlatCAMGeometry)
|
|
bounding_box = self.solid_geometry.envelope.buffer(self.options["noncoppermargin"])
|
|
if not self.options["noncopperrounded"]:
|
|
bounding_box = bounding_box.envelope
|
|
non_copper = bounding_box.difference(self.solid_geometry)
|
|
geo_obj.solid_geometry = non_copper
|
|
|
|
# TODO: Check for None
|
|
self.app.new_object("geometry", name, geo_init)
|
|
|
|
def on_generatebb_button_click(self, *args):
|
|
self.app.report_usage("gerber_on_generatebb_button")
|
|
self.read_form()
|
|
name = self.options["name"] + "_bbox"
|
|
|
|
def geo_init(geo_obj, app_obj):
|
|
assert isinstance(geo_obj, FlatCAMGeometry)
|
|
# Bounding box with rounded corners
|
|
bounding_box = self.solid_geometry.envelope.buffer(self.options["bboxmargin"])
|
|
if not self.options["bboxrounded"]: # Remove rounded corners
|
|
bounding_box = bounding_box.envelope
|
|
geo_obj.solid_geometry = bounding_box
|
|
|
|
self.app.new_object("geometry", name, geo_init)
|
|
|
|
def on_generatecutout_button_click(self, *args):
|
|
self.app.report_usage("gerber_on_generatecutout_button")
|
|
self.read_form()
|
|
name = self.options["name"] + "_cutout"
|
|
|
|
def geo_init(geo_obj, app_obj):
|
|
margin = self.options["cutoutmargin"] + self.options["cutouttooldia"]/2
|
|
gap_size = self.options["cutoutgapsize"] + self.options["cutouttooldia"]
|
|
minx, miny, maxx, maxy = self.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[self.options['gaps']]
|
|
geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
|
|
|
|
# TODO: Check for None
|
|
self.app.new_object("geometry", name, geo_init)
|
|
|
|
def on_iso_button_click(self, *args):
|
|
self.app.report_usage("gerber_on_iso_button")
|
|
self.read_form()
|
|
self.isolate()
|
|
|
|
def follow(self, outname=None):
|
|
"""
|
|
Creates a geometry object "following" the gerber paths.
|
|
|
|
:return: None
|
|
"""
|
|
|
|
default_name = self.options["name"] + "_follow"
|
|
follow_name = outname or default_name
|
|
|
|
def follow_init(follow_obj, app_obj):
|
|
# Propagate options
|
|
follow_obj.options["cnctooldia"] = self.options["isotooldia"]
|
|
follow_obj.solid_geometry = self.solid_geometry
|
|
app_obj.info("Follow geometry created: %s" % follow_obj.options["name"])
|
|
|
|
# TODO: Do something if this is None. Offer changing name?
|
|
self.app.new_object("geometry", follow_name, follow_init)
|
|
|
|
def isolate(self, dia=None, passes=None, overlap=None, outname=None, combine=None):
|
|
"""
|
|
Creates an isolation routing geometry object in the project.
|
|
|
|
:param dia: Tool diameter
|
|
:param passes: Number of tool widths to cut
|
|
:param overlap: Overlap between passes in fraction of tool diameter
|
|
:param outname: Base name of the output object
|
|
:return: None
|
|
"""
|
|
if dia is None:
|
|
dia = self.options["isotooldia"]
|
|
if passes is None:
|
|
passes = int(self.options["isopasses"])
|
|
if overlap is None:
|
|
overlap = self.options["isooverlap"]
|
|
if combine is None:
|
|
combine = self.options["combine_passes"]
|
|
else:
|
|
combine = bool(combine)
|
|
|
|
base_name = self.options["name"] + "_iso"
|
|
base_name = outname or base_name
|
|
|
|
def generate_envelope(offset, invert):
|
|
# isolation_geometry produces an envelope that is going on the left of the geometry
|
|
# (the copper features). To leave the least amount of burrs on the features
|
|
# the tool needs to travel on the right side of the features (this is called conventional milling)
|
|
# the first pass is the one cutting all of the features, so it needs to be reversed
|
|
# the other passes overlap preceding ones and cut the left over copper. It is better for them
|
|
# to cut on the right side of the left over copper i.e on the left side of the features.
|
|
geom = self.isolation_geometry(offset)
|
|
if invert:
|
|
if type(geom) is MultiPolygon:
|
|
pl = []
|
|
for p in geom:
|
|
pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
|
|
geom = MultiPolygon(pl)
|
|
elif type(geom) is Polygon:
|
|
geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
|
|
else:
|
|
raise "Unexpected Geometry"
|
|
return geom
|
|
|
|
if combine:
|
|
iso_name = base_name
|
|
|
|
# TODO: This is ugly. Create way to pass data into init function.
|
|
def iso_init(geo_obj, app_obj):
|
|
# Propagate options
|
|
geo_obj.options["cnctooldia"] = self.options["isotooldia"]
|
|
geo_obj.solid_geometry = []
|
|
for i in range(passes):
|
|
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
|
|
geom = generate_envelope (offset, i == 0)
|
|
geo_obj.solid_geometry.append(geom)
|
|
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
|
|
|
|
# TODO: Do something if this is None. Offer changing name?
|
|
self.app.new_object("geometry", iso_name, iso_init)
|
|
|
|
else:
|
|
for i in range(passes):
|
|
|
|
offset = (2 * i + 1) / 2.0 * dia - i * overlap * dia
|
|
if passes > 1:
|
|
iso_name = base_name + str(i + 1)
|
|
else:
|
|
iso_name = base_name
|
|
|
|
# TODO: This is ugly. Create way to pass data into init function.
|
|
def iso_init(geo_obj, app_obj):
|
|
# Propagate options
|
|
geo_obj.options["cnctooldia"] = self.options["isotooldia"]
|
|
geo_obj.solid_geometry = generate_envelope (offset, i == 0)
|
|
app_obj.info("Isolation geometry created: %s" % geo_obj.options["name"])
|
|
|
|
# TODO: Do something if this is None. Offer changing name?
|
|
self.app.new_object("geometry", iso_name, iso_init)
|
|
|
|
def on_plot_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('plot')
|
|
self.plot()
|
|
|
|
def on_solid_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('solid')
|
|
self.plot()
|
|
|
|
def on_multicolored_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('multicolored')
|
|
self.plot()
|
|
|
|
def convert_units(self, units):
|
|
"""
|
|
Converts the units of the object by scaling dimensions in all geometry
|
|
and options.
|
|
|
|
:param units: Units to which to convert the object: "IN" or "MM".
|
|
:type units: str
|
|
:return: None
|
|
:rtype: None
|
|
"""
|
|
|
|
factor = Gerber.convert_units(self, units)
|
|
|
|
self.options['isotooldia'] *= factor
|
|
self.options['cutoutmargin'] *= factor
|
|
self.options['cutoutgapsize'] *= factor
|
|
self.options['noncoppermargin'] *= factor
|
|
self.options['bboxmargin'] *= factor
|
|
|
|
def plot(self):
|
|
|
|
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
|
|
|
|
# Does all the required setup and returns False
|
|
# if the 'ptint' option is set to False.
|
|
if not FlatCAMObj.plot(self):
|
|
return
|
|
|
|
geometry = self.solid_geometry
|
|
|
|
# Make sure geometry is iterable.
|
|
try:
|
|
_ = iter(geometry)
|
|
except TypeError:
|
|
geometry = [geometry]
|
|
|
|
if self.options["multicolored"]:
|
|
linespec = '-'
|
|
else:
|
|
linespec = 'k-'
|
|
|
|
if self.options["solid"]:
|
|
for poly in geometry:
|
|
# TODO: Too many things hardcoded.
|
|
try:
|
|
patch = PolygonPatch(poly,
|
|
facecolor="#BBF268",
|
|
edgecolor="#006E20",
|
|
alpha=0.75,
|
|
zorder=2)
|
|
self.axes.add_patch(patch)
|
|
except AssertionError:
|
|
FlatCAMApp.App.log.warning("A geometry component was not a polygon:")
|
|
FlatCAMApp.App.log.warning(str(poly))
|
|
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()
|
|
|
|
def serialize(self):
|
|
return {
|
|
"options": self.options,
|
|
"kind": self.kind
|
|
}
|
|
|
|
|
|
class FlatCAMExcellon(FlatCAMObj, Excellon):
|
|
"""
|
|
Represents Excellon/Drill code.
|
|
"""
|
|
|
|
ui_type = ExcellonObjectUI
|
|
|
|
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": ""
|
|
"tooldia": 0.1,
|
|
"toolchange": False,
|
|
"toolchangez": 1.0,
|
|
"spindlespeed": None
|
|
})
|
|
|
|
# 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 build_ui(self):
|
|
FlatCAMObj.build_ui(self)
|
|
|
|
# Populate tool list
|
|
n = len(self.tools)
|
|
self.ui.tools_table.setColumnCount(2)
|
|
self.ui.tools_table.setHorizontalHeaderLabels(['#', 'Diameter'])
|
|
self.ui.tools_table.setRowCount(n)
|
|
self.ui.tools_table.setSortingEnabled(False)
|
|
i = 0
|
|
for tool in self.tools:
|
|
id = QtGui.QTableWidgetItem(tool)
|
|
id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
|
|
self.ui.tools_table.setItem(i, 0, id) # Tool name/id
|
|
dia = QtGui.QTableWidgetItem(str(self.tools[tool]['C']))
|
|
dia.setFlags(QtCore.Qt.ItemIsEnabled)
|
|
self.ui.tools_table.setItem(i, 1, dia) # Diameter
|
|
i += 1
|
|
|
|
# sort the tool diameter column
|
|
self.ui.tools_table.sortItems(1)
|
|
# all the tools are selected by default
|
|
self.ui.tools_table.selectColumn(0)
|
|
|
|
self.ui.tools_table.resizeColumnsToContents()
|
|
self.ui.tools_table.resizeRowsToContents()
|
|
self.ui.tools_table.horizontalHeader().setStretchLastSection(True)
|
|
self.ui.tools_table.verticalHeader().hide()
|
|
self.ui.tools_table.setSortingEnabled(True)
|
|
|
|
def set_ui(self, ui):
|
|
"""
|
|
Configures the user interface for this object.
|
|
Connects options to form fields.
|
|
|
|
:param ui: User interface object.
|
|
:type ui: ExcellonObjectUI
|
|
:return: None
|
|
"""
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"solid": self.ui.solid_cb,
|
|
"drillz": self.ui.cutz_entry,
|
|
"travelz": self.ui.travelz_entry,
|
|
"feedrate": self.ui.feedrate_entry,
|
|
"tooldia": self.ui.tooldia_entry,
|
|
"toolchange": self.ui.toolchange_cb,
|
|
"toolchangez": self.ui.toolchangez_entry,
|
|
"spindlespeed": self.ui.spindlespeed_entry
|
|
})
|
|
|
|
assert isinstance(self.ui, ExcellonObjectUI), \
|
|
"Expected a ExcellonObjectUI, got %s" % type(self.ui)
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
|
|
self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
|
|
self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
|
|
|
|
def get_selected_tools_list(self):
|
|
"""
|
|
Returns the keys to the self.tools dictionary corresponding
|
|
to the selections on the tool list in the GUI.
|
|
|
|
:return: List of tools.
|
|
:rtype: list
|
|
"""
|
|
return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
|
|
|
|
def generate_milling(self, tools=None, outname=None, tooldia=None):
|
|
"""
|
|
Note: This method is a good template for generic operations as
|
|
it takes it's options from parameters or otherwise from the
|
|
object's options and returns a success, msg tuple as feedback
|
|
for shell operations.
|
|
|
|
:return: Success/failure condition tuple (bool, str).
|
|
:rtype: tuple
|
|
"""
|
|
|
|
# Get the tools from the list. These are keys
|
|
# to self.tools
|
|
if tools is None:
|
|
tools = self.get_selected_tools_list()
|
|
|
|
if outname is None:
|
|
outname = self.options["name"] + "_mill"
|
|
|
|
if tooldia is None:
|
|
tooldia = self.options["tooldia"]
|
|
|
|
if len(tools) == 0:
|
|
self.app.inform.emit("Please select one or more tools from the list and try again.")
|
|
return False, "Error: No tools."
|
|
|
|
for tool in tools:
|
|
if self.tools[tool]["C"] < tooldia:
|
|
self.app.inform.emit("[warning] Milling tool is larger than hole size. Cancelled.")
|
|
return False, "Error: Milling tool is larger than hole."
|
|
|
|
def geo_init(geo_obj, app_obj):
|
|
assert isinstance(geo_obj, FlatCAMGeometry), \
|
|
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
|
|
app_obj.progress.emit(20)
|
|
|
|
geo_obj.solid_geometry = []
|
|
|
|
for hole in self.drills:
|
|
if hole['tool'] in tools:
|
|
geo_obj.solid_geometry.append(
|
|
Point(hole['point']).buffer(self.tools[hole['tool']]["C"] / 2 -
|
|
tooldia / 2).exterior
|
|
)
|
|
|
|
def geo_thread(app_obj):
|
|
app_obj.new_object("geometry", outname, geo_init)
|
|
app_obj.progress.emit(100)
|
|
|
|
# Create a promise with the new name
|
|
self.app.collection.promise(outname)
|
|
|
|
# Send to worker
|
|
self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
|
|
|
|
return True, ""
|
|
|
|
def on_generate_milling_button_click(self, *args):
|
|
self.app.report_usage("excellon_on_create_milling_button")
|
|
self.read_form()
|
|
|
|
self.generate_milling()
|
|
|
|
def on_create_cncjob_button_click(self, *args):
|
|
self.app.report_usage("excellon_on_create_cncjob_button")
|
|
self.read_form()
|
|
|
|
# Get the tools from the list
|
|
tools = self.get_selected_tools_list()
|
|
|
|
if len(tools) == 0:
|
|
self.app.inform.emit("Please select one or more tools from the list and try again.")
|
|
return
|
|
|
|
job_name = self.options["name"] + "_cnc"
|
|
|
|
# Object initialization function for app.new_object()
|
|
def job_init(job_obj, app_obj):
|
|
assert isinstance(job_obj, FlatCAMCNCjob), \
|
|
"Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
|
|
|
|
app_obj.progress.emit(20)
|
|
job_obj.z_cut = self.options["drillz"]
|
|
job_obj.z_move = self.options["travelz"]
|
|
job_obj.feedrate = self.options["feedrate"]
|
|
job_obj.spindlespeed = self.options["spindlespeed"]
|
|
# There could be more than one drill size...
|
|
# job_obj.tooldia = # TODO: duplicate variable!
|
|
# job_obj.options["tooldia"] =
|
|
|
|
tools_csv = ','.join(tools)
|
|
job_obj.generate_from_excellon_by_tool(self, tools_csv,
|
|
toolchange=self.options["toolchange"],
|
|
toolchangez=self.options["toolchangez"])
|
|
|
|
app_obj.progress.emit(50)
|
|
job_obj.gcode_parse()
|
|
|
|
app_obj.progress.emit(60)
|
|
job_obj.create_geometry()
|
|
|
|
app_obj.progress.emit(80)
|
|
|
|
# To be run in separate thread
|
|
def job_thread(app_obj):
|
|
app_obj.new_object("cncjob", job_name, job_init)
|
|
app_obj.progress.emit(100)
|
|
|
|
# Create promise for the new name.
|
|
self.app.collection.promise(job_name)
|
|
|
|
# Send to worker
|
|
# self.app.worker.add_task(job_thread, [self.app])
|
|
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
|
|
|
def on_plot_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('plot')
|
|
self.plot()
|
|
|
|
def on_solid_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('solid')
|
|
self.plot()
|
|
|
|
def convert_units(self, units):
|
|
factor = Excellon.convert_units(self, units)
|
|
|
|
self.options['drillz'] *= factor
|
|
self.options['travelz'] *= factor
|
|
self.options['feedrate'] *= factor
|
|
|
|
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()
|
|
|
|
|
|
class FlatCAMCNCjob(FlatCAMObj, CNCjob):
|
|
"""
|
|
Represents G-Code.
|
|
"""
|
|
|
|
ui_type = CNCObjectUI
|
|
|
|
def __init__(self, name, units="in", kind="generic", z_move=0.1,
|
|
feedrate=3.0, z_cut=-0.002, tooldia=0.0,
|
|
spindlespeed=None):
|
|
|
|
FlatCAMApp.App.log.debug("Creating CNCJob object...")
|
|
|
|
CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
|
|
feedrate=feedrate, z_cut=z_cut, tooldia=tooldia,
|
|
spindlespeed=spindlespeed)
|
|
|
|
FlatCAMObj.__init__(self, name)
|
|
|
|
self.kind = "cncjob"
|
|
|
|
self.options.update({
|
|
"plot": True,
|
|
"tooldia": 0.4 / 25.4, # 0.4mm in inches
|
|
"append": "",
|
|
"prepend": ""
|
|
})
|
|
|
|
# Attributes to be included in serialization
|
|
# Always append to it because it carries contents
|
|
# from predecessors.
|
|
self.ser_attrs += ['options', 'kind']
|
|
|
|
def set_ui(self, ui):
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
|
|
|
|
assert isinstance(self.ui, CNCObjectUI), \
|
|
"Expected a CNCObjectUI, got %s" % type(self.ui)
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"tooldia": self.ui.tooldia_entry,
|
|
"append": self.ui.append_text,
|
|
"prepend": self.ui.prepend_text
|
|
})
|
|
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
|
|
self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
|
|
|
|
def on_updateplot_button_click(self, *args):
|
|
"""
|
|
Callback for the "Updata Plot" button. Reads the form for updates
|
|
and plots the object.
|
|
"""
|
|
self.read_form()
|
|
self.plot()
|
|
|
|
def on_exportgcode_button_click(self, *args):
|
|
self.app.report_usage("cncjob_on_exportgcode_button")
|
|
|
|
try:
|
|
filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...",
|
|
directory=self.app.defaults["last_folder"])
|
|
except TypeError:
|
|
filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...")
|
|
|
|
preamble = str(self.ui.prepend_text.get_value())
|
|
postamble = str(self.ui.append_text.get_value())
|
|
|
|
self.export_gcode(filename, preamble=preamble, postamble=postamble)
|
|
|
|
def export_gcode(self, filename, preamble='', postamble=''):
|
|
f = open(filename, 'w')
|
|
f.write(preamble + '\n' + self.gcode + "\n" + postamble)
|
|
f.close()
|
|
|
|
# Just for adding it to the recent files list.
|
|
self.app.file_opened.emit("cncjob", filename)
|
|
|
|
self.app.inform.emit("Saved to: " + filename)
|
|
|
|
def on_plot_cb_click(self, *args):
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('plot')
|
|
self.plot()
|
|
|
|
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)
|
|
FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
|
|
self.options["tooldia"] *= factor
|
|
|
|
|
|
class FlatCAMGeometry(FlatCAMObj, Geometry):
|
|
"""
|
|
Geometric object not associated with a specific
|
|
format.
|
|
"""
|
|
|
|
ui_type = GeometryObjectUI
|
|
|
|
@staticmethod
|
|
def merge(geo_list, geo_final):
|
|
"""
|
|
Merges the geometry of objects in geo_list into
|
|
the geometry of geo_final.
|
|
|
|
:param geo_list: List of FlatCAMGeometry Objects to join.
|
|
:param geo_final: Destination FlatCAMGeometry object.
|
|
:return: None
|
|
"""
|
|
|
|
if geo_final.solid_geometry is None:
|
|
geo_final.solid_geometry = []
|
|
if type(geo_final.solid_geometry) is not list:
|
|
geo_final.solid_geometry = [geo_final.solid_geometry]
|
|
|
|
for geo in geo_list:
|
|
|
|
# Expand lists
|
|
if type(geo) is list:
|
|
FlatCAMGeometry.merge(geo, geo_final)
|
|
|
|
# If not list, just append
|
|
else:
|
|
geo_final.solid_geometry.append(geo.solid_geometry)
|
|
|
|
# try: # Iterable
|
|
# for shape in geo.solid_geometry:
|
|
# geo_final.solid_geometry.append(shape)
|
|
#
|
|
# except TypeError: # Non-iterable
|
|
# geo_final.solid_geometry.append(geo.solid_geometry)
|
|
|
|
def __init__(self, name):
|
|
FlatCAMObj.__init__(self, name)
|
|
Geometry.__init__(self)
|
|
|
|
self.kind = "geometry"
|
|
|
|
self.options.update({
|
|
"plot": True,
|
|
"cutz": -0.002,
|
|
"travelz": 0.1,
|
|
"feedrate": 5.0,
|
|
"spindlespeed": None,
|
|
"cnctooldia": 0.4 / 25.4,
|
|
"painttooldia": 0.0625,
|
|
"paintoverlap": 0.15,
|
|
"paintmargin": 0.01,
|
|
"paintmethod": "standard",
|
|
"multidepth": False,
|
|
"depthperpass": 0.002
|
|
})
|
|
|
|
# Attributes to be included in serialization
|
|
# Always append to it because it carries contents
|
|
# from predecessors.
|
|
self.ser_attrs += ['options', 'kind']
|
|
|
|
def build_ui(self):
|
|
FlatCAMObj.build_ui(self)
|
|
|
|
def set_ui(self, ui):
|
|
FlatCAMObj.set_ui(self, ui)
|
|
|
|
FlatCAMApp.App.log.debug("FlatCAMGeometry.set_ui()")
|
|
|
|
assert isinstance(self.ui, GeometryObjectUI), \
|
|
"Expected a GeometryObjectUI, got %s" % type(self.ui)
|
|
|
|
self.form_fields.update({
|
|
"plot": self.ui.plot_cb,
|
|
"cutz": self.ui.cutz_entry,
|
|
"travelz": self.ui.travelz_entry,
|
|
"feedrate": self.ui.cncfeedrate_entry,
|
|
"spindlespeed": self.ui.cncspindlespeed_entry,
|
|
"cnctooldia": self.ui.cnctooldia_entry,
|
|
"painttooldia": self.ui.painttooldia_entry,
|
|
"paintoverlap": self.ui.paintoverlap_entry,
|
|
"paintmargin": self.ui.paintmargin_entry,
|
|
"paintmethod": self.ui.paintmethod_combo,
|
|
"multidepth": self.ui.mpass_cb,
|
|
"depthperpass": self.ui.maxdepth_entry
|
|
})
|
|
|
|
self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
|
|
self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
|
|
self.ui.generate_paint_button.clicked.connect(self.on_paint_button_click)
|
|
|
|
def on_paint_button_click(self, *args):
|
|
self.app.report_usage("geometry_on_paint_button")
|
|
|
|
self.app.info("Click inside the desired polygon.")
|
|
self.read_form()
|
|
tooldia = self.options["painttooldia"]
|
|
overlap = self.options["paintoverlap"]
|
|
|
|
# Connection ID for the click event
|
|
subscription = None
|
|
|
|
# To be called after clicking on the plot.
|
|
def doit(event):
|
|
self.app.info("Painting polygon...")
|
|
self.app.plotcanvas.mpl_disconnect(subscription)
|
|
point = [event.xdata, event.ydata]
|
|
self.paint_poly(point, tooldia, overlap)
|
|
|
|
subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit)
|
|
|
|
def paint_poly(self, inside_pt, tooldia, overlap):
|
|
|
|
# Which polygon.
|
|
#poly = find_polygon(self.solid_geometry, inside_pt)
|
|
poly = self.find_polygon(inside_pt)
|
|
|
|
# No polygon?
|
|
if poly is None:
|
|
self.app.log.warning('No polygon found.')
|
|
self.app.inform.emit('[warning] No polygon found.')
|
|
return
|
|
|
|
proc = self.app.proc_container.new("Painting polygon.")
|
|
|
|
name = self.options["name"] + "_paint"
|
|
|
|
# Initializes the new geometry object
|
|
def gen_paintarea(geo_obj, app_obj):
|
|
assert isinstance(geo_obj, FlatCAMGeometry), \
|
|
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
|
|
#assert isinstance(app_obj, App)
|
|
|
|
if self.options["paintmethod"] == "seed":
|
|
cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]),
|
|
tooldia, overlap=overlap)
|
|
|
|
else:
|
|
cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]),
|
|
tooldia, overlap=overlap)
|
|
|
|
geo_obj.solid_geometry = list(cp.get_objects())
|
|
geo_obj.options["cnctooldia"] = tooldia
|
|
self.app.inform.emit("Done.")
|
|
|
|
def job_thread(app_obj):
|
|
try:
|
|
app_obj.new_object("geometry", name, gen_paintarea)
|
|
except Exception as e:
|
|
proc.done()
|
|
raise e
|
|
proc.done()
|
|
|
|
self.app.inform.emit("Polygon Paint started ...")
|
|
|
|
# Promise object with the new name
|
|
self.app.collection.promise(name)
|
|
|
|
# Background
|
|
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
|
|
|
def on_generatecnc_button_click(self, *args):
|
|
self.app.report_usage("geometry_on_generatecnc_button")
|
|
self.read_form()
|
|
self.generatecncjob()
|
|
|
|
def generatecncjob(self,
|
|
z_cut=None,
|
|
z_move=None,
|
|
feedrate=None,
|
|
tooldia=None,
|
|
outname=None,
|
|
spindlespeed=None,
|
|
multidepth=None,
|
|
depthperpass=None):
|
|
"""
|
|
Creates a CNCJob out of this Geometry object. The actual
|
|
work is done by the target FlatCAMCNCjob object's
|
|
`generate_from_geometry_2()` method.
|
|
|
|
:param z_cut: Cut depth (negative)
|
|
:param z_move: Hight of the tool when travelling (not cutting)
|
|
:param feedrate: Feed rate while cutting
|
|
:param tooldia: Tool diameter
|
|
:param outname: Name of the new object
|
|
:param spindlespeed: Spindle speed (RPM)
|
|
:return: None
|
|
"""
|
|
|
|
outname = outname if outname is not None else self.options["name"] + "_cnc"
|
|
z_cut = z_cut if z_cut is not None else self.options["cutz"]
|
|
z_move = z_move if z_move is not None else self.options["travelz"]
|
|
feedrate = feedrate if feedrate is not None else self.options["feedrate"]
|
|
tooldia = tooldia if tooldia is not None else self.options["cnctooldia"]
|
|
multidepth = multidepth if multidepth is not None else self.options["multidepth"]
|
|
depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"]
|
|
|
|
# To allow default value to be "" (optional in gui) and translate to None
|
|
# if not isinstance(spindlespeed, int):
|
|
# if isinstance(self.options["spindlespeed"], int) or \
|
|
# isinstance(self.options["spindlespeed"], float):
|
|
# spindlespeed = int(self.options["spindlespeed"])
|
|
# else:
|
|
# spindlespeed = None
|
|
|
|
if spindlespeed is None:
|
|
# int or None.
|
|
spindlespeed = self.options['spindlespeed']
|
|
|
|
# Object initialization function for app.new_object()
|
|
# RUNNING ON SEPARATE THREAD!
|
|
def job_init(job_obj, app_obj):
|
|
assert isinstance(job_obj, FlatCAMCNCjob), \
|
|
"Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
|
|
|
|
# Propagate options
|
|
job_obj.options["tooldia"] = tooldia
|
|
|
|
app_obj.progress.emit(20)
|
|
job_obj.z_cut = z_cut
|
|
job_obj.z_move = z_move
|
|
job_obj.feedrate = feedrate
|
|
job_obj.spindlespeed = spindlespeed
|
|
app_obj.progress.emit(40)
|
|
# TODO: The tolerance should not be hard coded. Just for testing.
|
|
job_obj.generate_from_geometry_2(self,
|
|
multidepth=multidepth,
|
|
depthpercut=depthperpass,
|
|
tolerance=0.0005)
|
|
|
|
app_obj.progress.emit(50)
|
|
job_obj.gcode_parse()
|
|
|
|
app_obj.progress.emit(80)
|
|
|
|
# To be run in separate thread
|
|
def job_thread(app_obj):
|
|
with self.app.proc_container.new("Generating CNC Job."):
|
|
app_obj.new_object("cncjob", outname, job_init)
|
|
app_obj.inform.emit("CNCjob created: %s" % outname)
|
|
app_obj.progress.emit(100)
|
|
|
|
# Create a promise with the name
|
|
self.app.collection.promise(outname)
|
|
|
|
# Send to worker
|
|
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
|
|
|
|
def on_plot_cb_click(self, *args): # TODO: args not needed
|
|
if self.muted_ui:
|
|
return
|
|
self.read_form_item('plot')
|
|
self.plot()
|
|
|
|
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_element(self, element):
|
|
try:
|
|
for sub_el in element:
|
|
self.plot_element(sub_el)
|
|
|
|
except TypeError: # Element is not iterable...
|
|
|
|
if type(element) == Polygon:
|
|
x, y = element.exterior.coords.xy
|
|
self.axes.plot(x, y, 'r-')
|
|
for ints in element.interiors:
|
|
x, y = ints.coords.xy
|
|
self.axes.plot(x, y, 'r-')
|
|
return
|
|
|
|
if type(element) == LineString or type(element) == LinearRing:
|
|
x, y = element.coords.xy
|
|
self.axes.plot(x, y, 'r-')
|
|
return
|
|
|
|
FlatCAMApp.App.log.warning("Did not plot:" + str(type(element)))
|
|
|
|
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.
|
|
# TODO: This method should not modify the object !!!
|
|
# try:
|
|
# _ = iter(self.solid_geometry)
|
|
# except TypeError:
|
|
# if self.solid_geometry is None:
|
|
# self.solid_geometry = []
|
|
# else:
|
|
# 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
|
|
#
|
|
# FlatCAMApp.App.log.warning("Did not plot:", str(type(geo)))
|
|
|
|
self.plot_element(self.solid_geometry)
|
|
|
|
self.app.plotcanvas.auto_adjust_axes()
|