Merge Geometry. Excellon coordinate parse fix. New GCode generation algorithm. Improved status bar.

This commit is contained in:
Juan Pablo Caram 2014-11-16 18:32:15 -05:00
parent 5659c3e7bd
commit cea41c827e
7 changed files with 686 additions and 73 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -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]

View File

@ -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;

View File

@ -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.

View File

@ -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.