Merge Geometry. Excellon coordinate parse fix. New GCode generation algorithm. Improved status bar.
This commit is contained in:
parent
5659c3e7bd
commit
cea41c827e
|
@ -9,6 +9,7 @@ import re
|
|||
import webbrowser
|
||||
import os
|
||||
import Tkinter
|
||||
import re
|
||||
|
||||
from PyQt4 import QtCore
|
||||
|
||||
|
@ -186,8 +187,12 @@ class App(QtCore.QObject):
|
|||
"zoom_out_key": '2',
|
||||
"zoom_in_key": '3',
|
||||
"zoom_ratio": 1.5,
|
||||
"point_clipboard_format": "(%.4f, %.4f)"
|
||||
"point_clipboard_format": "(%.4f, %.4f)",
|
||||
"zdownrate": None #
|
||||
})
|
||||
|
||||
###############################
|
||||
### Load defaults from file ###
|
||||
self.load_defaults()
|
||||
|
||||
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||
|
@ -195,6 +200,8 @@ class App(QtCore.QObject):
|
|||
self.defaults['serial'] = ''.join([random.choice(chars) for i in range(20)])
|
||||
self.save_defaults()
|
||||
|
||||
self.propagate_defaults()
|
||||
|
||||
def auto_save_defaults():
|
||||
try:
|
||||
self.save_defaults()
|
||||
|
@ -543,14 +550,18 @@ class App(QtCore.QObject):
|
|||
self.shell.append_error(''.join(traceback.format_exc()))
|
||||
#self.shell.append_error("?\n")
|
||||
|
||||
def info(self, text):
|
||||
def info(self, msg):
|
||||
"""
|
||||
Writes on the status bar.
|
||||
|
||||
:param text: Text to write.
|
||||
:param msg: Text to write.
|
||||
:return: None
|
||||
"""
|
||||
self.ui.info_label.setText(QtCore.QString(text))
|
||||
match = re.search("\[([^\]]+)\](.*)", msg)
|
||||
if match:
|
||||
self.ui.fcinfo.set_status(QtCore.QString(match.group(2)), level=match.group(1))
|
||||
else:
|
||||
self.ui.fcinfo.set_status(QtCore.QString(msg), level="info")
|
||||
|
||||
def load_defaults(self):
|
||||
"""
|
||||
|
@ -783,7 +794,7 @@ class App(QtCore.QObject):
|
|||
e = sys.exc_info()[0]
|
||||
App.log.error("Could not load defaults file.")
|
||||
App.log.error(str(e))
|
||||
self.inform.emit("ERROR: Could not load defaults file.")
|
||||
self.inform.emit("[error] Could not load defaults file.")
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -792,7 +803,7 @@ class App(QtCore.QObject):
|
|||
e = sys.exc_info()[0]
|
||||
App.log.error("Failed to parse defaults file.")
|
||||
App.log.error(str(e))
|
||||
self.inform.emit("ERROR: Failed to parse defaults file.")
|
||||
self.inform.emit("[error] Failed to parse defaults file.")
|
||||
return
|
||||
|
||||
# Update options
|
||||
|
@ -805,7 +816,7 @@ class App(QtCore.QObject):
|
|||
json.dump(defaults, f)
|
||||
f.close()
|
||||
except:
|
||||
self.inform.emit("ERROR: Failed to write defaults to file.")
|
||||
self.inform.emit("[error] Failed to write defaults to file.")
|
||||
return
|
||||
|
||||
self.inform.emit("Defaults saved.")
|
||||
|
@ -1442,10 +1453,14 @@ class App(QtCore.QObject):
|
|||
|
||||
# Opening the file happens here
|
||||
self.progress.emit(30)
|
||||
gerber_obj.parse_file(filename, follow=follow)
|
||||
try:
|
||||
gerber_obj.parse_file(filename, follow=follow)
|
||||
except IOError:
|
||||
app_obj.inform.emit("[error] Failed to open file: " + filename)
|
||||
app_obj.progress.emit(0)
|
||||
|
||||
# Further parsing
|
||||
self.progress.emit(70)
|
||||
self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here
|
||||
|
||||
# Object name
|
||||
name = outname or filename.split('/')[-1].split('\\')[-1]
|
||||
|
@ -1492,7 +1507,14 @@ class App(QtCore.QObject):
|
|||
# How the object should be initialized
|
||||
def obj_init(excellon_obj, app_obj):
|
||||
self.progress.emit(20)
|
||||
excellon_obj.parse_file(filename)
|
||||
|
||||
try:
|
||||
excellon_obj.parse_file(filename)
|
||||
except IOError:
|
||||
app_obj.inform.emit("[error] Cannot open file: " + filename)
|
||||
self.progress.emit(0) # TODO: self and app_bjj mixed
|
||||
raise IOError
|
||||
|
||||
excellon_obj.create_geometry()
|
||||
self.progress.emit(70)
|
||||
|
||||
|
@ -1599,14 +1621,14 @@ class App(QtCore.QObject):
|
|||
f = open(filename, 'r')
|
||||
except IOError:
|
||||
App.log.error("Failed to open project file: %s" % filename)
|
||||
self.inform.emit("ERROR: Failed to open project file: %s" % filename)
|
||||
self.inform.emit("[error] Failed to open project file: %s" % filename)
|
||||
return
|
||||
|
||||
try:
|
||||
d = json.load(f, object_hook=dict2obj)
|
||||
except:
|
||||
App.log.error("Failed to parse project file: %s" % filename)
|
||||
self.inform.emit("ERROR: Failed to parse project file: %s" % filename)
|
||||
self.inform.emit("[error] Failed to parse project file: %s" % filename)
|
||||
f.close()
|
||||
return
|
||||
|
||||
|
@ -1633,6 +1655,15 @@ class App(QtCore.QObject):
|
|||
self.inform.emit("Project loaded from: " + filename)
|
||||
App.log.debug("Project loaded")
|
||||
|
||||
def propagate_defaults(self):
|
||||
|
||||
routes = {
|
||||
"zdownrate": CNCjob
|
||||
}
|
||||
|
||||
for param in routes:
|
||||
routes[param].defaults[param] = self.defaults[param]
|
||||
|
||||
def plot_all(self):
|
||||
"""
|
||||
Re-generates all plots from all objects.
|
||||
|
@ -1948,6 +1979,7 @@ class App(QtCore.QObject):
|
|||
def set_sys(param, value):
|
||||
if param in self.defaults:
|
||||
self.defaults[param] = value
|
||||
self.propagate_defaults()
|
||||
return
|
||||
|
||||
return "ERROR: No such system parameter."
|
||||
|
@ -2165,14 +2197,14 @@ class App(QtCore.QObject):
|
|||
f = open('recent.json')
|
||||
except IOError:
|
||||
App.log.error("Failed to load recent item list.")
|
||||
self.inform.emit("ERROR: Failed to load recent item list.")
|
||||
self.inform.emit("[error] Failed to load recent item list.")
|
||||
return
|
||||
|
||||
try:
|
||||
self.recent = json.load(f)
|
||||
except json.scanner.JSONDecodeError:
|
||||
App.log.error("Failed to parse recent item list.")
|
||||
self.inform.emit("ERROR: Failed to parse recent item list.")
|
||||
self.inform.emit("[error] Failed to parse recent item list.")
|
||||
f.close()
|
||||
return
|
||||
f.close()
|
||||
|
@ -2190,11 +2222,13 @@ class App(QtCore.QObject):
|
|||
# Create menu items
|
||||
for recent in self.recent:
|
||||
filename = recent['filename'].split('/')[-1].split('\\')[-1]
|
||||
|
||||
action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
|
||||
|
||||
# Attach callback
|
||||
o = make_callback(openers[recent["kind"]], recent['filename'])
|
||||
|
||||
action.triggered.connect(o)
|
||||
|
||||
self.ui.recent.addAction(action)
|
||||
|
||||
# self.builder.get_object('open_recent').set_submenu(recent_menu)
|
||||
|
@ -2235,14 +2269,14 @@ class App(QtCore.QObject):
|
|||
f = urllib.urlopen(full_url)
|
||||
except:
|
||||
App.log.warning("Failed checking for latest version. Could not connect.")
|
||||
self.inform.emit("Failed checking for latest version. Could not connect.")
|
||||
self.inform.emit("[warning] Failed checking for latest version. Could not connect.")
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.load(f)
|
||||
except Exception, e:
|
||||
App.log.error("Could not parse information about latest version.")
|
||||
self.inform.emit("Could not parse information about latest version.")
|
||||
self.inform.emit("[error] Could not parse information about latest version.")
|
||||
App.log.debug("json.load(): %s" % str(e))
|
||||
f.close()
|
||||
return
|
||||
|
@ -2251,7 +2285,7 @@ class App(QtCore.QObject):
|
|||
|
||||
if self.version >= data["version"]:
|
||||
App.log.debug("FlatCAM is up to date!")
|
||||
self.inform.emit("FlatCAM is up to date!")
|
||||
self.inform.emit("[success] FlatCAM is up to date!")
|
||||
return
|
||||
|
||||
App.log.debug("Newer version available.")
|
||||
|
@ -2301,7 +2335,7 @@ class App(QtCore.QObject):
|
|||
try:
|
||||
self.collection.get_active().read_form()
|
||||
except:
|
||||
self.log.debug("There was no active object")
|
||||
self.log.debug("[warning] There was no active object")
|
||||
pass
|
||||
# Project options
|
||||
self.options_read_form()
|
||||
|
@ -2315,14 +2349,14 @@ class App(QtCore.QObject):
|
|||
try:
|
||||
f = open(filename, 'w')
|
||||
except IOError:
|
||||
App.log.error("ERROR: Failed to open file for saving:", filename)
|
||||
App.log.error("[error] Failed to open file for saving:", filename)
|
||||
return
|
||||
|
||||
# Write
|
||||
try:
|
||||
json.dump(d, f, default=to_dict)
|
||||
except:
|
||||
App.log.error("ERROR: File open but failed to write:", filename)
|
||||
App.log.error("[error] File open but failed to write:", filename)
|
||||
f.close()
|
||||
return
|
||||
|
||||
|
|
|
@ -197,12 +197,14 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||
################
|
||||
infobar = self.statusBar()
|
||||
|
||||
self.info_label = QtGui.QLabel("Welcome to FlatCAM.")
|
||||
self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
infobar.addWidget(self.info_label, stretch=1)
|
||||
#self.info_label = QtGui.QLabel("Welcome to FlatCAM.")
|
||||
#self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
#infobar.addWidget(self.info_label, stretch=1)
|
||||
self.fcinfo = FlatCAMInfoBar()
|
||||
infobar.addWidget(self.fcinfo, stretch=1)
|
||||
|
||||
self.position_label = QtGui.QLabel("")
|
||||
self.position_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
#self.position_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||
self.position_label.setMinimumWidth(110)
|
||||
infobar.addWidget(self.position_label)
|
||||
|
||||
|
@ -233,6 +235,48 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||
self.show()
|
||||
|
||||
|
||||
class FlatCAMInfoBar(QtGui.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FlatCAMInfoBar, self).__init__(parent=parent)
|
||||
|
||||
self.icon = QtGui.QLabel(self)
|
||||
self.icon.setGeometry(0, 0, 12, 12)
|
||||
self.pmap = QtGui.QPixmap('share/graylight12.png')
|
||||
self.icon.setPixmap(self.pmap)
|
||||
|
||||
layout = QtGui.QHBoxLayout()
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(self.icon)
|
||||
|
||||
self.text = QtGui.QLabel(self)
|
||||
self.text.setText("Hello!")
|
||||
|
||||
layout.addWidget(self.text)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def set_text_(self, text):
|
||||
self.text.setText(text)
|
||||
|
||||
def set_status(self, text, level="info"):
|
||||
level = str(level)
|
||||
self.pmap.fill()
|
||||
if level == "error":
|
||||
self.pmap = QtGui.QPixmap('share/redlight12.png')
|
||||
elif level == "success":
|
||||
self.pmap = QtGui.QPixmap('share/greenlight12.png')
|
||||
elif level == "warning":
|
||||
self.pmap = QtGui.QPixmap('share/yellowlight12.png')
|
||||
else:
|
||||
self.pmap = QtGui.QPixmap('share/graylight12.png')
|
||||
|
||||
self.icon.setPixmap(self.pmap)
|
||||
self.set_text_(text)
|
||||
|
||||
|
||||
class OptionsGroupUI(QtGui.QGroupBox):
|
||||
def __init__(self, title, parent=None):
|
||||
QtGui.QGroupBox.__init__(self, title, parent=parent)
|
||||
|
|
|
@ -988,7 +988,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
|
|||
# GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
|
||||
app_obj.progress.emit(40)
|
||||
# TODO: The tolerance should not be hard coded. Just for testing.
|
||||
job_obj.generate_from_geometry(self, tolerance=0.0005)
|
||||
#job_obj.generate_from_geometry(self, tolerance=0.0005)
|
||||
job_obj.generate_from_geometry_2(self, tolerance=0.0005)
|
||||
|
||||
# GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
|
||||
app_obj.progress.emit(50)
|
||||
|
|
487
camlib.py
487
camlib.py
|
@ -5,13 +5,21 @@
|
|||
# Date: 2/5/2014 #
|
||||
# MIT Licence #
|
||||
############################################################
|
||||
|
||||
#from __future__ import division
|
||||
import traceback
|
||||
|
||||
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
|
||||
from matplotlib.figure import Figure
|
||||
import re
|
||||
|
||||
import collections
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
from scipy.spatial import Delaunay, KDTree
|
||||
|
||||
from rtree import index as rtindex
|
||||
|
||||
# See: http://toblerity.org/shapely/manual.html
|
||||
from shapely.geometry import Polygon, LineString, Point, LinearRing
|
||||
from shapely.geometry import MultiPoint, MultiPolygon
|
||||
|
@ -54,20 +62,17 @@ class Geometry(object):
|
|||
# Units (in or mm)
|
||||
self.units = Geometry.defaults["init_units"]
|
||||
|
||||
# Final geometry: MultiPolygon
|
||||
# Final geometry: MultiPolygon or list (of geometry constructs)
|
||||
self.solid_geometry = None
|
||||
|
||||
# Attributes to be included in serialization
|
||||
self.ser_attrs = ['units', 'solid_geometry']
|
||||
|
||||
def union(self):
|
||||
"""
|
||||
Runs a cascaded union on the list of objects in
|
||||
solid_geometry.
|
||||
# Flattened geometry (list of paths only)
|
||||
self.flat_geometry = []
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.solid_geometry = [cascaded_union(self.solid_geometry)]
|
||||
# Flat geometry rtree index
|
||||
self.flat_geometry_rtree = rtindex.Index()
|
||||
|
||||
def add_circle(self, origin, radius):
|
||||
"""
|
||||
|
@ -112,6 +117,68 @@ class Geometry(object):
|
|||
print "Failed to run union on polygons."
|
||||
raise
|
||||
|
||||
def bounds(self):
|
||||
"""
|
||||
Returns coordinates of rectangular bounds
|
||||
of geometry: (xmin, ymin, xmax, ymax).
|
||||
"""
|
||||
log.debug("Geometry->bounds()")
|
||||
if self.solid_geometry is None:
|
||||
log.debug("solid_geometry is None")
|
||||
log.warning("solid_geometry not computed yet.")
|
||||
return 0, 0, 0, 0
|
||||
|
||||
if type(self.solid_geometry) is list:
|
||||
log.debug("type(solid_geometry) is list")
|
||||
# TODO: This can be done faster. See comment from Shapely mailing lists.
|
||||
if len(self.solid_geometry) == 0:
|
||||
log.debug('solid_geometry is empty []')
|
||||
return 0, 0, 0, 0
|
||||
log.debug('solid_geometry is not empty, returning cascaded union of items')
|
||||
return cascaded_union(self.solid_geometry).bounds
|
||||
else:
|
||||
log.debug("type(solid_geometry) is not list, returning .bounds property")
|
||||
return self.solid_geometry.bounds
|
||||
|
||||
def flatten_to_paths(self, geometry=None, reset=True):
|
||||
"""
|
||||
Creates a list of non-iterable linear geometry elements and
|
||||
indexes them in rtree.
|
||||
|
||||
:param geometry: Iterable geometry
|
||||
:param reset: Wether to clear (True) or append (False) to self.flat_geometry
|
||||
:return: self.flat_geometry, self.flat_geometry_rtree
|
||||
"""
|
||||
|
||||
if geometry is None:
|
||||
geometry = self.solid_geometry
|
||||
|
||||
if reset:
|
||||
self.flat_geometry = []
|
||||
|
||||
try:
|
||||
for geo in geometry:
|
||||
self.flatten_to_paths(geometry=geo, reset=False)
|
||||
except TypeError:
|
||||
if type(geometry) == Polygon:
|
||||
g = geometry.exterior
|
||||
self.flat_geometry.append(g)
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[0])
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[-1])
|
||||
|
||||
for interior in geometry.interiors:
|
||||
g = interior
|
||||
self.flat_geometry.append(g)
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[0])
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[-1])
|
||||
else:
|
||||
g = geometry
|
||||
self.flat_geometry.append(g)
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[0])
|
||||
self.flat_geometry_rtree.insert(len(self.flat_geometry)-1, g.coords[-1])
|
||||
|
||||
return self.flat_geometry, self.flat_geometry_rtree
|
||||
|
||||
def isolation_geometry(self, offset):
|
||||
"""
|
||||
Creates contours around geometry at a given
|
||||
|
@ -124,29 +191,6 @@ class Geometry(object):
|
|||
"""
|
||||
return self.solid_geometry.buffer(offset)
|
||||
|
||||
def bounds(self):
|
||||
"""
|
||||
Returns coordinates of rectangular bounds
|
||||
of geometry: (xmin, ymin, xmax, ymax).
|
||||
"""
|
||||
log.debug("Geometry->bounds()")
|
||||
if self.solid_geometry is None:
|
||||
log.debug("solid_geometry is None")
|
||||
log.warning("solid_geometry not computed yet.")
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
if type(self.solid_geometry) is list:
|
||||
log.debug("type(solid_geometry) is list")
|
||||
# TODO: This can be done faster. See comment from Shapely mailing lists.
|
||||
if len(self.solid_geometry) == 0:
|
||||
log.debug('solid_geometry is empty []')
|
||||
return (0, 0, 0, 0)
|
||||
log.debug('solid_geometry is not empty, returning cascaded union of items')
|
||||
return cascaded_union(self.solid_geometry).bounds
|
||||
else:
|
||||
log.debug("type(solid_geometry) is not list, returning .bounds property")
|
||||
return self.solid_geometry.bounds
|
||||
|
||||
def size(self):
|
||||
"""
|
||||
Returns (width, height) of rectangular
|
||||
|
@ -260,6 +304,16 @@ class Geometry(object):
|
|||
for attr in self.ser_attrs:
|
||||
setattr(self, attr, d[attr])
|
||||
|
||||
def union(self):
|
||||
"""
|
||||
Runs a cascaded union on the list of objects in
|
||||
solid_geometry.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.solid_geometry = [cascaded_union(self.solid_geometry)]
|
||||
|
||||
|
||||
|
||||
class ApertureMacro:
|
||||
"""
|
||||
|
@ -666,7 +720,11 @@ class Gerber (Geometry):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
defaults = {
|
||||
"steps_per_circle": 40
|
||||
}
|
||||
|
||||
def __init__(self, steps_per_circle=None):
|
||||
"""
|
||||
The constructor takes no parameters. Use ``gerber.parse_files()``
|
||||
or ``gerber.parse_lines()`` to populate the object from Gerber source.
|
||||
|
@ -676,7 +734,7 @@ class Gerber (Geometry):
|
|||
"""
|
||||
|
||||
# Initialize parent
|
||||
Geometry.__init__(self)
|
||||
Geometry.__init__(self)
|
||||
|
||||
self.solid_geometry = Polygon()
|
||||
|
||||
|
@ -778,8 +836,8 @@ class Gerber (Geometry):
|
|||
self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
|
||||
self.am2_re = re.compile(r'(.*)%$')
|
||||
|
||||
# TODO: This is bad.
|
||||
self.steps_per_circ = 40
|
||||
# How to discretize a circle.
|
||||
self.steps_per_circ = steps_per_circle or Gerber.defaults['steps_per_circle']
|
||||
|
||||
def scale(self, factor):
|
||||
"""
|
||||
|
@ -1836,8 +1894,13 @@ class CNCjob(Geometry):
|
|||
"C" (cut). B is "F" (fast) or "S" (slow).
|
||||
===================== =========================================
|
||||
"""
|
||||
|
||||
defaults = {
|
||||
"zdownrate": None
|
||||
}
|
||||
|
||||
def __init__(self, units="in", kind="generic", z_move=0.1,
|
||||
feedrate=3.0, z_cut=-0.002, tooldia=0.0):
|
||||
feedrate=3.0, z_cut=-0.002, tooldia=0.0, zdownrate=None):
|
||||
|
||||
Geometry.__init__(self)
|
||||
self.kind = kind
|
||||
|
@ -1854,6 +1917,11 @@ class CNCjob(Geometry):
|
|||
self.input_geometry_bounds = None
|
||||
self.gcode_parsed = None
|
||||
self.steps_per_circ = 20 # Used when parsing G-code arcs
|
||||
if zdownrate is not None:
|
||||
self.zdownrate = float(zdownrate)
|
||||
elif CNCjob.defaults["zdownrate"] is not None:
|
||||
self.zdownrate = float(CNCjob.defaults["zdownrate"])
|
||||
|
||||
|
||||
# Attributes to be included in serialization
|
||||
# Always append to it because it carries contents
|
||||
|
@ -1862,6 +1930,34 @@ class CNCjob(Geometry):
|
|||
'gcode', 'input_geometry_bounds', 'gcode_parsed',
|
||||
'steps_per_circ']
|
||||
|
||||
# Buffer for linear (No polygons or iterable geometry) elements
|
||||
# and their properties.
|
||||
self.flat_geometry = []
|
||||
|
||||
# 2D index of self.flat_geometry
|
||||
self.flat_geometry_rtree = rtindex.Index()
|
||||
|
||||
# Current insert position to flat_geometry
|
||||
self.fg_current_index = 0
|
||||
|
||||
def flatten(self, geo):
|
||||
"""
|
||||
Flattens the input geometry into an array of non-iterable geometry
|
||||
elements and indexes into rtree by their first and last coordinate
|
||||
pairs.
|
||||
|
||||
:param geo:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
for g in geo:
|
||||
self.flatten(g)
|
||||
except TypeError: # is not iterable
|
||||
self.flat_geometry.append({"path": geo})
|
||||
self.flat_geometry_rtree.insert(self.fg_current_index, geo.coords[0])
|
||||
self.flat_geometry_rtree.insert(self.fg_current_index, geo.coords[-1])
|
||||
self.fg_current_index += 1
|
||||
|
||||
def convert_units(self, units):
|
||||
factor = Geometry.convert_units(self, units)
|
||||
log.debug("CNCjob.convert_units()")
|
||||
|
@ -1986,14 +2082,17 @@ class CNCjob(Geometry):
|
|||
if not append:
|
||||
self.gcode = ""
|
||||
|
||||
# Initial G-Code
|
||||
self.gcode = self.unitcode[self.units.upper()] + "\n"
|
||||
self.gcode += self.absolutecode + "\n"
|
||||
self.gcode += self.feedminutecode + "\n"
|
||||
self.gcode += "F%.2f\n" % self.feedrate
|
||||
self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
|
||||
self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height
|
||||
self.gcode += "M03\n" # Spindle start
|
||||
self.gcode += self.pausecode + "\n"
|
||||
|
||||
|
||||
# Iterate over geometry and run individual methods
|
||||
# depending on type
|
||||
for geo in geometry.solid_geometry:
|
||||
|
||||
if type(geo) == Polygon:
|
||||
|
@ -2005,7 +2104,6 @@ class CNCjob(Geometry):
|
|||
continue
|
||||
|
||||
if type(geo) == Point:
|
||||
# TODO: point2gcode does not return anything...
|
||||
self.gcode += self.point2gcode(geo)
|
||||
continue
|
||||
|
||||
|
@ -2016,6 +2114,74 @@ class CNCjob(Geometry):
|
|||
|
||||
log.warning("G-code generation not implemented for %s" % (str(type(geo))))
|
||||
|
||||
# Finish
|
||||
self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
self.gcode += "G00 X0Y0\n"
|
||||
self.gcode += "M05\n" # Spindle stop
|
||||
|
||||
def generate_from_geometry_2(self, geometry, append=True, tooldia=None, tolerance=0):
|
||||
"""
|
||||
Second algorithm to generate from Geometry.
|
||||
|
||||
:param geometry:
|
||||
:param append:
|
||||
:param tooldia:
|
||||
:param tolerance:
|
||||
:return:
|
||||
"""
|
||||
assert isinstance(geometry, Geometry)
|
||||
flat_geometry, rtindex = geometry.flatten_to_paths()
|
||||
|
||||
if tooldia is not None:
|
||||
self.tooldia = tooldia
|
||||
|
||||
self.input_geometry_bounds = geometry.bounds()
|
||||
|
||||
if not append:
|
||||
self.gcode = ""
|
||||
|
||||
# Initial G-Code
|
||||
self.gcode = self.unitcode[self.units.upper()] + "\n"
|
||||
self.gcode += self.absolutecode + "\n"
|
||||
self.gcode += self.feedminutecode + "\n"
|
||||
self.gcode += "F%.2f\n" % self.feedrate
|
||||
self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height
|
||||
self.gcode += "M03\n" # Spindle start
|
||||
self.gcode += self.pausecode + "\n"
|
||||
|
||||
# Iterate over geometry and run individual methods
|
||||
# depending on type
|
||||
# for geo in flat_geometry:
|
||||
#
|
||||
# if type(geo) == LineString or type(geo) == LinearRing:
|
||||
# self.gcode += self.linear2gcode(geo, tolerance=tolerance)
|
||||
# continue
|
||||
#
|
||||
# if type(geo) == Point:
|
||||
# self.gcode += self.point2gcode(geo)
|
||||
# continue
|
||||
#
|
||||
# log.warning("G-code generation not implemented for %s" % (str(type(geo))))
|
||||
|
||||
hits = list(rtindex.nearest((0, 0), 1))
|
||||
while len(hits) > 0:
|
||||
geo = flat_geometry[hits[0]]
|
||||
|
||||
if type(geo) == LineString or type(geo) == LinearRing:
|
||||
self.gcode += self.linear2gcode(geo, tolerance=tolerance)
|
||||
elif type(geo) == Point:
|
||||
self.gcode += self.point2gcode(geo)
|
||||
else:
|
||||
log.warning("G-code generation not implemented for %s" % (str(type(geo))))
|
||||
|
||||
start_pt = geo.coords[0]
|
||||
stop_pt = geo.coords[-1]
|
||||
rtindex.delete(hits[0], start_pt)
|
||||
rtindex.delete(hits[0], stop_pt)
|
||||
hits = list(rtindex.nearest(stop_pt, 1))
|
||||
|
||||
|
||||
# Finish
|
||||
self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
self.gcode += "G00 X0Y0\n"
|
||||
self.gcode += "M05\n" # Spindle stop
|
||||
|
@ -2262,14 +2428,28 @@ class CNCjob(Geometry):
|
|||
t = "G0%d X%.4fY%.4f\n"
|
||||
path = list(target_polygon.exterior.coords) # Polygon exterior
|
||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
if self.zdownrate is not None:
|
||||
gcode += "F%.2f\n" % self.zdownrate
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
gcode += "F%.2f\n" % self.feedrate
|
||||
else:
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
for pt in path[1:]:
|
||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
for ints in target_polygon.interiors: # Polygon interiors
|
||||
path = list(ints.coords)
|
||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
if self.zdownrate is not None:
|
||||
gcode += "F%.2f\n" % self.zdownrate
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
gcode += "F%.2f\n" % self.feedrate
|
||||
else:
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
for pt in path[1:]:
|
||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
|
@ -2297,20 +2477,34 @@ class CNCjob(Geometry):
|
|||
t = "G0%d X%.4fY%.4f\n"
|
||||
path = list(target_linear.coords)
|
||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
if self.zdownrate is not None:
|
||||
gcode += "F%.2f\n" % self.zdownrate
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
gcode += "F%.2f\n" % self.feedrate
|
||||
else:
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
for pt in path[1:]:
|
||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
return gcode
|
||||
|
||||
def point2gcode(self, point):
|
||||
# TODO: This is not doing anything.
|
||||
gcode = ""
|
||||
t = "G0%d X%.4fY%.4f\n"
|
||||
path = list(point.coords)
|
||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
if self.zdownrate is not None:
|
||||
gcode += "F%.2f\n" % self.zdownrate
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
gcode += "F%.2f\n" % self.feedrate
|
||||
else:
|
||||
gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
|
||||
|
||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||
return gcode
|
||||
|
||||
def scale(self, factor):
|
||||
"""
|
||||
|
@ -2384,6 +2578,7 @@ def get_bounds(geometry_list):
|
|||
|
||||
return [xmin, ymin, xmax, ymax]
|
||||
|
||||
|
||||
def arc(center, radius, start, stop, direction, steps_per_circ):
|
||||
"""
|
||||
Creates a list of point along the specified arc.
|
||||
|
@ -2552,3 +2747,205 @@ def parse_gerber_number(strnumber, frac_digits):
|
|||
"""
|
||||
return int(strnumber)*(10**(-frac_digits))
|
||||
|
||||
|
||||
def voronoi(P):
|
||||
"""
|
||||
Returns a list of all edges of the voronoi diagram for the given input points.
|
||||
"""
|
||||
delauny = Delaunay(P)
|
||||
triangles = delauny.points[delauny.vertices]
|
||||
|
||||
circum_centers = np.array([triangle_csc(tri) for tri in triangles])
|
||||
long_lines_endpoints = []
|
||||
|
||||
lineIndices = []
|
||||
for i, triangle in enumerate(triangles):
|
||||
circum_center = circum_centers[i]
|
||||
for j, neighbor in enumerate(delauny.neighbors[i]):
|
||||
if neighbor != -1:
|
||||
lineIndices.append((i, neighbor))
|
||||
else:
|
||||
ps = triangle[(j+1)%3] - triangle[(j-1)%3]
|
||||
ps = np.array((ps[1], -ps[0]))
|
||||
|
||||
middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
|
||||
di = middle - triangle[j]
|
||||
|
||||
ps /= np.linalg.norm(ps)
|
||||
di /= np.linalg.norm(di)
|
||||
|
||||
if np.dot(di, ps) < 0.0:
|
||||
ps *= -1000.0
|
||||
else:
|
||||
ps *= 1000.0
|
||||
|
||||
long_lines_endpoints.append(circum_center + ps)
|
||||
lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
|
||||
|
||||
vertices = np.vstack((circum_centers, long_lines_endpoints))
|
||||
|
||||
# filter out any duplicate lines
|
||||
lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
|
||||
lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
|
||||
lineIndicesUnique = np.unique(lineIndicesTupled)
|
||||
|
||||
return vertices, lineIndicesUnique
|
||||
|
||||
|
||||
def triangle_csc(pts):
|
||||
rows, cols = pts.shape
|
||||
|
||||
A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
|
||||
[np.ones((1, rows)), np.zeros((1, 1))]])
|
||||
|
||||
b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
|
||||
x = np.linalg.solve(A,b)
|
||||
bary_coords = x[:-1]
|
||||
return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
|
||||
|
||||
|
||||
def voronoi_cell_lines(points, vertices, lineIndices):
|
||||
"""
|
||||
Returns a mapping from a voronoi cell to its edges.
|
||||
|
||||
:param points: shape (m,2)
|
||||
:param vertices: shape (n,2)
|
||||
:param lineIndices: shape (o,2)
|
||||
:rtype: dict point index -> list of shape (n,2) with vertex indices
|
||||
"""
|
||||
kd = KDTree(points)
|
||||
|
||||
cells = collections.defaultdict(list)
|
||||
for i1, i2 in lineIndices:
|
||||
v1, v2 = vertices[i1], vertices[i2]
|
||||
mid = (v1+v2)/2
|
||||
_, (p1Idx, p2Idx) = kd.query(mid, 2)
|
||||
cells[p1Idx].append((i1, i2))
|
||||
cells[p2Idx].append((i1, i2))
|
||||
|
||||
return cells
|
||||
|
||||
|
||||
def voronoi_edges2polygons(cells):
|
||||
"""
|
||||
Transforms cell edges into polygons.
|
||||
|
||||
:param cells: as returned from voronoi_cell_lines
|
||||
:rtype: dict point index -> list of vertex indices which form a polygon
|
||||
"""
|
||||
|
||||
# first, close the outer cells
|
||||
for pIdx, lineIndices_ in cells.items():
|
||||
dangling_lines = []
|
||||
for i1, i2 in lineIndices_:
|
||||
connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
|
||||
assert 1 <= len(connections) <= 2
|
||||
if len(connections) == 1:
|
||||
dangling_lines.append((i1, i2))
|
||||
assert len(dangling_lines) in [0, 2]
|
||||
if len(dangling_lines) == 2:
|
||||
(i11, i12), (i21, i22) = dangling_lines
|
||||
|
||||
# determine which line ends are unconnected
|
||||
connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
|
||||
i11Unconnected = len(connected) == 0
|
||||
|
||||
connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
|
||||
i21Unconnected = len(connected) == 0
|
||||
|
||||
startIdx = i11 if i11Unconnected else i12
|
||||
endIdx = i21 if i21Unconnected else i22
|
||||
|
||||
cells[pIdx].append((startIdx, endIdx))
|
||||
|
||||
# then, form polygons by storing vertex indices in (counter-)clockwise order
|
||||
polys = dict()
|
||||
for pIdx, lineIndices_ in cells.items():
|
||||
# get a directed graph which contains both directions and arbitrarily follow one of both
|
||||
directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
|
||||
directedGraphMap = collections.defaultdict(list)
|
||||
for (i1, i2) in directedGraph:
|
||||
directedGraphMap[i1].append(i2)
|
||||
orderedEdges = []
|
||||
currentEdge = directedGraph[0]
|
||||
while len(orderedEdges) < len(lineIndices_):
|
||||
i1 = currentEdge[1]
|
||||
i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
|
||||
nextEdge = (i1, i2)
|
||||
orderedEdges.append(nextEdge)
|
||||
currentEdge = nextEdge
|
||||
|
||||
polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
|
||||
|
||||
return polys
|
||||
|
||||
|
||||
def voronoi_polygons(points):
|
||||
"""
|
||||
Returns the voronoi polygon for each input point.
|
||||
|
||||
:param points: shape (n,2)
|
||||
:rtype: list of n polygons where each polygon is an array of vertices
|
||||
"""
|
||||
vertices, lineIndices = voronoi(points)
|
||||
cells = voronoi_cell_lines(points, vertices, lineIndices)
|
||||
polys = voronoi_edges2polygons(cells)
|
||||
polylist = []
|
||||
for i in xrange(len(points)):
|
||||
poly = vertices[np.asarray(polys[i])]
|
||||
polylist.append(poly)
|
||||
return polylist
|
||||
|
||||
|
||||
class Zprofile:
|
||||
def __init__(self):
|
||||
|
||||
# data contains lists of [x, y, z]
|
||||
self.data = []
|
||||
|
||||
# Computed voronoi polygons (shapely)
|
||||
self.polygons = []
|
||||
pass
|
||||
|
||||
def plot_polygons(self):
|
||||
axes = plt.subplot(1, 1, 1)
|
||||
|
||||
plt.axis([-0.05, 1.05, -0.05, 1.05])
|
||||
|
||||
for poly in self.polygons:
|
||||
p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
|
||||
axes.add_patch(p)
|
||||
|
||||
def init_from_csv(self, filename):
|
||||
pass
|
||||
|
||||
def init_from_string(self, zpstring):
|
||||
pass
|
||||
|
||||
def init_from_list(self, zplist):
|
||||
self.data = zplist
|
||||
|
||||
def generate_polygons(self):
|
||||
self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
|
||||
|
||||
def normalize(self, origin):
|
||||
pass
|
||||
|
||||
def paste(self, path):
|
||||
"""
|
||||
Return a list of dictionaries containing the parts of the original
|
||||
path and their z-axis offset.
|
||||
"""
|
||||
|
||||
# At most one region/polygon will contain the path
|
||||
containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
|
||||
|
||||
if len(containing) > 0:
|
||||
return [{"path": path, "z": self.data[containing[0]][2]}]
|
||||
|
||||
# All region indexes that intersect with the path
|
||||
crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
|
||||
|
||||
return [{"path": path.intersection(self.polygons[i]),
|
||||
"z": self.data[i][2]} for i in crossing]
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
<!--<a href="{{ pathto(master_doc) }}" class="icon icon-home"> {{ project }}</a>-->
|
||||
<!--<a href="http://flatcam.org" class="icon icon-home"> {{ project }}</a>-->
|
||||
<a href="http://flatcam.org">
|
||||
<img src="http://flatcam.org/static/images/fcweblogo1_halloween.png"
|
||||
<img src="http://flatcam.org/static/images/fcweblogo1.png"
|
||||
style="height: auto;
|
||||
width: auto;
|
||||
border-radius: 0px;
|
||||
|
|
|
@ -6,6 +6,8 @@ Shell Command Reference
|
|||
.. warning::
|
||||
The FlatCAM Shell is under development and its behavior might change in the future. This includes available commands and their syntax.
|
||||
|
||||
.. _add_circle:
|
||||
|
||||
add_circle
|
||||
~~~~~~~~~~
|
||||
Creates a circle in the given Geometry object.
|
||||
|
@ -17,6 +19,8 @@ Creates a circle in the given Geometry object.
|
|||
|
||||
radius: Radius of the circle.
|
||||
|
||||
.. _add_poly:
|
||||
|
||||
add_poly
|
||||
~~~~~~~~
|
||||
Creates a polygon in the given Geometry object.
|
||||
|
@ -26,6 +30,8 @@ Creates a polygon in the given Geometry object.
|
|||
|
||||
xi, yi: Coordinates of points in the polygon.
|
||||
|
||||
.. _add_rect:
|
||||
|
||||
add_rect
|
||||
~~~~~~~~
|
||||
Creates a rectange in the given Geometry object.
|
||||
|
@ -70,6 +76,8 @@ Creates a geometry object following gerber paths.
|
|||
|
||||
outname: Name of the output geometry object.
|
||||
|
||||
.. _geo_union:
|
||||
|
||||
geo_union
|
||||
~~~~~~~~~
|
||||
Runs a union operation (addition) on the components of the geometry object. For example, if it contains 2 intersecting polygons, this opperation adds them intoa single larger polygon.
|
||||
|
@ -114,6 +122,8 @@ Starts a new project. Clears objects from memory.
|
|||
> new
|
||||
No parameters.
|
||||
|
||||
.. _new_geometry:
|
||||
|
||||
new_geometry
|
||||
~~~~~~~~~~~~
|
||||
Creates a new empty geometry object.
|
||||
|
@ -121,6 +131,8 @@ Creates a new empty geometry object.
|
|||
> new_geometry <name>
|
||||
name: New object name
|
||||
|
||||
.. _offset:
|
||||
|
||||
offset
|
||||
~~~~~~
|
||||
Changes the position of the object.
|
||||
|
@ -200,6 +212,8 @@ Saves the FlatCAM project to file.
|
|||
> save_project <filename>
|
||||
filename: Path to file to save.
|
||||
|
||||
.. _scale:
|
||||
|
||||
scale
|
||||
~~~~~
|
||||
Resizes the object by a factor.
|
||||
|
|
|
@ -1,7 +1,130 @@
|
|||
Geometry Editor
|
||||
===============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The Geometry Editor is a drawing CAD that allows you to edit
|
||||
FlatCAM Geometry Objects or create new ones from scratch. This
|
||||
provides the ultimate flexibility by letting you specify precisely
|
||||
and arbitrarily what you want your CNC router to do.
|
||||
and arbitrarily what you want your CNC router to do.
|
||||
|
||||
Creating New Geometry Objects
|
||||
-----------------------------
|
||||
|
||||
To create a blank Geometry Object, simply click on the menu item
|
||||
**Edit→New Geometry Object** or click the **New Blank Geometry** button on
|
||||
the toolbar. A Geometry object with the name "New Geometry" will
|
||||
be added to your project list.
|
||||
|
||||
.. image:: editor1.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
FlatCAM Shell command :ref:`new_geometry`
|
||||
|
||||
|
||||
Editing Existing Geometry Objects
|
||||
---------------------------------
|
||||
|
||||
To edit a Geometry Object, select it from the project list and
|
||||
click on the menu item **Edit→Edit Geometry** or on the **Edit Geometry**
|
||||
toolbar button.
|
||||
|
||||
This will make a copy of the selected object in the editor and
|
||||
the editor toolbar buttons will become active.
|
||||
|
||||
Changes made to the geometry in the editor will not affect the
|
||||
Geometry Object until the **Edit->Update Geometry** button or
|
||||
**Update Geometry** toolbar button is clicked.
|
||||
This replaces the geometry in the currently selected Geometry
|
||||
Object (which can be different from which the editor copied its
|
||||
contents originally) with the geometry in the editor.
|
||||
|
||||
Selecting Shapes
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
When the **Selection Tool** is active in the toolbar (Hit ``Esc``), clicking on the
|
||||
plot will select the nearest shape. If one shape is inside the other,
|
||||
you might need to move the outer one to get to the inner one. This
|
||||
behavior might be improved in the future.
|
||||
|
||||
Holding the ``Control`` key while clicking will add the nearest shape
|
||||
to the set of selected objects.
|
||||
|
||||
Creating Shapes
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The shape creation tools in the editor are:
|
||||
|
||||
* Circle
|
||||
* Rectangle
|
||||
* Polygon
|
||||
* Path
|
||||
|
||||
.. image:: editor2.png
|
||||
:align: center
|
||||
|
||||
After clicking on the respective toolbar button, follow the instructions
|
||||
on the status bar.
|
||||
|
||||
Shapes that do not require a fixed number of clicks to complete, like
|
||||
polygons and paths, are complete by hitting the ``Space`` key.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The FlatCAM Shell commands :ref:`add_circle`, :ref:`add_poly` and :ref:`add_rect`,
|
||||
create shapes directly on a given Geometry Object.
|
||||
|
||||
Union
|
||||
~~~~~
|
||||
|
||||
Clicking on the **Union** tool after selecting two or more shapes
|
||||
will create a union. For closed shapes, their union is a polygon covering
|
||||
the area that all the selected shapes encompassed. Unions of disjoint shapes
|
||||
can still be created and is equivalent to grouping shapes.
|
||||
|
||||
.. image:: editor_union.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
The FlatCAM Shell command :ref:`geo_union` executes a union of
|
||||
all geometry in a Geometry object.
|
||||
|
||||
Moving and Copying
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The **Move** and **Copy** tools work on selected objects. As soon as the tool
|
||||
is selected (On the toolbar or the ``m`` and ``c`` keys) the reference point
|
||||
is set at the mouse pointer location. Clicking on the plot sets the target
|
||||
location and finalizes the operation. An outline of the shapes is shown
|
||||
while moving the mouse.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The FlatCAM Shell command :ref:`offset` will move (offset) all
|
||||
the geometry in a Geometry Object. This can also be done in
|
||||
the **Selected** panel for selected FlatCAM object.
|
||||
|
||||
Cancelling an operation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Hitting the ``Esc`` key cancels whatever tool/operation is active and
|
||||
selects the **Selection Tool**.
|
||||
|
||||
Deleting selected shapes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Selections are deleted by hitting the ``-`` sign key.
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
.. seealso::
|
||||
|
||||
The FlatCAM Shell command :ref:`scale` changes the size of the
|
||||
geometry in a Geometry Object.
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue