Merge Geometry. Excellon coordinate parse fix. New GCode generation algorithm. Improved status bar.
This commit is contained in:
@@ -9,6 +9,7 @@ import re
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
import os
|
import os
|
||||||
import Tkinter
|
import Tkinter
|
||||||
|
import re
|
||||||
|
|
||||||
from PyQt4 import QtCore
|
from PyQt4 import QtCore
|
||||||
|
|
||||||
@@ -186,8 +187,12 @@ class App(QtCore.QObject):
|
|||||||
"zoom_out_key": '2',
|
"zoom_out_key": '2',
|
||||||
"zoom_in_key": '3',
|
"zoom_in_key": '3',
|
||||||
"zoom_ratio": 1.5,
|
"zoom_ratio": 1.5,
|
||||||
"point_clipboard_format": "(%.4f, %.4f)"
|
"point_clipboard_format": "(%.4f, %.4f)",
|
||||||
|
"zdownrate": None #
|
||||||
})
|
})
|
||||||
|
|
||||||
|
###############################
|
||||||
|
### Load defaults from file ###
|
||||||
self.load_defaults()
|
self.load_defaults()
|
||||||
|
|
||||||
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
@@ -195,6 +200,8 @@ class App(QtCore.QObject):
|
|||||||
self.defaults['serial'] = ''.join([random.choice(chars) for i in range(20)])
|
self.defaults['serial'] = ''.join([random.choice(chars) for i in range(20)])
|
||||||
self.save_defaults()
|
self.save_defaults()
|
||||||
|
|
||||||
|
self.propagate_defaults()
|
||||||
|
|
||||||
def auto_save_defaults():
|
def auto_save_defaults():
|
||||||
try:
|
try:
|
||||||
self.save_defaults()
|
self.save_defaults()
|
||||||
@@ -543,14 +550,18 @@ class App(QtCore.QObject):
|
|||||||
self.shell.append_error(''.join(traceback.format_exc()))
|
self.shell.append_error(''.join(traceback.format_exc()))
|
||||||
#self.shell.append_error("?\n")
|
#self.shell.append_error("?\n")
|
||||||
|
|
||||||
def info(self, text):
|
def info(self, msg):
|
||||||
"""
|
"""
|
||||||
Writes on the status bar.
|
Writes on the status bar.
|
||||||
|
|
||||||
:param text: Text to write.
|
:param msg: Text to write.
|
||||||
:return: None
|
: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):
|
def load_defaults(self):
|
||||||
"""
|
"""
|
||||||
@@ -783,7 +794,7 @@ class App(QtCore.QObject):
|
|||||||
e = sys.exc_info()[0]
|
e = sys.exc_info()[0]
|
||||||
App.log.error("Could not load defaults file.")
|
App.log.error("Could not load defaults file.")
|
||||||
App.log.error(str(e))
|
App.log.error(str(e))
|
||||||
self.inform.emit("ERROR: Could not load defaults file.")
|
self.inform.emit("[error] Could not load defaults file.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -792,7 +803,7 @@ class App(QtCore.QObject):
|
|||||||
e = sys.exc_info()[0]
|
e = sys.exc_info()[0]
|
||||||
App.log.error("Failed to parse defaults file.")
|
App.log.error("Failed to parse defaults file.")
|
||||||
App.log.error(str(e))
|
App.log.error(str(e))
|
||||||
self.inform.emit("ERROR: Failed to parse defaults file.")
|
self.inform.emit("[error] Failed to parse defaults file.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update options
|
# Update options
|
||||||
@@ -805,7 +816,7 @@ class App(QtCore.QObject):
|
|||||||
json.dump(defaults, f)
|
json.dump(defaults, f)
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except:
|
||||||
self.inform.emit("ERROR: Failed to write defaults to file.")
|
self.inform.emit("[error] Failed to write defaults to file.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.inform.emit("Defaults saved.")
|
self.inform.emit("Defaults saved.")
|
||||||
@@ -1442,10 +1453,14 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
# Opening the file happens here
|
# Opening the file happens here
|
||||||
self.progress.emit(30)
|
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
|
# Further parsing
|
||||||
self.progress.emit(70)
|
self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here
|
||||||
|
|
||||||
# Object name
|
# Object name
|
||||||
name = outname or filename.split('/')[-1].split('\\')[-1]
|
name = outname or filename.split('/')[-1].split('\\')[-1]
|
||||||
@@ -1492,7 +1507,14 @@ class App(QtCore.QObject):
|
|||||||
# How the object should be initialized
|
# How the object should be initialized
|
||||||
def obj_init(excellon_obj, app_obj):
|
def obj_init(excellon_obj, app_obj):
|
||||||
self.progress.emit(20)
|
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()
|
excellon_obj.create_geometry()
|
||||||
self.progress.emit(70)
|
self.progress.emit(70)
|
||||||
|
|
||||||
@@ -1599,14 +1621,14 @@ class App(QtCore.QObject):
|
|||||||
f = open(filename, 'r')
|
f = open(filename, 'r')
|
||||||
except IOError:
|
except IOError:
|
||||||
App.log.error("Failed to open project file: %s" % filename)
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = json.load(f, object_hook=dict2obj)
|
d = json.load(f, object_hook=dict2obj)
|
||||||
except:
|
except:
|
||||||
App.log.error("Failed to parse project file: %s" % filename)
|
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()
|
f.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1633,6 +1655,15 @@ class App(QtCore.QObject):
|
|||||||
self.inform.emit("Project loaded from: " + filename)
|
self.inform.emit("Project loaded from: " + filename)
|
||||||
App.log.debug("Project loaded")
|
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):
|
def plot_all(self):
|
||||||
"""
|
"""
|
||||||
Re-generates all plots from all objects.
|
Re-generates all plots from all objects.
|
||||||
@@ -1948,6 +1979,7 @@ class App(QtCore.QObject):
|
|||||||
def set_sys(param, value):
|
def set_sys(param, value):
|
||||||
if param in self.defaults:
|
if param in self.defaults:
|
||||||
self.defaults[param] = value
|
self.defaults[param] = value
|
||||||
|
self.propagate_defaults()
|
||||||
return
|
return
|
||||||
|
|
||||||
return "ERROR: No such system parameter."
|
return "ERROR: No such system parameter."
|
||||||
@@ -2165,14 +2197,14 @@ class App(QtCore.QObject):
|
|||||||
f = open('recent.json')
|
f = open('recent.json')
|
||||||
except IOError:
|
except IOError:
|
||||||
App.log.error("Failed to load recent item list.")
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.recent = json.load(f)
|
self.recent = json.load(f)
|
||||||
except json.scanner.JSONDecodeError:
|
except json.scanner.JSONDecodeError:
|
||||||
App.log.error("Failed to parse recent item list.")
|
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()
|
f.close()
|
||||||
return
|
return
|
||||||
f.close()
|
f.close()
|
||||||
@@ -2190,11 +2222,13 @@ class App(QtCore.QObject):
|
|||||||
# Create menu items
|
# Create menu items
|
||||||
for recent in self.recent:
|
for recent in self.recent:
|
||||||
filename = recent['filename'].split('/')[-1].split('\\')[-1]
|
filename = recent['filename'].split('/')[-1].split('\\')[-1]
|
||||||
|
|
||||||
action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
|
action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
|
||||||
|
|
||||||
|
# Attach callback
|
||||||
o = make_callback(openers[recent["kind"]], recent['filename'])
|
o = make_callback(openers[recent["kind"]], recent['filename'])
|
||||||
|
|
||||||
action.triggered.connect(o)
|
action.triggered.connect(o)
|
||||||
|
|
||||||
self.ui.recent.addAction(action)
|
self.ui.recent.addAction(action)
|
||||||
|
|
||||||
# self.builder.get_object('open_recent').set_submenu(recent_menu)
|
# self.builder.get_object('open_recent').set_submenu(recent_menu)
|
||||||
@@ -2235,14 +2269,14 @@ class App(QtCore.QObject):
|
|||||||
f = urllib.urlopen(full_url)
|
f = urllib.urlopen(full_url)
|
||||||
except:
|
except:
|
||||||
App.log.warning("Failed checking for latest version. Could not connect.")
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
App.log.error("Could not parse information about latest version.")
|
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))
|
App.log.debug("json.load(): %s" % str(e))
|
||||||
f.close()
|
f.close()
|
||||||
return
|
return
|
||||||
@@ -2251,7 +2285,7 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
if self.version >= data["version"]:
|
if self.version >= data["version"]:
|
||||||
App.log.debug("FlatCAM is up to date!")
|
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
|
return
|
||||||
|
|
||||||
App.log.debug("Newer version available.")
|
App.log.debug("Newer version available.")
|
||||||
@@ -2301,7 +2335,7 @@ class App(QtCore.QObject):
|
|||||||
try:
|
try:
|
||||||
self.collection.get_active().read_form()
|
self.collection.get_active().read_form()
|
||||||
except:
|
except:
|
||||||
self.log.debug("There was no active object")
|
self.log.debug("[warning] There was no active object")
|
||||||
pass
|
pass
|
||||||
# Project options
|
# Project options
|
||||||
self.options_read_form()
|
self.options_read_form()
|
||||||
@@ -2315,14 +2349,14 @@ class App(QtCore.QObject):
|
|||||||
try:
|
try:
|
||||||
f = open(filename, 'w')
|
f = open(filename, 'w')
|
||||||
except IOError:
|
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
|
return
|
||||||
|
|
||||||
# Write
|
# Write
|
||||||
try:
|
try:
|
||||||
json.dump(d, f, default=to_dict)
|
json.dump(d, f, default=to_dict)
|
||||||
except:
|
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()
|
f.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -197,12 +197,14 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||||||
################
|
################
|
||||||
infobar = self.statusBar()
|
infobar = self.statusBar()
|
||||||
|
|
||||||
self.info_label = QtGui.QLabel("Welcome to FlatCAM.")
|
#self.info_label = QtGui.QLabel("Welcome to FlatCAM.")
|
||||||
self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
#self.info_label.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
infobar.addWidget(self.info_label, stretch=1)
|
#infobar.addWidget(self.info_label, stretch=1)
|
||||||
|
self.fcinfo = FlatCAMInfoBar()
|
||||||
|
infobar.addWidget(self.fcinfo, stretch=1)
|
||||||
|
|
||||||
self.position_label = QtGui.QLabel("")
|
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)
|
self.position_label.setMinimumWidth(110)
|
||||||
infobar.addWidget(self.position_label)
|
infobar.addWidget(self.position_label)
|
||||||
|
|
||||||
@@ -233,6 +235,48 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||||||
self.show()
|
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):
|
class OptionsGroupUI(QtGui.QGroupBox):
|
||||||
def __init__(self, title, parent=None):
|
def __init__(self, title, parent=None):
|
||||||
QtGui.QGroupBox.__init__(self, title, parent=parent)
|
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..."))
|
# GLib.idle_add(lambda: app_obj.set_progress_bar(0.4, "Analyzing Geometry..."))
|
||||||
app_obj.progress.emit(40)
|
app_obj.progress.emit(40)
|
||||||
# TODO: The tolerance should not be hard coded. Just for testing.
|
# 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..."))
|
# GLib.idle_add(lambda: app_obj.set_progress_bar(0.5, "Parsing G-Code..."))
|
||||||
app_obj.progress.emit(50)
|
app_obj.progress.emit(50)
|
||||||
|
|||||||
483
camlib.py
483
camlib.py
@@ -5,13 +5,21 @@
|
|||||||
# Date: 2/5/2014 #
|
# Date: 2/5/2014 #
|
||||||
# MIT Licence #
|
# MIT Licence #
|
||||||
############################################################
|
############################################################
|
||||||
|
#from __future__ import division
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
|
from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
import re
|
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
|
# See: http://toblerity.org/shapely/manual.html
|
||||||
from shapely.geometry import Polygon, LineString, Point, LinearRing
|
from shapely.geometry import Polygon, LineString, Point, LinearRing
|
||||||
from shapely.geometry import MultiPoint, MultiPolygon
|
from shapely.geometry import MultiPoint, MultiPolygon
|
||||||
@@ -54,20 +62,17 @@ class Geometry(object):
|
|||||||
# Units (in or mm)
|
# Units (in or mm)
|
||||||
self.units = Geometry.defaults["init_units"]
|
self.units = Geometry.defaults["init_units"]
|
||||||
|
|
||||||
# Final geometry: MultiPolygon
|
# Final geometry: MultiPolygon or list (of geometry constructs)
|
||||||
self.solid_geometry = None
|
self.solid_geometry = None
|
||||||
|
|
||||||
# Attributes to be included in serialization
|
# Attributes to be included in serialization
|
||||||
self.ser_attrs = ['units', 'solid_geometry']
|
self.ser_attrs = ['units', 'solid_geometry']
|
||||||
|
|
||||||
def union(self):
|
# Flattened geometry (list of paths only)
|
||||||
"""
|
self.flat_geometry = []
|
||||||
Runs a cascaded union on the list of objects in
|
|
||||||
solid_geometry.
|
|
||||||
|
|
||||||
:return: None
|
# Flat geometry rtree index
|
||||||
"""
|
self.flat_geometry_rtree = rtindex.Index()
|
||||||
self.solid_geometry = [cascaded_union(self.solid_geometry)]
|
|
||||||
|
|
||||||
def add_circle(self, origin, radius):
|
def add_circle(self, origin, radius):
|
||||||
"""
|
"""
|
||||||
@@ -112,6 +117,68 @@ class Geometry(object):
|
|||||||
print "Failed to run union on polygons."
|
print "Failed to run union on polygons."
|
||||||
raise
|
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):
|
def isolation_geometry(self, offset):
|
||||||
"""
|
"""
|
||||||
Creates contours around geometry at a given
|
Creates contours around geometry at a given
|
||||||
@@ -124,29 +191,6 @@ class Geometry(object):
|
|||||||
"""
|
"""
|
||||||
return self.solid_geometry.buffer(offset)
|
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):
|
def size(self):
|
||||||
"""
|
"""
|
||||||
Returns (width, height) of rectangular
|
Returns (width, height) of rectangular
|
||||||
@@ -260,6 +304,16 @@ class Geometry(object):
|
|||||||
for attr in self.ser_attrs:
|
for attr in self.ser_attrs:
|
||||||
setattr(self, attr, d[attr])
|
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:
|
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()``
|
The constructor takes no parameters. Use ``gerber.parse_files()``
|
||||||
or ``gerber.parse_lines()`` to populate the object from Gerber source.
|
or ``gerber.parse_lines()`` to populate the object from Gerber source.
|
||||||
@@ -778,8 +836,8 @@ class Gerber (Geometry):
|
|||||||
self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
|
self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
|
||||||
self.am2_re = re.compile(r'(.*)%$')
|
self.am2_re = re.compile(r'(.*)%$')
|
||||||
|
|
||||||
# TODO: This is bad.
|
# How to discretize a circle.
|
||||||
self.steps_per_circ = 40
|
self.steps_per_circ = steps_per_circle or Gerber.defaults['steps_per_circle']
|
||||||
|
|
||||||
def scale(self, factor):
|
def scale(self, factor):
|
||||||
"""
|
"""
|
||||||
@@ -1836,8 +1894,13 @@ class CNCjob(Geometry):
|
|||||||
"C" (cut). B is "F" (fast) or "S" (slow).
|
"C" (cut). B is "F" (fast) or "S" (slow).
|
||||||
===================== =========================================
|
===================== =========================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
"zdownrate": None
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, units="in", kind="generic", z_move=0.1,
|
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)
|
Geometry.__init__(self)
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
@@ -1854,6 +1917,11 @@ class CNCjob(Geometry):
|
|||||||
self.input_geometry_bounds = None
|
self.input_geometry_bounds = None
|
||||||
self.gcode_parsed = None
|
self.gcode_parsed = None
|
||||||
self.steps_per_circ = 20 # Used when parsing G-code arcs
|
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
|
# Attributes to be included in serialization
|
||||||
# Always append to it because it carries contents
|
# Always append to it because it carries contents
|
||||||
@@ -1862,6 +1930,34 @@ class CNCjob(Geometry):
|
|||||||
'gcode', 'input_geometry_bounds', 'gcode_parsed',
|
'gcode', 'input_geometry_bounds', 'gcode_parsed',
|
||||||
'steps_per_circ']
|
'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):
|
def convert_units(self, units):
|
||||||
factor = Geometry.convert_units(self, units)
|
factor = Geometry.convert_units(self, units)
|
||||||
log.debug("CNCjob.convert_units()")
|
log.debug("CNCjob.convert_units()")
|
||||||
@@ -1986,14 +2082,17 @@ class CNCjob(Geometry):
|
|||||||
if not append:
|
if not append:
|
||||||
self.gcode = ""
|
self.gcode = ""
|
||||||
|
|
||||||
|
# Initial G-Code
|
||||||
self.gcode = self.unitcode[self.units.upper()] + "\n"
|
self.gcode = self.unitcode[self.units.upper()] + "\n"
|
||||||
self.gcode += self.absolutecode + "\n"
|
self.gcode += self.absolutecode + "\n"
|
||||||
self.gcode += self.feedminutecode + "\n"
|
self.gcode += self.feedminutecode + "\n"
|
||||||
self.gcode += "F%.2f\n" % self.feedrate
|
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 += "M03\n" # Spindle start
|
||||||
self.gcode += self.pausecode + "\n"
|
self.gcode += self.pausecode + "\n"
|
||||||
|
|
||||||
|
# Iterate over geometry and run individual methods
|
||||||
|
# depending on type
|
||||||
for geo in geometry.solid_geometry:
|
for geo in geometry.solid_geometry:
|
||||||
|
|
||||||
if type(geo) == Polygon:
|
if type(geo) == Polygon:
|
||||||
@@ -2005,7 +2104,6 @@ class CNCjob(Geometry):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if type(geo) == Point:
|
if type(geo) == Point:
|
||||||
# TODO: point2gcode does not return anything...
|
|
||||||
self.gcode += self.point2gcode(geo)
|
self.gcode += self.point2gcode(geo)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -2016,6 +2114,74 @@ class CNCjob(Geometry):
|
|||||||
|
|
||||||
log.warning("G-code generation not implemented for %s" % (str(type(geo))))
|
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 Z%.4f\n" % self.z_move # Stop cutting
|
||||||
self.gcode += "G00 X0Y0\n"
|
self.gcode += "G00 X0Y0\n"
|
||||||
self.gcode += "M05\n" # Spindle stop
|
self.gcode += "M05\n" # Spindle stop
|
||||||
@@ -2262,14 +2428,28 @@ class CNCjob(Geometry):
|
|||||||
t = "G0%d X%.4fY%.4f\n"
|
t = "G0%d X%.4fY%.4f\n"
|
||||||
path = list(target_polygon.exterior.coords) # Polygon exterior
|
path = list(target_polygon.exterior.coords) # Polygon exterior
|
||||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
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:]:
|
for pt in path[1:]:
|
||||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||||
for ints in target_polygon.interiors: # Polygon interiors
|
for ints in target_polygon.interiors: # Polygon interiors
|
||||||
path = list(ints.coords)
|
path = list(ints.coords)
|
||||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
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:]:
|
for pt in path[1:]:
|
||||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||||
@@ -2297,20 +2477,34 @@ class CNCjob(Geometry):
|
|||||||
t = "G0%d X%.4fY%.4f\n"
|
t = "G0%d X%.4fY%.4f\n"
|
||||||
path = list(target_linear.coords)
|
path = list(target_linear.coords)
|
||||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
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:]:
|
for pt in path[1:]:
|
||||||
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
gcode += t % (1, pt[0], pt[1]) # Linear motion to point
|
||||||
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||||
return gcode
|
return gcode
|
||||||
|
|
||||||
def point2gcode(self, point):
|
def point2gcode(self, point):
|
||||||
# TODO: This is not doing anything.
|
|
||||||
gcode = ""
|
gcode = ""
|
||||||
t = "G0%d X%.4fY%.4f\n"
|
t = "G0%d X%.4fY%.4f\n"
|
||||||
path = list(point.coords)
|
path = list(point.coords)
|
||||||
gcode += t % (0, path[0][0], path[0][1]) # Move to first point
|
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
|
gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
|
||||||
|
return gcode
|
||||||
|
|
||||||
def scale(self, factor):
|
def scale(self, factor):
|
||||||
"""
|
"""
|
||||||
@@ -2384,6 +2578,7 @@ def get_bounds(geometry_list):
|
|||||||
|
|
||||||
return [xmin, ymin, xmax, ymax]
|
return [xmin, ymin, xmax, ymax]
|
||||||
|
|
||||||
|
|
||||||
def arc(center, radius, start, stop, direction, steps_per_circ):
|
def arc(center, radius, start, stop, direction, steps_per_circ):
|
||||||
"""
|
"""
|
||||||
Creates a list of point along the specified arc.
|
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))
|
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="{{ 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" class="icon icon-home"> {{ project }}</a>-->
|
||||||
<a href="http://flatcam.org">
|
<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;
|
style="height: auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Shell Command Reference
|
|||||||
.. warning::
|
.. warning::
|
||||||
The FlatCAM Shell is under development and its behavior might change in the future. This includes available commands and their syntax.
|
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
|
add_circle
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
Creates a circle in the given Geometry object.
|
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.
|
radius: Radius of the circle.
|
||||||
|
|
||||||
|
.. _add_poly:
|
||||||
|
|
||||||
add_poly
|
add_poly
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
Creates a polygon in the given Geometry object.
|
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.
|
xi, yi: Coordinates of points in the polygon.
|
||||||
|
|
||||||
|
.. _add_rect:
|
||||||
|
|
||||||
add_rect
|
add_rect
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
Creates a rectange in the given Geometry object.
|
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.
|
outname: Name of the output geometry object.
|
||||||
|
|
||||||
|
.. _geo_union:
|
||||||
|
|
||||||
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.
|
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
|
> new
|
||||||
No parameters.
|
No parameters.
|
||||||
|
|
||||||
|
.. _new_geometry:
|
||||||
|
|
||||||
new_geometry
|
new_geometry
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
Creates a new empty geometry object.
|
Creates a new empty geometry object.
|
||||||
@@ -121,6 +131,8 @@ Creates a new empty geometry object.
|
|||||||
> new_geometry <name>
|
> new_geometry <name>
|
||||||
name: New object name
|
name: New object name
|
||||||
|
|
||||||
|
.. _offset:
|
||||||
|
|
||||||
offset
|
offset
|
||||||
~~~~~~
|
~~~~~~
|
||||||
Changes the position of the object.
|
Changes the position of the object.
|
||||||
@@ -200,6 +212,8 @@ Saves the FlatCAM project to file.
|
|||||||
> save_project <filename>
|
> save_project <filename>
|
||||||
filename: Path to file to save.
|
filename: Path to file to save.
|
||||||
|
|
||||||
|
.. _scale:
|
||||||
|
|
||||||
scale
|
scale
|
||||||
~~~~~
|
~~~~~
|
||||||
Resizes the object by a factor.
|
Resizes the object by a factor.
|
||||||
|
|||||||
@@ -1,7 +1,130 @@
|
|||||||
Geometry Editor
|
Geometry Editor
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
The Geometry Editor is a drawing CAD that allows you to edit
|
The Geometry Editor is a drawing CAD that allows you to edit
|
||||||
FlatCAM Geometry Objects or create new ones from scratch. This
|
FlatCAM Geometry Objects or create new ones from scratch. This
|
||||||
provides the ultimate flexibility by letting you specify precisely
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user