-clean-up before merge

This commit is contained in:
Marius Stanciu 2019-01-03 21:25:08 +02:00 committed by Marius S
parent 421e9766ea
commit e48d2d2f49
266 changed files with 42425 additions and 0 deletions

37
FlatCAM.py Normal file
View File

@ -0,0 +1,37 @@
import sys
from PyQt5 import sip
from PyQt5 import QtGui, QtCore, QtWidgets
from FlatCAMApp import App
from multiprocessing import freeze_support
import VisPyPatches
if sys.platform == "win32":
# cx_freeze 'module win32' workaround
import OpenGL.platform.win32
def debug_trace():
"""
Set a tracepoint in the Python debugger that works with Qt
:return: None
"""
from PyQt5.QtCore import pyqtRemoveInputHook
#from pdb import set_trace
pyqtRemoveInputHook()
#set_trace()
# All X11 calling should be thread safe otherwise we have strange issues
# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads)
# NOTE: Never talk to the GUI from threads! This is why I commented the above.
if __name__ == '__main__':
freeze_support()
debug_trace()
VisPyPatches.apply_patches()
app = QtWidgets.QApplication(sys.argv)
fc = App()
sys.exit(app.exec_())

5945
FlatCAMApp.py Normal file

File diff suppressed because it is too large Load Diff

48
FlatCAMCommon.py Normal file
View File

@ -0,0 +1,48 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
class LoudDict(dict):
"""
A Dictionary with a callback for
item changes.
"""
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.callback = lambda x: None
def __setitem__(self, key, value):
"""
Overridden __setitem__ method. Will emit 'changed(QString)'
if the item was changed, with key as parameter.
"""
if key in self and self.__getitem__(key) == value:
return
dict.__setitem__(self, key, value)
self.callback(key)
def update(self, *args, **kwargs):
if len(args) > 1:
raise TypeError("update expected at most 1 arguments, got %d" % len(args))
other = dict(*args, **kwargs)
for key in other:
self[key] = other[key]
def set_change_callback(self, callback):
"""
Assigns a function as callback on item change. The callback
will receive the key of the object that was changed.
:param callback: Function to call on item change.
:type callback: func
:return: None
"""
self.callback = callback

5055
FlatCAMEditor.py Normal file

File diff suppressed because it is too large Load Diff

2364
FlatCAMGUI.py Normal file

File diff suppressed because it is too large Load Diff

4004
FlatCAMObj.py Normal file

File diff suppressed because it is too large Load Diff

28
FlatCAMPool.py Normal file
View File

@ -0,0 +1,28 @@
from PyQt5 import QtCore
from multiprocessing import Pool
import dill
def run_dill_encoded(what):
fun, args = dill.loads(what)
print("load", fun, args)
return fun(*args)
def apply_async(pool, fun, args):
print("...", fun, args)
print("dumps", dill.dumps((fun, args)))
return pool.map_async(run_dill_encoded, (dill.dumps((fun, args)),))
def func1():
print("func")
class WorkerPool(QtCore.QObject):
def __init__(self):
super(WorkerPool, self).__init__()
self.pool = Pool(2)
def add_task(self, task):
print("adding task", task)
# task['fcn'](*task['params'])
# print self.pool.map(task['fcn'], task['params'])
apply_async(self.pool, func1, ())

78
FlatCAMPostProc.py Normal file
View File

@ -0,0 +1,78 @@
from importlib.machinery import SourceFileLoader
import os
from abc import ABCMeta, abstractmethod
from datetime import datetime
import math
#module-root dictionary of postprocessors
import FlatCAMApp
postprocessors = {}
class ABCPostProcRegister(ABCMeta):
#handles postprocessors registration on instantation
def __new__(cls, clsname, bases, attrs):
newclass = super(ABCPostProcRegister, cls).__new__(cls, clsname, bases, attrs)
if object not in bases:
if newclass.__name__ in postprocessors:
FlatCAMApp.App.log.warning('Postprocessor %s has been overriden'%(newclass.__name__))
postprocessors[newclass.__name__] = newclass() # here is your register function
return newclass
class FlatCAMPostProc(object, metaclass=ABCPostProcRegister):
@abstractmethod
def start_code(self, p):
pass
@abstractmethod
def lift_code(self, p):
pass
@abstractmethod
def down_code(self, p):
pass
@abstractmethod
def toolchange_code(self, p):
pass
@abstractmethod
def up_to_zero_code(self, p):
pass
@abstractmethod
def rapid_code(self, p):
pass
@abstractmethod
def linear_code(self, p):
pass
@abstractmethod
def end_code(self, p):
pass
@abstractmethod
def feedrate_code(self, p):
pass
@abstractmethod
def spindle_code(self,p):
pass
@abstractmethod
def spindle_stop_code(self,p):
pass
def load_postprocessors(app):
postprocessors_path_search = [os.path.join(app.data_path,'postprocessors','*.py'),
os.path.join('postprocessors', '*.py')]
import glob
for path_search in postprocessors_path_search:
for file in glob.glob(path_search):
try:
SourceFileLoader('FlatCAMPostProcessor', file).load_module()
except Exception as e:
app.log.error(str(e))
return postprocessors

156
FlatCAMProcess.py Normal file
View File

@ -0,0 +1,156 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
from FlatCAMGUI import FlatCAMActivityView
from PyQt5 import QtCore
import weakref
# import logging
# log = logging.getLogger('base2')
# #log.setLevel(logging.DEBUG)
# log.setLevel(logging.WARNING)
# #log.setLevel(logging.INFO)
# formatter = logging.Formatter('[%(levelname)s] %(message)s')
# handler = logging.StreamHandler()
# handler.setFormatter(formatter)
# log.addHandler(handler)
class FCProcess(object):
app = None
def __init__(self, descr):
self.callbacks = {
"done": []
}
self.descr = descr
self.status = "Active"
def __del__(self):
self.done()
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.app.log.error("Abnormal termination of process!")
self.app.log.error(exc_type)
self.app.log.error(exc_val)
self.app.log.error(exc_tb)
self.done()
def done(self):
for fcn in self.callbacks["done"]:
fcn(self)
def connect(self, callback, event="done"):
if callback not in self.callbacks[event]:
self.callbacks[event].append(callback)
def disconnect(self, callback, event="done"):
try:
self.callbacks[event].remove(callback)
except ValueError:
pass
def set_status(self, status_string):
self.status = status_string
def status_msg(self):
return self.descr
class FCProcessContainer(object):
"""
This is the process container, or controller (as in MVC)
of the Process/Activity tracking.
FCProcessContainer keeps weak references to the FCProcess'es
such that their __del__ method is called when the user
looses track of their reference.
"""
app = None
def __init__(self):
self.procs = []
def add(self, proc):
self.procs.append(weakref.ref(proc))
def new(self, descr):
proc = FCProcess(descr)
proc.connect(self.on_done, event="done")
self.add(proc)
self.on_change(proc)
return proc
def on_change(self, proc):
pass
def on_done(self, proc):
self.remove(proc)
def remove(self, proc):
to_be_removed = []
for pref in self.procs:
if pref() == proc or pref() is None:
to_be_removed.append(pref)
for pref in to_be_removed:
self.procs.remove(pref)
class FCVisibleProcessContainer(QtCore.QObject, FCProcessContainer):
something_changed = QtCore.pyqtSignal()
def __init__(self, view):
assert isinstance(view, FlatCAMActivityView), \
"Expected a FlatCAMActivityView, got %s" % type(view)
FCProcessContainer.__init__(self)
QtCore.QObject.__init__(self)
self.view = view
self.something_changed.connect(self.update_view)
def on_done(self, proc):
self.app.log.debug("FCVisibleProcessContainer.on_done()")
super(FCVisibleProcessContainer, self).on_done(proc)
self.something_changed.emit()
def on_change(self, proc):
self.app.log.debug("FCVisibleProcessContainer.on_change()")
super(FCVisibleProcessContainer, self).on_change(proc)
self.something_changed.emit()
def update_view(self):
if len(self.procs) == 0:
self.view.set_idle()
elif len(self.procs) == 1:
self.view.set_busy(self.procs[0]().status_msg())
else:
self.view.set_busy("%d processes running." % len(self.procs))

85
FlatCAMTool.py Normal file
View File

@ -0,0 +1,85 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
from PyQt5.QtCore import Qt
class FlatCAMTool(QtWidgets.QWidget):
toolName = "FlatCAM Generic Tool"
def __init__(self, app, parent=None):
"""
:param app: The application this tool will run in.
:type app: App
:param parent: Qt Parent
:return: FlatCAMTool
"""
QtWidgets.QWidget.__init__(self, parent)
# self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
self.layout = QtWidgets.QVBoxLayout()
self.setLayout(self.layout)
self.app = app
self.menuAction = None
def install(self, icon=None, separator=None, **kwargs):
before = None
# 'pos' is the menu where the Action has to be installed
# if no 'pos' kwarg is provided then by default our Action will be installed in the menutool
# as it previously was
if 'pos' in kwargs:
pos = kwargs['pos']
else:
pos = self.app.ui.menutool
# 'before' is the Action in the menu stated by 'pos' kwarg, before which we want our Action to be installed
# if 'before' kwarg is not provided, by default our Action will be added in the last place.
if 'before' in kwargs:
before = (kwargs['before'])
# create the new Action
self.menuAction = QtWidgets.QAction(self)
# if provided, add an icon to this Action
if icon is not None:
self.menuAction.setIcon(icon)
# set the text name of the Action, which will be displayed in the menu
self.menuAction.setText(self.toolName)
# add a ToolTip to the new Action
# self.menuAction.setToolTip(self.toolTip) # currently not available
# insert the action in the position specified by 'before' and 'pos' kwargs
pos.insertAction(before, self.menuAction)
# if separator parameter is True add a Separator after the newly created Action
if separator is True:
pos.addSeparator()
self.menuAction.triggered.connect(self.run)
def run(self):
if self.app.tool_tab_locked is True:
return
# Remove anything else in the GUI
self.app.ui.tool_scroll_area.takeWidget()
# Put ourself in the GUI
self.app.ui.tool_scroll_area.setWidget(self)
# Switch notebook to tool page
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
self.show()

67
FlatCAMWorker.py Normal file
View File

@ -0,0 +1,67 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
from PyQt5 import QtCore
class Worker(QtCore.QObject):
"""
Implements a queue of tasks to be carried out in order
in a single independent thread.
"""
# avoid multiple tests for debug availability
pydevd_failed = False
task_completed = QtCore.pyqtSignal(str)
def __init__(self, app, name=None):
super(Worker, self).__init__()
self.app = app
self.name = name
def allow_debug(self):
"""
allow debuging/breakpoints in this threads
should work from PyCharm and PyDev
:return:
"""
if not self.pydevd_failed:
try:
import pydevd
pydevd.settrace(suspend=False, trace_only_current_thread=True)
except ImportError:
self.pydevd_failed=True
def run(self):
# self.app.log.debug("Worker Started!")
self.allow_debug()
# Tasks are queued in the event listener.
self.app.worker_task.connect(self.do_worker_task)
def do_worker_task(self, task):
# self.app.log.debug("Running task: %s" % str(task))
self.allow_debug()
if ('worker_name' in task and task['worker_name'] == self.name) or \
('worker_name' not in task and self.name is None):
try:
task['fcn'](*task['params'])
except Exception as e:
self.app.thread_exception.emit(e)
raise e
finally:
self.task_completed.emit(self.name)
# self.app.log.debug("Task ignored.")

44
FlatCAMWorkerStack.py Normal file
View File

@ -0,0 +1,44 @@
from PyQt5 import QtCore
from FlatCAMWorker import Worker
import multiprocessing
class WorkerStack(QtCore.QObject):
worker_task = QtCore.pyqtSignal(dict) # 'worker_name', 'func', 'params'
thread_exception = QtCore.pyqtSignal(object)
def __init__(self):
super(WorkerStack, self).__init__()
self.workers = []
self.threads = []
self.load = {} # {'worker_name': tasks_count}
# Create workers crew
for i in range(0, 2):
worker = Worker(self, 'Slogger-' + str(i))
thread = QtCore.QThread()
worker.moveToThread(thread)
# worker.connect(thread, QtCore.SIGNAL("started()"), worker.run)
thread.started.connect(worker.run)
worker.task_completed.connect(self.on_task_completed)
thread.start()
self.workers.append(worker)
self.threads.append(thread)
self.load[worker.name] = 0
def __del__(self):
for thread in self.threads:
thread.terminate()
def add_task(self, task):
worker_name = min(self.load, key=self.load.get)
self.load[worker_name] += 1
self.worker_task.emit({'worker_name': worker_name, 'fcn': task['fcn'], 'params': task['params']})
def on_task_completed(self, worker_name):
self.load[str(worker_name)] -= 1

710
GUIElements.py Normal file
View File

@ -0,0 +1,710 @@
from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
from copy import copy
import re
import logging
log = logging.getLogger('base')
EDIT_SIZE_HINT = 70
class RadioSet(QtWidgets.QWidget):
activated_custom = QtCore.pyqtSignal()
def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
"""
The choices are specified as a list of dictionaries containing:
* 'label': Shown in the UI
* 'value': The value returned is selected
:param choices: List of choices. See description.
:param orientation: 'horizontal' (default) of 'vertical'.
:param parent: Qt parent widget.
:type choices: list
"""
super(RadioSet, self).__init__(parent)
self.choices = copy(choices)
if orientation == 'horizontal':
layout = QtWidgets.QHBoxLayout()
else:
layout = QtWidgets.QVBoxLayout()
group = QtWidgets.QButtonGroup(self)
for choice in self.choices:
choice['radio'] = QtWidgets.QRadioButton(choice['label'])
group.addButton(choice['radio'])
layout.addWidget(choice['radio'], stretch=0)
choice['radio'].toggled.connect(self.on_toggle)
layout.setContentsMargins(0, 0, 0, 0)
if stretch is False:
pass
else:
layout.addStretch()
self.setLayout(layout)
self.group_toggle_fn = lambda: None
def on_toggle(self):
# log.debug("Radio toggled")
radio = self.sender()
if radio.isChecked():
self.group_toggle_fn()
self.activated_custom.emit()
return
def get_value(self):
for choice in self.choices:
if choice['radio'].isChecked():
return choice['value']
log.error("No button was toggled in RadioSet.")
return None
def set_value(self, val):
for choice in self.choices:
if choice['value'] == val:
choice['radio'].setChecked(True)
return
log.error("Value given is not part of this RadioSet: %s" % str(val))
# class RadioGroupChoice(QtWidgets.QWidget):
# def __init__(self, label_1, label_2, to_check, hide_list, show_list, parent=None):
# """
# The choices are specified as a list of dictionaries containing:
#
# * 'label': Shown in the UI
# * 'value': The value returned is selected
#
# :param choices: List of choices. See description.
# :param orientation: 'horizontal' (default) of 'vertical'.
# :param parent: Qt parent widget.
# :type choices: list
# """
# super().__init__(parent)
#
# group = QtGui.QButtonGroup(self)
#
# self.lbl1 = label_1
# self.lbl2 = label_2
# self.hide_list = hide_list
# self.show_list = show_list
#
# self.btn1 = QtGui.QRadioButton(str(label_1))
# self.btn2 = QtGui.QRadioButton(str(label_2))
# group.addButton(self.btn1)
# group.addButton(self.btn2)
#
# if to_check == 1:
# self.btn1.setChecked(True)
# else:
# self.btn2.setChecked(True)
#
# self.btn1.toggled.connect(lambda: self.btn_state(self.btn1))
# self.btn2.toggled.connect(lambda: self.btn_state(self.btn2))
#
# def btn_state(self, btn):
# if btn.text() == self.lbl1:
# if btn.isChecked() is True:
# self.show_widgets(self.show_list)
# self.hide_widgets(self.hide_list)
# else:
# self.show_widgets(self.hide_list)
# self.hide_widgets(self.show_list)
#
# def hide_widgets(self, lst):
# for wgt in lst:
# wgt.hide()
#
# def show_widgets(self, lst):
# for wgt in lst:
# wgt.show()
class LengthEntry(QtWidgets.QLineEdit):
def __init__(self, output_units='IN', parent=None):
super(LengthEntry, self).__init__(parent)
self.output_units = output_units
self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
# Unit conversion table OUTPUT-INPUT
self.scales = {
'IN': {'IN': 1.0,
'MM': 1/25.4},
'MM': {'IN': 25.4,
'MM': 1.0}
}
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(LengthEntry, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(LengthEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def returnPressed(self, *args, **kwargs):
val = self.get_value()
if val is not None:
self.set_text(str(val))
else:
log.warning("Could not interpret entry: %s" % self.get_text())
def get_value(self):
raw = str(self.text()).strip(' ')
# match = self.format_re.search(raw)
try:
units = raw[-2:]
units = self.scales[self.output_units][units.upper()]
value = raw[:-2]
return float(eval(value))*units
except IndexError:
value = raw
return float(eval(value))
except KeyError:
value = raw
return float(eval(value))
except:
log.warning("Could not parse value in entry: %s" % str(raw))
return None
def set_value(self, val):
self.setText(str('%.4f' % val))
def sizeHint(self):
default_hint_size = super(LengthEntry, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FloatEntry(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(FloatEntry, self).__init__(parent)
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(FloatEntry, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(FloatEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def returnPressed(self, *args, **kwargs):
val = self.get_value()
if val is not None:
self.set_text(str(val))
else:
log.warning("Could not interpret entry: %s" % self.text())
def get_value(self):
raw = str(self.text()).strip(' ')
evaled = 0.0
try:
evaled = eval(raw)
except:
if evaled is not None:
log.error("Could not evaluate: %s" % str(raw))
return None
return float(evaled)
def set_value(self, val):
if val is not None:
self.setText("%.6f" % val)
else:
self.setText("")
def sizeHint(self):
default_hint_size = super(FloatEntry, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FloatEntry2(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(FloatEntry2, self).__init__(parent)
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(FloatEntry2, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(FloatEntry2, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def get_value(self):
raw = str(self.text()).strip(' ')
evaled = 0.0
try:
evaled = eval(raw)
except:
if evaled is not None:
log.error("Could not evaluate: %s" % str(raw))
return None
return float(evaled)
def set_value(self, val):
self.setText("%.6f" % val)
def sizeHint(self):
default_hint_size = super(FloatEntry2, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class IntEntry(QtWidgets.QLineEdit):
def __init__(self, parent=None, allow_empty=False, empty_val=None):
super(IntEntry, self).__init__(parent)
self.allow_empty = allow_empty
self.empty_val = empty_val
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(IntEntry, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(IntEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def get_value(self):
if self.allow_empty:
if str(self.text()) == "":
return self.empty_val
# make the text() first a float and then int because if text is a float type,
# the int() can't convert directly a "text float" into a int type.
ret_val = float(self.text())
ret_val = int(ret_val)
return ret_val
def set_value(self, val):
if val == self.empty_val and self.allow_empty:
self.setText("")
return
self.setText(str(val))
def sizeHint(self):
default_hint_size = super(IntEntry, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FCEntry(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(FCEntry, self).__init__(parent)
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(FCEntry, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(FCEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def get_value(self):
return str(self.text())
def set_value(self, val):
self.setText(str(val))
def sizeHint(self):
default_hint_size = super(FCEntry, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FCEntry2(FCEntry):
def __init__(self, parent=None):
super(FCEntry2, self).__init__(parent)
self.readyToEdit = True
def set_value(self, val):
self.setText('%.5f' % float(val))
class EvalEntry(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(EvalEntry, self).__init__(parent)
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(EvalEntry, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(EvalEntry, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def returnPressed(self, *args, **kwargs):
val = self.get_value()
if val is not None:
self.setText(str(val))
else:
log.warning("Could not interpret entry: %s" % self.get_text())
def get_value(self):
raw = str(self.text()).strip(' ')
evaled = 0.0
try:
evaled = eval(raw)
except:
if evaled is not None:
log.error("Could not evaluate: %s" % str(raw))
return None
return evaled
def set_value(self, val):
self.setText(str(val))
def sizeHint(self):
default_hint_size = super(EvalEntry, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class EvalEntry2(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super(EvalEntry2, self).__init__(parent)
self.readyToEdit = True
def mousePressEvent(self, e, Parent=None):
super(EvalEntry2, self).mousePressEvent(e) # required to deselect on 2e click
if self.readyToEdit:
self.selectAll()
self.readyToEdit = False
def focusOutEvent(self, e):
super(EvalEntry2, self).focusOutEvent(e) # required to remove cursor on focusOut
self.deselect()
self.readyToEdit = True
def get_value(self):
raw = str(self.text()).strip(' ')
evaled = 0.0
try:
evaled = eval(raw)
except:
if evaled is not None:
log.error("Could not evaluate: %s" % str(raw))
return None
return evaled
def set_value(self, val):
self.setText(str(val))
def sizeHint(self):
default_hint_size = super(EvalEntry2, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FCCheckBox(QtWidgets.QCheckBox):
def __init__(self, label='', parent=None):
super(FCCheckBox, self).__init__(str(label), parent)
def get_value(self):
return self.isChecked()
def set_value(self, val):
self.setChecked(val)
def toggle(self):
self.set_value(not self.get_value())
class FCTextArea(QtWidgets.QPlainTextEdit):
def __init__(self, parent=None):
super(FCTextArea, self).__init__(parent)
def set_value(self, val):
self.setPlainText(val)
def get_value(self):
return str(self.toPlainText())
def sizeHint(self):
default_hint_size = super(FCTextArea, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FCTextAreaRich(QtWidgets.QTextEdit):
def __init__(self, parent=None):
super(FCTextAreaRich, self).__init__(parent)
def set_value(self, val):
self.setText(val)
def get_value(self):
return str(self.toPlainText())
def sizeHint(self):
default_hint_size = super(FCTextAreaRich, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class FCComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(FCComboBox, self).__init__(parent)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
def wheelEvent(self, *args, **kwargs):
pass
def get_value(self):
return str(self.currentText())
def set_value(self, val):
self.setCurrentIndex(self.findText(str(val)))
class FCInputDialog(QtWidgets.QInputDialog):
def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None):
super(FCInputDialog, self).__init__(parent)
self.allow_empty = ok
self.empty_val = val
if title is None:
self.title = 'title'
else:
self.title = title
if text is None:
self.text = 'text'
else:
self.text = text
if min is None:
self.min = 0
else:
self.min = min
if max is None:
self.max = 0
else:
self.max = max
if decimals is None:
self.decimals = 6
else:
self.decimals = decimals
def get_value(self):
self.val,self.ok = self.getDouble(self, self.title, self.text, min=self.min,
max=self.max, decimals=self.decimals)
return [self.val, self.ok]
# "Transform", "Enter the Angle value:"
def set_value(self, val):
pass
class FCButton(QtWidgets.QPushButton):
def __init__(self, parent=None):
super(FCButton, self).__init__(parent)
def get_value(self):
return self.isChecked()
def set_value(self, val):
self.setText(str(val))
class FCTab(QtWidgets.QTabWidget):
def __init__(self, parent=None):
super(FCTab, self).__init__(parent)
self.setTabsClosable(True)
self.tabCloseRequested.connect(self.closeTab)
def deleteTab(self, currentIndex):
widget = self.widget(currentIndex)
if widget is not None:
widget.deleteLater()
self.removeTab(currentIndex)
def closeTab(self, currentIndex):
self.removeTab(currentIndex)
def protectTab(self, currentIndex):
self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
class VerticalScrollArea(QtWidgets.QScrollArea):
"""
This widget extends QtGui.QScrollArea to make a vertical-only
scroll area that also expands horizontally to accomodate
its contents.
"""
def __init__(self, parent=None):
QtWidgets.QScrollArea.__init__(self, parent=parent)
self.setWidgetResizable(True)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
def eventFilter(self, source, event):
"""
The event filter gets automatically installed when setWidget()
is called.
:param source:
:param event:
:return:
"""
if event.type() == QtCore.QEvent.Resize and source == self.widget():
# log.debug("VerticalScrollArea: Widget resized:")
# log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width())
# log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width())
self.setMinimumWidth(self.widget().sizeHint().width() +
self.verticalScrollBar().sizeHint().width())
# if self.verticalScrollBar().isVisible():
# log.debug(" Scroll bar visible")
# self.setMinimumWidth(self.widget().minimumSizeHint().width() +
# self.verticalScrollBar().width())
# else:
# log.debug(" Scroll bar hidden")
# self.setMinimumWidth(self.widget().minimumSizeHint().width())
return QtWidgets.QWidget.eventFilter(self, source, event)
class OptionalInputSection:
def __init__(self, cb, optinputs, logic=True):
"""
Associates the a checkbox with a set of inputs.
:param cb: Checkbox that enables the optional inputs.
:param optinputs: List of widgets that are optional.
:param logic: When True the logic is normal, when False the logic is in reverse
It means that for logic=True, when the checkbox is checked the widgets are Enabled, and
for logic=False, when the checkbox is checked the widgets are Disabled
:return:
"""
assert isinstance(cb, FCCheckBox), \
"Expected an FCCheckBox, got %s" % type(cb)
self.cb = cb
self.optinputs = optinputs
self.logic = logic
self.on_cb_change()
self.cb.stateChanged.connect(self.on_cb_change)
def on_cb_change(self):
if self.cb.checkState():
for widget in self.optinputs:
if self.logic is True:
widget.setEnabled(True)
else:
widget.setEnabled(False)
else:
for widget in self.optinputs:
if self.logic is True:
widget.setEnabled(False)
else:
widget.setEnabled(True)
class FCTable(QtWidgets.QTableWidget):
def __init__(self, parent=None):
super(FCTable, self).__init__(parent)
def sizeHint(self):
default_hint_size = super(FCTable, self).sizeHint()
return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
def getHeight(self):
height = self.horizontalHeader().height()
for i in range(self.rowCount()):
height += self.rowHeight(i)
return height
def getWidth(self):
width = self.verticalHeader().width()
for i in range(self.columnCount()):
width += self.columnWidth(i)
return width
# color is in format QtGui.Qcolor(r, g, b, alfa) with or without alfa
def setColortoRow(self, rowIndex, color):
for j in range(self.columnCount()):
self.item(rowIndex, j).setBackground(color)
# if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
def mousePressEvent(self, event):
if self.itemAt(event.pos()) is None:
self.clearSelection()
else:
QtWidgets.QTableWidget.mousePressEvent(self, event)
def setupContextMenu(self):
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
def addContextMenu(self, entry, call_function, icon=None):
action_name = str(entry)
action = QtWidgets.QAction(self)
action.setText(action_name)
if icon:
assert isinstance(icon, QtGui.QIcon), \
"Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
action.setIcon(icon)
self.addAction(action)
action.triggered.connect(call_function)
class FCSpinner(QtWidgets.QSpinBox):
def __init__(self, parent=None):
super(FCSpinner, self).__init__(parent)
def get_value(self):
return str(self.value())
def set_value(self, val):
try:
k = int(val)
except Exception as e:
raise e
self.setValue(k)
# def sizeHint(self):
# default_hint_size = super(FCSpinner, self).sizeHint()
# return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
class Dialog_box(QtWidgets.QWidget):
def __init__(self, title=None, label=None):
"""
:param title: string with the window title
:param label: string with the message inside the dialog box
"""
super(Dialog_box, self).__init__()
self.location = (0, 0)
self.ok = False
dialog_box = QtWidgets.QInputDialog()
dialog_box.setFixedWidth(270)
self.location, self.ok = dialog_box.getText(self, title, label)

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2014-2018 Juan Pablo Caram
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

815
ObjectCollection.py Normal file
View File

@ -0,0 +1,815 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
# from PyQt5.QtCore import QModelIndex
from FlatCAMObj import *
import inspect # TODO: Remove
import FlatCAMApp
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtCore import Qt
class KeySensitiveListView(QtWidgets.QTreeView):
"""
QtGui.QListView extended to emit a signal on key press.
"""
def __init__(self, app, parent=None):
super(KeySensitiveListView, self).__init__(parent)
self.setHeaderHidden(True)
self.setEditTriggers(QtWidgets.QTreeView.SelectedClicked)
# self.setRootIsDecorated(False)
# self.setExpandsOnDoubleClick(False)
# Enable dragging and dropping onto the GUI
self.setAcceptDrops(True)
self.filename = ""
self.app = app
keyPressed = QtCore.pyqtSignal(int)
def keyPressEvent(self, event):
super(KeySensitiveListView, self).keyPressEvent(event)
self.keyPressed.emit(event.key())
def dragEnterEvent(self, event):
if event.mimeData().hasUrls:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
if event.mimeData().hasUrls:
event.accept()
else:
event.ignore()
def dropEvent(self, event):
if event.mimeData().hasUrls:
event.setDropAction(QtCore.Qt.CopyAction)
event.accept()
for url in event.mimeData().urls():
self.filename = str(url.toLocalFile())
if self.filename == "":
self.app.inform.emit("Open cancelled.")
else:
if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
self.app.worker_task.emit({'fcn': self.app.open_gerber,
'params': [self.filename]})
else:
event.ignore()
if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
self.app.worker_task.emit({'fcn': self.app.open_excellon,
'params': [self.filename]})
else:
event.ignore()
if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
self.app.worker_task.emit({'fcn': self.app.open_gcode,
'params': [self.filename]})
else:
event.ignore()
if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
object_type = 'geometry'
self.app.worker_task.emit({'fcn': self.app.import_svg,
'params': [self.filename, object_type, None]})
if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
object_type = 'geometry'
self.app.worker_task.emit({'fcn': self.app.import_dxf,
'params': [self.filename, object_type, None]})
if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
# self.app.open_project() is not Thread Safe
self.app.open_project(self.filename)
else:
event.ignore()
else:
event.ignore()
class TreeItem:
"""
Item of a tree model
"""
def __init__(self, data, icon=None, obj=None, parent_item=None):
self.parent_item = parent_item
self.item_data = data # Columns string data
self.icon = icon # Decoration
self.obj = obj # FlatCAMObj
self.child_items = []
if parent_item:
parent_item.append_child(self)
def append_child(self, item):
self.child_items.append(item)
item.set_parent_item(self)
def remove_child(self, item):
child = self.child_items.pop(self.child_items.index(item))
child.obj.clear(True)
child.obj.delete()
del child.obj
del child
def remove_children(self):
for child in self.child_items:
child.obj.clear()
child.obj.delete()
del child.obj
del child
self.child_items = []
def child(self, row):
return self.child_items[row]
def child_count(self):
return len(self.child_items)
def column_count(self):
return len(self.item_data)
def data(self, column):
return self.item_data[column]
def row(self):
return self.parent_item.child_items.index(self)
def set_parent_item(self, parent_item):
self.parent_item = parent_item
def __del__(self):
del self.icon
class ObjectCollection(QtCore.QAbstractItemModel):
"""
Object storage and management.
"""
groups = [
("gerber", "Gerber"),
("excellon", "Excellon"),
("geometry", "Geometry"),
("cncjob", "CNC Job")
]
classdict = {
"gerber": FlatCAMGerber,
"excellon": FlatCAMExcellon,
"cncjob": FlatCAMCNCjob,
"geometry": FlatCAMGeometry
}
icon_files = {
"gerber": "share/flatcam_icon16.png",
"excellon": "share/drill16.png",
"cncjob": "share/cnc16.png",
"geometry": "share/geometry16.png"
}
root_item = None
# app = None
def __init__(self, app, parent=None):
QtCore.QAbstractItemModel.__init__(self)
### Icons for the list view
self.icons = {}
for kind in ObjectCollection.icon_files:
self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
# Create root tree view item
self.root_item = TreeItem(["root"])
# Create group items
self.group_items = {}
for kind, title in ObjectCollection.groups:
item = TreeItem([title], self.icons[kind])
self.group_items[kind] = item
self.root_item.append_child(item)
# Create test sub-items
# for i in self.root_item.m_child_items:
# print i.data(0)
# i.append_child(TreeItem(["empty"]))
### Data ###
self.checked_indexes = []
# Names of objects that are expected to become available.
# For example, when the creation of a new object will run
# in the background and will complete some time in the
# future. This is a way to reserve the name and to let other
# tasks know that they have to wait until available.
self.promises = set()
### View
self.view = KeySensitiveListView(app)
self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.view.setModel(self)
self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
font = QtGui.QFont()
font.setPixelSize(12)
font.setFamily("Seagoe UI")
self.view.setFont(font)
## GUI Events
self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change)
self.view.activated.connect(self.on_item_activated)
self.view.keyPressed.connect(self.on_key)
self.view.clicked.connect(self.on_mouse_down)
self.view.customContextMenuRequested.connect(self.on_menu_request)
self.click_modifier = None
def promise(self, obj_name):
FlatCAMApp.App.log.debug("Object %s has been promised." % obj_name)
self.promises.add(obj_name)
def has_promises(self):
return len(self.promises) > 0
def on_key(self, key):
modifiers = QtWidgets.QApplication.keyboardModifiers()
active = self.get_active()
selected = self.get_selected()
if modifiers == QtCore.Qt.ControlModifier:
if key == QtCore.Qt.Key_A:
self.app.on_selectall()
if key == QtCore.Qt.Key_C:
self.app.on_copy_object()
if key == QtCore.Qt.Key_E:
self.app.on_fileopenexcellon()
if key == QtCore.Qt.Key_G:
self.app.on_fileopengerber()
if key == QtCore.Qt.Key_M:
self.app.measurement_tool.run()
if key == QtCore.Qt.Key_O:
self.app.on_file_openproject()
if key == QtCore.Qt.Key_S:
self.app.on_file_saveproject()
return
elif modifiers == QtCore.Qt.ShiftModifier:
# Toggle axis
if key == QtCore.Qt.Key_G:
if self.toggle_axis is False:
self.app.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
self.app.plotcanvas.h_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
self.app.plotcanvas.redraw()
self.app.toggle_axis = True
else:
self.app.plotcanvas.v_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
self.app.plotcanvas.h_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
self.appplotcanvas.redraw()
self.app.toggle_axis = False
# Rotate Object by 90 degree CCW
if key == QtCore.Qt.Key_R:
self.app.on_rotate(silent=True, preset=-90)
return
else:
# Zoom Fit
if key == QtCore.Qt.Key_1:
self.app.on_zoom_fit(None)
# Zoom In
if key == QtCore.Qt.Key_2:
self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], self.app.mouse)
# Zoom Out
if key == QtCore.Qt.Key_3:
self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], self.app.mouse)
# Delete
if key == QtCore.Qt.Key_Delete and active:
# Delete via the application to
# ensure cleanup of the GUI
active.app.on_delete()
# Space = Toggle Active/Inactive
if key == QtCore.Qt.Key_Space:
for select in selected:
select.ui.plot_cb.toggle()
self.app.delete_selection_shape()
# Copy Object Name
if key == QtCore.Qt.Key_C:
self.app.on_copy_name()
# Copy Object Name
if key == QtCore.Qt.Key_E:
self.app.object2editor()
# Grid toggle
if key == QtCore.Qt.Key_G:
self.app.geo_editor.grid_snap_btn.trigger()
# Jump to coords
if key == QtCore.Qt.Key_J:
self.app.on_jump_to()
# Move tool toggle
if key == QtCore.Qt.Key_M:
self.app.move_tool.toggle()
# New Geometry
if key == QtCore.Qt.Key_N:
self.app.on_new_geometry()
# Change Units
if key == QtCore.Qt.Key_Q:
if self.app.options["units"] == 'MM':
self.app.general_options_form.general_group.units_radio.set_value("IN")
else:
self.app.general_options_form.general_group.units_radio.set_value("MM")
self.app.on_toggle_units()
# Rotate Object by 90 degree CW
if key == QtCore.Qt.Key_R:
self.app.on_rotate(silent=True, preset=90)
# Shell toggle
if key == QtCore.Qt.Key_S:
self.app.on_toggle_shell()
# Transform Tool
if key == QtCore.Qt.Key_T:
self.app.transform_tool.run()
# Zoom Fit
if key == QtCore.Qt.Key_V:
self.app.on_zoom_fit(None)
# Mirror on X the selected object(s)
if key == QtCore.Qt.Key_X:
self.app.on_flipx()
# Mirror on Y the selected object(s)
if key == QtCore.Qt.Key_Y:
self.app.on_flipy()
# Show shortcut list
if key == QtCore.Qt.Key_Ampersand:
self.app.on_shortcut_list()
if key == QtCore.Qt.Key_QuoteLeft:
self.app.on_shortcut_list()
return
def on_mouse_down(self, event):
FlatCAMApp.App.log.debug("Mouse button pressed on list")
def on_menu_request(self, pos):
sel = len(self.view.selectedIndexes()) > 0
self.app.ui.menuprojectenable.setEnabled(sel)
self.app.ui.menuprojectdisable.setEnabled(sel)
self.app.ui.menuprojectdelete.setEnabled(sel)
if sel:
self.app.ui.menuprojectgeneratecnc.setVisible(True)
for obj in self.get_selected():
if type(obj) != FlatCAMGeometry:
self.app.ui.menuprojectgeneratecnc.setVisible(False)
else:
self.app.ui.menuprojectgeneratecnc.setVisible(False)
self.app.ui.menuproject.popup(self.view.mapToGlobal(pos))
def index(self, row, column=0, parent=None, *args, **kwargs):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def parent(self, index=None):
if not index.isValid():
return QtCore.QModelIndex()
parent_item = index.internalPointer().parent_item
if parent_item == self.root_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, index=None, *args, **kwargs):
if index.column() > 0:
return 0
if not index.isValid():
parent_item = self.root_item
else:
parent_item = index.internalPointer()
return parent_item.child_count()
def columnCount(self, index=None, *args, **kwargs):
if index.isValid():
return index.internalPointer().column_count()
else:
return self.root_item.column_count()
def data(self, index, role=None):
if not index.isValid():
return None
if role in [Qt.DisplayRole, Qt.EditRole]:
obj = index.internalPointer().obj
if obj:
return obj.options["name"]
else:
return index.internalPointer().data(index.column())
if role == Qt.ForegroundRole:
obj = index.internalPointer().obj
if obj:
return QtGui.QBrush(QtCore.Qt.black) if obj.options["plot"] else QtGui.QBrush(QtCore.Qt.darkGray)
else:
return index.internalPointer().data(index.column())
elif role == Qt.DecorationRole:
icon = index.internalPointer().icon
if icon:
return icon
else:
return QtGui.QPixmap()
else:
return None
def setData(self, index, data, role=None):
if index.isValid():
obj = index.internalPointer().obj
if obj:
old_name = obj.options['name']
# rename the object
obj.options["name"] = str(data)
new_name = obj.options['name']
# update the SHELL auto-completer model data
try:
self.app.myKeywords.remove(old_name)
self.app.myKeywords.append(new_name)
self.app.shell._edit.set_model_data(self.app.myKeywords)
except:
log.debug(
"setData() --> Could not remove the old object name from auto-completer model list")
obj.build_ui()
self.app.inform.emit("Object renamed from %s to %s" % (old_name, new_name))
return True
def flags(self, index):
if not index.isValid():
return 0
# Prevent groups from selection
if not index.internalPointer().obj:
return Qt.ItemIsEnabled
else:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
return QtWidgets.QAbstractItemModel.flags(self, index)
# def data(self, index, role=Qt.Qt.DisplayRole):
# if not index.isValid() or not 0 <= index.row() < self.rowCount():
# return QtCore.QVariant()
# row = index.row()
# if role == Qt.Qt.DisplayRole:
# return self.object_list[row].options["name"]
# if role == Qt.Qt.DecorationRole:
# return self.icons[self.object_list[row].kind]
# # if role == Qt.Qt.CheckStateRole:
# # if row in self.checked_indexes:
# # return Qt.Qt.Checked
# # else:
# # return Qt.Qt.Unchecked
def print_list(self):
for obj in self.get_list():
print(obj)
def append(self, obj, active=False):
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
name = obj.options["name"]
# Check promises and clear if exists
if name in self.promises:
self.promises.remove(name)
FlatCAMApp.App.log.debug("Promised object %s became available." % name)
FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises))
# Prevent same name
while name in self.get_names():
## Create a new name
# Ends with number?
FlatCAMApp.App.log.debug("new_object(): Object name (%s) exists, changing." % name)
match = re.search(r'(.*[^\d])?(\d+)$', name)
if match: # Yes: Increment the number!
base = match.group(1) or ''
num = int(match.group(2))
name = base + str(num + 1)
else: # No: add a number!
name += "_1"
obj.options["name"] = name
obj.set_ui(obj.ui_type())
# Required before appending (Qt MVC)
group = self.group_items[obj.kind]
group_index = self.index(group.row(), 0, QtCore.QModelIndex())
self.beginInsertRows(group_index, group.child_count(), group.child_count())
# Append new item
obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
# Required after appending (Qt MVC)
self.endInsertRows()
# Expand group
if group.child_count() is 1:
self.view.setExpanded(group_index, True)
def get_names(self):
"""
Gets a list of the names of all objects in the collection.
:return: List of names.
:rtype: list
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
return [x.options['name'] for x in self.get_list()]
def get_bounds(self):
"""
Finds coordinates bounding all objects in the collection.
:return: [xmin, ymin, xmax, ymax]
:rtype: list
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()")
# TODO: Move the operation out of here.
xmin = Inf
ymin = Inf
xmax = -Inf
ymax = -Inf
# for obj in self.object_list:
for obj in self.get_list():
try:
gxmin, gymin, gxmax, gymax = obj.bounds()
xmin = min([xmin, gxmin])
ymin = min([ymin, gymin])
xmax = max([xmax, gxmax])
ymax = max([ymax, gymax])
except:
FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry.")
return [xmin, ymin, xmax, ymax]
def get_by_name(self, name, isCaseSensitive=None):
"""
Fetches the FlatCAMObj with the given `name`.
:param name: The name of the object.
:type name: str
:return: The requested object or None if no such object.
:rtype: FlatCAMObj or None
"""
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
if isCaseSensitive is None or isCaseSensitive is True:
for obj in self.get_list():
if obj.options['name'] == name:
return obj
else:
for obj in self.get_list():
if obj.options['name'].lower() == name.lower():
return obj
return None
def delete_active(self):
selections = self.view.selectedIndexes()
if len(selections) == 0:
return
active = selections[0].internalPointer()
group = active.parent_item
# update the SHELL auto-completer model data
name = active.obj.options['name']
try:
self.app.myKeywords.remove(name)
self.app.shell._edit.set_model_data(self.app.myKeywords)
except:
log.debug(
"delete_active() --> Could not remove the old object name from auto-completer model list")
self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), active.row(), active.row())
group.remove_child(active)
# after deletion of object store the current list of objects into the self.app.all_objects_list
self.app.all_objects_list = self.get_list()
self.endRemoveRows()
# always go to the Project Tab after object deletion as it may be done with a shortcut key
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
def delete_all(self):
FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
self.beginResetModel()
self.checked_indexes = []
for group in self.root_item.child_items:
group.remove_children()
self.endResetModel()
self.app.plotcanvas.redraw()
self.app.all_objects_list.clear()
self.app.geo_editor.clear()
self.app.exc_editor.clear()
self.app.dblsidedtool.reset_fields()
self.app.panelize_tool.reset_fields()
self.app.cutout_tool.reset_fields()
self.app.film_tool.reset_fields()
def get_active(self):
"""
Returns the active object or None
:return: FlatCAMObj or None
"""
selections = self.view.selectedIndexes()
if len(selections) == 0:
return None
return selections[0].internalPointer().obj
def get_selected(self):
"""
Returns list of objects selected in the view.
:return: List of objects
"""
return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
def get_non_selected(self):
"""
Returns list of objects non-selected in the view.
:return: List of objects
"""
l = self.get_list()
for sel in self.get_selected():
l.remove(sel)
return l
def set_active(self, name):
"""
Selects object by name from the project list. This triggers the
list_selection_changed event and call on_list_selection_changed.
:param name: Name of the FlatCAM Object
:return: None
"""
try:
obj = self.get_by_name(name)
item = obj.item
group = self.group_items[obj.kind]
group_index = self.index(group.row(), 0, QtCore.QModelIndex())
item_index = self.index(item.row(), 0, group_index)
self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Select)
except Exception as e:
log.error("[ERROR] Cause: %s" % str(e))
raise
def set_inactive(self, name):
"""
Unselect object by name from the project list. This triggers the
list_selection_changed event and call on_list_selection_changed.
:param name: Name of the FlatCAM Object
:return: None
"""
obj = self.get_by_name(name)
item = obj.item
group = self.group_items[obj.kind]
group_index = self.index(group.row(), 0, QtCore.QModelIndex())
item_index = self.index(item.row(), 0, group_index)
self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Deselect)
def set_all_inactive(self):
"""
Unselect all objects from the project list. This triggers the
list_selection_changed event and call on_list_selection_changed.
:return: None
"""
for name in self.get_names():
self.set_inactive(name)
def on_list_selection_change(self, current, previous):
FlatCAMApp.App.log.debug("on_list_selection_change()")
FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
try:
obj = current.indexes()[0].internalPointer().obj
except IndexError:
FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
try:
self.app.ui.selected_scroll_area.takeWidget()
except:
FlatCAMApp.App.log.debug("Nothing to remove")
self.app.setup_component_editor()
return
if obj:
obj.build_ui()
def on_item_activated(self, index):
"""
Double-click or Enter on item.
:param index: Index of the item in the list.
:return: None
"""
a_idx = index.internalPointer().obj
if a_idx is None:
return
else:
try:
a_idx.build_ui()
except Exception as e:
self.app.inform.emit("[ERROR] Cause of error: %s" % str(e))
raise
def get_list(self):
obj_list = []
for group in self.root_item.child_items:
for item in group.child_items:
obj_list.append(item.obj)
return obj_list
def update_view(self):
self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

1166
ObjectUI.py Normal file

File diff suppressed because it is too large Load Diff

453
ParseDXF.py Normal file
View File

@ -0,0 +1,453 @@
import re
import itertools
import math
import ezdxf
from shapely.geometry import LinearRing, LineString, Point, Polygon
from shapely.affinity import translate, rotate, scale, skew, affine_transform
import numpy
import logging
log = logging.getLogger('base2')
import FlatCAMApp
from ParseFont import *
from ParseDXF_Spline import *
def distance(pt1, pt2):
return math.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
def dxfpoint2shapely(point):
geo = Point(point.dxf.location).buffer(0.01)
return geo
def dxfline2shapely(line):
try:
start = (line.dxf.start[0], line.dxf.start[1])
stop = (line.dxf.end[0], line.dxf.end[1])
except Exception as e:
log.debug(str(e))
return None
geo = LineString([start, stop])
return geo
def dxfcircle2shapely(circle, n_points=100):
ocs = circle.ocs()
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
if circle.dxf.extrusion != (0, 0, 1):
center_pt = ocs.to_wcs(circle.dxf.center)
else:
center_pt = circle.dxf.center
radius = circle.dxf.radius
geo = Point(center_pt).buffer(radius, int(n_points / 4))
return geo
def dxfarc2shapely(arc, n_points=100):
# ocs = arc.ocs()
# # if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
# if arc.dxf.extrusion != (0, 0, 1):
# arc_center = ocs.to_wcs(arc.dxf.center)
# start_angle = math.radians(arc.dxf.start_angle) + math.pi
# end_angle = math.radians(arc.dxf.end_angle) + math.pi
# dir = 'CW'
# else:
# arc_center = arc.dxf.center
# start_angle = math.radians(arc.dxf.start_angle)
# end_angle = math.radians(arc.dxf.end_angle)
# dir = 'CCW'
#
# center_x = arc_center[0]
# center_y = arc_center[1]
# radius = arc.dxf.radius
#
# point_list = []
#
# if start_angle > end_angle:
# start_angle += 2 * math.pi
#
# line_seg = int((n_points * (end_angle - start_angle)) / math.pi)
# step_angle = (end_angle - start_angle) / float(line_seg)
#
# angle = start_angle
# for step in range(line_seg + 1):
# if dir == 'CCW':
# x = center_x + radius * math.cos(angle)
# y = center_y + radius * math.sin(angle)
# else:
# x = center_x + radius * math.cos(-angle)
# y = center_y + radius * math.sin(-angle)
# point_list.append((x, y))
# angle += step_angle
#
#
# log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f, dir=%s" %
# (center_x, center_y, radius, start_angle, end_angle, step_angle, dir))
#
# geo = LineString(point_list)
# return geo
ocs = arc.ocs()
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
if arc.dxf.extrusion != (0, 0, 1):
arc_center = ocs.to_wcs(arc.dxf.center)
start_angle = arc.dxf.start_angle + 180
end_angle = arc.dxf.end_angle + 180
dir = 'CW'
else:
arc_center = arc.dxf.center
start_angle = arc.dxf.start_angle
end_angle = arc.dxf.end_angle
dir = 'CCW'
center_x = arc_center[0]
center_y = arc_center[1]
radius = arc.dxf.radius
point_list = []
if start_angle > end_angle:
start_angle = start_angle - 360
angle = start_angle
step_angle = float(abs(end_angle - start_angle) / n_points)
while angle <= end_angle:
if dir == 'CCW':
x = center_x + radius * math.cos(math.radians(angle))
y = center_y + radius * math.sin(math.radians(angle))
else:
x = center_x + radius * math.cos(math.radians(-angle))
y = center_y + radius * math.sin(math.radians(-angle))
point_list.append((x, y))
angle += abs(step_angle)
# in case the number of segments do not cover everything until the end of the arc
if angle != end_angle:
if dir == 'CCW':
x = center_x + radius * math.cos(math.radians(end_angle))
y = center_y + radius * math.sin(math.radians(end_angle))
else:
x = center_x + radius * math.cos(math.radians(- end_angle))
y = center_y + radius * math.sin(math.radians(- end_angle))
point_list.append((x, y))
# log.debug("X = %.3f, Y = %.3f, Radius = %.3f, start_angle = %.1f, stop_angle = %.1f, step_angle = %.3f" %
# (center_x, center_y, radius, start_angle, end_angle, step_angle))
geo = LineString(point_list)
return geo
def dxfellipse2shapely(ellipse, ellipse_segments=100):
# center = ellipse.dxf.center
# start_angle = ellipse.dxf.start_param
# end_angle = ellipse.dxf.end_param
ocs = ellipse.ocs()
# if the extrusion attribute is not (0, 0, 1) then we have to change the coordinate system from OCS to WCS
if ellipse.dxf.extrusion != (0, 0, 1):
center = ocs.to_wcs(ellipse.dxf.center)
start_angle = ocs.to_wcs(ellipse.dxf.start_param)
end_angle = ocs.to_wcs(ellipse.dxf.end_param)
dir = 'CW'
else:
center = ellipse.dxf.center
start_angle = ellipse.dxf.start_param
end_angle = ellipse.dxf.end_param
dir = 'CCW'
# print("Dir = %s" % dir)
major_axis = ellipse.dxf.major_axis
ratio = ellipse.dxf.ratio
points_list = []
major_axis = Vector(major_axis)
major_x = major_axis[0]
major_y = major_axis[1]
if start_angle >= end_angle:
end_angle += 2.0 * math.pi
line_seg = int((ellipse_segments * (end_angle - start_angle)) / math.pi)
step_angle = abs(end_angle - start_angle) / float(line_seg)
angle = start_angle
for step in range(line_seg + 1):
if dir == 'CW':
major_dim = normalize_2(major_axis)
minor_dim = normalize_2(Vector([ratio * k for k in major_axis]))
vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
vy = (minor_dim[0] - minor_dim[1]) * math.sin(angle)
x = center[0] + major_x * vx - major_y * vy
y = center[1] + major_y * vx + major_x * vy
angle += step_angle
else:
major_dim = normalize_2(major_axis)
minor_dim = (Vector([ratio * k for k in major_dim]))
vx = (major_dim[0] + major_dim[1]) * math.cos(angle)
vy = (minor_dim[0] + minor_dim[1]) * math.sin(angle)
x = center[0] + major_x * vx + major_y * vy
y = center[1] + major_y * vx + major_x * vy
angle += step_angle
points_list.append((x, y))
geo = LineString(points_list)
return geo
def dxfpolyline2shapely(polyline):
final_pts = []
pts = polyline.points()
for i in pts:
final_pts.append((i[0], i[1]))
if polyline.is_closed:
final_pts.append(final_pts[0])
geo = LineString(final_pts)
return geo
def dxflwpolyline2shapely(lwpolyline):
final_pts = []
for point in lwpolyline:
x, y, _, _, _ = point
final_pts.append((x, y))
if lwpolyline.closed:
final_pts.append(final_pts[0])
geo = LineString(final_pts)
return geo
def dxfsolid2shapely(solid):
iterator = 0
corner_list = []
try:
corner_list.append(solid[iterator])
iterator += 1
except:
return Polygon(corner_list)
def dxfspline2shapely(spline):
with spline.edit_data() as spline_data:
ctrl_points = spline_data.control_points
knot_values = spline_data.knot_values
is_closed = spline.closed
degree = spline.dxf.degree
x_list, y_list, _ = spline2Polyline(ctrl_points, degree=degree, closed=is_closed, segments=20, knots=knot_values)
points_list = zip(x_list, y_list)
geo = LineString(points_list)
return geo
def dxftrace2shapely(trace):
iterator = 0
corner_list = []
try:
corner_list.append(trace[iterator])
iterator += 1
except:
return Polygon(corner_list)
def getdxfgeo(dxf_object):
msp = dxf_object.modelspace()
geos = get_geo(dxf_object, msp)
# geo_block = get_geo_from_block(dxf_object)
return geos
def get_geo_from_insert(dxf_object, insert):
geo_block_transformed = []
phi = insert.dxf.rotation
tr = insert.dxf.insert
sx = insert.dxf.xscale
sy = insert.dxf.yscale
r_count = insert.dxf.row_count
r_spacing = insert.dxf.row_spacing
c_count = insert.dxf.column_count
c_spacing = insert.dxf.column_spacing
# print(phi, tr)
# identify the block given the 'INSERT' type entity name
block = dxf_object.blocks[insert.dxf.name]
block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
# get a list of geometries found in the block
geo_block = get_geo(dxf_object, block)
# iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
for geo in geo_block:
# get the bounds of the geometry
# minx, miny, maxx, maxy = geo.bounds
if tr[0] != 0 or tr[1] != 0:
geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
# support for array block insertions
if r_count > 1:
for r in range(r_count):
geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
if c_count > 1:
for c in range(c_count):
geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
if sx != 1 or sy != 1:
geo = scale(geo, sx, sy)
if phi != 0:
geo = rotate(geo, phi, origin=tr)
geo_block_transformed.append(geo)
return geo_block_transformed
def get_geo(dxf_object, container):
# store shapely geometry here
geo = []
for dxf_entity in container:
g = []
# print("Entity", dxf_entity.dxftype())
if dxf_entity.dxftype() == 'POINT':
g = dxfpoint2shapely(dxf_entity,)
elif dxf_entity.dxftype() == 'LINE':
g = dxfline2shapely(dxf_entity,)
elif dxf_entity.dxftype() == 'CIRCLE':
g = dxfcircle2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'ARC':
g = dxfarc2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'ELLIPSE':
g = dxfellipse2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'LWPOLYLINE':
g = dxflwpolyline2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'POLYLINE':
g = dxfpolyline2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'SOLID':
g = dxfsolid2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'TRACE':
g = dxftrace2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'SPLINE':
g = dxfspline2shapely(dxf_entity)
elif dxf_entity.dxftype() == 'INSERT':
g = get_geo_from_insert(dxf_object, dxf_entity)
else:
log.debug(" %s is not supported yet." % dxf_entity.dxftype())
if g is not None:
if type(g) == list:
for subg in g:
geo.append(subg)
else:
geo.append(g)
return geo
def getdxftext(exf_object, object_type, units=None):
pass
# def get_geo_from_block(dxf_object):
# geo_block_transformed = []
#
# msp = dxf_object.modelspace()
# # iterate through all 'INSERT' entities found in modelspace msp
# for insert in msp.query('INSERT'):
# phi = insert.dxf.rotation
# tr = insert.dxf.insert
# sx = insert.dxf.xscale
# sy = insert.dxf.yscale
# r_count = insert.dxf.row_count
# r_spacing = insert.dxf.row_spacing
# c_count = insert.dxf.column_count
# c_spacing = insert.dxf.column_spacing
#
# # print(phi, tr)
#
# # identify the block given the 'INSERT' type entity name
# print(insert.dxf.name)
# block = dxf_object.blocks[insert.dxf.name]
# block_coords = (block.block.dxf.base_point[0], block.block.dxf.base_point[1])
#
# # get a list of geometries found in the block
# # store shapely geometry here
# geo_block = []
#
# for dxf_entity in block:
# g = []
# # print("Entity", dxf_entity.dxftype())
# if dxf_entity.dxftype() == 'POINT':
# g = dxfpoint2shapely(dxf_entity, )
# elif dxf_entity.dxftype() == 'LINE':
# g = dxfline2shapely(dxf_entity, )
# elif dxf_entity.dxftype() == 'CIRCLE':
# g = dxfcircle2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'ARC':
# g = dxfarc2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'ELLIPSE':
# g = dxfellipse2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'LWPOLYLINE':
# g = dxflwpolyline2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'POLYLINE':
# g = dxfpolyline2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'SOLID':
# g = dxfsolid2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'TRACE':
# g = dxftrace2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'SPLINE':
# g = dxfspline2shapely(dxf_entity)
# elif dxf_entity.dxftype() == 'INSERT':
# log.debug("Not supported yet.")
# else:
# log.debug("Not supported yet.")
#
# if g is not None:
# if type(g) == list:
# for subg in g:
# geo_block.append(subg)
# else:
# geo_block.append(g)
#
# # iterate over the geometries found and apply any transformation found in the 'INSERT' entity attributes
# for geo in geo_block:
# if tr[0] != 0 or tr[1] != 0:
# geo = translate(geo, (tr[0] - block_coords[0]), (tr[1] - block_coords[1]))
#
# # support for array block insertions
# if r_count > 1:
# for r in range(r_count):
# geo_block_transformed.append(translate(geo, (tr[0] + (r * r_spacing) - block_coords[0]), 0))
#
# if c_count > 1:
# for c in range(c_count):
# geo_block_transformed.append(translate(geo, 0, (tr[1] + (c * c_spacing) - block_coords[1])))
#
# if sx != 1 or sy != 1:
# geo = scale(geo, sx, sy)
# if phi != 0:
# geo = rotate(geo, phi, origin=tr)
#
# geo_block_transformed.append(geo)
# return geo_block_transformed

809
ParseDXF_Spline.py Normal file
View File

@ -0,0 +1,809 @@
# Author: vvlachoudis@gmail.com
# Vasilis Vlachoudis
# Date: 20-Oct-2015
import math
import sys
def norm(v):
return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def normalize_2(v):
m = norm(v)
return [v[0]/m, v[1]/m, v[2]/m]
# ------------------------------------------------------------------------------
# Convert a B-spline to polyline with a fixed number of segments
#
# FIXME to become adaptive
# ------------------------------------------------------------------------------
def spline2Polyline(xyz, degree, closed, segments, knots):
'''
:param xyz: DXF spline control points
:param degree: degree of the Spline curve
:param closed: closed Spline
:type closed: bool
:param segments: how many lines to use for Spline approximation
:param knots: DXF spline knots
:return: x,y,z coordinates (each is a list)
'''
# Check if last point coincide with the first one
if (Vector(xyz[0]) - Vector(xyz[-1])).length2() < 1e-10:
# it is already closed, treat it as open
closed = False
# FIXME we should verify if it is periodic,.... but...
# I am not sure :)
if closed:
xyz.extend(xyz[:degree])
knots = None
else:
# make base-1
knots.insert(0, 0)
npts = len(xyz)
if degree<1 or degree>3:
#print "invalid degree"
return None,None,None
# order:
k = degree+1
if npts < k:
#print "not enough control points"
return None,None,None
# resolution:
nseg = segments * npts
# WARNING: base 1
b = [0.0]*(npts*3+1) # polygon points
h = [1.0]*(npts+1) # set all homogeneous weighting factors to 1.0
p = [0.0]*(nseg*3+1) # returned curved points
i = 1
for pt in xyz:
b[i] = pt[0]
b[i+1] = pt[1]
b[i+2] = pt[2]
i +=3
#if periodic:
if closed:
_rbsplinu(npts, k, nseg, b, h, p, knots)
else:
_rbspline(npts, k, nseg, b, h, p, knots)
x = []
y = []
z = []
for i in range(1,3*nseg+1,3):
x.append(p[i])
y.append(p[i+1])
z.append(p[i+2])
# for i,xyz in enumerate(zip(x,y,z)):
# print i,xyz
return x,y,z
# ------------------------------------------------------------------------------
# Subroutine to generate a B-spline open knot vector with multiplicity
# equal to the order at the ends.
# c = order of the basis function
# n = the number of defining polygon vertices
# n+2 = index of x[] for the first occurence of the maximum knot vector value
# n+order = maximum value of the knot vector -- $n + c$
# x[] = array containing the knot vector
# ------------------------------------------------------------------------------
def _knot(n, order):
x = [0.0]*(n+order+1)
for i in range(2, n+order+1):
if i>order and i<n+2:
x[i] = x[i-1] + 1.0
else:
x[i] = x[i-1]
return x
# ------------------------------------------------------------------------------
# Subroutine to generate a B-spline uniform (periodic) knot vector.
#
# order = order of the basis function
# n = the number of defining polygon vertices
# n+order = maximum value of the knot vector -- $n + order$
# x[] = array containing the knot vector
# ------------------------------------------------------------------------------
def _knotu(n, order):
x = [0]*(n+order+1)
for i in range(2, n+order+1):
x[i] = float(i-1)
return x
# ------------------------------------------------------------------------------
# Subroutine to generate rational B-spline basis functions--open knot vector
# C code for An Introduction to NURBS
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
# All rights reserved.
# Name: rbasis
# Subroutines called: none
# Book reference: Chapter 4, Sec. 4. , p 296
# c = order of the B-spline basis function
# d = first term of the basis function recursion relation
# e = second term of the basis function recursion relation
# h[] = array containing the homogeneous weights
# npts = number of defining polygon vertices
# nplusc = constant -- npts + c -- maximum number of knot values
# r[] = array containing the rational basis functions
# r[1] contains the basis function associated with B1 etc.
# t = parameter value
# temp[] = temporary array
# x[] = knot vector
# ------------------------------------------------------------------------------
def _rbasis(c, t, npts, x, h, r):
nplusc = npts + c
temp = [0.0]*(nplusc+1)
# calculate the first order non-rational basis functions n[i]
for i in range(1, nplusc):
if x[i] <= t < x[i+1]:
temp[i] = 1.0
else:
temp[i] = 0.0
# calculate the higher order non-rational basis functions
for k in range(2,c+1):
for i in range(1,nplusc-k+1):
# if the lower order basis function is zero skip the calculation
if temp[i] != 0.0:
d = ((t-x[i])*temp[i])/(x[i+k-1]-x[i])
else:
d = 0.0
# if the lower order basis function is zero skip the calculation
if temp[i+1] != 0.0:
e = ((x[i+k]-t)*temp[i+1])/(x[i+k]-x[i+1])
else:
e = 0.0
temp[i] = d + e
# pick up last point
if t >= x[nplusc]:
temp[npts] = 1.0
# calculate sum for denominator of rational basis functions
s = 0.0
for i in range(1,npts+1):
s += temp[i]*h[i]
# form rational basis functions and put in r vector
for i in range(1, npts+1):
if s != 0.0:
r[i] = (temp[i]*h[i])/s
else:
r[i] = 0
# ------------------------------------------------------------------------------
# Generates a rational B-spline curve using a uniform open knot vector.
#
# C code for An Introduction to NURBS
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
# All rights reserved.
#
# Name: rbspline.c
# Subroutines called: _knot, rbasis
# Book reference: Chapter 4, Alg. p. 297
#
# b = array containing the defining polygon vertices
# b[1] contains the x-component of the vertex
# b[2] contains the y-component of the vertex
# b[3] contains the z-component of the vertex
# h = array containing the homogeneous weighting factors
# k = order of the B-spline basis function
# nbasis = array containing the basis functions for a single value of t
# nplusc = number of knot values
# npts = number of defining polygon vertices
# p[,] = array containing the curve points
# p[1] contains the x-component of the point
# p[2] contains the y-component of the point
# p[3] contains the z-component of the point
# p1 = number of points to be calculated on the curve
# t = parameter value 0 <= t <= npts - k + 1
# x[] = array containing the knot vector
# ------------------------------------------------------------------------------
def _rbspline(npts, k, p1, b, h, p, x):
nplusc = npts + k
nbasis = [0.0]*(npts+1) # zero and re-dimension the basis array
# generate the uniform open knot vector
if x is None or len(x) != nplusc+1:
x = _knot(npts, k)
icount = 0
# calculate the points on the rational B-spline curve
t = 0
step = float(x[nplusc])/float(p1-1)
for i1 in range(1, p1+1):
if x[nplusc] - t < 5e-6:
t = x[nplusc]
# generate the basis function for this value of t
nbasis = [0.0]*(npts+1) # zero and re-dimension the knot vector and the basis array
_rbasis(k, t, npts, x, h, nbasis)
# generate a point on the curve
for j in range(1, 4):
jcount = j
p[icount+j] = 0.0
# Do local matrix multiplication
for i in range(1, npts+1):
p[icount+j] += nbasis[i]*b[jcount]
jcount += 3
icount += 3
t += step
# ------------------------------------------------------------------------------
# Subroutine to generate a rational B-spline curve using an uniform periodic knot vector
#
# C code for An Introduction to NURBS
# by David F. Rogers. Copyright (C) 2000 David F. Rogers,
# All rights reserved.
#
# Name: rbsplinu.c
# Subroutines called: _knotu, _rbasis
# Book reference: Chapter 4, Alg. p. 298
#
# b[] = array containing the defining polygon vertices
# b[1] contains the x-component of the vertex
# b[2] contains the y-component of the vertex
# b[3] contains the z-component of the vertex
# h[] = array containing the homogeneous weighting factors
# k = order of the B-spline basis function
# nbasis = array containing the basis functions for a single value of t
# nplusc = number of knot values
# npts = number of defining polygon vertices
# p[,] = array containing the curve points
# p[1] contains the x-component of the point
# p[2] contains the y-component of the point
# p[3] contains the z-component of the point
# p1 = number of points to be calculated on the curve
# t = parameter value 0 <= t <= npts - k + 1
# x[] = array containing the knot vector
# ------------------------------------------------------------------------------
def _rbsplinu(npts, k, p1, b, h, p, x=None):
nplusc = npts + k
nbasis = [0.0]*(npts+1) # zero and re-dimension the basis array
# generate the uniform periodic knot vector
if x is None or len(x) != nplusc+1:
# zero and re dimension the knot vector and the basis array
x = _knotu(npts, k)
icount = 0
# calculate the points on the rational B-spline curve
t = k-1
step = (float(npts)-(k-1))/float(p1-1)
for i1 in range(1, p1+1):
if x[nplusc] - t < 5e-6:
t = x[nplusc]
# generate the basis function for this value of t
nbasis = [0.0]*(npts+1)
_rbasis(k, t, npts, x, h, nbasis)
# generate a point on the curve
for j in range(1,4):
jcount = j
p[icount+j] = 0.0
# Do local matrix multiplication
for i in range(1,npts+1):
p[icount+j] += nbasis[i]*b[jcount]
jcount += 3
icount += 3
t += step
# Accuracy for comparison operators
_accuracy = 1E-15
def Cmp0(x):
"""Compare against zero within _accuracy"""
return abs(x)<_accuracy
def gauss(A, B):
"""Solve A*X = B using the Gauss elimination method"""
n = len(A)
s = [0.0] * n
X = [0.0] * n
p = [i for i in range(n)]
for i in range(n):
s[i] = max([abs(x) for x in A[i]])
for k in range(n - 1):
# select j>=k so that
# |A[p[j]][k]| / s[p[i]] >= |A[p[i]][k]| / s[p[i]] for i = k,k+1,...,n
j = k
ap = abs(A[p[j]][k]) / s[p[j]]
for i in range(k + 1, n):
api = abs(A[p[i]][k]) / s[p[i]]
if api > ap:
j = i
ap = api
if j != k: p[k], p[j] = p[j], p[k] # Swap values
for i in range(k + 1, n):
z = A[p[i]][k] / A[p[k]][k]
A[p[i]][k] = z
for j in range(k + 1, n):
A[p[i]][j] -= z * A[p[k]][j]
for k in range(n - 1):
for i in range(k + 1, n):
B[p[i]] -= A[p[i]][k] * B[p[k]]
for i in range(n - 1, -1, -1):
X[i] = B[p[i]]
for j in range(i + 1, n):
X[i] -= A[p[i]][j] * X[j]
X[i] /= A[p[i]][i]
return X
# Vector class
# Inherits from List
class Vector(list):
"""Vector class"""
def __init__(self, x=3, *args):
"""Create a new vector,
Vector(size), Vector(list), Vector(x,y,z,...)"""
list.__init__(self)
if isinstance(x, int) and not args:
for i in range(x):
self.append(0.0)
elif isinstance(x, (list, tuple)):
for i in x:
self.append(float(i))
else:
self.append(float(x))
for i in args:
self.append(float(i))
# ----------------------------------------------------------------------
def set(self, x, y, z=None):
"""Set vector"""
self[0] = x
self[1] = y
if z: self[2] = z
# ----------------------------------------------------------------------
def __repr__(self):
return "[%s]" % (", ".join([repr(x) for x in self]))
# ----------------------------------------------------------------------
def __str__(self):
return "[%s]" % (", ".join([("%15g" % (x)).strip() for x in self]))
# ----------------------------------------------------------------------
def eq(self, v, acc=_accuracy):
"""Test for equality with vector v within accuracy"""
if len(self) != len(v): return False
s2 = 0.0
for a, b in zip(self, v):
s2 += (a - b) ** 2
return s2 <= acc ** 2
def __eq__(self, v):
return self.eq(v)
# ----------------------------------------------------------------------
def __neg__(self):
"""Negate vector"""
new = Vector(len(self))
for i, s in enumerate(self):
new[i] = -s
return new
# ----------------------------------------------------------------------
def __add__(self, v):
"""Add 2 vectors"""
size = min(len(self), len(v))
new = Vector(size)
for i in range(size):
new[i] = self[i] + v[i]
return new
# ----------------------------------------------------------------------
def __iadd__(self, v):
"""Add vector v to self"""
for i in range(min(len(self), len(v))):
self[i] += v[i]
return self
# ----------------------------------------------------------------------
def __sub__(self, v):
"""Subtract 2 vectors"""
size = min(len(self), len(v))
new = Vector(size)
for i in range(size):
new[i] = self[i] - v[i]
return new
# ----------------------------------------------------------------------
def __isub__(self, v):
"""Subtract vector v from self"""
for i in range(min(len(self), len(v))):
self[i] -= v[i]
return self
# ----------------------------------------------------------------------
# Scale or Dot product
# ----------------------------------------------------------------------
def __mul__(self, v):
"""scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
if isinstance(v, list):
return self.dot(v)
else:
return Vector([x * v for x in self])
# ----------------------------------------------------------------------
# Scale or Dot product
# ----------------------------------------------------------------------
def __rmul__(self, v):
"""scale*Vector() or Vector()*Vector() - Scale vector or dot product"""
if isinstance(v, Vector):
return self.dot(v)
else:
return Vector([x * v for x in self])
# ----------------------------------------------------------------------
# Divide by floating point
# ----------------------------------------------------------------------
def __div__(self, b):
return Vector([x / b for x in self])
# ----------------------------------------------------------------------
def __xor__(self, v):
"""Cross product"""
return self.cross(v)
# ----------------------------------------------------------------------
def dot(self, v):
"""Dot product of 2 vectors"""
s = 0.0
for a, b in zip(self, v):
s += a * b
return s
# ----------------------------------------------------------------------
def cross(self, v):
"""Cross product of 2 vectors"""
if len(self) == 3:
return Vector(self[1] * v[2] - self[2] * v[1],
self[2] * v[0] - self[0] * v[2],
self[0] * v[1] - self[1] * v[0])
elif len(self) == 2:
return self[0] * v[1] - self[1] * v[0]
else:
raise Exception("Cross product needs 2d or 3d vectors")
# ----------------------------------------------------------------------
def length2(self):
"""Return length squared of vector"""
s2 = 0.0
for s in self:
s2 += s ** 2
return s2
# ----------------------------------------------------------------------
def length(self):
"""Return length of vector"""
s2 = 0.0
for s in self:
s2 += s ** 2
return math.sqrt(s2)
__abs__ = length
# ----------------------------------------------------------------------
def arg(self):
"""return vector angle"""
return math.atan2(self[1], self[0])
# ----------------------------------------------------------------------
def norm(self):
"""Normalize vector and return length"""
l = self.length()
if l > 0.0:
invlen = 1.0 / l
for i in range(len(self)):
self[i] *= invlen
return l
normalize = norm
# ----------------------------------------------------------------------
def unit(self):
"""return a unit vector"""
v = self.clone()
v.norm()
return v
# ----------------------------------------------------------------------
def clone(self):
"""Clone vector"""
return Vector(self)
# ----------------------------------------------------------------------
def x(self):
return self[0]
def y(self):
return self[1]
def z(self):
return self[2]
# ----------------------------------------------------------------------
def orthogonal(self):
"""return a vector orthogonal to self"""
xx = abs(self.x())
yy = abs(self.y())
if len(self) >= 3:
zz = abs(self.z())
if xx < yy:
if xx < zz:
return Vector(0.0, self.z(), -self.y())
else:
return Vector(self.y(), -self.x(), 0.0)
else:
if yy < zz:
return Vector(-self.z(), 0.0, self.x())
else:
return Vector(self.y(), -self.x(), 0.0)
else:
return Vector(-self.y(), self.x())
# ----------------------------------------------------------------------
def direction(self, zero=_accuracy):
"""return containing the direction if normalized with any of the axis"""
v = self.clone()
l = v.norm()
if abs(l) <= zero: return "O"
if abs(v[0] - 1.0) < zero:
return "X"
elif abs(v[0] + 1.0) < zero:
return "-X"
elif abs(v[1] - 1.0) < zero:
return "Y"
elif abs(v[1] + 1.0) < zero:
return "-Y"
elif abs(v[2] - 1.0) < zero:
return "Z"
elif abs(v[2] + 1.0) < zero:
return "-Z"
else:
# nothing special about the direction, return N
return "N"
# ----------------------------------------------------------------------
# Set the vector directly in polar coordinates
# @param ma magnitude of vector
# @param ph azimuthal angle in radians
# @param th polar angle in radians
# ----------------------------------------------------------------------
def setPolar(self, ma, ph, th):
"""Set the vector directly in polar coordinates"""
sf = math.sin(ph)
cf = math.cos(ph)
st = math.sin(th)
ct = math.cos(th)
self[0] = ma * st * cf
self[1] = ma * st * sf
self[2] = ma * ct
# ----------------------------------------------------------------------
def phi(self):
"""return the azimuth angle."""
if Cmp0(self.x()) and Cmp0(self.y()):
return 0.0
return math.atan2(self.y(), self.x())
# ----------------------------------------------------------------------
def theta(self):
"""return the polar angle."""
if Cmp0(self.x()) and Cmp0(self.y()) and Cmp0(self.z()):
return 0.0
return math.atan2(self.perp(), self.z())
# ----------------------------------------------------------------------
def cosTheta(self):
"""return cosine of the polar angle."""
ptot = self.length()
if Cmp0(ptot):
return 1.0
else:
return self.z() / ptot
# ----------------------------------------------------------------------
def perp2(self):
"""return the transverse component squared
(R^2 in cylindrical coordinate system)."""
return self.x() * self.x() + self.y() * self.y()
# ----------------------------------------------------------------------
def perp(self):
"""@return the transverse component
(R in cylindrical coordinate system)."""
return math.sqrt(self.perp2())
# ----------------------------------------------------------------------
# Return a random 3D vector
# ----------------------------------------------------------------------
# @staticmethod
# def random():
# cosTheta = 2.0 * random.random() - 1.0
# sinTheta = math.sqrt(1.0 - cosTheta ** 2)
# phi = 2.0 * math.pi * random.random()
# return Vector(math.cos(phi) * sinTheta, math.sin(phi) * sinTheta, cosTheta)
# #===============================================================================
# # Cardinal cubic spline class
# #===============================================================================
# class CardinalSpline:
# def __init__(self, A=0.5):
# # The default matrix is the Catmull-Rom spline
# # which is equal to Cardinal matrix
# # for A = 0.5
# #
# # Note: Vasilis
# # The A parameter should be the fraction in t where
# # the second derivative is zero
# self.setMatrix(A)
#
# #-----------------------------------------------------------------------
# # Set the matrix according to Cardinal
# #-----------------------------------------------------------------------
# def setMatrix(self, A=0.5):
# self.M = []
# self.M.append([ -A, 2.-A, A-2., A ])
# self.M.append([2.*A, A-3., 3.-2.*A, -A ])
# self.M.append([ -A, 0., A, 0.])
# self.M.append([ 0., 1., 0, 0.])
#
# #-----------------------------------------------------------------------
# # Evaluate Cardinal spline at position t
# # @param P list or tuple with 4 points y positions
# # @param t [0..1] fraction of interval from points 1..2
# # @param k index of starting 4 elements in P
# # @return spline evaluation
# #-----------------------------------------------------------------------
# def __call__(self, P, t, k=1):
# T = [t*t*t, t*t, t, 1.0]
# R = [0.0]*4
# for i in range(4):
# for j in range(4):
# R[i] += T[j] * self.M[j][i]
# y = 0.0
# for i in range(4):
# y += R[i]*P[k+i-1]
#
# return y
#
# #-----------------------------------------------------------------------
# # Return the coefficients of a 3rd degree polynomial
# # f(x) = a t^3 + b t^2 + c t + d
# # @return [a, b, c, d]
# #-----------------------------------------------------------------------
# def coefficients(self, P, k=1):
# C = [0.0]*4
# for i in range(4):
# for j in range(4):
# C[i] += self.M[i][j] * P[k+j-1]
# return C
#
# #-----------------------------------------------------------------------
# # Evaluate the value of the spline using the coefficients
# #-----------------------------------------------------------------------
# def evaluate(self, C, t):
# return ((C[0]*t + C[1])*t + C[2])*t + C[3]
#
# #===============================================================================
# # Cubic spline ensuring that the first and second derivative are continuous
# # adapted from Penelope Manual Appending B.1
# # It requires all the points (xi,yi) and the assumption on how to deal
# # with the second derivative on the extremities
# # Option 1: assume zero as second derivative on both ends
# # Option 2: assume the same as the next or previous one
# #===============================================================================
# class CubicSpline:
# def __init__(self, X, Y):
# self.X = X
# self.Y = Y
# self.n = len(X)
#
# # Option #1
# s1 = 0.0 # zero based = s0
# sN = 0.0 # zero based = sN-1
#
# # Construct the tri-diagonal matrix
# A = []
# B = [0.0] * (self.n-2)
# for i in range(self.n-2):
# A.append([0.0] * (self.n-2))
#
# for i in range(1,self.n-1):
# hi = self.h(i)
# Hi = 2.0*(self.h(i-1) + hi)
# j = i-1
# A[j][j] = Hi
# if i+1<self.n-1:
# A[j][j+1] = A[j+1][j] = hi
#
# if i==1:
# B[j] = 6.*(self.d(i) - self.d(j)) - hi*s1
# elif i<self.n-2:
# B[j] = 6.*(self.d(i) - self.d(j))
# else:
# B[j] = 6.*(self.d(i) - self.d(j)) - hi*sN
#
#
# self.s = gauss(A,B)
# self.s.insert(0,s1)
# self.s.append(sN)
# # print ">> s <<"
# # pprint(self.s)
#
# #-----------------------------------------------------------------------
# def h(self, i):
# return self.X[i+1] - self.X[i]
#
# #-----------------------------------------------------------------------
# def d(self, i):
# return (self.Y[i+1] - self.Y[i]) / (self.X[i+1] - self.X[i])
#
# #-----------------------------------------------------------------------
# def coefficients(self, i):
# """return coefficients of cubic spline for interval i a*x**3+b*x**2+c*x+d"""
# hi = self.h(i)
# si = self.s[i]
# si1 = self.s[i+1]
# xi = self.X[i]
# xi1 = self.X[i+1]
# fi = self.Y[i]
# fi1 = self.Y[i+1]
#
# a = 1./(6.*hi)*(si*xi1**3 - si1*xi**3 + 6.*(fi*xi1 - fi1*xi)) + hi/6.*(si1*xi - si*xi1)
# b = 1./(2.*hi)*(si1*xi**2 - si*xi1**2 + 2*(fi1 - fi)) + hi/6.*(si - si1)
# c = 1./(2.*hi)*(si*xi1 - si1*xi)
# d = 1./(6.*hi)*(si1-si)
#
# return [d,c,b,a]
#
# #-----------------------------------------------------------------------
# def __call__(self, i, x):
# # FIXME should interpolate to find the interval
# C = self.coefficients(i)
# return ((C[0]*x + C[1])*x + C[2])*x + C[3]
#
# #-----------------------------------------------------------------------
# # @return evaluation of cubic spline at x using coefficients C
# #-----------------------------------------------------------------------
# def evaluate(self, C, x):
# return ((C[0]*x + C[1])*x + C[2])*x + C[3]
#
# #-----------------------------------------------------------------------
# # Return evaluated derivative at x using coefficients C
# #-----------------------------------------------------------------------
# def derivative(self, C, x):
# a = 3.0*C[0] # derivative coefficients
# b = 2.0*C[1] # ... for sampling with rejection
# c = C[2]
# return (3.0*C[0]*x + 2.0*C[1])*x + C[2]
#

332
ParseFont.py Normal file
View File

@ -0,0 +1,332 @@
#########################################################################
### Borrowed code from 'https://github.com/gddc/ttfquery/blob/master/ ###
### and made it work with Python 3 #############
#########################################################################
import re, os, sys, glob
import itertools
from shapely.geometry import Point, Polygon
from shapely.affinity import translate, scale, rotate
from shapely.geometry import MultiPolygon
from shapely.geometry.base import BaseGeometry
import freetype as ft
from fontTools import ttLib
import logging
log = logging.getLogger('base2')
class ParseFont():
FONT_SPECIFIER_NAME_ID = 4
FONT_SPECIFIER_FAMILY_ID = 1
@staticmethod
def get_win32_font_path():
"""Get User-specific font directory on Win32"""
try:
import winreg
except ImportError:
return os.path.join(os.environ['WINDIR'], 'Fonts')
else:
k = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
try:
# should check that k is valid? How?
return winreg.QueryValueEx(k, "Fonts")[0]
finally:
winreg.CloseKey(k)
@staticmethod
def get_linux_font_paths():
"""Get system font directories on Linux/Unix
Uses /usr/sbin/chkfontpath to get the list
of system-font directories, note that many
of these will *not* be truetype font directories.
If /usr/sbin/chkfontpath isn't available, uses
returns a set of common Linux/Unix paths
"""
executable = '/usr/sbin/chkfontpath'
if os.path.isfile(executable):
data = os.popen(executable).readlines()
match = re.compile('\d+: (.+)')
set = []
for line in data:
result = match.match(line)
if result:
set.append(result.group(1))
return set
else:
directories = [
# what seems to be the standard installation point
"/usr/X11R6/lib/X11/fonts/TTF/",
# common application, not really useful
"/usr/lib/openoffice/share/fonts/truetype/",
# documented as a good place to install new fonts...
"/usr/share/fonts",
"/usr/local/share/fonts",
# seems to be where fonts are installed for an individual user?
"~/.fonts",
]
dir_set = []
for directory in directories:
directory = directory = os.path.expanduser(os.path.expandvars(directory))
try:
if os.path.isdir(directory):
for path, children, files in os.walk(directory):
dir_set.append(path)
except (IOError, OSError, TypeError, ValueError):
pass
return dir_set
@staticmethod
def get_mac_font_paths():
"""Get system font directories on MacOS
"""
directories = [
# okay, now the OS X variants...
"~/Library/Fonts/",
"/Library/Fonts/",
"/Network/Library/Fonts/",
"/System/Library/Fonts/",
"System Folder:Fonts:",
]
dir_set = []
for directory in directories:
directory = directory = os.path.expanduser(os.path.expandvars(directory))
try:
if os.path.isdir(directory):
for path, children, files in os.walk(directory):
dir_set.append(path)
except (IOError, OSError, TypeError, ValueError):
pass
return dir_set
@staticmethod
def get_win32_fonts(font_directory=None):
"""Get list of explicitly *installed* font names"""
import winreg
if font_directory is None:
font_directory = ParseFont.get_win32_font_path()
k = None
items = {}
for keyName in (
r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts",
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Fonts",
):
try:
k = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
keyName
)
except OSError as err:
pass
if not k:
# couldn't open either WinNT or Win98 key???
return glob.glob(os.path.join(font_directory, '*.ttf'))
try:
# should check that k is valid? How?
for index in range(winreg.QueryInfoKey(k)[1]):
key, value, _ = winreg.EnumValue(k, index)
if not os.path.dirname(value):
value = os.path.join(font_directory, value)
value = os.path.abspath(value).lower()
if value[-4:] == '.ttf':
items[value] = 1
return list(items.keys())
finally:
winreg.CloseKey(k)
@staticmethod
def get_font_name(font_path):
"""
Get the short name from the font's names table
From 'https://github.com/gddc/ttfquery/blob/master/ttfquery/describe.py'
and
http://www.starrhorne.com/2012/01/18/
how-to-extract-font-names-from-ttf-files-using-python-and-our-old-friend-the-command-line.html
ported to Python 3 here: https://gist.github.com/pklaus/dce37521579513c574d0
"""
name = ""
family = ""
font = ttLib.TTFont(font_path)
for record in font['name'].names:
if b'\x00' in record.string:
name_str = record.string.decode('utf-16-be')
else:
# name_str = record.string.decode('utf-8')
name_str = record.string.decode('latin-1')
if record.nameID == ParseFont.FONT_SPECIFIER_NAME_ID and not name:
name = name_str
elif record.nameID == ParseFont.FONT_SPECIFIER_FAMILY_ID and not family:
family = name_str
if name and family:
break
return name, family
def __init__(self, parent=None):
super(ParseFont, self).__init__()
# regular fonts
self.regular_f = {}
# bold fonts
self.bold_f = {}
# italic fonts
self.italic_f = {}
# bold and italic fonts
self.bold_italic_f = {}
def get_fonts(self, paths=None):
"""
Find fonts in paths, or the system paths if not given
"""
files = {}
if paths is None:
if sys.platform == 'win32':
font_directory = ParseFont.get_win32_font_path()
paths = [font_directory,]
# now get all installed fonts directly...
for f in self.get_win32_fonts(font_directory):
files[f] = 1
elif sys.platform == 'linux':
paths = ParseFont.get_linux_font_paths()
else:
paths = ParseFont.get_mac_font_paths()
elif isinstance(paths, str):
paths = [paths]
for path in paths:
for file in glob.glob(os.path.join(path, '*.ttf')):
files[os.path.abspath(file)] = 1
return list(files.keys())
def get_fonts_by_types(self):
system_fonts = self.get_fonts()
# split the installed fonts by type: regular, bold, italic (oblique), bold-italic and
# store them in separate dictionaries {name: file_path/filename.ttf}
for font in system_fonts:
name, family = ParseFont.get_font_name(font)
if 'Bold' in name and 'Italic' in name:
name = name.replace(" Bold Italic", '')
self.bold_italic_f.update({name: font})
elif 'Bold' in name and 'Oblique' in name:
name = name.replace(" Bold Oblique", '')
self.bold_italic_f.update({name: font})
elif 'Bold' in name:
name = name.replace(" Bold", '')
self.bold_f.update({name: font})
elif 'SemiBold' in name:
name = name.replace(" SemiBold", '')
self.bold_f.update({name: font})
elif 'DemiBold' in name:
name = name.replace(" DemiBold", '')
self.bold_f.update({name: font})
elif 'Demi' in name:
name = name.replace(" Demi", '')
self.bold_f.update({name: font})
elif 'Italic' in name:
name = name.replace(" Italic", '')
self.italic_f.update({name: font})
elif 'Oblique' in name:
name = name.replace(" Italic", '')
self.italic_f.update({name: font})
else:
try:
name = name.replace(" Regular", '')
except:
pass
self.regular_f.update({name: font})
log.debug("Font parsing is finished.")
def font_to_geometry(self, char_string, font_name, font_type, font_size, units='MM', coordx=0, coordy=0):
path = []
scaled_path = []
path_filename = ""
regular_dict = self.regular_f
bold_dict = self.bold_f
italic_dict = self.italic_f
bold_italic_dict = self.bold_italic_f
try:
if font_type == 'bi':
path_filename = bold_italic_dict[font_name]
elif font_type == 'bold':
path_filename = bold_dict[font_name]
elif font_type == 'italic':
path_filename = italic_dict[font_name]
elif font_type == 'regular':
path_filename = regular_dict[font_name]
except Exception as e:
log.debug("[error_notcl] Font Loading: %s" % str(e))
return"[ERROR] Font Loading: %s" % str(e)
face = ft.Face(path_filename)
face.set_char_size(int(font_size) * 64)
pen_x = coordx
previous = 0
# done as here: https://www.freetype.org/freetype2/docs/tutorial/step2.html
for char in char_string:
glyph_index = face.get_char_index(char)
try:
if previous > 0 and glyph_index > 0:
delta = face.get_kerning(previous, glyph_index)
pen_x += delta.x
except:
pass
face.load_glyph(glyph_index)
# face.load_char(char, flags=8)
slot = face.glyph
outline = slot.outline
start, end = 0, 0
for i in range(len(outline.contours)):
end = outline.contours[i]
points = outline.points[start:end + 1]
points.append(points[0])
char_geo = Polygon(points)
char_geo = translate(char_geo, xoff=pen_x, yoff=coordy)
path.append(char_geo)
start = end + 1
pen_x += slot.advance.x
previous = glyph_index
for item in path:
if units == 'MM':
scaled_path.append(scale(item, 0.0080187969924812, 0.0080187969924812, origin=(coordx, coordy)))
else:
scaled_path.append(scale(item, 0.00031570066, 0.00031570066, origin=(coordx, coordy)))
return MultiPolygon(scaled_path)

652
ParseSVG.py Normal file
View File

@ -0,0 +1,652 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 12/18/2015 #
# MIT Licence #
# #
# SVG Features supported: #
# * Groups #
# * Rectangles (w/ rounded corners) #
# * Circles #
# * Ellipses #
# * Polygons #
# * Polylines #
# * Lines #
# * Paths #
# * All transformations #
# #
# Reference: www.w3.org/TR/SVG/Overview.html #
############################################################
# import xml.etree.ElementTree as ET
from lxml import etree as ET
import re
import itertools
from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path
from svg.path.path import Move
from shapely.geometry import LinearRing, LineString, Point, Polygon
from shapely.affinity import translate, rotate, scale, skew, affine_transform
import numpy as np
import logging
from ParseFont import *
log = logging.getLogger('base2')
def svgparselength(lengthstr):
"""
Parse an SVG length string into a float and a units
string, if any.
:param lengthstr: SVG length string.
:return: Number and units pair.
:rtype: tuple(float, str|None)
"""
integer_re_str = r'[+-]?[0-9]+'
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?'
match = re.search(length_re_str, lengthstr)
if match:
return float(match.group(1)), match.group(2)
return
def path2shapely(path, object_type, res=1.0):
"""
Converts an svg.path.Path into a Shapely
LinearRing or LinearString.
:rtype : LinearRing
:rtype : LineString
:param path: svg.path.Path instance
:param res: Resolution (minimum step along path)
:return: Shapely geometry object
"""
points = []
geometry = []
geo_element = None
for component in path:
# Line
if isinstance(component, Line):
start = component.start
x, y = start.real, start.imag
if len(points) == 0 or points[-1] != (x, y):
points.append((x, y))
end = component.end
points.append((end.real, end.imag))
continue
# Arc, CubicBezier or QuadraticBezier
if isinstance(component, Arc) or \
isinstance(component, CubicBezier) or \
isinstance(component, QuadraticBezier):
# How many points to use in the discrete representation.
length = component.length(res / 10.0)
steps = int(length / res + 0.5)
# solve error when step is below 1,
# it may cause other problems, but LineString needs at least two points
if steps == 0:
steps = 1
frac = 1.0 / steps
# print length, steps, frac
for i in range(steps):
point = component.point(i * frac)
x, y = point.real, point.imag
if len(points) == 0 or points[-1] != (x, y):
points.append((x, y))
end = component.point(1.0)
points.append((end.real, end.imag))
continue
# Move
if isinstance(component, Move):
if object_type == 'geometry':
geo_element = LineString(points)
elif object_type == 'gerber':
# Didn't worked out using Polygon because if there is a large outline it will envelope everything
# and create issues with intersections. I will let the parameter obj_type present though
# geo_element = Polygon(points)
geo_element = LineString(points)
else:
log.error("[error]: Not a valid target object.")
if not points:
continue
else:
geometry.append(geo_element)
points = []
continue
log.warning("I don't know what this is: %s" % str(component))
continue
# if there are still points in points then add them as a LineString
if points:
geo_element = LineString(points)
geometry.append(geo_element)
points = []
# if path.closed:
# return Polygon(points).buffer(0)
# # return LinearRing(points)
# else:
# return LineString(points)
return geometry
def svgrect2shapely(rect, n_points=32):
"""
Converts an SVG rect into Shapely geometry.
:param rect: Rect Element
:type rect: xml.etree.ElementTree.Element
:return: shapely.geometry.polygon.LinearRing
"""
w = svgparselength(rect.get('width'))[0]
h = svgparselength(rect.get('height'))[0]
x_obj = rect.get('x')
if x_obj is not None:
x = svgparselength(x_obj)[0]
else:
x = 0
y_obj = rect.get('y')
if y_obj is not None:
y = svgparselength(y_obj)[0]
else:
y = 0
rxstr = rect.get('rx')
rystr = rect.get('ry')
if rxstr is None and rystr is None: # Sharp corners
pts = [
(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)
]
else: # Rounded corners
rx = 0.0 if rxstr is None else svgparselength(rxstr)[0]
ry = 0.0 if rystr is None else svgparselength(rystr)[0]
n_points = int(n_points / 4 + 0.5)
t = np.arange(n_points, dtype=float) / n_points / 4
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75))
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75))
lower_right = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t)
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t)
upper_right = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25))
y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25))
upper_left = [(x_[i], y_[i]) for i in range(n_points)]
x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5))
y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5))
lower_left = [(x_[i], y_[i]) for i in range(n_points)]
pts = [(x + rx, y), (x - rx + w, y)] + \
lower_right + \
[(x + w, y + ry), (x + w, y + h - ry)] + \
upper_right + \
[(x + w - rx, y + h), (x + rx, y + h)] + \
upper_left + \
[(x, y + h - ry), (x, y + ry)] + \
lower_left
return Polygon(pts).buffer(0)
# return LinearRing(pts)
def svgcircle2shapely(circle):
"""
Converts an SVG circle into Shapely geometry.
:param circle: Circle Element
:type circle: xml.etree.ElementTree.Element
:return: Shapely representation of the circle.
:rtype: shapely.geometry.polygon.LinearRing
"""
# cx = float(circle.get('cx'))
# cy = float(circle.get('cy'))
# r = float(circle.get('r'))
cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet
cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet
r = svgparselength(circle.get('r'))[0] # TODO: No units support yet
# TODO: No resolution specified.
return Point(cx, cy).buffer(r)
def svgellipse2shapely(ellipse, n_points=64):
"""
Converts an SVG ellipse into Shapely geometry
:param ellipse: Ellipse Element
:type ellipse: xml.etree.ElementTree.Element
:param n_points: Number of discrete points in output.
:return: Shapely representation of the ellipse.
:rtype: shapely.geometry.polygon.LinearRing
"""
cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet
cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet
rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet
ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet
t = np.arange(n_points, dtype=float) / n_points
x = cx + rx * np.cos(2 * np.pi * t)
y = cy + ry * np.sin(2 * np.pi * t)
pts = [(x[i], y[i]) for i in range(n_points)]
return Polygon(pts).buffer(0)
# return LinearRing(pts)
def svgline2shapely(line):
"""
:param line: Line element
:type line: xml.etree.ElementTree.Element
:return: Shapely representation on the line.
:rtype: shapely.geometry.polygon.LinearRing
"""
x1 = svgparselength(line.get('x1'))[0]
y1 = svgparselength(line.get('y1'))[0]
x2 = svgparselength(line.get('x2'))[0]
y2 = svgparselength(line.get('y2'))[0]
return LineString([(x1, y1), (x2, y2)])
def svgpolyline2shapely(polyline):
ptliststr = polyline.get('points')
points = parse_svg_point_list(ptliststr)
return LineString(points)
def svgpolygon2shapely(polygon):
ptliststr = polygon.get('points')
points = parse_svg_point_list(ptliststr)
return Polygon(points).buffer(0)
# return LinearRing(points)
def getsvggeo(node, object_type):
"""
Extracts and flattens all geometry from an SVG node
into a list of Shapely geometry.
:param node: xml.etree.ElementTree.Element
:return: List of Shapely geometry
:rtype: list
"""
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
geo = []
# Recurse
if len(node) > 0:
for child in node:
subgeo = getsvggeo(child, object_type)
if subgeo is not None:
geo += subgeo
# Parse
elif kind == 'path':
log.debug("***PATH***")
P = parse_path(node.get('d'))
P = path2shapely(P, object_type)
# for path, the resulting geometry is already a list so no need to create a new one
geo = P
elif kind == 'rect':
log.debug("***RECT***")
R = svgrect2shapely(node)
geo = [R]
elif kind == 'circle':
log.debug("***CIRCLE***")
C = svgcircle2shapely(node)
geo = [C]
elif kind == 'ellipse':
log.debug("***ELLIPSE***")
E = svgellipse2shapely(node)
geo = [E]
elif kind == 'polygon':
log.debug("***POLYGON***")
poly = svgpolygon2shapely(node)
geo = [poly]
elif kind == 'line':
log.debug("***LINE***")
line = svgline2shapely(node)
geo = [line]
elif kind == 'polyline':
log.debug("***POLYLINE***")
pline = svgpolyline2shapely(node)
geo = [pline]
else:
log.warning("Unknown kind: " + kind)
geo = None
# ignore transformation for unknown kind
if geo is not None:
# Transformations
if 'transform' in node.attrib:
trstr = node.get('transform')
trlist = parse_svg_transform(trstr)
# log.debug(trlist)
# Transformations are applied in reverse order
for tr in trlist[::-1]:
if tr[0] == 'translate':
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
elif tr[0] == 'scale':
geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'rotate':
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
for geoi in geo]
elif tr[0] == 'skew':
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'matrix':
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
else:
raise Exception('Unknown transformation: %s', tr)
return geo
def getsvgtext(node, object_type, units='MM'):
"""
Extracts and flattens all geometry from an SVG node
into a list of Shapely geometry.
:param node: xml.etree.ElementTree.Element
:return: List of Shapely geometry
:rtype: list
"""
kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1)
geo = []
# Recurse
if len(node) > 0:
for child in node:
subgeo = getsvgtext(child, object_type, units=units)
if subgeo is not None:
geo += subgeo
# Parse
elif kind == 'tspan':
current_attrib = node.attrib
txt = node.text
style_dict = {}
parrent_attrib = node.getparent().attrib
style = parrent_attrib['style']
try:
style_list = style.split(';')
for css in style_list:
style_dict[css.rpartition(':')[0]] = css.rpartition(':')[-1]
pos_x = float(current_attrib['x'])
pos_y = float(current_attrib['y'])
# should have used the instance from FlatCAMApp.App but how? without reworking everything ...
pf = ParseFont()
pf.get_fonts_by_types()
font_name = style_dict['font-family'].replace("'", '')
if style_dict['font-style'] == 'italic' and style_dict['font-weight'] == 'bold':
font_type = 'bi'
elif style_dict['font-weight'] == 'bold':
font_type = 'bold'
elif style_dict['font-style'] == 'italic':
font_type = 'italic'
else:
font_type = 'regular'
# value of 2.2 should have been 2.83 (conversion value from pixels to points)
# but the dimensions from Inkscape did not corelate with the ones after importing in FlatCAM
# so I adjusted this
font_size = svgparselength(style_dict['font-size'])[0] * 2.2
geo = [pf.font_to_geometry(txt,
font_name=font_name,
font_size=font_size,
font_type=font_type,
units=units,
coordx=pos_x,
coordy=pos_y)
]
geo = [(scale(g, 1.0, -1.0)) for g in geo]
except Exception as e:
log.debug(str(e))
else:
geo = None
# ignore transformation for unknown kind
if geo is not None:
# Transformations
if 'transform' in node.attrib:
trstr = node.get('transform')
trlist = parse_svg_transform(trstr)
# log.debug(trlist)
# Transformations are applied in reverse order
for tr in trlist[::-1]:
if tr[0] == 'translate':
geo = [translate(geoi, tr[1], tr[2]) for geoi in geo]
elif tr[0] == 'scale':
geo = [scale(geoi, tr[0], tr[1], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'rotate':
geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3]))
for geoi in geo]
elif tr[0] == 'skew':
geo = [skew(geoi, tr[1], tr[2], origin=(0, 0))
for geoi in geo]
elif tr[0] == 'matrix':
geo = [affine_transform(geoi, tr[1:]) for geoi in geo]
else:
raise Exception('Unknown transformation: %s', tr)
return geo
def parse_svg_point_list(ptliststr):
"""
Returns a list of coordinate pairs extracted from the "points"
attribute in SVG polygons and polyline's.
:param ptliststr: "points" attribute string in polygon or polyline.
:return: List of tuples with coordinates.
"""
pairs = []
last = None
pos = 0
i = 0
for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')):
val = float(ptliststr[pos:match.start()])
if i % 2 == 1:
pairs.append((last, val))
else:
last = val
pos = match.end()
i += 1
# Check for last element
val = float(ptliststr[pos:])
if i % 2 == 1:
pairs.append((last, val))
else:
log.warning("Incomplete coordinates.")
return pairs
def parse_svg_transform(trstr):
"""
Parses an SVG transform string into a list
of transform names and their parameters.
Possible transformations are:
* Translate: translate(<tx> [<ty>]), which specifies
a translation by tx and ty. If <ty> is not provided,
it is assumed to be zero. Result is
['translate', tx, ty]
* Scale: scale(<sx> [<sy>]), which specifies a scale operation
by sx and sy. If <sy> is not provided, it is assumed to be
equal to <sx>. Result is: ['scale', sx, sy]
* Rotate: rotate(<rotate-angle> [<cx> <cy>]), which specifies
a rotation by <rotate-angle> degrees about a given point.
If optional parameters <cx> and <cy> are not supplied,
the rotate is about the origin of the current user coordinate
system. Result is: ['rotate', rotate-angle, cx, cy]
* Skew: skewX(<skew-angle>), which specifies a skew
transformation along the x-axis. skewY(<skew-angle>), which
specifies a skew transformation along the y-axis.
Result is ['skew', angle-x, angle-y]
* Matrix: matrix(<a> <b> <c> <d> <e> <f>), which specifies a
transformation in the form of a transformation matrix of six
values. matrix(a,b,c,d,e,f) is equivalent to applying the
transformation matrix [a b c d e f]. Result is
['matrix', a, b, c, d, e, f]
Note: All parameters to the transformations are "numbers",
i.e. no units present.
:param trstr: SVG transform string.
:type trstr: str
:return: List of transforms.
:rtype: list
"""
trlist = []
assert isinstance(trstr, str)
trstr = trstr.strip(' ')
integer_re_str = r'[+-]?[0-9]+'
number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \
r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)'
# num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing
comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))'
translate_re_str = r'translate\s*\(\s*(' + \
number_re_str + r')(?:' + \
comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
scale_re_str = r'scale\s*\(\s*(' + \
number_re_str + r')' + \
r'(?:' + comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
skew_re_str = r'skew([XY])\s*\(\s*(' + \
number_re_str + r')\s*\)'
rotate_re_str = r'rotate\s*\(\s*(' + \
number_re_str + r')' + \
r'(?:' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + \
comma_or_space_re_str + \
r'(' + number_re_str + r'))?\s*\)'
matrix_re_str = r'matrix\s*\(\s*' + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')' + comma_or_space_re_str + \
r'(' + number_re_str + r')\s*\)'
while len(trstr) > 0:
match = re.search(r'^' + translate_re_str, trstr)
if match:
trlist.append([
'translate',
float(match.group(1)),
float(match.group(2)) if match.group else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + scale_re_str, trstr)
if match:
trlist.append([
'translate',
float(match.group(1)),
float(match.group(2)) if not None else float(match.group(1))
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + skew_re_str, trstr)
if match:
trlist.append([
'skew',
float(match.group(2)) if match.group(1) == 'X' else 0.0,
float(match.group(2)) if match.group(1) == 'Y' else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + rotate_re_str, trstr)
if match:
trlist.append([
'rotate',
float(match.group(1)),
float(match.group(2)) if match.group(2) else 0.0,
float(match.group(3)) if match.group(3) else 0.0
])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
match = re.search(r'^' + matrix_re_str, trstr)
if match:
trlist.append(['matrix'] + [float(x) for x in match.groups()])
trstr = trstr[len(match.group(0)):].strip(' ')
continue
# raise Exception("Don't know how to parse: %s" % trstr)
log.error("[error] Don't know how to parse: %s" % trstr)
return trlist
# if __name__ == "__main__":
# tree = ET.parse('tests/svg/drawing.svg')
# root = tree.getroot()
# ns = re.search(r'\{(.*)\}', root.tag).group(1)
# print(ns)
# for geo in getsvggeo(root):
# print(geo)

228
PlotCanvas.py Normal file
View File

@ -0,0 +1,228 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://caram.cl/software/flatcam #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
from PyQt5 import QtCore
import logging
from VisPyCanvas import VisPyCanvas
from VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
from vispy.scene.visuals import InfiniteLine, Line
import numpy as np
from vispy.geometry import Rect
import time
log = logging.getLogger('base')
class PlotCanvas(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
def __init__(self, container, app):
"""
The constructor configures the VisPy figure that
will contain all plots, creates the base axes and connects
events to the plotting area.
:param container: The parent container in which to draw plots.
:rtype: PlotCanvas
"""
super(PlotCanvas, self).__init__()
self.app = app
# Parent container
self.container = container
# workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
# which might decrease performance
self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
# Attach to parent
self.vispy_canvas = VisPyCanvas()
self.vispy_canvas.create_native()
self.vispy_canvas.native.setParent(self.app.ui)
self.container.addWidget(self.vispy_canvas.native)
### AXIS ###
self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=True,
parent=self.vispy_canvas.view.scene)
self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 1.0), vertical=False,
parent=self.vispy_canvas.view.scene)
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
self.draw_workspace()
# if self.app.defaults['global_workspace'] is True:
# if self.app.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
# self.wkspace_t = Line(pos=)
self.shape_collections = []
self.shape_collection = self.new_shape_collection()
self.app.pool_recreated.connect(self.on_pool_recreated)
self.text_collection = self.new_text_collection()
# TODO: Should be setting to show/hide CNC job annotations (global or per object)
self.text_collection.enabled = False
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
def draw_workspace(self):
a = np.empty((0, 0))
a4p_in = np.array([(0, 0), (8.3, 0), (8.3, 11.7), (0, 11.7)])
a4l_in = np.array([(0, 0), (11.7, 0), (11.7, 8.3), (0, 8.3)])
a3p_in = np.array([(0, 0), (11.7, 0), (11.7, 16.5), (0, 16.5)])
a3l_in = np.array([(0, 0), (16.5, 0), (16.5, 11.7), (0, 11.7)])
a4p_mm = np.array([(0, 0), (210, 0), (210, 297), (0, 297)])
a4l_mm = np.array([(0, 0), (297, 0), (297,210), (0, 210)])
a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
if self.app.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
if self.app.defaults['global_workspaceT'] == 'A4P':
a = a4p_mm
elif self.app.defaults['global_workspaceT'] == 'A4L':
a = a4l_mm
elif self.app.defaults['global_workspaceT'] == 'A3P':
a = a3p_mm
elif self.app.defaults['global_workspaceT'] == 'A3L':
a = a3l_mm
else:
if self.app.defaults['global_workspaceT'] == 'A4P':
a = a4p_in
elif self.app.defaults['global_workspaceT'] == 'A4L':
a = a4l_in
elif self.app.defaults['global_workspaceT'] == 'A3P':
a = a3p_in
elif self.app.defaults['global_workspaceT'] == 'A3L':
a = a3l_in
self.delete_workspace()
self.b_line = Line(pos=a[0:2], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.r_line = Line(pos=a[1:3], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.t_line = Line(pos=a[2:4], color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
self.l_line = Line(pos=np.array((a[0], a[3])), color=(0.70, 0.3, 0.3, 1.0),
antialias= True, method='agg', parent=self.vispy_canvas.view.scene)
if self.app.defaults['global_workspace'] is False:
self.delete_workspace()
# delete the workspace lines from the plot by removing the parent
def delete_workspace(self):
try:
self.b_line.parent = None
self.r_line.parent = None
self.t_line.parent = None
self.l_line.parent = None
except:
pass
# redraw the workspace lines on the plot by readding them to the parent view.scene
def restore_workspace(self):
try:
self.b_line.parent = self.vispy_canvas.view.scene
self.r_line.parent = self.vispy_canvas.view.scene
self.t_line.parent = self.vispy_canvas.view.scene
self.l_line.parent = self.vispy_canvas.view.scene
except:
pass
def vis_connect(self, event_name, callback):
return getattr(self.vispy_canvas.events, event_name).connect(callback)
def vis_disconnect(self, event_name, callback):
getattr(self.vispy_canvas.events, event_name).disconnect(callback)
def zoom(self, factor, center=None):
"""
Zooms the plot by factor around a given
center point. Takes care of re-drawing.
:param factor: Number by which to scale the plot.
:type factor: float
:param center: Coordinates [x, y] of the point around which to scale the plot.
:type center: list
:return: None
"""
self.vispy_canvas.view.camera.zoom(factor, center)
def new_shape_group(self):
return ShapeGroup(self.shape_collection)
def new_shape_collection(self, **kwargs):
# sc = ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
# self.shape_collections.append(sc)
# return sc
return ShapeCollection(parent=self.vispy_canvas.view.scene, pool=self.app.pool, **kwargs)
def new_cursor(self):
c = Cursor(pos=np.empty((0, 2)), parent=self.vispy_canvas.view.scene)
c.antialias = 0
return c
def new_text_group(self):
return TextGroup(self.text_collection)
def new_text_collection(self, **kwargs):
return TextCollection(parent=self.vispy_canvas.view.scene, **kwargs)
def fit_view(self, rect=None):
# Lock updates in other threads
self.shape_collection.lock_updates()
if not rect:
rect = Rect(-1, -1, 20, 20)
try:
rect.left, rect.right = self.shape_collection.bounds(axis=0)
rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
except TypeError:
pass
self.vispy_canvas.view.camera.rect = rect
self.shape_collection.unlock_updates()
def fit_center(self, loc, rect=None):
# Lock updates in other threads
self.shape_collection.lock_updates()
if not rect:
try:
rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
except TypeError:
pass
self.vispy_canvas.view.camera.rect = rect
self.shape_collection.unlock_updates()
def clear(self):
pass
def redraw(self):
self.shape_collection.redraw([])
self.text_collection.redraw()
def on_pool_recreated(self, pool):
self.shape_collection.pool = pool

1407
README.md Normal file

File diff suppressed because it is too large Load Diff

167
VisPyCanvas.py Normal file
View File

@ -0,0 +1,167 @@
import numpy as np
from PyQt5.QtGui import QPalette
import vispy.scene as scene
from vispy.scene.cameras.base_camera import BaseCamera
from vispy.color import Color
import time
white = Color("#ffffff" )
black = Color("#000000")
class VisPyCanvas(scene.SceneCanvas):
def __init__(self, config=None):
scene.SceneCanvas.__init__(self, keys=None, config=config)
self.unfreeze()
back_color = str(QPalette().color(QPalette.Window).name())
self.central_widget.bgcolor = back_color
self.central_widget.border_color = back_color
self.grid_widget = self.central_widget.add_grid(margin=10)
self.grid_widget.spacing = 0
top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
top_padding.height_max = 0
self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
self.yaxis.width_max = 55
self.grid_widget.add_widget(self.yaxis, row=1, col=0)
self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
self.xaxis.height_max = 25
self.grid_widget.add_widget(self.xaxis, row=2, col=1)
right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
# right_padding.width_max = 24
right_padding.width_max = 0
view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
view.camera = Camera(aspect=1, rect=(-100,-100,500,500))
# Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
# it is necessary to call manually
self.grid_widget._update_child_widget_dim()
self.xaxis.link_view(view)
self.yaxis.link_view(view)
grid1 = scene.GridLines(parent=view.scene, color='dimgray')
grid1.set_gl_state(depth_test=False)
self.view = view
self.grid = grid1
self.freeze()
# self.measure_fps()
def translate_coords(self, pos):
tr = self.grid.get_transform('canvas', 'visual')
return tr.map(pos)
def translate_coords_2(self, pos):
tr = self.grid.get_transform('visual', 'document')
return tr.map(pos)
class Camera(scene.PanZoomCamera):
def __init__(self, **kwargs):
super(Camera, self).__init__(**kwargs)
self.minimum_scene_size = 0.01
self.maximum_scene_size = 10000
self.last_event = None
self.last_time = 0
# Default mouse button for panning is RMB
self.pan_button_setting = "2"
def zoom(self, factor, center=None):
center = center if (center is not None) else self.center
super(Camera, self).zoom(factor, center)
def viewbox_mouse_event(self, event):
"""
The SubScene received a mouse event; update transform
accordingly.
Parameters
----------
event : instance of Event
The event.
"""
if event.handled or not self.interactive:
return
# Limit mouse move events
last_event = event.last_event
t = time.time()
if t - self.last_time > 0.015:
self.last_time = t
if self.last_event:
last_event = self.last_event
self.last_event = None
else:
if not self.last_event:
self.last_event = last_event
event.handled = True
return
# Scrolling
BaseCamera.viewbox_mouse_event(self, event)
if event.type == 'mouse_wheel':
center = self._scene_transform.imap(event.pos)
scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
self.limited_zoom(scale, center)
event.handled = True
elif event.type == 'mouse_move':
if event.press_event is None:
return
modifiers = event.mouse_event.modifiers
# self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
if event.button == int(self.pan_button_setting) and not modifiers:
# Translate
p1 = np.array(last_event.pos)[:2]
p2 = np.array(event.pos)[:2]
p1s = self._transform.imap(p1)
p2s = self._transform.imap(p2)
self.pan(p1s-p2s)
event.handled = True
elif event.button in [2, 3] and 'Shift' in modifiers:
# Zoom
p1c = np.array(last_event.pos)[:2]
p2c = np.array(event.pos)[:2]
scale = ((1 + self.zoom_factor) **
((p1c-p2c) * np.array([1, -1])))
center = self._transform.imap(event.press_event.pos[:2])
self.limited_zoom(scale, center)
event.handled = True
else:
event.handled = False
elif event.type == 'mouse_press':
# accept the event if it is button 1 or 2.
# This is required in order to receive future events
event.handled = event.button in [1, 2, 3]
else:
event.handled = False
def limited_zoom(self, scale, center):
try:
zoom_in = scale[1] < 1
except IndexError:
zoom_in = scale < 1
if (not zoom_in and self.rect.width < self.maximum_scene_size) \
or (zoom_in and self.rect.width > self.minimum_scene_size):
self.zoom(scale, center)

126
VisPyPatches.py Normal file
View File

@ -0,0 +1,126 @@
from vispy.visuals import markers, LineVisual, InfiniteLineVisual
from vispy.visuals.axis import Ticker, _get_ticks_talbot
from vispy.scene.widgets import Grid
import numpy as np
def apply_patches():
# Patch MarkersVisual to have crossed lines marker
cross_lines = """
float cross(vec2 pointcoord, float size)
{
//vbar
float r1 = abs(pointcoord.x - 0.5)*size;
float r2 = abs(pointcoord.y - 0.5)*size - $v_size/2;
float vbar = max(r1,r2);
//hbar
float r3 = abs(pointcoord.y - 0.5)*size;
float r4 = abs(pointcoord.x - 0.5)*size - $v_size/2;
float hbar = max(r3,r4);
return min(vbar, hbar);
}
"""
markers._marker_dict['++'] = cross_lines
markers.marker_types = tuple(sorted(list(markers._marker_dict.copy().keys())))
# # Add clear_data method to LineVisual to have possibility of clearing data
# def clear_data(self):
# self._bounds = None
# self._pos = None
# self._changed['pos'] = True
# self.update()
#
# LineVisual.clear_data = clear_data
# Patch VisPy Grid to prevent updating layout on PaintGL, which cause low fps
def _prepare_draw(self, view):
pass
def _update_clipper(self):
super(Grid, self)._update_clipper()
try:
self._update_child_widget_dim()
except Exception as e:
print(e)
Grid._prepare_draw = _prepare_draw
Grid._update_clipper = _update_clipper
# Patch InfiniteLine visual to 1px width
def _prepare_draw(self, view=None):
"""This method is called immediately before each draw.
The *view* argument indicates which view is about to be drawn.
"""
GL = None
from vispy.app._default_app import default_app
if default_app is not None and \
default_app.backend_name != 'ipynb_webgl':
try:
import OpenGL.GL as GL
except Exception: # can be other than ImportError sometimes
pass
if GL:
GL.glDisable(GL.GL_LINE_SMOOTH)
GL.glLineWidth(1.0)
if self._changed['pos']:
self.pos_buf.set_data(self._pos)
self._changed['pos'] = False
if self._changed['color']:
self._program.vert['color'] = self._color
self._changed['color'] = False
InfiniteLineVisual._prepare_draw = _prepare_draw
# Patch AxisVisual to have less axis labels
def _get_tick_frac_labels(self):
"""Get the major ticks, minor ticks, and major labels"""
minor_num = 4 # number of minor ticks per major division
if (self.axis.scale_type == 'linear'):
domain = self.axis.domain
if domain[1] < domain[0]:
flip = True
domain = domain[::-1]
else:
flip = False
offset = domain[0]
scale = domain[1] - domain[0]
transforms = self.axis.transforms
length = self.axis.pos[1] - self.axis.pos[0] # in logical coords
n_inches = np.sqrt(np.sum(length ** 2)) / transforms.dpi
# major = np.linspace(domain[0], domain[1], num=11)
# major = MaxNLocator(10).tick_values(*domain)
major = _get_ticks_talbot(domain[0], domain[1], n_inches, 1)
labels = ['%g' % x for x in major]
majstep = major[1] - major[0]
minor = []
minstep = majstep / (minor_num + 1)
minstart = 0 if self.axis._stop_at_major[0] else -1
minstop = -1 if self.axis._stop_at_major[1] else 0
for i in range(minstart, len(major) + minstop):
maj = major[0] + i * majstep
minor.extend(np.linspace(maj + minstep,
maj + majstep - minstep,
minor_num))
major_frac = (major - offset) / scale
minor_frac = (np.array(minor) - offset) / scale
major_frac = major_frac[::-1] if flip else major_frac
use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
major_frac = major_frac[use_mask]
labels = [l for li, l in enumerate(labels) if use_mask[li]]
minor_frac = minor_frac[(minor_frac > -0.0001) &
(minor_frac < 1.0001)]
elif self.axis.scale_type == 'logarithmic':
return NotImplementedError
elif self.axis.scale_type == 'power':
return NotImplementedError
return major_frac, minor_frac, labels
Ticker._get_tick_frac_labels = _get_tick_frac_labels

90
VisPyTesselators.py Normal file
View File

@ -0,0 +1,90 @@
from OpenGL import GLU
class GLUTess:
def __init__(self):
"""
OpenGL GLU triangulation class
"""
self.tris = []
self.pts = []
self.vertex_index = 0
def _on_begin_primitive(self, type):
pass
def _on_new_vertex(self, vertex):
self.tris.append(vertex)
# Force GLU to return separate triangles (GLU_TRIANGLES)
def _on_edge_flag(self, flag):
pass
def _on_combine(self, coords, data, weight):
return (coords[0], coords[1], coords[2])
def _on_error(self, errno):
print("GLUTess error:", errno)
def _on_end_primitive(self):
pass
def triangulate(self, polygon):
"""
Triangulates polygon
:param polygon: shapely.geometry.polygon
Polygon to tessellate
:return: list, list
Array of triangle vertex indices [t0i0, t0i1, t0i2, t1i0, t1i1, ... ]
Array of polygon points [(x0, y0), (x1, y1), ... ]
"""
# Create tessellation object
tess = GLU.gluNewTess()
# Setup callbacks
GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, self._on_begin_primitive)
GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, self._on_new_vertex)
GLU.gluTessCallback(tess, GLU.GLU_TESS_EDGE_FLAG, self._on_edge_flag)
GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, self._on_combine)
GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR, self._on_error)
GLU.gluTessCallback(tess, GLU.GLU_TESS_END, self._on_end_primitive)
# Reset data
del self.tris[:]
del self.pts[:]
self.vertex_index = 0
# Define polygon
GLU.gluTessBeginPolygon(tess, None)
def define_contour(contour):
vertices = list(contour.coords) # Get vertices coordinates
if vertices[0] == vertices[-1]: # Open ring
vertices = vertices[:-1]
self.pts += vertices
GLU.gluTessBeginContour(tess) # Start contour
# Set vertices
for vertex in vertices:
point = (vertex[0], vertex[1], 0)
GLU.gluTessVertex(tess, point, self.vertex_index)
self.vertex_index += 1
GLU.gluTessEndContour(tess) # End contour
# Polygon exterior
define_contour(polygon.exterior)
# Interiors
for interior in polygon.interiors:
define_contour(interior)
# Start tessellation
GLU.gluTessEndPolygon(tess)
# Free resources
GLU.gluDeleteTess(tess)
return self.tris, self.pts

596
VisPyVisuals.py Normal file
View File

@ -0,0 +1,596 @@
from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
from vispy.scene.visuals import VisualNode, generate_docstring, visuals
from vispy.gloo import set_state
from vispy.color import Color
from shapely.geometry import Polygon, LineString, LinearRing
import threading
import numpy as np
from VisPyTesselators import GLUTess
class FlatCAMLineVisual(LineVisual):
def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
method='gl', antialias=False):
LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
method='gl', antialias=True)
def clear_data(self):
self._bounds = None
self._pos = None
self._changed['pos'] = True
self.update()
def _update_shape_buffers(data, triangulation='glu'):
"""
Translates Shapely geometry to internal buffers for speedup redraws
:param data: dict
Input shape data
:param triangulation: str
Triangulation engine
"""
mesh_vertices = [] # Vertices for mesh
mesh_tris = [] # Faces for mesh
mesh_colors = [] # Face colors
line_pts = [] # Vertices for line
line_colors = [] # Line color
geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
if geo is not None and not geo.is_empty:
simple = geo.simplify(tolerance) if tolerance else geo # Simplified shape
pts = [] # Shape line points
tri_pts = [] # Mesh vertices
tri_tris = [] # Mesh faces
if type(geo) == LineString:
# Prepare lines
pts = _linestring_to_segments(list(simple.coords))
elif type(geo) == LinearRing:
# Prepare lines
pts = _linearring_to_segments(list(simple.coords))
elif type(geo) == Polygon:
# Prepare polygon faces
if face_color is not None:
if triangulation == 'glu':
gt = GLUTess()
tri_tris, tri_pts = gt.triangulate(simple)
else:
print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
# Prepare polygon edges
if color is not None:
pts = _linearring_to_segments(list(simple.exterior.coords))
for ints in simple.interiors:
pts += _linearring_to_segments(list(ints.coords))
# Appending data for mesh
if len(tri_pts) > 0 and len(tri_tris) > 0:
mesh_tris += tri_tris
mesh_vertices += tri_pts
mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
# Appending data for line
if len(pts) > 0:
line_pts += pts
line_colors += [Color(color).rgba] * len(pts)
# Store buffers
data['line_pts'] = line_pts
data['line_colors'] = line_colors
data['mesh_vertices'] = mesh_vertices
data['mesh_tris'] = mesh_tris
data['mesh_colors'] = mesh_colors
# Clear shapely geometry
del data['geometry']
return data
def _linearring_to_segments(arr):
# Close linear ring
"""
Translates linear ring to line segments
:param arr: numpy.array
Array of linear ring vertices
:return: numpy.array
Line segments
"""
if arr[0] != arr[-1]:
arr.append(arr[0])
return _linestring_to_segments(arr)
def _linestring_to_segments(arr):
"""
Translates line strip to segments
:param arr: numpy.array
Array of line strip vertices
:return: numpy.array
Line segments
"""
return [arr[i // 2] for i in range(0, len(arr) * 2)][1:-1]
class ShapeGroup(object):
def __init__(self, collection):
"""
Represents group of shapes in collection
:param collection: ShapeCollection
Collection to work with
"""
self._collection = collection
self._indexes = []
self._visible = True
self._color = None
def add(self, **kwargs):
"""
Adds shape to collection and store index in group
:param kwargs: keyword arguments
Arguments for ShapeCollection.add function
"""
self._indexes.append(self._collection.add(**kwargs))
def clear(self, update=False):
"""
Removes group shapes from collection, clear indexes
:param update: bool
Set True to redraw collection
"""
for i in self._indexes:
self._collection.remove(i, False)
del self._indexes[:]
if update:
self._collection.redraw([]) # Skip waiting results
def redraw(self):
"""
Redraws shape collection
"""
self._collection.redraw(self._indexes)
@property
def visible(self):
"""
Visibility of group
:return: bool
"""
return self._visible
@visible.setter
def visible(self, value):
"""
Visibility of group
:param value: bool
"""
self._visible = value
for i in self._indexes:
self._collection.data[i]['visible'] = value
self._collection.redraw([])
class ShapeCollectionVisual(CompoundVisual):
def __init__(self, line_width=1, triangulation='gpc', layers=3, pool=None, **kwargs):
"""
Represents collection of shapes to draw on VisPy scene
:param line_width: float
Width of lines/edges
:param triangulation: str
Triangulation method used for polygons translation
'vispy' - VisPy lib triangulation
'gpc' - Polygon2 lib
:param layers: int
Layers count
Each layer adds 2 visuals on VisPy scene. Be careful: more layers cause less fps
:param kwargs:
"""
self.data = {}
self.last_key = -1
# Thread locks
self.key_lock = threading.Lock()
self.results_lock = threading.Lock()
self.update_lock = threading.Lock()
# Process pool
self.pool = pool
self.results = {}
self._meshes = [MeshVisual() for _ in range(0, layers)]
# self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
self._lines = [FlatCAMLineVisual(antialias=True) for _ in range(0, layers)]
self._line_width = line_width
self._triangulation = triangulation
visuals_ = [self._lines[i // 2] if i % 2 else self._meshes[i // 2] for i in range(0, layers * 2)]
CompoundVisual.__init__(self, visuals_, **kwargs)
for m in self._meshes:
pass
m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
for l in self._lines:
pass
l.set_gl_state(blend=True)
self.freeze()
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
update=False, layer=1, tolerance=0.01):
"""
Adds shape to collection
:return:
:param shape: shapely.geometry
Shapely geometry object
:param color: str, tuple
Line/edge color
:param face_color: str, tuple
Polygon face color
:param visible: bool
Shape visibility
:param update: bool
Set True to redraw collection
:param layer: int
Layer number. 0 - lowest.
:param tolerance: float
Geometry simplifying tolerance
:return: int
Index of shape
"""
# Get new key
self.key_lock.acquire(True)
self.last_key += 1
key = self.last_key
self.key_lock.release()
# Prepare data for translation
self.data[key] = {'geometry': shape, 'color': color, 'alpha': alpha, 'face_color': face_color,
'visible': visible, 'layer': layer, 'tolerance': tolerance}
# Add data to process pool if pool exists
try:
self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
except:
self.data[key] = _update_shape_buffers(self.data[key])
if update:
self.redraw() # redraw() waits for pool process end
return key
def remove(self, key, update=False):
"""
Removes shape from collection
:param key: int
Shape index to remove
:param update:
Set True to redraw collection
"""
# Remove process result
self.results_lock.acquire(True)
if key in list(self.results.copy().keys()):
del self.results[key]
self.results_lock.release()
# Remove data
del self.data[key]
if update:
self.__update()
def clear(self, update=False):
"""
Removes all shapes from collection
:param update: bool
Set True to redraw collection
"""
self.data.clear()
if update:
self.__update()
def __update(self):
"""
Merges internal buffers, sets data to visuals, redraws collection on scene
"""
mesh_vertices = [[] for _ in range(0, len(self._meshes))] # Vertices for mesh
mesh_tris = [[] for _ in range(0, len(self._meshes))] # Faces for mesh
mesh_colors = [[] for _ in range(0, len(self._meshes))] # Face colors
line_pts = [[] for _ in range(0, len(self._lines))] # Vertices for line
line_colors = [[] for _ in range(0, len(self._lines))] # Line color
# Lock sub-visuals updates
self.update_lock.acquire(True)
# Merge shapes buffers
for data in list(self.data.values()):
if data['visible'] and 'line_pts' in data:
try:
line_pts[data['layer']] += data['line_pts']
line_colors[data['layer']] += data['line_colors']
mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
for x in data['mesh_tris']]
mesh_vertices[data['layer']] += data['mesh_vertices']
mesh_colors[data['layer']] += data['mesh_colors']
except Exception as e:
print("Data error", e)
# Updating meshes
for i, mesh in enumerate(self._meshes):
if len(mesh_vertices[i]) > 0:
set_state(polygon_offset_fill=False)
mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
.reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
else:
mesh.set_data()
mesh._bounds_changed()
# Updating lines
for i, line in enumerate(self._lines):
if len(line_pts[i]) > 0:
line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
else:
line.clear_data()
line._bounds_changed()
self._bounds_changed()
self.update_lock.release()
def redraw(self, indexes=None):
"""
Redraws collection
:param indexes: list
Shape indexes to get from process pool
"""
# Only one thread can update data
self.results_lock.acquire(True)
for i in list(self.data.copy().keys()) if not indexes else indexes:
if i in list(self.results.copy().keys()):
try:
self.results[i].wait() # Wait for process results
if i in self.data:
self.data[i] = self.results[i].get()[0] # Store translated data
del self.results[i]
except Exception as e:
print(e, indexes)
self.results_lock.release()
self.__update()
def lock_updates(self):
self.update_lock.acquire(True)
def unlock_updates(self):
self.update_lock.release()
class TextGroup(object):
def __init__(self, collection):
self._collection = collection
self._index = None
self._visible = None
def set(self, **kwargs):
"""
Adds text to collection and store index
:param kwargs: keyword arguments
Arguments for TextCollection.add function
"""
self._index = self._collection.add(**kwargs)
def clear(self, update=False):
"""
Removes text from collection, clear index
:param update: bool
Set True to redraw collection
"""
if self._index is not None:
self._collection.remove(self._index, False)
self._index = None
if update:
self._collection.redraw()
def redraw(self):
"""
Redraws text collection
"""
self._collection.redraw()
@property
def visible(self):
"""
Visibility of group
:return: bool
"""
return self._visible
@visible.setter
def visible(self, value):
"""
Visibility of group
:param value: bool
"""
self._visible = value
self._collection.data[self._index]['visible'] = value
self._collection.redraw()
class TextCollectionVisual(TextVisual):
def __init__(self, **kwargs):
"""
Represents collection of shapes to draw on VisPy scene
:param kwargs: keyword arguments
Arguments to pass for TextVisual
"""
self.data = {}
self.last_key = -1
self.lock = threading.Lock()
super(TextCollectionVisual, self).__init__(**kwargs)
self.freeze()
def add(self, text, pos, visible=True, update=True):
"""
Adds array of text to collection
:param text: list
Array of strings ['str1', 'str2', ... ]
:param pos: list
Array of string positions [(0, 0), (10, 10), ... ]
:param update: bool
Set True to redraw collection
:return: int
Index of array
"""
# Get new key
self.lock.acquire(True)
self.last_key += 1
key = self.last_key
self.lock.release()
# Prepare data for translation
self.data[key] = {'text': text, 'pos': pos, 'visible': visible}
if update:
self.redraw()
return key
def remove(self, key, update=False):
"""
Removes shape from collection
:param key: int
Shape index to remove
:param update:
Set True to redraw collection
"""
del self.data[key]
if update:
self.__update()
def clear(self, update=False):
"""
Removes all shapes from colleciton
:param update: bool
Set True to redraw collection
"""
self.data.clear()
if update:
self.__update()
def __update(self):
"""
Merges internal buffers, sets data to visuals, redraws collection on scene
"""
labels = []
pos = []
# Merge buffers
for data in list(self.data.values()):
if data['visible']:
try:
labels += data['text']
pos += data['pos']
except Exception as e:
print("Data error", e)
# Updating text
if len(labels) > 0:
self.text = labels
self.pos = pos
else:
self.text = None
self.pos = (0, 0)
self._bounds_changed()
def redraw(self):
"""
Redraws collection
"""
self.__update()
# Add 'enabled' property to visual nodes
def create_fast_node(subclass):
# Create a new subclass of Node.
# Decide on new class name
clsname = subclass.__name__
if not (clsname.endswith('Visual') and
issubclass(subclass, visuals.BaseVisual)):
raise RuntimeError('Class "%s" must end with Visual, and must '
'subclass BaseVisual' % clsname)
clsname = clsname[:-6]
# Generate new docstring based on visual docstring
try:
doc = generate_docstring(subclass, clsname)
except Exception:
# If parsing fails, just return the original Visual docstring
doc = subclass.__doc__
# New __init__ method
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent', None)
name = kwargs.pop('name', None)
self.name = name # to allow __str__ before Node.__init__
self._visual_superclass = subclass
# parent: property,
# _parent: attribute of Node class
# __parent: attribute of fast_node class
self.__parent = parent
self._enabled = False
subclass.__init__(self, *args, **kwargs)
self.unfreeze()
VisualNode.__init__(self, parent=parent, name=name)
self.freeze()
# Create new class
cls = type(clsname, (VisualNode, subclass),
{'__init__': __init__, '__doc__': doc})
# 'Enabled' property clears/restores 'parent' property of Node class
# Scene will be painted quicker than when using 'visible' property
def get_enabled(self):
return self._enabled
def set_enabled(self, enabled):
if enabled:
self.parent = self.__parent # Restore parent
else:
if self.parent: # Store parent
self.__parent = self.parent
self.parent = None
cls.enabled = property(get_enabled, set_enabled)
return cls
ShapeCollection = create_fast_node(ShapeCollectionVisual)
TextCollection = create_fast_node(TextCollectionVisual)
Cursor = create_fast_node(MarkersVisual)

6435
camlib.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,169 @@
from PyQt5 import QtGui
from GUIElements import FCEntry
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
import math
class ToolCalculator(FlatCAMTool):
toolName = "Calculators"
v_shapeName = "V-Shape Tool Calculator"
unitsName = "Units Calculator"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
self.app = app
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.layout.addWidget(title_label)
## V-shape Tool Calculator
self.v_shape_spacer_label = QtWidgets.QLabel(" ")
self.layout.addWidget(self.v_shape_spacer_label)
## Title of the V-shape Tools Calculator
v_shape_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.v_shapeName)
self.layout.addWidget(v_shape_title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
self.tipDia_label = QtWidgets.QLabel("Tip Diameter:")
self.tipDia_entry = FCEntry()
self.tipDia_entry.setFixedWidth(70)
self.tipDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.tipDia_entry.setToolTip('This is the diameter of the tool tip.\n'
'The manufacturer specifies it.')
self.tipAngle_label = QtWidgets.QLabel("Tip Angle:")
self.tipAngle_entry = FCEntry()
self.tipAngle_entry.setFixedWidth(70)
self.tipAngle_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.tipAngle_entry.setToolTip("This is the angle of the tip of the tool.\n"
"It is specified by manufacturer.")
self.cutDepth_label = QtWidgets.QLabel("Cut Z:")
self.cutDepth_entry = FCEntry()
self.cutDepth_entry.setFixedWidth(70)
self.cutDepth_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.cutDepth_entry.setToolTip("This is the depth to cut into the material.\n"
"In the CNCJob is the CutZ parameter.")
self.effectiveToolDia_label = QtWidgets.QLabel("Tool Diameter:")
self.effectiveToolDia_entry = FCEntry()
self.effectiveToolDia_entry.setFixedWidth(70)
self.effectiveToolDia_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.effectiveToolDia_entry.setToolTip("This is the tool diameter to be entered into\n"
"FlatCAM Gerber section.\n"
"In the CNCJob section it is called >Tool dia<.")
# self.effectiveToolDia_entry.setEnabled(False)
form_layout.addRow(self.tipDia_label, self.tipDia_entry)
form_layout.addRow(self.tipAngle_label, self.tipAngle_entry)
form_layout.addRow(self.cutDepth_label, self.cutDepth_entry)
form_layout.addRow(self.effectiveToolDia_label, self.effectiveToolDia_entry)
## Buttons
self.calculate_button = QtWidgets.QPushButton("Calculate")
self.calculate_button.setFixedWidth(70)
self.calculate_button.setToolTip(
"Calculate either the Cut Z or the effective tool diameter,\n "
"depending on which is desired and which is known. "
)
self.empty_label = QtWidgets.QLabel(" ")
form_layout.addRow(self.empty_label, self.calculate_button)
## Units Calculator
self.unists_spacer_label = QtWidgets.QLabel(" ")
self.layout.addWidget(self.unists_spacer_label)
## Title of the Units Calculator
units_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.unitsName)
self.layout.addWidget(units_label)
#Form Layout
form_units_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_units_layout)
inch_label = QtWidgets.QLabel("INCH")
mm_label = QtWidgets.QLabel("MM")
self.inch_entry = FCEntry()
self.inch_entry.setFixedWidth(70)
self.inch_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.inch_entry.setToolTip("Here you enter the value to be converted from INCH to MM")
self.mm_entry = FCEntry()
self.mm_entry.setFixedWidth(70)
self.mm_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.mm_entry.setToolTip("Here you enter the value to be converted from MM to INCH")
form_units_layout.addRow(mm_label, inch_label)
form_units_layout.addRow(self.mm_entry, self.inch_entry)
self.layout.addStretch()
## Signals
self.cutDepth_entry.textChanged.connect(self.on_calculate_tool_dia)
self.cutDepth_entry.editingFinished.connect(self.on_calculate_tool_dia)
self.tipDia_entry.editingFinished.connect(self.on_calculate_tool_dia)
self.tipAngle_entry.editingFinished.connect(self.on_calculate_tool_dia)
self.calculate_button.clicked.connect(self.on_calculate_tool_dia)
self.mm_entry.editingFinished.connect(self.on_calculate_inch_units)
self.inch_entry.editingFinished.connect(self.on_calculate_mm_units)
## Initialize form
if self.app.defaults["units"] == 'MM':
self.tipDia_entry.set_value('0.2')
self.tipAngle_entry.set_value('45')
self.cutDepth_entry.set_value('0.25')
self.effectiveToolDia_entry.set_value('0.39')
else:
self.tipDia_entry.set_value('7.87402')
self.tipAngle_entry.set_value('45')
self.cutDepth_entry.set_value('9.84252')
self.effectiveToolDia_entry.set_value('15.35433')
self.mm_entry.set_value('0')
self.inch_entry.set_value('0')
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Calc. Tool")
def on_calculate_tool_dia(self):
# Calculation:
# Manufacturer gives total angle of the the tip but we need only half of it
# tangent(half_tip_angle) = opposite side / adjacent = part_of _real_dia / depth_of_cut
# effective_diameter = tip_diameter + part_of_real_dia_left_side + part_of_real_dia_right_side
# tool is symmetrical therefore: part_of_real_dia_left_side = part_of_real_dia_right_side
# effective_diameter = tip_diameter + (2 * part_of_real_dia_left_side)
# effective diameter = tip_diameter + (2 * depth_of_cut * tangent(half_tip_angle))
try:
tip_diameter = float(self.tipDia_entry.get_value())
half_tip_angle = float(self.tipAngle_entry.get_value()) / 2
cut_depth = float(self.cutDepth_entry.get_value())
except TypeError:
return
tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))
self.effectiveToolDia_entry.set_value("%.4f" % tool_diameter)
def on_calculate_inch_units(self):
self.inch_entry.set_value('%.6f' % (float(self.mm_entry.get_value()) / 25.4))
def on_calculate_mm_units(self):
self.mm_entry.set_value('%.6f' % (float(self.inch_entry.get_value()) * 25.4))
# end of file

390
flatcamTools/ToolCutout.py Normal file
View File

@ -0,0 +1,390 @@
from FlatCAMTool import FlatCAMTool
from copy import copy,deepcopy
from ObjectCollection import *
from FlatCAMApp import *
from PyQt5 import QtGui, QtCore, QtWidgets
from GUIElements import IntEntry, RadioSet, LengthEntry
from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
class ToolCutout(FlatCAMTool):
toolName = "Cutout PCB Tool"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.layout.addWidget(title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
## Type of object to be cutout
self.type_obj_combo = QtWidgets.QComboBox()
self.type_obj_combo.addItem("Gerber")
self.type_obj_combo.addItem("Excellon")
self.type_obj_combo.addItem("Geometry")
# we get rid of item1 ("Excellon") as it is not suitable for creating film
self.type_obj_combo.view().setRowHidden(1, True)
self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
# self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
self.type_obj_combo_label.setToolTip(
"Specify the type of object to be cutout.\n"
"It can be of type: Gerber or Geometry.\n"
"What is selected here will dictate the kind\n"
"of objects that will populate the 'Object' combobox."
)
form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
## Object to be cutout
self.obj_combo = QtWidgets.QComboBox()
self.obj_combo.setModel(self.app.collection)
self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.obj_combo.setCurrentIndex(1)
self.object_label = QtWidgets.QLabel("Object:")
self.object_label.setToolTip(
"Object to be cutout. "
)
form_layout.addRow(self.object_label, self.obj_combo)
# Tool Diameter
self.dia = FCEntry()
self.dia_label = QtWidgets.QLabel("Tool Dia:")
self.dia_label.setToolTip(
"Diameter of the tool used to cutout\n"
"the PCB shape out of the surrounding material."
)
form_layout.addRow(self.dia_label, self.dia)
# Margin
self.margin = FCEntry()
self.margin_label = QtWidgets.QLabel("Margin:")
self.margin_label.setToolTip(
"Margin over bounds. A positive value here\n"
"will make the cutout of the PCB further from\n"
"the actual PCB border"
)
form_layout.addRow(self.margin_label, self.margin)
# Gapsize
self.gapsize = FCEntry()
self.gapsize_label = QtWidgets.QLabel("Gap size:")
self.gapsize_label.setToolTip(
"The size of the gaps in the cutout\n"
"used to keep the board connected to\n"
"the surrounding material (the one \n"
"from which the PCB is cutout)."
)
form_layout.addRow(self.gapsize_label, self.gapsize)
## Title2
title_ff_label = QtWidgets.QLabel("<font size=4><b>FreeForm Cutout</b></font>")
self.layout.addWidget(title_ff_label)
## Form Layout
form_layout_2 = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout_2)
# How gaps wil be rendered:
# lr - left + right
# tb - top + bottom
# 4 - left + right +top + bottom
# 2lr - 2*left + 2*right
# 2tb - 2*top + 2*bottom
# 8 - 2*left + 2*right +2*top + 2*bottom
# Gaps
self.gaps = FCEntry()
self.gaps_label = QtWidgets.QLabel("Type of gaps: ")
self.gaps_label.setToolTip(
"Number of gaps used for the cutout.\n"
"There can be maximum 8 bridges/gaps.\n"
"The choices are:\n"
"- lr - left + right\n"
"- tb - top + bottom\n"
"- 4 - left + right +top + bottom\n"
"- 2lr - 2*left + 2*right\n"
"- 2tb - 2*top + 2*bottom\n"
"- 8 - 2*left + 2*right +2*top + 2*bottom"
)
form_layout_2.addRow(self.gaps_label, self.gaps)
## Buttons
hlay = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay)
hlay.addStretch()
self.ff_cutout_object_btn = QtWidgets.QPushButton(" FreeForm Cutout Object ")
self.ff_cutout_object_btn.setToolTip(
"Cutout the selected object.\n"
"The cutout shape can be any shape.\n"
"Useful when the PCB has a non-rectangular shape.\n"
"But if the object to be cutout is of Gerber Type,\n"
"it needs to be an outline of the actual board shape."
)
hlay.addWidget(self.ff_cutout_object_btn)
## Title3
title_rct_label = QtWidgets.QLabel("<font size=4><b>Rectangular Cutout</b></font>")
self.layout.addWidget(title_rct_label)
## Form Layout
form_layout_3 = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout_3)
gapslabel_rect = QtWidgets.QLabel('Type of gaps:')
gapslabel_rect.setToolTip(
"Where to place the gaps:\n"
"- one gap Top / one gap Bottom\n"
"- one gap Left / one gap Right\n"
"- one gap on each of the 4 sides."
)
self.gaps_rect_radio = RadioSet([{'label': 'T/B', 'value': 'tb'},
{'label': 'L/R', 'value': 'lr'},
{'label': '4', 'value': '4'}])
form_layout_3.addRow(gapslabel_rect, self.gaps_rect_radio)
hlay2 = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay2)
hlay2.addStretch()
self.rect_cutout_object_btn = QtWidgets.QPushButton("Rectangular Cutout Object")
self.rect_cutout_object_btn.setToolTip(
"Cutout the selected object.\n"
"The resulting cutout shape is\n"
"always of a rectangle form and it will be\n"
"the bounding box of the Object."
)
hlay2.addWidget(self.rect_cutout_object_btn)
self.layout.addStretch()
## Init GUI
self.dia.set_value(1)
self.margin.set_value(0)
self.gapsize.set_value(1)
self.gaps.set_value(4)
self.gaps_rect_radio.set_value("4")
## Signals
self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
def on_type_obj_index_changed(self, index):
obj_type = self.type_obj_combo.currentIndex()
self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.obj_combo.setCurrentIndex(0)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Cutout Tool")
def on_freeform_cutout(self):
def subtract_rectangle(obj_, x0, y0, x1, y1):
pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
obj_.subtract_polygon(pts)
name = self.obj_combo.currentText()
# Get source object.
try:
cutout_obj = self.app.collection.get_by_name(str(name))
except:
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
return "Could not retrieve object: %s" % name
if cutout_obj is None:
self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
try:
dia = float(self.dia.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
return
try:
margin = float(self.margin.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
return
try:
gapsize = float(self.gapsize.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
return
try:
gaps = self.gaps.get_value()
except TypeError:
self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
return
if 0 in {dia}:
self.app.inform.emit("[warning_notcl]Tool Diameter is zero value. Change it to a positive integer.")
return "Tool Diameter is zero value. Change it to a positive integer."
if gaps not in ['lr', 'tb', '2lr', '2tb', '4', '8']:
self.app.inform.emit("[warning_notcl] Gaps value can be only one of: 'lr', 'tb', '2lr', '2tb', 4 or 8. "
"Fill in a correct value and retry. ")
return
# Get min and max data for each object as we just cut rectangles across X or Y
xmin, ymin, xmax, ymax = cutout_obj.bounds()
px = 0.5 * (xmin + xmax) + margin
py = 0.5 * (ymin + ymax) + margin
lenghtx = (xmax - xmin) + (margin * 2)
lenghty = (ymax - ymin) + (margin * 2)
gapsize = gapsize + (dia / 2)
if isinstance(cutout_obj,FlatCAMGeometry):
# rename the obj name so it can be identified as cutout
cutout_obj.options["name"] += "_cutout"
else:
cutout_obj.isolate(dia=dia, passes=1, overlap=1, combine=False, outname="_temp")
ext_obj = self.app.collection.get_by_name("_temp")
def geo_init(geo_obj, app_obj):
geo_obj.solid_geometry = obj_exteriors
outname = cutout_obj.options["name"] + "_cutout"
obj_exteriors = ext_obj.get_exteriors()
self.app.new_object('geometry', outname, geo_init)
self.app.collection.set_all_inactive()
self.app.collection.set_active("_temp")
self.app.on_delete()
cutout_obj = self.app.collection.get_by_name(outname)
if int(gaps) == 8 or gaps == '2lr':
subtract_rectangle(cutout_obj,
xmin - gapsize, # botleft_x
py - gapsize + lenghty / 4, # botleft_y
xmax + gapsize, # topright_x
py + gapsize + lenghty / 4) # topright_y
subtract_rectangle(cutout_obj,
xmin - gapsize,
py - gapsize - lenghty / 4,
xmax + gapsize,
py + gapsize - lenghty / 4)
if int(gaps) == 8 or gaps == '2tb':
subtract_rectangle(cutout_obj,
px - gapsize + lenghtx / 4,
ymin - gapsize,
px + gapsize + lenghtx / 4,
ymax + gapsize)
subtract_rectangle(cutout_obj,
px - gapsize - lenghtx / 4,
ymin - gapsize,
px + gapsize - lenghtx / 4,
ymax + gapsize)
if int(gaps) == 4 or gaps == 'lr':
subtract_rectangle(cutout_obj,
xmin - gapsize,
py - gapsize,
xmax + gapsize,
py + gapsize)
if int(gaps) == 4 or gaps == 'tb':
subtract_rectangle(cutout_obj,
px - gapsize,
ymin - gapsize,
px + gapsize,
ymax + gapsize)
cutout_obj.plot()
self.app.inform.emit("[success] Any form CutOut operation finished.")
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
def on_rectangular_cutout(self):
name = self.obj_combo.currentText()
# Get source object.
try:
cutout_obj = self.app.collection.get_by_name(str(name))
except:
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
return "Could not retrieve object: %s" % name
if cutout_obj is None:
self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
try:
dia = float(self.dia.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Tool diameter value is missing. Add it and retry.")
return
try:
margin = float(self.margin.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Margin value is missing. Add it and retry.")
return
try:
gapsize = float(self.gapsize.get_value())
except TypeError:
self.app.inform.emit("[warning_notcl] Gap size value is missing. Add it and retry.")
return
try:
gaps = self.gaps_rect_radio.get_value()
except TypeError:
self.app.inform.emit("[warning_notcl] Number of gaps value is missing. Add it and retry.")
return
if 0 in {dia}:
self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.")
return "Tool Diameter is zero value. Change it to a positive integer."
def geo_init(geo_obj, app_obj):
real_margin = margin + (dia / 2)
real_gap_size = gapsize + dia
minx, miny, maxx, maxy = cutout_obj.bounds()
minx -= real_margin
maxx += real_margin
miny -= real_margin
maxy += real_margin
midx = 0.5 * (minx + maxx)
midy = 0.5 * (miny + maxy)
hgap = 0.5 * real_gap_size
pts = [[midx - hgap, maxy],
[minx, maxy],
[minx, midy + hgap],
[minx, midy - hgap],
[minx, miny],
[midx - hgap, miny],
[midx + hgap, miny],
[maxx, miny],
[maxx, midy - hgap],
[maxx, midy + hgap],
[maxx, maxy],
[midx + hgap, maxy]]
cases = {"tb": [[pts[0], pts[1], pts[4], pts[5]],
[pts[6], pts[7], pts[10], pts[11]]],
"lr": [[pts[9], pts[10], pts[1], pts[2]],
[pts[3], pts[4], pts[7], pts[8]]],
"4": [[pts[0], pts[1], pts[2]],
[pts[3], pts[4], pts[5]],
[pts[6], pts[7], pts[8]],
[pts[9], pts[10], pts[11]]]}
cuts = cases[gaps]
geo_obj.solid_geometry = cascaded_union([LineString(segment) for segment in cuts])
# TODO: Check for None
self.app.new_object("geometry", name + "_cutout", geo_init)
self.app.inform.emit("[success] Rectangular CutOut operation finished.")
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
def reset_fields(self):
self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

View File

@ -0,0 +1,467 @@
from PyQt5 import QtGui
from GUIElements import RadioSet, EvalEntry, LengthEntry
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
from shapely.geometry import Point
from shapely import affinity
from PyQt5 import QtCore
class DblSidedTool(FlatCAMTool):
toolName = "Double-Sided PCB Tool"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.layout.addWidget(title_label)
self.empty_lb = QtWidgets.QLabel("")
self.layout.addWidget(self.empty_lb)
## Grid Layout
grid_lay = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay)
## Gerber Object to mirror
self.gerber_object_combo = QtWidgets.QComboBox()
self.gerber_object_combo.setModel(self.app.collection)
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_object_combo.setCurrentIndex(1)
self.botlay_label = QtWidgets.QLabel("<b>GERBER:</b>")
self.botlay_label.setToolTip(
"Gerber to be mirrored."
)
self.mirror_gerber_button = QtWidgets.QPushButton("Mirror")
self.mirror_gerber_button.setToolTip(
"Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it."
)
self.mirror_gerber_button.setFixedWidth(40)
# grid_lay.addRow("Bottom Layer:", self.object_combo)
grid_lay.addWidget(self.botlay_label, 0, 0)
grid_lay.addWidget(self.gerber_object_combo, 1, 0, 1, 2)
grid_lay.addWidget(self.mirror_gerber_button, 1, 3)
## Excellon Object to mirror
self.exc_object_combo = QtWidgets.QComboBox()
self.exc_object_combo.setModel(self.app.collection)
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
self.exc_object_combo.setCurrentIndex(1)
self.excobj_label = QtWidgets.QLabel("<b>EXCELLON:</b>")
self.excobj_label.setToolTip(
"Excellon Object to be mirrored."
)
self.mirror_exc_button = QtWidgets.QPushButton("Mirror")
self.mirror_exc_button.setToolTip(
"Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it."
)
self.mirror_exc_button.setFixedWidth(40)
# grid_lay.addRow("Bottom Layer:", self.object_combo)
grid_lay.addWidget(self.excobj_label, 2, 0)
grid_lay.addWidget(self.exc_object_combo, 3, 0, 1, 2)
grid_lay.addWidget(self.mirror_exc_button, 3, 3)
## Geometry Object to mirror
self.geo_object_combo = QtWidgets.QComboBox()
self.geo_object_combo.setModel(self.app.collection)
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.geo_object_combo.setCurrentIndex(1)
self.geoobj_label = QtWidgets.QLabel("<b>GEOMETRY</b>:")
self.geoobj_label.setToolTip(
"Geometry Obj to be mirrored."
)
self.mirror_geo_button = QtWidgets.QPushButton("Mirror")
self.mirror_geo_button.setToolTip(
"Mirrors (flips) the specified object around \n"
"the specified axis. Does not create a new \n"
"object, but modifies it."
)
self.mirror_geo_button.setFixedWidth(40)
# grid_lay.addRow("Bottom Layer:", self.object_combo)
grid_lay.addWidget(self.geoobj_label, 4, 0)
grid_lay.addWidget(self.geo_object_combo, 5, 0, 1, 2)
grid_lay.addWidget(self.mirror_geo_button, 5, 3)
## Axis
self.mirror_axis = RadioSet([{'label': 'X', 'value': 'X'},
{'label': 'Y', 'value': 'Y'}])
self.mirax_label = QtWidgets.QLabel("Mirror Axis:")
self.mirax_label.setToolTip(
"Mirror vertically (X) or horizontally (Y)."
)
# grid_lay.addRow("Mirror Axis:", self.mirror_axis)
self.empty_lb1 = QtWidgets.QLabel("")
grid_lay.addWidget(self.empty_lb1, 6, 0)
grid_lay.addWidget(self.mirax_label, 7, 0)
grid_lay.addWidget(self.mirror_axis, 7, 1)
## Axis Location
self.axis_location = RadioSet([{'label': 'Point', 'value': 'point'},
{'label': 'Box', 'value': 'box'}])
self.axloc_label = QtWidgets.QLabel("Axis Ref:")
self.axloc_label.setToolTip(
"The axis should pass through a <b>point</b> or cut\n "
"a specified <b>box</b> (in a Geometry object) in \n"
"the middle."
)
# grid_lay.addRow("Axis Location:", self.axis_location)
grid_lay.addWidget(self.axloc_label, 8, 0)
grid_lay.addWidget(self.axis_location, 8, 1)
self.empty_lb2 = QtWidgets.QLabel("")
grid_lay.addWidget(self.empty_lb2, 9, 0)
## Point/Box
self.point_box_container = QtWidgets.QVBoxLayout()
self.pb_label = QtWidgets.QLabel("<b>Point/Box:</b>")
self.pb_label.setToolTip(
"Specify the point (x, y) through which the mirror axis \n "
"passes or the Geometry object containing a rectangle \n"
"that the mirror axis cuts in half."
)
# grid_lay.addRow("Point/Box:", self.point_box_container)
self.add_point_button = QtWidgets.QPushButton("Add")
self.add_point_button.setToolTip(
"Add the <b>point (x, y)</b> through which the mirror axis \n "
"passes or the Object containing a rectangle \n"
"that the mirror axis cuts in half.\n"
"The point is captured by pressing SHIFT key\n"
"and left mouse clicking on canvas or you can enter them manually."
)
self.add_point_button.setFixedWidth(40)
grid_lay.addWidget(self.pb_label, 10, 0)
grid_lay.addLayout(self.point_box_container, 11, 0, 1, 3)
grid_lay.addWidget(self.add_point_button, 11, 3)
self.point_entry = EvalEntry()
self.point_box_container.addWidget(self.point_entry)
self.box_combo = QtWidgets.QComboBox()
self.box_combo.setModel(self.app.collection)
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(1)
self.box_combo_type = QtWidgets.QComboBox()
self.box_combo_type.addItem("Gerber Reference Box Object")
self.box_combo_type.addItem("Excellon Reference Box Object")
self.box_combo_type.addItem("Geometry Reference Box Object")
self.point_box_container.addWidget(self.box_combo_type)
self.point_box_container.addWidget(self.box_combo)
self.box_combo.hide()
self.box_combo_type.hide()
## Alignment holes
self.ah_label = QtWidgets.QLabel("<b>Alignment Drill Coordinates:</b>")
self.ah_label.setToolTip(
"Alignment holes (x1, y1), (x2, y2), ... "
"on one side of the mirror axis. For each set of (x, y) coordinates\n"
"entered here, a pair of drills will be created: one on the\n"
"coordinates entered and one in mirror position over the axis\n"
"selected above in the 'Mirror Axis'."
)
self.layout.addWidget(self.ah_label)
grid_lay1 = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay1)
self.alignment_holes = EvalEntry()
self.add_drill_point_button = QtWidgets.QPushButton("Add")
self.add_drill_point_button.setToolTip(
"Add alignment drill holes coords (x1, y1), (x2, y2), ... \n"
"on one side of the mirror axis.\n"
"The point(s) can be captured by pressing SHIFT key\n"
"and left mouse clicking on canvas. Or you can enter them manually."
)
self.add_drill_point_button.setFixedWidth(40)
grid_lay1.addWidget(self.alignment_holes, 0, 0, 1, 2)
grid_lay1.addWidget(self.add_drill_point_button, 0, 3)
## Drill diameter for alignment holes
self.dt_label = QtWidgets.QLabel("<b>Alignment Drill Creation</b>:")
self.dt_label.setToolTip(
"Create a set of alignment drill holes\n"
"with the specified diameter,\n"
"at the specified coordinates."
)
self.layout.addWidget(self.dt_label)
grid_lay2 = QtWidgets.QGridLayout()
self.layout.addLayout(grid_lay2)
self.drill_dia = LengthEntry()
self.dd_label = QtWidgets.QLabel("Drill diam.:")
self.dd_label.setToolTip(
"Diameter of the drill for the "
"alignment holes."
)
grid_lay2.addWidget(self.dd_label, 0, 0)
grid_lay2.addWidget(self.drill_dia, 0, 1)
## Buttons
self.create_alignment_hole_button = QtWidgets.QPushButton("Create Excellon Object")
self.create_alignment_hole_button.setToolTip(
"Creates an Excellon Object containing the\n"
"specified alignment holes and their mirror\n"
"images.")
# self.create_alignment_hole_button.setFixedWidth(40)
grid_lay2.addWidget(self.create_alignment_hole_button, 1,0, 1, 2)
self.reset_button = QtWidgets.QPushButton("Reset")
self.reset_button.setToolTip(
"Resets all the fields.")
self.reset_button.setFixedWidth(40)
grid_lay2.addWidget(self.reset_button, 1, 2)
self.layout.addStretch()
## Signals
self.create_alignment_hole_button.clicked.connect(self.on_create_alignment_holes)
self.mirror_gerber_button.clicked.connect(self.on_mirror_gerber)
self.mirror_exc_button.clicked.connect(self.on_mirror_exc)
self.mirror_geo_button.clicked.connect(self.on_mirror_geo)
self.add_point_button.clicked.connect(self.on_point_add)
self.add_drill_point_button.clicked.connect(self.on_drill_add)
self.reset_button.clicked.connect(self.reset_fields)
self.box_combo_type.currentIndexChanged.connect(self.on_combo_box_type)
self.axis_location.group_toggle_fn = self.on_toggle_pointbox
self.drill_values = ""
## Initialize form
self.mirror_axis.set_value('X')
self.axis_location.set_value('point')
self.drill_dia.set_value(1)
def on_combo_box_type(self):
obj_type = self.box_combo_type.currentIndex()
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(0)
def on_create_alignment_holes(self):
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
try:
px, py = self.point_entry.get_value()
except TypeError:
self.app.inform.emit("[warning_notcl] 'Point' reference is selected and 'Point' coordinates "
"are missing. Add them and retry.")
return
else:
selection_index = self.box_combo.currentIndex()
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
bb_obj = model_index.internalPointer().obj
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
dia = self.drill_dia.get_value()
tools = {"1": {"C": dia}}
# holes = self.alignment_holes.get_value()
holes = eval('[{}]'.format(self.alignment_holes.text()))
if not holes:
self.app.inform.emit("[warning_notcl] There are no Alignment Drill Coordinates to use. Add them and retry.")
return
drills = []
for hole in holes:
point = Point(hole)
point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py))
drills.append({"point": point, "tool": "1"})
drills.append({"point": point_mirror, "tool": "1"})
def obj_init(obj_inst, app_inst):
obj_inst.tools = tools
obj_inst.drills = drills
obj_inst.create_geometry()
self.app.new_object("excellon", "Alignment Drills", obj_init)
self.drill_values = ''
def on_mirror_gerber(self):
selection_index = self.gerber_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.gerber_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Gerber object loaded ...")
return
if not isinstance(fcobj, FlatCAMGerber):
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
try:
px, py = self.point_entry.get_value()
except TypeError:
self.app.inform.emit("[warning_notcl] 'Point' coordinates missing. "
"Using Origin (0, 0) as mirroring reference.")
px, py = (0, 0)
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.object_changed.emit(fcobj)
fcobj.plot()
def on_mirror_exc(self):
selection_index = self.exc_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.exc_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Excellon object loaded ...")
return
if not isinstance(fcobj, FlatCAMExcellon):
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
px, py = self.point_entry.get_value()
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.object_changed.emit(fcobj)
fcobj.plot()
def on_mirror_geo(self):
selection_index = self.geo_object_combo.currentIndex()
# fcobj = self.app.collection.object_list[selection_index]
model_index = self.app.collection.index(selection_index, 0, self.geo_object_combo.rootModelIndex())
try:
fcobj = model_index.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Geometry object loaded ...")
return
if not isinstance(fcobj, FlatCAMGeometry):
self.app.inform.emit("[error_notcl] Only Gerber, Excellon and Geometry objects can be mirrored.")
return
axis = self.mirror_axis.get_value()
mode = self.axis_location.get_value()
if mode == "point":
px, py = self.point_entry.get_value()
else:
selection_index_box = self.box_combo.currentIndex()
model_index_box = self.app.collection.index(selection_index_box, 0, self.box_combo.rootModelIndex())
try:
bb_obj = model_index_box.internalPointer().obj
except Exception as e:
self.app.inform.emit("[warning_notcl] There is no Box object loaded ...")
return
xmin, ymin, xmax, ymax = bb_obj.bounds()
px = 0.5 * (xmin + xmax)
py = 0.5 * (ymin + ymax)
fcobj.mirror(axis, [px, py])
self.app.object_changed.emit(fcobj)
fcobj.plot()
def on_point_add(self):
val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
self.point_entry.set_value(val)
def on_drill_add(self):
self.drill_values += (self.app.defaults["global_point_clipboard_format"] %
(self.app.pos[0], self.app.pos[1])) + ','
self.alignment_holes.set_value(self.drill_values)
def on_toggle_pointbox(self):
if self.axis_location.get_value() == "point":
self.point_entry.show()
self.box_combo.hide()
self.box_combo_type.hide()
self.add_point_button.setDisabled(False)
else:
self.point_entry.hide()
self.box_combo.show()
self.box_combo_type.show()
self.add_point_button.setDisabled(True)
def reset_fields(self):
self.gerber_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.exc_object_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
self.geo_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.gerber_object_combo.setCurrentIndex(0)
self.exc_object_combo.setCurrentIndex(0)
self.geo_object_combo.setCurrentIndex(0)
self.box_combo.setCurrentIndex(0)
self.box_combo_type.setCurrentIndex(0)
self.drill_values = ""
self.point_entry.set_value("")
self.alignment_holes.set_value("")
## Initialize form
self.mirror_axis.set_value('X')
self.axis_location.set_value('point')
self.drill_dia.set_value(1)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "2-Sided Tool")
self.reset_fields()

206
flatcamTools/ToolFilm.py Normal file
View File

@ -0,0 +1,206 @@
from FlatCAMTool import FlatCAMTool
from GUIElements import RadioSet, FloatEntry
from PyQt5 import QtGui, QtCore, QtWidgets
class Film(FlatCAMTool):
toolName = "Film PCB Tool"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
# Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.layout.addWidget(title_label)
# Form Layout
tf_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(tf_form_layout)
# Type of object for which to create the film
self.tf_type_obj_combo = QtWidgets.QComboBox()
self.tf_type_obj_combo.addItem("Gerber")
self.tf_type_obj_combo.addItem("Excellon")
self.tf_type_obj_combo.addItem("Geometry")
# we get rid of item1 ("Excellon") as it is not suitable for creating film
self.tf_type_obj_combo.view().setRowHidden(1, True)
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
self.tf_type_obj_combo_label.setToolTip(
"Specify the type of object for which to create the film.\n"
"The object can be of type: Gerber or Geometry.\n"
"The selection here decide the type of objects that will be\n"
"in the Film Object combobox."
)
tf_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
# List of objects for which we can create the film
self.tf_object_combo = QtWidgets.QComboBox()
self.tf_object_combo.setModel(self.app.collection)
self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.tf_object_combo.setCurrentIndex(1)
self.tf_object_label = QtWidgets.QLabel("Film Object:")
self.tf_object_label.setToolTip(
"Object for which to create the film."
)
tf_form_layout.addRow(self.tf_object_label, self.tf_object_combo)
# Type of Box Object to be used as an envelope for film creation
# Within this we can create negative
self.tf_type_box_combo = QtWidgets.QComboBox()
self.tf_type_box_combo.addItem("Gerber")
self.tf_type_box_combo.addItem("Excellon")
self.tf_type_box_combo.addItem("Geometry")
# we get rid of item1 ("Excellon") as it is not suitable for box when creating film
self.tf_type_box_combo.view().setRowHidden(1, True)
self.tf_type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.tf_type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.tf_type_box_combo_label = QtWidgets.QLabel("Box Type:")
self.tf_type_box_combo_label.setToolTip(
"Specify the type of object to be used as an container for\n"
"film creation. It can be: Gerber or Geometry type."
"The selection here decide the type of objects that will be\n"
"in the Box Object combobox."
)
tf_form_layout.addRow(self.tf_type_box_combo_label, self.tf_type_box_combo)
# Box
self.tf_box_combo = QtWidgets.QComboBox()
self.tf_box_combo.setModel(self.app.collection)
self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.tf_box_combo.setCurrentIndex(1)
self.tf_box_combo_label = QtWidgets.QLabel("Box Object:")
self.tf_box_combo_label.setToolTip(
"The actual object that is used a container for the\n "
"selected object for which we create the film.\n"
"Usually it is the PCB outline but it can be also the\n"
"same object for which the film is created.")
tf_form_layout.addRow(self.tf_box_combo_label, self.tf_box_combo)
# Film Type
self.film_type = RadioSet([{'label': 'Positive', 'value': 'pos'},
{'label': 'Negative', 'value': 'neg'}])
self.film_type_label = QtWidgets.QLabel("Film Type:")
self.film_type_label.setToolTip(
"Generate a Positive black film or a Negative film.\n"
"Positive means that it will print the features\n"
"with black on a white canvas.\n"
"Negative means that it will print the features\n"
"with white on a black canvas.\n"
"The Film format is SVG."
)
tf_form_layout.addRow(self.film_type_label, self.film_type)
# Boundary for negative film generation
self.boundary_entry = FloatEntry()
self.boundary_label = QtWidgets.QLabel("Border:")
self.boundary_label.setToolTip(
"Specify a border around the object.\n"
"Only for negative film.\n"
"It helps if we use as a Box Object the same \n"
"object as in Film Object. It will create a thick\n"
"black bar around the actual print allowing for a\n"
"better delimitation of the outline features which are of\n"
"white color like the rest and which may confound with the\n"
"surroundings if not for this border."
)
tf_form_layout.addRow(self.boundary_label, self.boundary_entry)
# Buttons
hlay = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay)
hlay.addStretch()
self.film_object_button = QtWidgets.QPushButton("Save Film")
self.film_object_button.setToolTip(
"Create a Film for the selected object, within\n"
"the specified box. Does not create a new \n "
"FlatCAM object, but directly save it in SVG format\n"
"which can be opened with Inkscape."
)
hlay.addWidget(self.film_object_button)
self.layout.addStretch()
## Signals
self.film_object_button.clicked.connect(self.on_film_creation)
self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
## Initialize form
self.film_type.set_value('neg')
self.boundary_entry.set_value(0.0)
def on_type_obj_index_changed(self, index):
obj_type = self.tf_type_obj_combo.currentIndex()
self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.tf_object_combo.setCurrentIndex(0)
def on_type_box_index_changed(self, index):
obj_type = self.tf_type_box_combo.currentIndex()
self.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.tf_box_combo.setCurrentIndex(0)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Film Tool")
def on_film_creation(self):
try:
name = self.tf_object_combo.currentText()
except:
self.app.inform.emit("[error_notcl] No Film object selected. Load a Film object and retry.")
return
try:
boxname = self.tf_box_combo.currentText()
except:
self.app.inform.emit("[error_notcl] No Box object selected. Load a Box object and retry.")
return
border = float(self.boundary_entry.get_value())
if border is None:
border = 0
self.app.inform.emit("Generating Film ...")
if self.film_type.get_value() == "pos":
try:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive",
directory=self.app.get_last_save_folder(), filter="*.svg")
except TypeError:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG positive")
filename = str(filename)
if str(filename) == "":
self.app.inform.emit("Export SVG positive cancelled.")
return
else:
self.app.export_svg_black(name, boxname, filename)
else:
try:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative",
directory=self.app.get_last_save_folder(), filter="*.svg")
except TypeError:
filename, _ = QtWidgets.QFileDialog.getSaveFileName(caption="Export SVG negative")
filename = str(filename)
if str(filename) == "":
self.app.inform.emit("Export SVG negative cancelled.")
return
else:
self.app.export_svg_negative(name, boxname, filename, border)
def reset_fields(self):
self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

172
flatcamTools/ToolImage.py Normal file
View File

@ -0,0 +1,172 @@
from FlatCAMTool import FlatCAMTool
from GUIElements import RadioSet, FloatEntry, FCComboBox, IntEntry
from PyQt5 import QtGui, QtCore, QtWidgets
class ToolImage(FlatCAMTool):
toolName = "Image as Object"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
# Title
title_label = QtWidgets.QLabel("<font size=4><b>IMAGE to PCB</b></font>")
self.layout.addWidget(title_label)
# Form Layout
ti_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(ti_form_layout)
# Type of object to create for the image
self.tf_type_obj_combo = FCComboBox()
self.tf_type_obj_combo.addItem("Gerber")
self.tf_type_obj_combo.addItem("Geometry")
self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.tf_type_obj_combo.setItemIcon(1, QtGui.QIcon("share/geometry16.png"))
self.tf_type_obj_combo_label = QtWidgets.QLabel("Object Type:")
self.tf_type_obj_combo_label.setToolTip(
"Specify the type of object to create from the image.\n"
"It can be of type: Gerber or Geometry."
)
ti_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo)
# DPI value of the imported image
self.dpi_entry = IntEntry()
self.dpi_label = QtWidgets.QLabel("DPI value:")
self.dpi_label.setToolTip(
"Specify a DPI value for the image."
)
ti_form_layout.addRow(self.dpi_label, self.dpi_entry)
self.emty_lbl = QtWidgets.QLabel("")
self.layout.addWidget(self.emty_lbl)
self.detail_label = QtWidgets.QLabel("<font size=4><b>Level of detail:</b>")
self.layout.addWidget(self.detail_label)
ti2_form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(ti2_form_layout)
# Type of image interpretation
self.image_type = RadioSet([{'label': 'B/W', 'value': 'black'},
{'label': 'Color', 'value': 'color'}])
self.image_type_label = QtWidgets.QLabel("<b>Image type:</b>")
self.image_type_label.setToolTip(
"Choose a method for the image interpretation.\n"
"B/W means a black & white image. Color means a colored image."
)
ti2_form_layout.addRow(self.image_type_label, self.image_type)
# Mask value of the imported image when image monochrome
self.mask_bw_entry = IntEntry()
self.mask_bw_label = QtWidgets.QLabel("Mask value <b>B/W</b>:")
self.mask_bw_label.setToolTip(
"Mask for monochrome image.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry.\n"
"0 means no detail and 255 means everything \n"
"(which is totally black)."
)
ti2_form_layout.addRow(self.mask_bw_label, self.mask_bw_entry)
# Mask value of the imported image for RED color when image color
self.mask_r_entry = IntEntry()
self.mask_r_label = QtWidgets.QLabel("Mask value <b>R:</b>")
self.mask_r_label.setToolTip(
"Mask for RED color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry."
)
ti2_form_layout.addRow(self.mask_r_label, self.mask_r_entry)
# Mask value of the imported image for GREEN color when image color
self.mask_g_entry = IntEntry()
self.mask_g_label = QtWidgets.QLabel("Mask value <b>G:</b>")
self.mask_g_label.setToolTip(
"Mask for GREEN color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry."
)
ti2_form_layout.addRow(self.mask_g_label, self.mask_g_entry)
# Mask value of the imported image for BLUE color when image color
self.mask_b_entry = IntEntry()
self.mask_b_label = QtWidgets.QLabel("Mask value <b>B:</b>")
self.mask_b_label.setToolTip(
"Mask for BLUE color.\n"
"Takes values between [0 ... 255].\n"
"Decides the level of details to include\n"
"in the resulting geometry."
)
ti2_form_layout.addRow(self.mask_b_label, self.mask_b_entry)
# Buttons
hlay = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay)
hlay.addStretch()
self.import_button = QtWidgets.QPushButton("Import image")
self.import_button.setToolTip(
"Open a image of raster type and then import it in FlatCAM."
)
hlay.addWidget(self.import_button)
self.layout.addStretch()
## Signals
self.import_button.clicked.connect(self.on_file_importimage)
## Initialize form
self.dpi_entry.set_value(96)
self.image_type.set_value('black')
self.mask_bw_entry.set_value(250)
self.mask_r_entry.set_value(250)
self.mask_g_entry.set_value(250)
self.mask_b_entry.set_value(250)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Image Tool")
def on_file_importimage(self):
"""
Callback for menu item File->Import IMAGE.
:param type_of_obj: to import the IMAGE as Geometry or as Gerber
:type type_of_obj: str
:return: None
"""
mask = []
self.app.log.debug("on_file_importimage()")
filter = "Image Files(*.BMP *.PNG *.JPG *.JPEG);;" \
"Bitmap File (*.BMP);;" \
"PNG File (*.PNG);;" \
"Jpeg File (*.JPG);;" \
"All Files (*.*)"
try:
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE",
directory=self.app.get_last_folder(), filter=filter)
except TypeError:
filename, _ = QtWidgets.QFileDialog.getOpenFileName(caption="Import IMAGE", filter=filter)
filename = str(filename)
type = self.tf_type_obj_combo.get_value().lower()
dpi = self.dpi_entry.get_value()
mode = self.image_type.get_value()
mask = [self.mask_bw_entry.get_value(), self.mask_r_entry.get_value(),self.mask_g_entry.get_value(),
self.mask_b_entry.get_value()]
if filename == "":
self.app.inform.emit("Open cancelled.")
else:
self.app.worker_task.emit({'fcn': self.app.import_image,
'params': [filename, type, dpi, mode, mask]})
# self.import_svg(filename, "geometry")

View File

@ -0,0 +1,352 @@
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
from VisPyVisuals import *
from copy import copy
from math import sqrt
class Measurement(FlatCAMTool):
toolName = "Measurement Tool"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
self.layout.addWidget(title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
form_layout_child_1 = QtWidgets.QFormLayout()
form_layout_child_1_1 = QtWidgets.QFormLayout()
form_layout_child_1_2 = QtWidgets.QFormLayout()
form_layout_child_2 = QtWidgets.QFormLayout()
form_layout_child_3 = QtWidgets.QFormLayout()
self.start_label = QtWidgets.QLabel("<b>Start</b> Coords:")
self.start_label.setToolTip("This is measuring Start point coordinates.")
self.stop_label = QtWidgets.QLabel("<b>Stop</b> Coords:")
self.stop_label.setToolTip("This is the measuring Stop point coordinates.")
self.distance_x_label = QtWidgets.QLabel("Dx:")
self.distance_x_label.setToolTip("This is the distance measured over the X axis.")
self.distance_y_label = QtWidgets.QLabel("Dy:")
self.distance_y_label.setToolTip("This is the distance measured over the Y axis.")
self.total_distance_label = QtWidgets.QLabel("<b>DISTANCE:</b>")
self.total_distance_label.setToolTip("This is the point to point Euclidian distance.")
self.units_entry_1 = FCEntry()
self.units_entry_1.setToolTip("Those are the units in which the distance is measured.")
self.units_entry_1.setDisabled(True)
self.units_entry_1.setFocusPolicy(QtCore.Qt.NoFocus)
self.units_entry_1.setFrame(False)
self.units_entry_1.setFixedWidth(30)
self.units_entry_2 = FCEntry()
self.units_entry_2.setToolTip("Those are the units in which the distance is measured.")
self.units_entry_2.setDisabled(True)
self.units_entry_2.setFocusPolicy(QtCore.Qt.NoFocus)
self.units_entry_2.setFrame(False)
self.units_entry_2.setFixedWidth(30)
self.units_entry_3 = FCEntry()
self.units_entry_3.setToolTip("Those are the units in which the distance is measured.")
self.units_entry_3.setDisabled(True)
self.units_entry_3.setFocusPolicy(QtCore.Qt.NoFocus)
self.units_entry_3.setFrame(False)
self.units_entry_3.setFixedWidth(30)
self.units_entry_4 = FCEntry()
self.units_entry_4.setToolTip("Those are the units in which the distance is measured.")
self.units_entry_4.setDisabled(True)
self.units_entry_4.setFocusPolicy(QtCore.Qt.NoFocus)
self.units_entry_4.setFrame(False)
self.units_entry_4.setFixedWidth(30)
self.units_entry_5 = FCEntry()
self.units_entry_5.setToolTip("Those are the units in which the distance is measured.")
self.units_entry_5.setDisabled(True)
self.units_entry_5.setFocusPolicy(QtCore.Qt.NoFocus)
self.units_entry_5.setFrame(False)
self.units_entry_5.setFixedWidth(30)
self.start_entry = FCEntry()
self.start_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.start_entry.setToolTip("This is measuring Start point coordinates.")
self.start_entry.setFixedWidth(100)
self.stop_entry = FCEntry()
self.stop_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.stop_entry.setToolTip("This is the measuring Stop point coordinates.")
self.stop_entry.setFixedWidth(100)
self.distance_x_entry = FCEntry()
self.distance_x_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_x_entry.setToolTip("This is the distance measured over the X axis.")
self.distance_x_entry.setFixedWidth(100)
self.distance_y_entry = FCEntry()
self.distance_y_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.distance_y_entry.setToolTip("This is the distance measured over the Y axis.")
self.distance_y_entry.setFixedWidth(100)
self.total_distance_entry = FCEntry()
self.total_distance_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.total_distance_entry.setToolTip("This is the point to point Euclidian distance.")
self.total_distance_entry.setFixedWidth(100)
self.measure_btn = QtWidgets.QPushButton("Measure")
self.measure_btn.setFixedWidth(70)
self.layout.addWidget(self.measure_btn)
form_layout_child_1.addRow(self.start_entry, self.units_entry_1)
form_layout_child_1_1.addRow(self.stop_entry, self.units_entry_2)
form_layout_child_1_2.addRow(self.distance_x_entry, self.units_entry_3)
form_layout_child_2.addRow(self.distance_y_entry, self.units_entry_4)
form_layout_child_3.addRow(self.total_distance_entry, self.units_entry_5)
form_layout.addRow(self.start_label, form_layout_child_1)
form_layout.addRow(self.stop_label, form_layout_child_1_1)
form_layout.addRow(self.distance_x_label, form_layout_child_1_2)
form_layout.addRow(self.distance_y_label, form_layout_child_2)
form_layout.addRow(self.total_distance_label, form_layout_child_3)
# initial view of the layout
self.start_entry.set_value('(0, 0)')
self.stop_entry.set_value('(0, 0)')
self.distance_x_entry.set_value('0')
self.distance_y_entry.set_value('0')
self.total_distance_entry.set_value('0')
self.units_entry_1.set_value(str(self.units))
self.units_entry_2.set_value(str(self.units))
self.units_entry_3.set_value(str(self.units))
self.units_entry_4.set_value(str(self.units))
self.units_entry_5.set_value(str(self.units))
self.layout.addStretch()
self.clicked_meas = 0
self.point1 = None
self.point2 = None
# the default state is disabled for the Move command
# self.setVisible(False)
self.active = False
# VisPy visuals
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
self.measure_btn.clicked.connect(self.toggle)
def run(self):
if self.app.tool_tab_locked is True:
return
self.toggle()
# Remove anything else in the GUI
self.app.ui.tool_scroll_area.takeWidget()
# Put ourself in the GUI
self.app.ui.tool_scroll_area.setWidget(self)
# Switch notebook to tool page
self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
self.show()
self.app.ui.notebook.setTabText(2, "Meas. Tool")
def on_key_release_meas(self, event):
if event.key == 'escape':
# abort the measurement action
self.toggle()
return
if event.key == 'G':
# toggle grid status
self.app.ui.grid_snap_btn.trigger()
return
def toggle(self):
# the self.active var is doing the 'toggle'
if self.active is True:
# DISABLE the Measuring TOOL
self.active = False
# disconnect the mouse/key events from functions of measurement tool
self.app.plotcanvas.vis_disconnect('mouse_move', self.on_mouse_move_meas)
self.app.plotcanvas.vis_disconnect('mouse_press', self.on_click_meas)
self.app.plotcanvas.vis_disconnect('key_release', self.on_key_release_meas)
# reconnect the mouse/key events to the functions from where the tool was called
if self.app.call_source == 'app':
self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot)
self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot)
self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
elif self.app.call_source == 'geo_editor':
self.app.geo_editor.canvas.vis_connect('mouse_move', self.app.geo_editor.on_canvas_move)
self.app.geo_editor.canvas.vis_connect('mouse_press', self.app.geo_editor.on_canvas_click)
self.app.geo_editor.canvas.vis_connect('key_press', self.app.geo_editor.on_canvas_key)
self.app.geo_editor.canvas.vis_connect('key_release', self.app.geo_editor.on_canvas_key_release)
self.app.geo_editor.canvas.vis_connect('mouse_release', self.app.geo_editor.on_canvas_click_release)
elif self.app.call_source == 'exc_editor':
self.app.exc_editor.canvas.vis_connect('mouse_move', self.app.exc_editor.on_canvas_move)
self.app.exc_editor.canvas.vis_connect('mouse_press', self.app.exc_editor.on_canvas_click)
self.app.exc_editor.canvas.vis_connect('key_press', self.app.exc_editor.on_canvas_key)
self.app.exc_editor.canvas.vis_connect('key_release', self.app.exc_editor.on_canvas_key_release)
self.app.exc_editor.canvas.vis_connect('mouse_release', self.app.exc_editor.on_canvas_click_release)
self.clicked_meas = 0
self.app.command_active = None
# delete the measuring line
self.delete_shape()
return
else:
# ENABLE the Measuring TOOL
self.active = True
self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
# we disconnect the mouse/key handlers from wherever the measurement tool was called
if self.app.call_source == 'app':
self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
self.app.plotcanvas.vis_disconnect('key_press', self.app.on_key_over_plot)
self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
elif self.app.call_source == 'geo_editor':
self.app.geo_editor.canvas.vis_disconnect('mouse_move', self.app.geo_editor.on_canvas_move)
self.app.geo_editor.canvas.vis_disconnect('mouse_press', self.app.geo_editor.on_canvas_click)
self.app.geo_editor.canvas.vis_disconnect('key_press', self.app.geo_editor.on_canvas_key)
self.app.geo_editor.canvas.vis_disconnect('key_release', self.app.geo_editor.on_canvas_key_release)
self.app.geo_editor.canvas.vis_disconnect('mouse_release', self.app.geo_editor.on_canvas_click_release)
elif self.app.call_source == 'exc_editor':
self.app.exc_editor.canvas.vis_disconnect('mouse_move', self.app.exc_editor.on_canvas_move)
self.app.exc_editor.canvas.vis_disconnect('mouse_press', self.app.exc_editor.on_canvas_click)
self.app.exc_editor.canvas.vis_disconnect('key_press', self.app.exc_editor.on_canvas_key)
self.app.exc_editor.canvas.vis_disconnect('key_release', self.app.exc_editor.on_canvas_key_release)
self.app.exc_editor.canvas.vis_disconnect('mouse_release', self.app.exc_editor.on_canvas_click_release)
# we can safely connect the app mouse events to the measurement tool
self.app.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_meas)
self.app.plotcanvas.vis_connect('mouse_press', self.on_click_meas)
self.app.plotcanvas.vis_connect('key_release', self.on_key_release_meas)
self.app.command_active = "Measurement"
# initial view of the layout
self.start_entry.set_value('(0, 0)')
self.stop_entry.set_value('(0, 0)')
self.distance_x_entry.set_value('0')
self.distance_y_entry.set_value('0')
self.total_distance_entry.set_value('0')
self.units_entry_1.set_value(str(self.units))
self.units_entry_2.set_value(str(self.units))
self.units_entry_3.set_value(str(self.units))
self.units_entry_4.set_value(str(self.units))
self.units_entry_5.set_value(str(self.units))
self.app.inform.emit("MEASURING: Click on the Start point ...")
def on_click_meas(self, event):
# mouse click will be accepted only if the left button is clicked
# this is necessary because right mouse click and middle mouse click
# are used for panning on the canvas
if event.button == 1:
if self.clicked_meas == 0:
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas[0], pos_canvas[1]
self.point1 = pos
self.start_entry.set_value("(%.4f, %.4f)" % pos)
self.app.inform.emit("MEASURING: Click on the Destination point ...")
if self.clicked_meas == 1:
try:
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# delete the selection bounding box
self.delete_shape()
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas[0], pos_canvas[1]
dx = pos[0] - self.point1[0]
dy = pos[1] - self.point1[1]
d = sqrt(dx**2 + dy**2)
self.stop_entry.set_value("(%.4f, %.4f)" % pos)
self.app.inform.emit("MEASURING: Result D(x) = %.4f | D(y) = %.4f | Distance = %.4f" %
(abs(dx), abs(dy), abs(d)))
self.distance_x_entry.set_value('%.4f' % abs(dx))
self.distance_y_entry.set_value('%.4f' % abs(dy))
self.total_distance_entry.set_value('%.4f' % abs(d))
self.clicked_meas = 0
self.toggle()
# delete the measuring line
self.delete_shape()
return
except TypeError:
pass
self.clicked_meas = 1
def on_mouse_move_meas(self, event):
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
self.app.app_cursor.enabled = True
# Update cursor
self.app.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
else:
pos = pos_canvas
self.app.app_enabled = False
self.point2 = (pos[0], pos[1])
if self.clicked_meas == 1:
self.update_meas_shape([self.point2, self.point1])
def update_meas_shape(self, pos):
self.delete_shape()
self.draw_shape(pos)
def delete_shape(self):
self.sel_shapes.clear()
self.sel_shapes.redraw()
def draw_shape(self, coords):
self.meas_line = LineString(coords)
self.sel_shapes.add(self.meas_line, color='black', update=True, layer=0, tolerance=None)
def set_meas_units(self, units):
self.meas.units_label.setText("[" + self.app.options["units"].lower() + "]")
# end of file

238
flatcamTools/ToolMove.py Normal file
View File

@ -0,0 +1,238 @@
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
from VisPyVisuals import *
from io import StringIO
from copy import copy
class ToolMove(FlatCAMTool):
toolName = "Move"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
self.layout.setContentsMargins(0, 0, 3, 0)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum)
self.clicked_move = 0
self.point1 = None
self.point2 = None
# the default state is disabled for the Move command
self.setVisible(False)
self.sel_rect = None
self.old_coords = []
# VisPy visuals
self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
def install(self, icon=None, separator=None, **kwargs):
FlatCAMTool.install(self, icon, separator, **kwargs)
def run(self):
if self.app.tool_tab_locked is True:
return
self.toggle()
def on_left_click(self, event):
# mouse click will be accepted only if the left button is clicked
# this is necessary because right mouse click and middle mouse click
# are used for panning on the canvas
if event.button == 1:
if self.clicked_move == 0:
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
if self.point1 is None:
self.point1 = pos
else:
self.point2 = copy(self.point1)
self.point1 = pos
self.app.inform.emit("MOVE: Click on the Destination point ...")
if self.clicked_move == 1:
try:
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# delete the selection bounding box
self.delete_shape()
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
dx = pos[0] - self.point1[0]
dy = pos[1] - self.point1[1]
proc = self.app.proc_container.new("Moving ...")
def job_move(app_obj):
obj_list = self.app.collection.get_selected()
try:
if not obj_list:
self.app.inform.emit("[warning_notcl] No object(s) selected.")
return "fail"
else:
for sel_obj in obj_list:
sel_obj.offset((dx, dy))
sel_obj.plot()
# Update the object bounding box options
a,b,c,d = sel_obj.bounds()
sel_obj.options['xmin'] = a
sel_obj.options['ymin'] = b
sel_obj.options['xmax'] = c
sel_obj.options['ymax'] = d
# self.app.collection.set_active(sel_obj.options['name'])
except Exception as e:
proc.done()
self.app.inform.emit('[error_notcl] '
'ToolMove.on_left_click() --> %s' % str(e))
return "fail"
proc.done()
# delete the selection bounding box
self.delete_shape()
self.app.worker_task.emit({'fcn': job_move, 'params': [self]})
self.clicked_move = 0
self.toggle()
self.app.inform.emit("[success]Object was moved ...")
return
except TypeError:
self.app.inform.emit('[error_notcl] '
'ToolMove.on_left_click() --> Error when mouse left click.')
return
self.clicked_move = 1
def on_move(self, event):
pos_canvas = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos)
# if GRID is active we need to get the snapped positions
if self.app.grid_status() == True:
pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
else:
pos = pos_canvas
if self.point1 is None:
dx = pos[0]
dy = pos[1]
else:
dx = pos[0] - self.point1[0]
dy = pos[1] - self.point1[1]
if self.clicked_move == 1:
self.update_sel_bbox((dx, dy))
def on_key_press(self, event):
if event.key == 'escape':
# abort the move action
self.app.inform.emit("[warning_notcl]Move action cancelled.")
self.toggle()
return
def toggle(self):
if self.isVisible():
self.setVisible(False)
self.app.plotcanvas.vis_disconnect('mouse_move', self.on_move)
self.app.plotcanvas.vis_disconnect('mouse_press', self.on_left_click)
self.app.plotcanvas.vis_disconnect('key_release', self.on_key_press)
self.app.plotcanvas.vis_connect('key_press', self.app.on_key_over_plot)
self.clicked_move = 0
# signal that there is no command active
self.app.command_active = None
# delete the selection box
self.delete_shape()
return
else:
self.setVisible(True)
# signal that there is a command active and it is 'Move'
self.app.command_active = "Move"
if self.app.collection.get_selected():
self.app.inform.emit("MOVE: Click on the Start point ...")
# draw the selection box
self.draw_sel_bbox()
else:
self.setVisible(False)
# signal that there is no command active
self.app.command_active = None
self.app.inform.emit("[warning_notcl]MOVE action cancelled. No object(s) to move.")
def draw_sel_bbox(self):
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit("[warning_notcl]Object(s) not selected")
self.toggle()
else:
# if we have an object selected then we can safely activate the mouse events
self.app.plotcanvas.vis_connect('mouse_move', self.on_move)
self.app.plotcanvas.vis_connect('mouse_press', self.on_left_click)
self.app.plotcanvas.vis_connect('key_release', self.on_key_press)
# first get a bounding box to fit all
for obj in obj_list:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
xmaximal = max(xmaxlist)
ymaximal = max(ymaxlist)
p1 = (xminimal, yminimal)
p2 = (xmaximal, yminimal)
p3 = (xmaximal, ymaximal)
p4 = (xminimal, ymaximal)
self.old_coords = [p1, p2, p3, p4]
self.draw_shape(self.old_coords)
def update_sel_bbox(self, pos):
self.delete_shape()
pt1 = (self.old_coords[0][0] + pos[0], self.old_coords[0][1] + pos[1])
pt2 = (self.old_coords[1][0] + pos[0], self.old_coords[1][1] + pos[1])
pt3 = (self.old_coords[2][0] + pos[0], self.old_coords[2][1] + pos[1])
pt4 = (self.old_coords[3][0] + pos[0], self.old_coords[3][1] + pos[1])
self.draw_shape([pt1, pt2, pt3, pt4])
def delete_shape(self):
self.sel_shapes.clear()
self.sel_shapes.redraw()
def draw_shape(self, coords):
self.sel_rect = Polygon(coords)
blue_t = Color('blue')
blue_t.alpha = 0.2
self.sel_shapes.add(self.sel_rect, color='blue', face_color=blue_t, update=True, layer=0, tolerance=None)
# end of file

View File

@ -0,0 +1,882 @@
from FlatCAMTool import FlatCAMTool
from copy import copy,deepcopy
# from GUIElements import IntEntry, RadioSet, FCEntry
# from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
from ObjectCollection import *
import time
class NonCopperClear(FlatCAMTool, Gerber):
toolName = "Non-Copper Clearing Tool"
def __init__(self, app):
self.app = app
FlatCAMTool.__init__(self, app)
Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
self.tools_frame = QtWidgets.QFrame()
self.tools_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.tools_frame)
self.tools_box = QtWidgets.QVBoxLayout()
self.tools_box.setContentsMargins(0, 0, 0, 0)
self.tools_frame.setLayout(self.tools_box)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.tools_box.addWidget(title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.tools_box.addLayout(form_layout)
## Object
self.object_combo = QtWidgets.QComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(1)
self.object_label = QtWidgets.QLabel("Gerber:")
self.object_label.setToolTip(
"Gerber object to be cleared of excess copper. "
)
e_lab_0 = QtWidgets.QLabel('')
form_layout.addRow(self.object_label, self.object_combo)
form_layout.addRow(e_lab_0)
#### Tools ####
self.tools_table_label = QtWidgets.QLabel('<b>Tools Table</b>')
self.tools_table_label.setToolTip(
"Tools pool from which the algorithm\n"
"will pick the ones used for copper clearing."
)
self.tools_box.addWidget(self.tools_table_label)
self.tools_table = FCTable()
self.tools_box.addWidget(self.tools_table)
self.tools_table.setColumnCount(4)
self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'TT', ''])
self.tools_table.setColumnHidden(3, True)
self.tools_table.setSortingEnabled(False)
# self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.tools_table.horizontalHeaderItem(0).setToolTip(
"This is the Tool Number.\n"
"Non copper clearing will start with the tool with the biggest \n"
"diameter, continuing until there are no more tools.\n"
"Only tools that create NCC clearing geometry will still be present\n"
"in the resulting geometry. This is because with some tools\n"
"this function will not be able to create painting geometry."
)
self.tools_table.horizontalHeaderItem(1).setToolTip(
"Tool Diameter. It's value (in current FlatCAM units) \n"
"is the cut width into the material.")
self.tools_table.horizontalHeaderItem(2).setToolTip(
"The Tool Type (TT) can be:<BR>"
"- <B>Circular</B> with 1 ... 4 teeth -> it is informative only. Being circular, <BR>"
"the cut width in material is exactly the tool diameter.<BR>"
"- <B>Ball</B> -> informative only and make reference to the Ball type endmill.<BR>"
"- <B>V-Shape</B> -> it will disable de Z-Cut parameter in the resulting geometry UI form "
"and enable two additional UI form fields in the resulting geometry: V-Tip Dia and "
"V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such "
"as the cut width into material will be equal with the value in the Tool Diameter "
"column of this table.<BR>"
"Choosing the <B>V-Shape</B> Tool Type automatically will select the Operation Type "
"in the resulting geometry as Isolation.")
self.empty_label = QtWidgets.QLabel('')
self.tools_box.addWidget(self.empty_label)
#### Add a new Tool ####
hlay = QtWidgets.QHBoxLayout()
self.tools_box.addLayout(hlay)
self.addtool_entry_lbl = QtWidgets.QLabel('<b>Tool Dia:</b>')
self.addtool_entry_lbl.setToolTip(
"Diameter for the new tool to add in the Tool Table"
)
self.addtool_entry = FloatEntry()
# hlay.addWidget(self.addtool_label)
# hlay.addStretch()
hlay.addWidget(self.addtool_entry_lbl)
hlay.addWidget(self.addtool_entry)
grid2 = QtWidgets.QGridLayout()
self.tools_box.addLayout(grid2)
self.addtool_btn = QtWidgets.QPushButton('Add')
self.addtool_btn.setToolTip(
"Add a new tool to the Tool Table\n"
"with the diameter specified above."
)
# self.copytool_btn = QtWidgets.QPushButton('Copy')
# self.copytool_btn.setToolTip(
# "Copy a selection of tools in the Tool Table\n"
# "by first selecting a row in the Tool Table."
# )
self.deltool_btn = QtWidgets.QPushButton('Delete')
self.deltool_btn.setToolTip(
"Delete a selection of tools in the Tool Table\n"
"by first selecting a row(s) in the Tool Table."
)
grid2.addWidget(self.addtool_btn, 0, 0)
# grid2.addWidget(self.copytool_btn, 0, 1)
grid2.addWidget(self.deltool_btn, 0,2)
self.empty_label_0 = QtWidgets.QLabel('')
self.tools_box.addWidget(self.empty_label_0)
grid3 = QtWidgets.QGridLayout()
self.tools_box.addLayout(grid3)
e_lab_1 = QtWidgets.QLabel('')
grid3.addWidget(e_lab_1, 0, 0)
nccoverlabel = QtWidgets.QLabel('Overlap:')
nccoverlabel.setToolTip(
"How much (fraction) of the tool width to overlap each tool pass.\n"
"Example:\n"
"A value here of 0.25 means 25% from the tool diameter found above.\n\n"
"Adjust the value starting with lower values\n"
"and increasing it if areas that should be cleared are still \n"
"not cleared.\n"
"Lower values = faster processing, faster execution on PCB.\n"
"Higher values = slow processing and slow execution on CNC\n"
"due of too many paths."
)
grid3.addWidget(nccoverlabel, 1, 0)
self.ncc_overlap_entry = FloatEntry()
grid3.addWidget(self.ncc_overlap_entry, 1, 1)
nccmarginlabel = QtWidgets.QLabel('Margin:')
nccmarginlabel.setToolTip(
"Bounding box margin."
)
grid3.addWidget(nccmarginlabel, 2, 0)
self.ncc_margin_entry = FloatEntry()
grid3.addWidget(self.ncc_margin_entry, 2, 1)
# Method
methodlabel = QtWidgets.QLabel('Method:')
methodlabel.setToolTip(
"Algorithm for non-copper clearing:<BR>"
"<B>Standard</B>: Fixed step inwards.<BR>"
"<B>Seed-based</B>: Outwards from seed.<BR>"
"<B>Line-based</B>: Parallel lines."
)
grid3.addWidget(methodlabel, 3, 0)
self.ncc_method_radio = RadioSet([
{"label": "Standard", "value": "standard"},
{"label": "Seed-based", "value": "seed"},
{"label": "Straight lines", "value": "lines"}
], orientation='vertical', stretch=False)
grid3.addWidget(self.ncc_method_radio, 3, 1)
# Connect lines
pathconnectlabel = QtWidgets.QLabel("Connect:")
pathconnectlabel.setToolTip(
"Draw lines between resulting\n"
"segments to minimize tool lifts."
)
grid3.addWidget(pathconnectlabel, 4, 0)
self.ncc_connect_cb = FCCheckBox()
grid3.addWidget(self.ncc_connect_cb, 4, 1)
contourlabel = QtWidgets.QLabel("Contour:")
contourlabel.setToolTip(
"Cut around the perimeter of the polygon\n"
"to trim rough edges."
)
grid3.addWidget(contourlabel, 5, 0)
self.ncc_contour_cb = FCCheckBox()
grid3.addWidget(self.ncc_contour_cb, 5, 1)
restlabel = QtWidgets.QLabel("Rest M.:")
restlabel.setToolTip(
"If checked, use 'rest machining'.\n"
"Basically it will clear copper outside PCB features,\n"
"using the biggest tool and continue with the next tools,\n"
"from bigger to smaller, to clear areas of copper that\n"
"could not be cleared by previous tool, until there is\n"
"no more copper to clear or there are no more tools.\n"
"If not checked, use the standard algorithm."
)
grid3.addWidget(restlabel, 6, 0)
self.ncc_rest_cb = FCCheckBox()
grid3.addWidget(self.ncc_rest_cb, 6, 1)
self.generate_ncc_button = QtWidgets.QPushButton('Generate Geometry')
self.generate_ncc_button.setToolTip(
"Create the Geometry Object\n"
"for non-copper routing."
)
self.tools_box.addWidget(self.generate_ncc_button)
self.units = ''
self.ncc_tools = {}
self.tooluid = 0
# store here the default data for Geometry Data
self.default_data = {}
self.obj_name = ""
self.ncc_obj = None
self.tools_box.addStretch()
self.addtool_btn.clicked.connect(self.on_tool_add)
self.deltool_btn.clicked.connect(self.on_tool_delete)
self.generate_ncc_button.clicked.connect(self.on_ncc)
def install(self, icon=None, separator=None, **kwargs):
FlatCAMTool.install(self, icon, separator, **kwargs)
def run(self):
FlatCAMTool.run(self)
self.tools_frame.show()
self.set_ui()
self.build_ui()
self.app.ui.notebook.setTabText(2, "NCC Tool")
def set_ui(self):
self.ncc_overlap_entry.set_value(self.app.defaults["gerber_nccoverlap"])
self.ncc_margin_entry.set_value(self.app.defaults["gerber_nccmargin"])
self.ncc_method_radio.set_value(self.app.defaults["gerber_nccmethod"])
self.ncc_connect_cb.set_value(self.app.defaults["gerber_nccconnect"])
self.ncc_contour_cb.set_value(self.app.defaults["gerber_ncccontour"])
self.ncc_rest_cb.set_value(self.app.defaults["gerber_nccrest"])
self.tools_table.setupContextMenu()
self.tools_table.addContextMenu(
"Add", lambda: self.on_tool_add(dia=None, muted=None), icon=QtGui.QIcon("share/plus16.png"))
self.tools_table.addContextMenu(
"Delete", lambda:
self.on_tool_delete(rows_to_delete=None, all=None), icon=QtGui.QIcon("share/delete32.png"))
# init the working variables
self.default_data.clear()
self.default_data.update({
"name": '_ncc',
"plot": self.app.defaults["geometry_plot"],
"tooldia": self.app.defaults["geometry_painttooldia"],
"cutz": self.app.defaults["geometry_cutz"],
"vtipdia": 0.1,
"vtipangle": 30,
"travelz": self.app.defaults["geometry_travelz"],
"feedrate": self.app.defaults["geometry_feedrate"],
"feedrate_z": self.app.defaults["geometry_feedrate_z"],
"feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"],
"dwell": self.app.defaults["geometry_dwell"],
"dwelltime": self.app.defaults["geometry_dwelltime"],
"multidepth": self.app.defaults["geometry_multidepth"],
"ppname_g": self.app.defaults["geometry_ppname_g"],
"depthperpass": self.app.defaults["geometry_depthperpass"],
"extracut": self.app.defaults["geometry_extracut"],
"toolchange": self.app.defaults["geometry_toolchange"],
"toolchangez": self.app.defaults["geometry_toolchangez"],
"endz": self.app.defaults["geometry_endz"],
"spindlespeed": self.app.defaults["geometry_spindlespeed"],
"toolchangexy": self.app.defaults["geometry_toolchangexy"],
"startz": self.app.defaults["geometry_startz"],
"paintmargin": self.app.defaults["geometry_paintmargin"],
"paintmethod": self.app.defaults["geometry_paintmethod"],
"selectmethod": self.app.defaults["geometry_selectmethod"],
"pathconnect": self.app.defaults["geometry_pathconnect"],
"paintcontour": self.app.defaults["geometry_paintcontour"],
"paintoverlap": self.app.defaults["geometry_paintoverlap"],
"nccoverlap": self.app.defaults["gerber_nccoverlap"],
"nccmargin": self.app.defaults["gerber_nccmargin"],
"nccmethod": self.app.defaults["gerber_nccmethod"],
"nccconnect": self.app.defaults["gerber_nccconnect"],
"ncccontour": self.app.defaults["gerber_ncccontour"],
"nccrest": self.app.defaults["gerber_nccrest"]
})
try:
dias = [float(eval(dia)) for dia in self.app.defaults["gerber_ncctools"].split(",")]
except:
log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Gerber Object -> NCC Tools.")
return
self.tooluid = 0
self.ncc_tools.clear()
for tool_dia in dias:
self.tooluid += 1
self.ncc_tools.update({
int(self.tooluid): {
'tooldia': float('%.4f' % tool_dia),
'offset': 'Path',
'offset_value': 0.0,
'type': 'Iso',
'tool_type': 'V',
'data': dict(self.default_data),
'solid_geometry': []
}
})
self.obj_name = ""
self.ncc_obj = None
self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
def build_ui(self):
self.ui_disconnect()
# updated units
self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
if self.units == "IN":
self.addtool_entry.set_value(0.039)
else:
self.addtool_entry.set_value(1)
sorted_tools = []
for k, v in self.ncc_tools.items():
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
sorted_tools.sort()
n = len(sorted_tools)
self.tools_table.setRowCount(n)
tool_id = 0
for tool_sorted in sorted_tools:
for tooluid_key, tooluid_value in self.ncc_tools.items():
if float('%.4f' % tooluid_value['tooldia']) == tool_sorted:
tool_id += 1
id = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
row_no = tool_id - 1
self.tools_table.setItem(row_no, 0, id) # Tool name/id
# Make sure that the drill diameter when in MM is with no more than 2 decimals
# There are no drill bits in MM with more than 3 decimals diameter
# For INCH the decimals should be no more than 3. There are no drills under 10mils
if self.units == 'MM':
dia = QtWidgets.QTableWidgetItem('%.2f' % tooluid_value['tooldia'])
else:
dia = QtWidgets.QTableWidgetItem('%.3f' % tooluid_value['tooldia'])
dia.setFlags(QtCore.Qt.ItemIsEnabled)
tool_type_item = QtWidgets.QComboBox()
for item in self.tool_type_item_options:
tool_type_item.addItem(item)
tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
idx = tool_type_item.findText(tooluid_value['tool_type'])
tool_type_item.setCurrentIndex(idx)
tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key)))
self.tools_table.setItem(row_no, 1, dia) # Diameter
self.tools_table.setCellWidget(row_no, 2, tool_type_item)
### REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
self.tools_table.setItem(row_no, 3, tool_uid_item) # Tool unique ID
# make the diameter column editable
for row in range(tool_id):
self.tools_table.item(row, 1).setFlags(
QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
# all the tools are selected by default
self.tools_table.selectColumn(0)
#
self.tools_table.resizeColumnsToContents()
self.tools_table.resizeRowsToContents()
vertical_header = self.tools_table.verticalHeader()
vertical_header.hide()
self.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
horizontal_header = self.tools_table.horizontalHeader()
horizontal_header.setMinimumSectionSize(10)
horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
horizontal_header.resizeSection(0, 20)
horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
# self.tools_table.setSortingEnabled(True)
# sort by tool diameter
# self.tools_table.sortItems(1)
self.tools_table.setMinimumHeight(self.tools_table.getHeight())
self.tools_table.setMaximumHeight(self.tools_table.getHeight())
self.app.report_usage("gerber_on_ncc_button")
self.ui_connect()
def ui_connect(self):
self.tools_table.itemChanged.connect(self.on_tool_edit)
def ui_disconnect(self):
try:
# if connected, disconnect the signal from the slot on item_changed as it creates issues
self.tools_table.itemChanged.disconnect(self.on_tool_edit)
except:
pass
def on_tool_add(self, dia=None, muted=None):
self.ui_disconnect()
if dia:
tool_dia = dia
else:
tool_dia = self.addtool_entry.get_value()
if tool_dia is None:
self.build_ui()
self.app.inform.emit("[warning_notcl] Please enter a tool diameter to add, in Float format.")
return
# construct a list of all 'tooluid' in the self.tools
tool_uid_list = []
for tooluid_key in self.ncc_tools:
tool_uid_item = int(tooluid_key)
tool_uid_list.append(tool_uid_item)
# find maximum from the temp_uid, add 1 and this is the new 'tooluid'
if not tool_uid_list:
max_uid = 0
else:
max_uid = max(tool_uid_list)
self.tooluid = int(max_uid + 1)
tool_dias = []
for k, v in self.ncc_tools.items():
for tool_v in v.keys():
if tool_v == 'tooldia':
tool_dias.append(float('%.4f' % v[tool_v]))
if float('%.4f' % tool_dia) in tool_dias:
if muted is None:
self.app.inform.emit("[warning_notcl]Adding tool cancelled. Tool already in Tool Table.")
self.tools_table.itemChanged.connect(self.on_tool_edit)
return
else:
if muted is None:
self.app.inform.emit("[success] New tool added to Tool Table.")
self.ncc_tools.update({
int(self.tooluid): {
'tooldia': float('%.4f' % tool_dia),
'offset': 'Path',
'offset_value': 0.0,
'type': 'Iso',
'tool_type': 'V',
'data': dict(self.default_data),
'solid_geometry': []
}
})
self.build_ui()
def on_tool_edit(self):
self.ui_disconnect()
tool_dias = []
for k, v in self.ncc_tools.items():
for tool_v in v.keys():
if tool_v == 'tooldia':
tool_dias.append(float('%.4f' % v[tool_v]))
for row in range(self.tools_table.rowCount()):
new_tool_dia = float(self.tools_table.item(row, 1).text())
tooluid = int(self.tools_table.item(row, 3).text())
# identify the tool that was edited and get it's tooluid
if new_tool_dia not in tool_dias:
self.ncc_tools[tooluid]['tooldia'] = new_tool_dia
self.app.inform.emit("[success] Tool from Tool Table was edited.")
self.build_ui()
return
else:
# identify the old tool_dia and restore the text in tool table
for k, v in self.ncc_tools.items():
if k == tooluid:
old_tool_dia = v['tooldia']
break
restore_dia_item = self.tools_table.item(row, 1)
restore_dia_item.setText(str(old_tool_dia))
self.app.inform.emit("[warning_notcl] Edit cancelled. New diameter value is already in the Tool Table.")
self.build_ui()
def on_tool_delete(self, rows_to_delete=None, all=None):
self.ui_disconnect()
deleted_tools_list = []
if all:
self.paint_tools.clear()
self.build_ui()
return
if rows_to_delete:
try:
for row in rows_to_delete:
tooluid_del = int(self.tools_table.item(row, 3).text())
deleted_tools_list.append(tooluid_del)
except TypeError:
deleted_tools_list.append(rows_to_delete)
for t in deleted_tools_list:
self.ncc_tools.pop(t, None)
self.build_ui()
return
try:
if self.tools_table.selectedItems():
for row_sel in self.tools_table.selectedItems():
row = row_sel.row()
if row < 0:
continue
tooluid_del = int(self.tools_table.item(row, 3).text())
deleted_tools_list.append(tooluid_del)
for t in deleted_tools_list:
self.ncc_tools.pop(t, None)
except AttributeError:
self.app.inform.emit("[warning_notcl]Delete failed. Select a tool to delete.")
return
except Exception as e:
log.debug(str(e))
self.app.inform.emit("[success] Tool(s) deleted from Tool Table.")
self.build_ui()
def on_ncc(self):
over = self.ncc_overlap_entry.get_value()
over = over if over else self.app.defaults["gerber_nccoverlap"]
margin = self.ncc_margin_entry.get_value()
margin = margin if margin else self.app.defaults["gerber_nccmargin"]
connect = self.ncc_connect_cb.get_value()
connect = connect if connect else self.app.defaults["gerber_nccconnect"]
contour = self.ncc_contour_cb.get_value()
contour = contour if contour else self.app.defaults["gerber_ncccontour"]
clearing_method = self.ncc_rest_cb.get_value()
clearing_method = clearing_method if clearing_method else self.app.defaults["gerber_nccrest"]
pol_method = self.ncc_method_radio.get_value()
pol_method = pol_method if pol_method else self.app.defaults["gerber_nccmethod"]
self.obj_name = self.object_combo.currentText()
# Get source object.
try:
self.ncc_obj = self.app.collection.get_by_name(self.obj_name)
except:
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % self.obj_name)
return "Could not retrieve object: %s" % self.obj_name
# Prepare non-copper polygons
try:
bounding_box = self.ncc_obj.solid_geometry.envelope.buffer(distance=margin, join_style=JOIN_STYLE.mitre)
except AttributeError:
self.app.inform.emit("[error_notcl]No Gerber file available.")
return
# calculate the empty area by substracting the solid_geometry from the object bounding box geometry
empty = self.ncc_obj.get_empty_area(bounding_box)
if type(empty) is Polygon:
empty = MultiPolygon([empty])
# clear non copper using standard algorithm
if clearing_method == False:
self.clear_non_copper(
empty=empty,
over=over,
pol_method=pol_method,
connect=connect,
contour=contour
)
# clear non copper using rest machining algorithm
else:
self.clear_non_copper_rest(
empty=empty,
over=over,
pol_method=pol_method,
connect=connect,
contour=contour
)
def clear_non_copper(self, empty, over, pol_method, outname=None, connect=True, contour=True):
name = outname if outname else self.obj_name + "_ncc"
# Sort tools in descending order
sorted_tools = []
for k, v in self.ncc_tools.items():
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
sorted_tools.sort(reverse=True)
# Do job in background
proc = self.app.proc_container.new("Clearing Non-Copper areas.")
def initialize(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry), \
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
cleared_geo = []
# Already cleared area
cleared = MultiPolygon()
# flag for polygons not cleared
app_obj.poly_not_cleared = False
# Generate area for each tool
offset = sum(sorted_tools)
current_uid = int(1)
for tool in sorted_tools:
self.app.inform.emit('[success] Non-Copper Clearing with ToolDia = %s started.' % str(tool))
cleared_geo[:] = []
# Get remaining tools offset
offset -= (tool - 1e-12)
# Area to clear
area = empty.buffer(-offset)
try:
area = area.difference(cleared)
except:
continue
# Transform area to MultiPolygon
if type(area) is Polygon:
area = MultiPolygon([area])
if area.geoms:
if len(area.geoms) > 0:
for p in area.geoms:
try:
if pol_method == 'standard':
cp = self.clear_polygon(p, tool, self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
elif pol_method == 'seed':
cp = self.clear_polygon2(p, tool, self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
else:
cp = self.clear_polygon3(p, tool, self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
if cp:
cleared_geo += list(cp.get_objects())
except:
log.warning("Polygon can not be cleared.")
app_obj.poly_not_cleared = True
continue
# check if there is a geometry at all in the cleared geometry
if cleared_geo:
# Overall cleared area
cleared = empty.buffer(-offset * (1 + over)).buffer(-tool / 1.999999).buffer(
tool / 1.999999)
# clean-up cleared geo
cleared = cleared.buffer(0)
# find the tooluid associated with the current tool_dia so we know where to add the tool
# solid_geometry
for k, v in self.ncc_tools.items():
if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
current_uid = int(k)
# add the solid_geometry to the current too in self.paint_tools dictionary
# and then reset the temporary list that stored that solid_geometry
v['solid_geometry'] = deepcopy(cleared_geo)
v['data']['name'] = name
break
geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
else:
log.debug("There are no geometries in the cleared polygon.")
geo_obj.options["cnctooldia"] = tool
geo_obj.multigeo = True
def job_thread(app_obj):
try:
app_obj.new_object("geometry", name, initialize)
except Exception as e:
proc.done()
self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper() --> %s' % str(e))
return
proc.done()
if app_obj.poly_not_cleared is False:
self.app.inform.emit('[success] NCC Tool finished.')
else:
self.app.inform.emit('[warning_notcl] NCC Tool finished but some PCB features could not be cleared. '
'Check the result.')
# reset the variable for next use
app_obj.poly_not_cleared = False
# focus on Selected Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
self.tools_frame.hide()
self.app.ui.notebook.setTabText(2, "Tools")
# Promise object with the new name
self.app.collection.promise(name)
# Background
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
# clear copper with 'rest-machining' algorithm
def clear_non_copper_rest(self, empty, over, pol_method, outname=None, connect=True, contour=True):
name = outname if outname is not None else self.obj_name + "_ncc_rm"
# Sort tools in descending order
sorted_tools = []
for k, v in self.ncc_tools.items():
sorted_tools.append(float('%.4f' % float(v['tooldia'])))
sorted_tools.sort(reverse=True)
# Do job in background
proc = self.app.proc_container.new("Clearing Non-Copper areas.")
def initialize_rm(geo_obj, app_obj):
assert isinstance(geo_obj, FlatCAMGeometry), \
"Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
cleared_geo = []
cleared_by_last_tool = []
rest_geo = []
current_uid = 1
# repurposed flag for final object, geo_obj. True if it has any solid_geometry, False if not.
app_obj.poly_not_cleared = True
area = empty.buffer(0)
# Generate area for each tool
while sorted_tools:
tool = sorted_tools.pop(0)
self.app.inform.emit('[success] Non-Copper Rest Clearing with ToolDia = %s started.' % str(tool))
tool_used = tool - 1e-12
cleared_geo[:] = []
# Area to clear
for poly in cleared_by_last_tool:
try:
area = area.difference(poly)
except:
pass
cleared_by_last_tool[:] = []
# Transform area to MultiPolygon
if type(area) is Polygon:
area = MultiPolygon([area])
# add the rest that was not able to be cleared previously; area is a MultyPolygon
# and rest_geo it's a list
allparts = [p.buffer(0) for p in area.geoms]
allparts += deepcopy(rest_geo)
rest_geo[:] = []
area = MultiPolygon(deepcopy(allparts))
allparts[:] = []
if area.geoms:
if len(area.geoms) > 0:
for p in area.geoms:
try:
if pol_method == 'standard':
cp = self.clear_polygon(p, tool_used, self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
elif pol_method == 'seed':
cp = self.clear_polygon2(p, tool_used,
self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
else:
cp = self.clear_polygon3(p, tool_used,
self.app.defaults["gerber_circle_steps"],
overlap=over, contour=contour, connect=connect)
cleared_geo.append(list(cp.get_objects()))
except:
log.warning("Polygon can't be cleared.")
# this polygon should be added to a list and then try clear it with a smaller tool
rest_geo.append(p)
# check if there is a geometry at all in the cleared geometry
if cleared_geo:
# Overall cleared area
cleared_area = list(self.flatten_list(cleared_geo))
# cleared = MultiPolygon([p.buffer(tool_used / 2).buffer(-tool_used / 2)
# for p in cleared_area])
# here we store the poly's already processed in the original geometry by the current tool
# into cleared_by_last_tool list
# this will be sustracted from the original geometry_to_be_cleared and make data for
# the next tool
buffer_value = tool_used / 2
for p in cleared_area:
poly = p.buffer(buffer_value)
cleared_by_last_tool.append(poly)
# find the tooluid associated with the current tool_dia so we know
# where to add the tool solid_geometry
for k, v in self.ncc_tools.items():
if float('%.4f' % v['tooldia']) == float('%.4f' % tool):
current_uid = int(k)
# add the solid_geometry to the current too in self.paint_tools dictionary
# and then reset the temporary list that stored that solid_geometry
v['solid_geometry'] = deepcopy(cleared_area)
v['data']['name'] = name
cleared_area[:] = []
break
geo_obj.tools[current_uid] = dict(self.ncc_tools[current_uid])
else:
log.debug("There are no geometries in the cleared polygon.")
geo_obj.multigeo = True
geo_obj.options["cnctooldia"] = tool
# check to see if geo_obj.tools is empty
# it will be updated only if there is a solid_geometry for tools
if geo_obj.tools:
return
else:
# I will use this variable for this purpose although it was meant for something else
# signal that we have no geo in the object therefore don't create it
app_obj.poly_not_cleared = False
return "fail"
def job_thread(app_obj):
try:
app_obj.new_object("geometry", name, initialize_rm)
except Exception as e:
proc.done()
self.app.inform.emit('[error_notcl] NCCTool.clear_non_copper_rest() --> %s' % str(e))
return
if app_obj.poly_not_cleared is True:
self.app.inform.emit('[success] NCC Tool finished.')
# focus on Selected Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
else:
self.app.inform.emit('[error_notcl] NCC Tool finished but could not clear the object '
'with current settings.')
# focus on Project Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
proc.done()
# reset the variable for next use
app_obj.poly_not_cleared = False
self.tools_frame.hide()
self.app.ui.notebook.setTabText(2, "Tools")
# Promise object with the new name
self.app.collection.promise(name)
# Background
self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})

1106
flatcamTools/ToolPaint.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,369 @@
from FlatCAMTool import FlatCAMTool
from copy import copy, deepcopy
from ObjectCollection import *
import time
class Panelize(FlatCAMTool):
toolName = "Panelize PCB Tool"
def __init__(self, app):
super(Panelize, self).__init__(self)
self.app = app
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % self.toolName)
self.layout.addWidget(title_label)
## Form Layout
form_layout = QtWidgets.QFormLayout()
self.layout.addLayout(form_layout)
## Type of object to be panelized
self.type_obj_combo = QtWidgets.QComboBox()
self.type_obj_combo.addItem("Gerber")
self.type_obj_combo.addItem("Excellon")
self.type_obj_combo.addItem("Geometry")
self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.type_obj_combo_label = QtWidgets.QLabel("Object Type:")
self.type_obj_combo_label.setToolTip(
"Specify the type of object to be panelized\n"
"It can be of type: Gerber, Excellon or Geometry.\n"
"The selection here decide the type of objects that will be\n"
"in the Object combobox."
)
form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
## Object to be panelized
self.object_combo = QtWidgets.QComboBox()
self.object_combo.setModel(self.app.collection)
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(1)
self.object_label = QtWidgets.QLabel("Object:")
self.object_label.setToolTip(
"Object to be panelized. This means that it will\n"
"be duplicated in an array of rows and columns."
)
form_layout.addRow(self.object_label, self.object_combo)
## Type of Box Object to be used as an envelope for panelization
self.type_box_combo = QtWidgets.QComboBox()
self.type_box_combo.addItem("Gerber")
self.type_box_combo.addItem("Excellon")
self.type_box_combo.addItem("Geometry")
# we get rid of item1 ("Excellon") as it is not suitable for use as a "box" for panelizing
self.type_box_combo.view().setRowHidden(1, True)
self.type_box_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
self.type_box_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
self.type_box_combo_label = QtWidgets.QLabel("Box Type:")
self.type_box_combo_label.setToolTip(
"Specify the type of object to be used as an container for\n"
"panelization. It can be: Gerber or Geometry type.\n"
"The selection here decide the type of objects that will be\n"
"in the Box Object combobox."
)
form_layout.addRow(self.type_box_combo_label, self.type_box_combo)
## Box
self.box_combo = QtWidgets.QComboBox()
self.box_combo.setModel(self.app.collection)
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(1)
self.box_combo_label = QtWidgets.QLabel("Box Object:")
self.box_combo_label.setToolTip(
"The actual object that is used a container for the\n "
"selected object that is to be panelized."
)
form_layout.addRow(self.box_combo_label, self.box_combo)
## Spacing Columns
self.spacing_columns = FloatEntry()
self.spacing_columns.set_value(0.0)
self.spacing_columns_label = QtWidgets.QLabel("Spacing cols:")
self.spacing_columns_label.setToolTip(
"Spacing between columns of the desired panel.\n"
"In current units."
)
form_layout.addRow(self.spacing_columns_label, self.spacing_columns)
## Spacing Rows
self.spacing_rows = FloatEntry()
self.spacing_rows.set_value(0.0)
self.spacing_rows_label = QtWidgets.QLabel("Spacing rows:")
self.spacing_rows_label.setToolTip(
"Spacing between rows of the desired panel.\n"
"In current units."
)
form_layout.addRow(self.spacing_rows_label, self.spacing_rows)
## Columns
self.columns = IntEntry()
self.columns.set_value(1)
self.columns_label = QtWidgets.QLabel("Columns:")
self.columns_label.setToolTip(
"Number of columns of the desired panel"
)
form_layout.addRow(self.columns_label, self.columns)
## Rows
self.rows = IntEntry()
self.rows.set_value(1)
self.rows_label = QtWidgets.QLabel("Rows:")
self.rows_label.setToolTip(
"Number of rows of the desired panel"
)
form_layout.addRow(self.rows_label, self.rows)
## Constrains
self.constrain_cb = FCCheckBox("Constrain panel within:")
self.constrain_cb.setToolTip(
"Area define by DX and DY within to constrain the panel.\n"
"DX and DY values are in current units.\n"
"Regardless of how many columns and rows are desired,\n"
"the final panel will have as many columns and rows as\n"
"they fit completely within selected area."
)
form_layout.addRow(self.constrain_cb)
self.x_width_entry = FloatEntry()
self.x_width_entry.set_value(0.0)
self.x_width_lbl = QtWidgets.QLabel("Width (DX):")
self.x_width_lbl.setToolTip(
"The width (DX) within which the panel must fit.\n"
"In current units."
)
form_layout.addRow(self.x_width_lbl, self.x_width_entry)
self.y_height_entry = FloatEntry()
self.y_height_entry.set_value(0.0)
self.y_height_lbl = QtWidgets.QLabel("Height (DY):")
self.y_height_lbl.setToolTip(
"The height (DY)within which the panel must fit.\n"
"In current units."
)
form_layout.addRow(self.y_height_lbl, self.y_height_entry)
self.constrain_sel = OptionalInputSection(
self.constrain_cb, [self.x_width_lbl, self.x_width_entry, self.y_height_lbl, self.y_height_entry])
## Buttons
hlay_2 = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay_2)
hlay_2.addStretch()
self.panelize_object_button = QtWidgets.QPushButton("Panelize Object")
self.panelize_object_button.setToolTip(
"Panelize the specified object around the specified box.\n"
"In other words it creates multiple copies of the source object,\n"
"arranged in a 2D array of rows and columns."
)
hlay_2.addWidget(self.panelize_object_button)
self.layout.addStretch()
## Signals
self.panelize_object_button.clicked.connect(self.on_panelize)
self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
self.type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
# list to hold the temporary objects
self.objs = []
# final name for the panel object
self.outname = ""
# flag to signal the constrain was activated
self.constrain_flag = False
def on_type_obj_index_changed(self):
obj_type = self.type_obj_combo.currentIndex()
self.object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.object_combo.setCurrentIndex(0)
def on_type_box_index_changed(self):
obj_type = self.type_box_combo.currentIndex()
self.box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
self.box_combo.setCurrentIndex(0)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Panel. Tool")
def on_panelize(self):
name = self.object_combo.currentText()
# Get source object.
try:
obj = self.app.collection.get_by_name(str(name))
except:
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % name)
return "Could not retrieve object: %s" % name
panel_obj = obj
if panel_obj is None:
self.app.inform.emit("[error_notcl]Object not found: %s" % panel_obj)
return "Object not found: %s" % panel_obj
boxname = self.box_combo.currentText()
try:
box = self.app.collection.get_by_name(boxname)
except:
self.app.inform.emit("[error_notcl]Could not retrieve object: %s" % boxname)
return "Could not retrieve object: %s" % boxname
if box is None:
self.app.inform.emit("[warning]No object Box. Using instead %s" % panel_obj)
box = panel_obj
self.outname = name + '_panelized'
spacing_columns = self.spacing_columns.get_value()
spacing_columns = spacing_columns if spacing_columns is not None else 0
spacing_rows = self.spacing_rows.get_value()
spacing_rows = spacing_rows if spacing_rows is not None else 0
rows = self.rows.get_value()
rows = rows if rows is not None else 1
columns = self.columns.get_value()
columns = columns if columns is not None else 1
constrain_dx = self.x_width_entry.get_value()
constrain_dy = self.y_height_entry.get_value()
if 0 in {columns, rows}:
self.app.inform.emit("[error_notcl]Columns or Rows are zero value. Change them to a positive integer.")
return "Columns or Rows are zero value. Change them to a positive integer."
xmin, ymin, xmax, ymax = box.bounds()
lenghtx = xmax - xmin + spacing_columns
lenghty = ymax - ymin + spacing_rows
# check if constrain within an area is desired
if self.constrain_cb.isChecked():
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
# adjust the number of columns and/or rows so the panel will fit within the panel constraint area
if (panel_lengthx > constrain_dx) or (panel_lengthy > constrain_dy):
self.constrain_flag = True
while panel_lengthx > constrain_dx:
columns -= 1
panel_lengthx = ((xmax - xmin) * columns) + (spacing_columns * (columns - 1))
while panel_lengthy > constrain_dy:
rows -= 1
panel_lengthy = ((ymax - ymin) * rows) + (spacing_rows * (rows - 1))
def clean_temp():
# deselect all to avoid delete selected object when run delete from shell
self.app.collection.set_all_inactive()
for del_obj in self.objs:
self.app.collection.set_active(del_obj.options['name'])
self.app.on_delete()
self.objs[:] = []
def panelize():
if panel_obj is not None:
self.app.inform.emit("Generating panel ... Please wait.")
self.app.progress.emit(10)
if isinstance(panel_obj, FlatCAMExcellon):
currenty = 0.0
self.app.progress.emit(0)
def initialize_local_excellon(obj_init, app):
obj_init.tools = panel_obj.tools
# drills are offset, so they need to be deep copied
obj_init.drills = deepcopy(panel_obj.drills)
obj_init.offset([float(currentx), float(currenty)])
obj_init.create_geometry()
self.objs.append(obj_init)
self.app.progress.emit(0)
for row in range(rows):
currentx = 0.0
for col in range(columns):
local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
self.app.new_object("excellon", local_outname, initialize_local_excellon, plot=False,
autoselected=False)
currentx += lenghtx
currenty += lenghty
else:
currenty = 0
self.app.progress.emit(0)
def initialize_local_geometry(obj_init, app):
obj_init.solid_geometry = panel_obj.solid_geometry
obj_init.offset([float(currentx), float(currenty)]),
self.objs.append(obj_init)
self.app.progress.emit(0)
for row in range(rows):
currentx = 0
for col in range(columns):
local_outname = self.outname + ".tmp." + str(col) + "." + str(row)
self.app.new_object("geometry", local_outname, initialize_local_geometry, plot=False,
autoselected=False)
currentx += lenghtx
currenty += lenghty
def job_init_geometry(obj_fin, app_obj):
FlatCAMGeometry.merge(self.objs, obj_fin)
def job_init_excellon(obj_fin, app_obj):
# merge expects tools to exist in the target object
obj_fin.tools = panel_obj.tools.copy()
FlatCAMExcellon.merge(self.objs, obj_fin)
if isinstance(panel_obj, FlatCAMExcellon):
self.app.progress.emit(50)
self.app.new_object("excellon", self.outname, job_init_excellon, plot=True, autoselected=True)
else:
self.app.progress.emit(50)
self.app.new_object("geometry", self.outname, job_init_geometry, plot=True, autoselected=True)
else:
self.app.inform.emit("[error_notcl] Obj is None")
return "ERROR: Obj is None"
panelize()
clean_temp()
if self.constrain_flag is False:
self.app.inform.emit("[success]Panel done...")
else:
self.constrain_flag = False
self.app.inform.emit("[warning] Too big for the constrain area. Final panel has %s columns and %s rows" %
(columns, rows))
# proc = self.app.proc_container.new("Generating panel ... Please wait.")
#
# def job_thread(app_obj):
# try:
# panelize()
# except Exception as e:
# proc.done()
# raise e
# proc.done()
#
# self.app.collection.promise(self.outname)
# self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
def reset_fields(self):
self.object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
self.box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

View File

@ -0,0 +1,123 @@
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtCore import Qt
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
class Properties(FlatCAMTool):
toolName = "Properties"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
# this way I can hide/show the frame
self.properties_frame = QtWidgets.QFrame()
self.properties_frame.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.properties_frame)
self.properties_box = QtWidgets.QVBoxLayout()
self.properties_box.setContentsMargins(0, 0, 0, 0)
self.properties_frame.setLayout(self.properties_box)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>&nbsp;%s</b></font>" % self.toolName)
self.properties_box.addWidget(title_label)
# self.layout.setMargin(0) # PyQt4
self.properties_box.setContentsMargins(0, 0, 0, 0) # PyQt5
self.vlay = QtWidgets.QVBoxLayout()
self.properties_box.addLayout(self.vlay)
self.treeWidget = QtWidgets.QTreeWidget()
self.treeWidget.setColumnCount(2)
self.treeWidget.setHeaderHidden(True)
self.treeWidget.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
self.treeWidget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
self.vlay.addWidget(self.treeWidget)
self.vlay.setStretch(0,0)
def run(self):
if self.app.tool_tab_locked is True:
return
# this reset the TreeWidget
self.treeWidget.clear()
self.properties_frame.show()
FlatCAMTool.run(self)
self.properties()
def properties(self):
obj_list = self.app.collection.get_selected()
if not obj_list:
self.app.inform.emit("[error_notcl] Properties Tool was not displayed. No object selected.")
self.app.ui.notebook.setTabText(2, "Tools")
self.properties_frame.hide()
self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
return
for obj in obj_list:
self.addItems(obj)
self.app.inform.emit("[success] Object Properties are displayed.")
self.app.ui.notebook.setTabText(2, "Properties Tool")
def addItems(self, obj):
parent = self.treeWidget.invisibleRootItem()
font = QtGui.QFont()
font.setBold(True)
obj_type = self.addParent(parent, 'TYPE', expanded=True, color=QtGui.QColor("#000000"), font=font)
obj_name = self.addParent(parent, 'NAME', expanded=True, color=QtGui.QColor("#000000"), font=font)
dims = self.addParent(parent, 'Dimensions', expanded=True, color=QtGui.QColor("#000000"), font=font)
options = self.addParent(parent, 'Options', color=QtGui.QColor("#000000"), font=font)
separator = self.addParent(parent, '')
self.addChild(obj_type, [obj.kind.upper()])
self.addChild(obj_name, [obj.options['name']])
# calculate physical dimensions
xmin, ymin, xmax, ymax = obj.bounds()
length = abs(xmax - xmin)
width = abs(ymax - ymin)
self.addChild(dims, ['Length:', '%.4f %s' % (
length, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
self.addChild(dims, ['Width:', '%.4f %s' % (
width, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
if self.app.general_options_form.general_group.units_radio.get_value().lower() == 'mm':
area = (length * width) / 100
self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'cm2')], True)
else:
area = length * width
self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'in2')], True)
for option in obj.options:
if option is 'name':
continue
self.addChild(options, [str(option), str(obj.options[option])], True)
self.addChild(separator, [''])
def addParent(self, parent, title, expanded=False, color=None, font=None):
item = QtWidgets.QTreeWidgetItem(parent, [title])
item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
item.setExpanded(expanded)
if color is not None:
# item.setTextColor(0, color) # PyQt4
item.setForeground(0, QtGui.QBrush(color))
if font is not None:
item.setFont(0, font)
return item
def addChild(self, parent, title, column1=None):
item = QtWidgets.QTreeWidgetItem(parent)
item.setText(0, str(title[0]))
if column1 is not None:
item.setText(1, str(title[1]))
# end of file

361
flatcamTools/ToolShell.py Normal file
View File

@ -0,0 +1,361 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
############################################################
import html
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import Qt, QStringListModel
from PyQt5.QtGui import QColor, QKeySequence, QPalette, QTextCursor
from PyQt5.QtWidgets import QLineEdit, QSizePolicy, QTextEdit, QVBoxLayout, QWidget, QCompleter, QAction
class _BrowserTextEdit(QTextEdit):
def __init__(self):
QTextEdit.__init__(self)
self.menu = None
def contextMenuEvent(self, event):
self.menu = self.createStandardContextMenu(event.pos())
clear_action = QAction("Clear", self)
clear_action.setShortcut(QKeySequence(Qt.Key_Delete)) # it's not working, the shortcut
self.menu.addAction(clear_action)
clear_action.triggered.connect(self.clear)
self.menu.exec_(event.globalPos())
def clear(self):
QTextEdit.clear(self)
text = "FlatCAM 3000\n(c) 2014-2019 Juan Pablo Caram\n\nType help to get started.\n\n"
text = html.escape(text)
text = text.replace('\n', '<br/>')
self.moveCursor(QTextCursor.End)
self.insertHtml(text)
class _ExpandableTextEdit(QTextEdit):
"""
Class implements edit line, which expands themselves automatically
"""
historyNext = pyqtSignal()
historyPrev = pyqtSignal()
def __init__(self, termwidget, *args):
QTextEdit.__init__(self, *args)
self.setStyleSheet("font: 9pt \"Courier\";")
self._fittedHeight = 1
self.textChanged.connect(self._fit_to_document)
self._fit_to_document()
self._termWidget = termwidget
self.completer = MyCompleter()
self.model = QStringListModel()
self.completer.setModel(self.model)
self.set_model_data(keyword_list=[])
self.completer.insertText.connect(self.insertCompletion)
def set_model_data(self, keyword_list):
self.model.setStringList(keyword_list)
def insertCompletion(self, completion):
tc = self.textCursor()
extra = (len(completion) - len(self.completer.completionPrefix()))
tc.movePosition(QTextCursor.Left)
tc.movePosition(QTextCursor.EndOfWord)
tc.insertText(completion[-extra:])
self.setTextCursor(tc)
self.completer.popup().hide()
def focusInEvent(self, event):
if self.completer:
self.completer.setWidget(self)
QTextEdit.focusInEvent(self, event)
def keyPressEvent(self, event):
"""
Catch keyboard events. Process Enter, Up, Down
"""
if event.matches(QKeySequence.InsertParagraphSeparator):
text = self.toPlainText()
if self._termWidget.is_command_complete(text):
self._termWidget.exec_current_command()
return
elif event.matches(QKeySequence.MoveToNextLine):
text = self.toPlainText()
cursor_pos = self.textCursor().position()
textBeforeEnd = text[cursor_pos:]
if len(textBeforeEnd.split('\n')) <= 1:
self.historyNext.emit()
return
elif event.matches(QKeySequence.MoveToPreviousLine):
text = self.toPlainText()
cursor_pos = self.textCursor().position()
text_before_start = text[:cursor_pos]
# lineCount = len(textBeforeStart.splitlines())
line_count = len(text_before_start.split('\n'))
if len(text_before_start) > 0 and \
(text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
line_count += 1
if line_count <= 1:
self.historyPrev.emit()
return
elif event.matches(QKeySequence.MoveToNextPage) or \
event.matches(QKeySequence.MoveToPreviousPage):
return self._termWidget.browser().keyPressEvent(event)
tc = self.textCursor()
if event.key() == Qt.Key_Tab and self.completer.popup().isVisible():
self.completer.insertText.emit(self.completer.getSelected())
self.completer.setCompletionMode(QCompleter.PopupCompletion)
return
QTextEdit.keyPressEvent(self, event)
tc.select(QTextCursor.WordUnderCursor)
cr = self.cursorRect()
if len(tc.selectedText()) > 0:
self.completer.setCompletionPrefix(tc.selectedText())
popup = self.completer.popup()
popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+ self.completer.popup().verticalScrollBar().sizeHint().width())
self.completer.complete(cr)
else:
self.completer.popup().hide()
def sizeHint(self):
"""
QWidget sizeHint impelemtation
"""
hint = QTextEdit.sizeHint(self)
hint.setHeight(self._fittedHeight)
return hint
def _fit_to_document(self):
"""
Update widget height to fit all text
"""
documentsize = self.document().size().toSize()
self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
self.setMaximumHeight(self._fittedHeight)
self.updateGeometry()
def insertFromMimeData(self, mime_data):
# Paste only plain text.
self.insertPlainText(mime_data.text())
class MyCompleter(QCompleter):
insertText = pyqtSignal(str)
def __init__(self, parent=None):
QCompleter.__init__(self)
self.setCompletionMode(QCompleter.PopupCompletion)
self.highlighted.connect(self.setHighlighted)
def setHighlighted(self, text):
self.lastSelected = text
def getSelected(self):
return self.lastSelected
class TermWidget(QWidget):
"""
Widget wich represents terminal. It only displays text and allows to enter text.
All highlevel logic should be implemented by client classes
User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
"""
def __init__(self, *args):
QWidget.__init__(self, *args)
self._browser = _BrowserTextEdit()
self._browser.setStyleSheet("font: 9pt \"Courier\";")
self._browser.setReadOnly(True)
self._browser.document().setDefaultStyleSheet(
self._browser.document().defaultStyleSheet() +
"span {white-space:pre;}")
self._edit = _ExpandableTextEdit(self, self)
self._edit.historyNext.connect(self._on_history_next)
self._edit.historyPrev.connect(self._on_history_prev)
self._edit.setFocus()
self.setFocusProxy(self._edit)
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._browser)
layout.addWidget(self._edit)
self._history = [''] # current empty line
self._historyIndex = 0
def open_proccessing(self, detail=None):
"""
Open processing and disable using shell commands again until all commands are finished
:param detail: text detail about what is currently called from TCL to python
:return: None
"""
self._edit.setTextColor(Qt.white)
self._edit.setTextBackgroundColor(Qt.darkGreen)
if detail is None:
self._edit.setPlainText("...proccessing...")
else:
self._edit.setPlainText("...proccessing... [%s]" % detail)
self._edit.setDisabled(True)
self._edit.setFocus()
def close_proccessing(self):
"""
Close processing and enable using shell commands again
:return:
"""
self._edit.setTextColor(Qt.black)
self._edit.setTextBackgroundColor(Qt.white)
self._edit.setPlainText('')
self._edit.setDisabled(False)
self._edit.setFocus()
def _append_to_browser(self, style, text):
"""
Convert text to HTML for inserting it to browser
"""
assert style in ('in', 'out', 'err')
text = html.escape(text)
text = text.replace('\n', '<br/>')
if style == 'in':
text = '<span style="font-weight: bold;">%s</span>' % text
elif style == 'err':
text = '<span style="font-weight: bold; color: red;">%s</span>' % text
else:
text = '<span>%s</span>' % text # without span <br/> is ignored!!!
scrollbar = self._browser.verticalScrollBar()
old_value = scrollbar.value()
scrollattheend = old_value == scrollbar.maximum()
self._browser.moveCursor(QTextCursor.End)
self._browser.insertHtml(text)
"""TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
and stops moving. As quick fix of this problem, now we always scroll down when add new text.
To fix it correctly, srcoll to the bottom, if before intput has been resized,
scrollbar was in the bottom, and remove next lien
"""
scrollattheend = True
if scrollattheend:
scrollbar.setValue(scrollbar.maximum())
else:
scrollbar.setValue(old_value)
def exec_current_command(self):
"""
Save current command in the history. Append it to the log. Clear edit line
Reimplement in the child classes to actually execute command
"""
text = str(self._edit.toPlainText())
self._append_to_browser('in', '> ' + text + '\n')
if len(self._history) < 2 or\
self._history[-2] != text: # don't insert duplicating items
if text[-1] == '\n':
self._history.insert(-1, text[:-1])
else:
self._history.insert(-1, text)
self._historyIndex = len(self._history) - 1
self._history[-1] = ''
self._edit.clear()
if not text[-1] == '\n':
text += '\n'
self.child_exec_command(text)
def child_exec_command(self, text):
"""
Reimplement in the child classes
"""
pass
def add_line_break_to_input(self):
self._edit.textCursor().insertText('\n')
def append_output(self, text):
"""Appent text to output widget
"""
self._append_to_browser('out', text)
def append_error(self, text):
"""Appent error text to output widget. Text is drawn with red background
"""
self._append_to_browser('err', text)
def is_command_complete(self, text):
"""
Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
"""
return True
def browser(self):
return self._browser
def _on_history_next(self):
"""
Down pressed, show next item from the history
"""
if (self._historyIndex + 1) < len(self._history):
self._historyIndex += 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
def _on_history_prev(self):
"""
Up pressed, show previous item from the history
"""
if self._historyIndex > 0:
if self._historyIndex == (len(self._history) - 1):
self._history[-1] = self._edit.toPlainText()
self._historyIndex -= 1
self._edit.setPlainText(self._history[self._historyIndex])
self._edit.moveCursor(QTextCursor.End)
class FCShell(TermWidget):
def __init__(self, sysShell, *args):
TermWidget.__init__(self, *args)
self._sysShell = sysShell
def is_command_complete(self, text):
def skipQuotes(text):
quote = text[0]
text = text[1:]
endIndex = str(text).index(quote)
return text[endIndex:]
while text:
if text[0] in ('"', "'"):
try:
text = skipQuotes(text)
except ValueError:
return False
text = text[1:]
return True
def child_exec_command(self, text):
self._sysShell.exec_command(text)

View File

@ -0,0 +1,756 @@
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtCore import Qt
from GUIElements import FCEntry, FCButton, OptionalInputSection
from FlatCAMTool import FlatCAMTool
from FlatCAMObj import *
class ToolTransform(FlatCAMTool):
toolName = "Object Transform"
rotateName = "Rotate"
skewName = "Skew/Shear"
scaleName = "Scale"
flipName = "Mirror (Flip)"
offsetName = "Offset"
def __init__(self, app):
FlatCAMTool.__init__(self, app)
self.transform_lay = QtWidgets.QVBoxLayout()
self.layout.addLayout(self.transform_lay)
## Title
title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
self.transform_lay.addWidget(title_label)
self.empty_label = QtWidgets.QLabel("")
self.empty_label.setFixedWidth(50)
self.empty_label1 = QtWidgets.QLabel("")
self.empty_label1.setFixedWidth(70)
self.empty_label2 = QtWidgets.QLabel("")
self.empty_label2.setFixedWidth(70)
self.empty_label3 = QtWidgets.QLabel("")
self.empty_label3.setFixedWidth(70)
self.empty_label4 = QtWidgets.QLabel("")
self.empty_label4.setFixedWidth(70)
self.transform_lay.addWidget(self.empty_label)
## Rotate Title
rotate_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.rotateName)
self.transform_lay.addWidget(rotate_title_label)
## Layout
form_layout = QtWidgets.QFormLayout()
self.transform_lay.addLayout(form_layout)
form_child = QtWidgets.QFormLayout()
self.rotate_label = QtWidgets.QLabel("Angle:")
self.rotate_label.setToolTip(
"Angle for Rotation action, in degrees.\n"
"Float number between -360 and 359.\n"
"Positive numbers for CW motion.\n"
"Negative numbers for CCW motion."
)
self.rotate_label.setFixedWidth(50)
self.rotate_entry = FCEntry()
self.rotate_entry.setFixedWidth(60)
self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.rotate_button = FCButton()
self.rotate_button.set_value("Rotate")
self.rotate_button.setToolTip(
"Rotate the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects."
)
self.rotate_button.setFixedWidth(60)
form_child.addRow(self.rotate_entry, self.rotate_button)
form_layout.addRow(self.rotate_label, form_child)
self.transform_lay.addWidget(self.empty_label1)
## Skew Title
skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
self.transform_lay.addWidget(skew_title_label)
## Form Layout
form1_layout = QtWidgets.QFormLayout()
self.transform_lay.addLayout(form1_layout)
form1_child_1 = QtWidgets.QFormLayout()
form1_child_2 = QtWidgets.QFormLayout()
self.skewx_label = QtWidgets.QLabel("Angle X:")
self.skewx_label.setToolTip(
"Angle for Skew action, in degrees.\n"
"Float number between -360 and 359."
)
self.skewx_label.setFixedWidth(50)
self.skewx_entry = FCEntry()
self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.skewx_entry.setFixedWidth(60)
self.skewx_button = FCButton()
self.skewx_button.set_value("Skew X")
self.skewx_button.setToolTip(
"Skew/shear the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.")
self.skewx_button.setFixedWidth(60)
self.skewy_label = QtWidgets.QLabel("Angle Y:")
self.skewy_label.setToolTip(
"Angle for Skew action, in degrees.\n"
"Float number between -360 and 359."
)
self.skewy_label.setFixedWidth(50)
self.skewy_entry = FCEntry()
self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.skewy_entry.setFixedWidth(60)
self.skewy_button = FCButton()
self.skewy_button.set_value("Skew Y")
self.skewy_button.setToolTip(
"Skew/shear the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.")
self.skewy_button.setFixedWidth(60)
form1_child_1.addRow(self.skewx_entry, self.skewx_button)
form1_child_2.addRow(self.skewy_entry, self.skewy_button)
form1_layout.addRow(self.skewx_label, form1_child_1)
form1_layout.addRow(self.skewy_label, form1_child_2)
self.transform_lay.addWidget(self.empty_label2)
## Scale Title
scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
self.transform_lay.addWidget(scale_title_label)
## Form Layout
form2_layout = QtWidgets.QFormLayout()
self.transform_lay.addLayout(form2_layout)
form2_child_1 = QtWidgets.QFormLayout()
form2_child_2 = QtWidgets.QFormLayout()
self.scalex_label = QtWidgets.QLabel("Factor X:")
self.scalex_label.setToolTip(
"Factor for Scale action over X axis."
)
self.scalex_label.setFixedWidth(50)
self.scalex_entry = FCEntry()
self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.scalex_entry.setFixedWidth(60)
self.scalex_button = FCButton()
self.scalex_button.set_value("Scale X")
self.scalex_button.setToolTip(
"Scale the selected object(s).\n"
"The point of reference depends on \n"
"the Scale reference checkbox state.")
self.scalex_button.setFixedWidth(60)
self.scaley_label = QtWidgets.QLabel("Factor Y:")
self.scaley_label.setToolTip(
"Factor for Scale action over Y axis."
)
self.scaley_label.setFixedWidth(50)
self.scaley_entry = FCEntry()
self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.scaley_entry.setFixedWidth(60)
self.scaley_button = FCButton()
self.scaley_button.set_value("Scale Y")
self.scaley_button.setToolTip(
"Scale the selected object(s).\n"
"The point of reference depends on \n"
"the Scale reference checkbox state.")
self.scaley_button.setFixedWidth(60)
self.scale_link_cb = FCCheckBox()
self.scale_link_cb.set_value(True)
self.scale_link_cb.setText("Link")
self.scale_link_cb.setToolTip(
"Scale the selected object(s)\n"
"using the Scale Factor X for both axis.")
self.scale_link_cb.setFixedWidth(50)
self.scale_zero_ref_cb = FCCheckBox()
self.scale_zero_ref_cb.set_value(True)
self.scale_zero_ref_cb.setText("Scale Reference")
self.scale_zero_ref_cb.setToolTip(
"Scale the selected object(s)\n"
"using the origin reference when checked,\n"
"and the center of the biggest bounding box\n"
"of the selected objects when unchecked.")
form2_child_1.addRow(self.scalex_entry, self.scalex_button)
form2_child_2.addRow(self.scaley_entry, self.scaley_button)
form2_layout.addRow(self.scalex_label, form2_child_1)
form2_layout.addRow(self.scaley_label, form2_child_2)
form2_layout.addRow(self.scale_link_cb, self.scale_zero_ref_cb)
self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
self.transform_lay.addWidget(self.empty_label3)
## Offset Title
offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
self.transform_lay.addWidget(offset_title_label)
## Form Layout
form3_layout = QtWidgets.QFormLayout()
self.transform_lay.addLayout(form3_layout)
form3_child_1 = QtWidgets.QFormLayout()
form3_child_2 = QtWidgets.QFormLayout()
self.offx_label = QtWidgets.QLabel("Value X:")
self.offx_label.setToolTip(
"Value for Offset action on X axis."
)
self.offx_label.setFixedWidth(50)
self.offx_entry = FCEntry()
self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.offx_entry.setFixedWidth(60)
self.offx_button = FCButton()
self.offx_button.set_value("Offset X")
self.offx_button.setToolTip(
"Offset the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.\n")
self.offx_button.setFixedWidth(60)
self.offy_label = QtWidgets.QLabel("Value Y:")
self.offy_label.setToolTip(
"Value for Offset action on Y axis."
)
self.offy_label.setFixedWidth(50)
self.offy_entry = FCEntry()
self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.offy_entry.setFixedWidth(60)
self.offy_button = FCButton()
self.offy_button.set_value("Offset Y")
self.offy_button.setToolTip(
"Offset the selected object(s).\n"
"The point of reference is the middle of\n"
"the bounding box for all selected objects.\n")
self.offy_button.setFixedWidth(60)
form3_child_1.addRow(self.offx_entry, self.offx_button)
form3_child_2.addRow(self.offy_entry, self.offy_button)
form3_layout.addRow(self.offx_label, form3_child_1)
form3_layout.addRow(self.offy_label, form3_child_2)
self.transform_lay.addWidget(self.empty_label4)
## Flip Title
flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
self.transform_lay.addWidget(flip_title_label)
## Form Layout
form4_layout = QtWidgets.QFormLayout()
self.transform_lay.addLayout(form4_layout)
form4_child = QtWidgets.QFormLayout()
form4_child_1 = QtWidgets.QFormLayout()
self.flipx_button = FCButton()
self.flipx_button.set_value("Flip on X")
self.flipx_button.setToolTip(
"Flip the selected object(s) over the X axis.\n"
"Does not create a new object.\n "
)
self.flipx_button.setFixedWidth(60)
self.flipy_button = FCButton()
self.flipy_button.set_value("Flip on Y")
self.flipy_button.setToolTip(
"Flip the selected object(s) over the X axis.\n"
"Does not create a new object.\n "
)
self.flipy_button.setFixedWidth(60)
self.flip_ref_cb = FCCheckBox()
self.flip_ref_cb.set_value(True)
self.flip_ref_cb.setText("Ref Pt")
self.flip_ref_cb.setToolTip(
"Flip the selected object(s)\n"
"around the point in Point Entry Field.\n"
"\n"
"The point coordinates can be captured by\n"
"left click on canvas together with pressing\n"
"SHIFT key. \n"
"Then click Add button to insert coordinates.\n"
"Or enter the coords in format (x, y) in the\n"
"Point Entry field and click Flip on X(Y)")
self.flip_ref_cb.setFixedWidth(50)
self.flip_ref_label = QtWidgets.QLabel("Point:")
self.flip_ref_label.setToolTip(
"Coordinates in format (x, y) used as reference for mirroring.\n"
"The 'x' in (x, y) will be used when using Flip on X and\n"
"the 'y' in (x, y) will be used when using Flip on Y and"
)
self.flip_ref_label.setFixedWidth(50)
self.flip_ref_entry = EvalEntry2("(0, 0)")
self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.flip_ref_entry.setFixedWidth(60)
self.flip_ref_button = FCButton()
self.flip_ref_button.set_value("Add")
self.flip_ref_button.setToolTip(
"The point coordinates can be captured by\n"
"left click on canvas together with pressing\n"
"SHIFT key. Then click Add button to insert.")
self.flip_ref_button.setFixedWidth(60)
form4_child.addRow(self.flipx_button, self.flipy_button)
form4_child_1.addRow(self.flip_ref_entry, self.flip_ref_button)
form4_layout.addRow(self.empty_label, form4_child)
form4_layout.addRow(self.flip_ref_cb)
form4_layout.addRow(self.flip_ref_label, form4_child_1)
self.ois_flip = OptionalInputSection(self.flip_ref_cb,
[self.flip_ref_entry, self.flip_ref_button], logic=True)
self.transform_lay.addStretch()
## Signals
self.rotate_button.clicked.connect(self.on_rotate)
self.skewx_button.clicked.connect(self.on_skewx)
self.skewy_button.clicked.connect(self.on_skewy)
self.scalex_button.clicked.connect(self.on_scalex)
self.scaley_button.clicked.connect(self.on_scaley)
self.offx_button.clicked.connect(self.on_offx)
self.offy_button.clicked.connect(self.on_offy)
self.flipx_button.clicked.connect(self.on_flipx)
self.flipy_button.clicked.connect(self.on_flipy)
self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
self.rotate_entry.returnPressed.connect(self.on_rotate)
self.skewx_entry.returnPressed.connect(self.on_skewx)
self.skewy_entry.returnPressed.connect(self.on_skewy)
self.scalex_entry.returnPressed.connect(self.on_scalex)
self.scaley_entry.returnPressed.connect(self.on_scaley)
self.offx_entry.returnPressed.connect(self.on_offx)
self.offy_entry.returnPressed.connect(self.on_offy)
## Initialize form
self.rotate_entry.set_value('0')
self.skewx_entry.set_value('0')
self.skewy_entry.set_value('0')
self.scalex_entry.set_value('1')
self.scaley_entry.set_value('1')
self.offx_entry.set_value('0')
self.offy_entry.set_value('0')
self.flip_ref_cb.setChecked(False)
def run(self):
FlatCAMTool.run(self)
self.app.ui.notebook.setTabText(2, "Transform Tool")
def on_rotate(self):
try:
value = float(self.rotate_entry.get_value())
except Exception as e:
self.app.inform.emit("[error] Failed to rotate due of: %s" % str(e))
return
self.app.worker_task.emit({'fcn': self.on_rotate_action,
'params': [value]})
# self.on_rotate_action(value)
return
def on_flipx(self):
# self.on_flip("Y")
axis = 'Y'
self.app.worker_task.emit({'fcn': self.on_flip,
'params': [axis]})
return
def on_flipy(self):
# self.on_flip("X")
axis = 'X'
self.app.worker_task.emit({'fcn': self.on_flip,
'params': [axis]})
return
def on_flip_add_coords(self):
val = self.app.defaults["global_point_clipboard_format"] % (self.app.pos[0], self.app.pos[1])
self.flip_ref_entry.set_value(val)
def on_skewx(self):
try:
value = float(self.skewx_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Skew!")
return
# self.on_skew("X", value)
axis = 'X'
self.app.worker_task.emit({'fcn': self.on_skew,
'params': [axis, value]})
return
def on_skewy(self):
try:
value = float(self.skewy_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Skew!")
return
# self.on_skew("Y", value)
axis = 'Y'
self.app.worker_task.emit({'fcn': self.on_skew,
'params': [axis, value]})
return
def on_scalex(self):
try:
xvalue = float(self.scalex_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Scale!")
return
# scaling to zero has no sense so we remove it, because scaling with 1 does nothing
if xvalue == 0:
xvalue = 1
if self.scale_link_cb.get_value():
yvalue = xvalue
else:
yvalue = 1
axis = 'X'
point = (0, 0)
if self.scale_zero_ref_cb.get_value():
self.app.worker_task.emit({'fcn': self.on_scale,
'params': [axis, xvalue, yvalue, point]})
# self.on_scale("X", xvalue, yvalue, point=(0,0))
else:
# self.on_scale("X", xvalue, yvalue)
self.app.worker_task.emit({'fcn': self.on_scale,
'params': [axis, xvalue, yvalue]})
return
def on_scaley(self):
xvalue = 1
try:
yvalue = float(self.scaley_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Scale!")
return
# scaling to zero has no sense so we remove it, because scaling with 1 does nothing
if yvalue == 0:
yvalue = 1
axis = 'Y'
point = (0, 0)
if self.scale_zero_ref_cb.get_value():
self.app.worker_task.emit({'fcn': self.on_scale,
'params': [axis, xvalue, yvalue, point]})
# self.on_scale("Y", xvalue, yvalue, point=(0,0))
else:
# self.on_scale("Y", xvalue, yvalue)
self.app.worker_task.emit({'fcn': self.on_scale,
'params': [axis, xvalue, yvalue]})
return
def on_offx(self):
try:
value = float(self.offx_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Offset!")
return
# self.on_offset("X", value)
axis = 'X'
self.app.worker_task.emit({'fcn': self.on_offset,
'params': [axis, value]})
return
def on_offy(self):
try:
value = float(self.offy_entry.get_value())
except:
self.app.inform.emit("[warning_notcl] No value for Offset!")
return
# self.on_offset("Y", value)
axis = 'Y'
self.app.worker_task.emit({'fcn': self.on_offset,
'params': [axis, value]})
return
def on_rotate_action(self, num):
obj_list = self.app.collection.get_selected()
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
if not obj_list:
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to rotate!")
return
else:
with self.app.proc_container.new("Appying Rotate"):
try:
# first get a bounding box to fit all
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
pass
else:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
xmaximal = max(xmaxlist)
ymaximal = max(ymaxlist)
self.app.progress.emit(20)
for sel_obj in obj_list:
px = 0.5 * (xminimal + xmaximal)
py = 0.5 * (yminimal + ymaximal)
if isinstance(sel_obj, FlatCAMCNCjob):
self.app.inform.emit("CNCJob objects can't be rotated.")
else:
sel_obj.rotate(-num, point=(px, py))
sel_obj.plot()
self.app.object_changed.emit(sel_obj)
# add information to the object that it was changed and how much
sel_obj.options['rotate'] = num
self.app.inform.emit('Object(s) were rotated ...')
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[error_notcl] Due of %s, rotation movement was not executed." % str(e))
return
def on_flip(self, axis):
obj_list = self.app.collection.get_selected()
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
if not obj_list:
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to flip!")
return
else:
with self.app.proc_container.new("Applying Flip"):
try:
# get mirroring coords from the point entry
if self.flip_ref_cb.isChecked():
px, py = eval('{}'.format(self.flip_ref_entry.text()))
# get mirroing coords from the center of an all-enclosing bounding box
else:
# first get a bounding box to fit all
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
pass
else:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
xmaximal = max(xmaxlist)
ymaximal = max(ymaxlist)
px = 0.5 * (xminimal + xmaximal)
py = 0.5 * (yminimal + ymaximal)
self.app.progress.emit(20)
# execute mirroring
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
self.app.inform.emit("CNCJob objects can't be mirrored/flipped.")
else:
if axis is 'X':
obj.mirror('X', (px, py))
# add information to the object that it was changed and how much
# the axis is reversed because of the reference
if 'mirror_y' in obj.options:
obj.options['mirror_y'] = not obj.options['mirror_y']
else:
obj.options['mirror_y'] = True
obj.plot()
self.app.inform.emit('Flipped on the Y axis ...')
elif axis is 'Y':
obj.mirror('Y', (px, py))
# add information to the object that it was changed and how much
# the axis is reversed because of the reference
if 'mirror_x' in obj.options:
obj.options['mirror_x'] = not obj.options['mirror_x']
else:
obj.options['mirror_x'] = True
obj.plot()
self.app.inform.emit('Flipped on the X axis ...')
self.app.object_changed.emit(obj)
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[error_notcl] Due of %s, Flip action was not executed." % str(e))
return
def on_skew(self, axis, num):
obj_list = self.app.collection.get_selected()
xminlist = []
yminlist = []
if not obj_list:
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to shear/skew!")
return
else:
with self.app.proc_container.new("Applying Skew"):
try:
# first get a bounding box to fit all
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
pass
else:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
self.app.progress.emit(20)
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
self.app.inform.emit("CNCJob objects can't be skewed.")
else:
if axis is 'X':
obj.skew(num, 0, point=(xminimal, yminimal))
# add information to the object that it was changed and how much
obj.options['skew_x'] = num
elif axis is 'Y':
obj.skew(0, num, point=(xminimal, yminimal))
# add information to the object that it was changed and how much
obj.options['skew_y'] = num
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were skewed on %s axis ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[error_notcl] Due of %s, Skew action was not executed." % str(e))
return
def on_scale(self, axis, xfactor, yfactor, point=None):
obj_list = self.app.collection.get_selected()
xminlist = []
yminlist = []
xmaxlist = []
ymaxlist = []
if not obj_list:
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to scale!")
return
else:
with self.app.proc_container.new("Applying Scale"):
try:
# first get a bounding box to fit all
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
pass
else:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
xmaxlist.append(xmax)
ymaxlist.append(ymax)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
xmaximal = max(xmaxlist)
ymaximal = max(ymaxlist)
self.app.progress.emit(20)
if point is None:
px = 0.5 * (xminimal + xmaximal)
py = 0.5 * (yminimal + ymaximal)
else:
px = 0
py = 0
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
self.app.inform.emit("CNCJob objects can't be scaled.")
else:
obj.scale(xfactor, yfactor, point=(px, py))
# add information to the object that it was changed and how much
obj.options['scale_x'] = xfactor
obj.options['scale_y'] = yfactor
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were scaled on %s axis ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[error_notcl] Due of %s, Scale action was not executed." % str(e))
return
def on_offset(self, axis, num):
obj_list = self.app.collection.get_selected()
xminlist = []
yminlist = []
if not obj_list:
self.app.inform.emit("[warning_notcl] No object selected. Please Select an object to offset!")
return
else:
with self.app.proc_container.new("Applying Offset"):
try:
# first get a bounding box to fit all
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
pass
else:
xmin, ymin, xmax, ymax = obj.bounds()
xminlist.append(xmin)
yminlist.append(ymin)
# get the minimum x,y and maximum x,y for all objects selected
xminimal = min(xminlist)
yminimal = min(yminlist)
self.app.progress.emit(20)
for obj in obj_list:
if isinstance(obj, FlatCAMCNCjob):
self.app.inform.emit("CNCJob objects can't be offseted.")
else:
if axis is 'X':
obj.offset((num, 0))
# add information to the object that it was changed and how much
obj.options['offset_x'] = num
elif axis is 'Y':
obj.offset((0, num))
# add information to the object that it was changed and how much
obj.options['offset_y'] = num
obj.plot()
self.app.object_changed.emit(obj)
self.app.inform.emit('Object(s) were offseted on %s axis ...' % str(axis))
self.app.progress.emit(100)
except Exception as e:
self.app.inform.emit("[error_notcl] Due of %s, Offset action was not executed." % str(e))
return
# end of file

16
flatcamTools/__init__.py Normal file
View File

@ -0,0 +1,16 @@
import sys
from flatcamTools.ToolMeasurement import Measurement
from flatcamTools.ToolPanelize import Panelize
from flatcamTools.ToolFilm import Film
from flatcamTools.ToolMove import ToolMove
from flatcamTools.ToolDblSided import DblSidedTool
from flatcamTools.ToolCutout import ToolCutout
from flatcamTools.ToolCalculators import ToolCalculator
from flatcamTools.ToolProperties import Properties
from flatcamTools.ToolImage import ToolImage
from flatcamTools.ToolPaint import ToolPaint
from flatcamTools.ToolNonCopperClear import NonCopperClear
from flatcamTools.ToolTransform import ToolTransform
from flatcamTools.ToolShell import FCShell

91
make_win.py Normal file
View File

@ -0,0 +1,91 @@
############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://flatcam.org #
# Author: Juan Pablo Caram (c) #
# Date: 12/20/2018 #
# MIT Licence #
# #
# Creates a portable copy of FlatCAM, including Python #
# itself and all dependencies. #
# #
# This is not an aid to install FlatCAM from source on #
# Windows platforms. It is only useful when FlatCAM is up #
# and running and ready to be packaged. #
############################################################
# Files not needed: Qt, tk.dll, tcl.dll, tk/, tcl/, vtk/,
# scipy.lib.lapack.flapack.pyd, scipy.lib.blas.fblas.pyd,
# numpy.core._dotblas.pyd, scipy.sparse.sparsetools._bsr.pyd,
# scipy.sparse.sparsetools._csr.pyd, scipy.sparse.sparsetools._csc.pyd,
# scipy.sparse.sparsetools._coo.pyd
import os, site, sys, platform
from cx_Freeze import setup, Executable
# this is done to solve the tkinter not being found
PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__))
os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6')
os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6')
# Get the site-package folder, not everybody will install
# Python into C:\PythonXX
site_dir = site.getsitepackages()[1]
include_files = []
include_files.append((os.path.join(site_dir, "shapely"), "shapely"))
include_files.append((os.path.join(site_dir, "svg"), "svg"))
include_files.append((os.path.join(site_dir, "svg/path"), "svg"))
include_files.append((os.path.join(site_dir, "vispy"), "vispy"))
include_files.append((os.path.join(site_dir, "vispy/app"), "vispy/app"))
include_files.append((os.path.join(site_dir, "vispy/app/backends"), "vispy/app/backends"))
include_files.append((os.path.join(site_dir, "rtree"), "rtree"))
if platform.architecture()[0] == '64bit':
include_files.append((os.path.join(site_dir, "google"), "google"))
include_files.append((os.path.join(site_dir, "google/protobuf"), "google/protobuf"))
include_files.append((os.path.join(site_dir, "ortools"), "ortools"))
include_files.append(("share", "lib/share"))
include_files.append(("postprocessors", "lib/postprocessors"))
include_files.append(("README.md", "README.md"))
include_files.append(("LICENSE", "LICENSE"))
base = None
# Lets not open the console while running the app
if sys.platform == "win32":
base = "Win32GUI"
if platform.architecture()[0] == '64bit':
buildOptions = dict(
include_files=include_files,
excludes=['scipy','pytz'],
# packages=['OpenGL','numpy','vispy','ortools','google']
packages=['numpy','google', 'rasterio'] # works for Python 3.7
# packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5
)
else:
buildOptions = dict(
include_files=include_files,
excludes=['scipy', 'pytz'],
# packages=['OpenGL','numpy','vispy','ortools','google']
packages=['numpy', 'rasterio'] # works for Python 3.7
# packages = ['opengl', 'numpy', 'google', 'rasterio'] # works for Python 3.6.5
)
print("INCLUDE_FILES", include_files)
#execfile('clean.py')
setup(
name="FlatCAM",
author="Juan Pablo Caram",
version="3000",
description="FlatCAM: 2D Computer Aided PCB Manufacturing",
options=dict(build_exe=buildOptions),
executables=[Executable("FlatCAM.py", icon='share/flatcam_icon48.ico', base=base)]
)

View File

@ -0,0 +1,119 @@
from FlatCAMPostProc import *
# for Roland Postprocessors it is mandatory for the postprocessor name (python file and class name, both of them must be
# the same) to contain the following keyword, case-sensitive: 'Roland' without the quotes.
class Roland_MDX_20(FlatCAMPostProc):
coordinate_format = "%.1f"
feedrate_format = '%.1f'
feedrate_rapid_format = '%.1f'
def start_code(self, p):
gcode = ';;^IN;' + '\n'
gcode += '^PA;'
return gcode
def startz_code(self, p):
return ''
def lift_code(self, p):
if p.units.upper() == 'IN':
z = p.z_move / 25.4
else:
z = p.z_move
gcode = self.feedrate_rapid_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
return gcode
def down_code(self, p):
if p.units.upper() == 'IN':
z = p.z_cut / 25.4
else:
z = p.z_cut
gcode = self.feedrate_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
return gcode
def toolchange_code(self, p):
return ''
def up_to_zero_code(self, p):
gcode = self.feedrate_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + '0' + ';'
return gcode
def position_code(self, p):
if p.units.upper() == 'IN':
x = p.x / 25.4
y = p.y / 25.4
else:
x = p.x
y = p.y
return ('Z' + self.coordinate_format + ',' + self.coordinate_format) % (float(x * 40.0), float(y * 40.0))
def rapid_code(self, p):
if p.units.upper() == 'IN':
z = p.z_move / 25.4
else:
z = p.z_move
gcode = self.feedrate_rapid_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
return gcode
def linear_code(self, p):
if p.units.upper() == 'IN':
z = p.z / 25.4
else:
z = p.z
gcode = self.feedrate_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
return gcode
def end_code(self, p):
if p.units.upper() == 'IN':
z = p.endz / 25.4
else:
z = p.endz
gcode = self.feedrate_rapid_code(p) + '\n'
gcode += self.position_code(p).format(**p) + ',' + str(float(z * 40.0)) + ';'
return gcode
def feedrate_code(self, p):
fr_sec = p.feedrate / 60
# valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
if p.feedrate >= 900:
fr_sec = 15
if p.feedrate < 6:
fr_sec = 6
return 'V' + str(self.feedrate_format % fr_sec) + ';'
def feedrate_z_code(self, p):
fr_sec = p.feedrate_z / 60
# valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
if p.feedrate_z >= 900:
fr_sec = 15
if p.feedrate_z < 6:
fr_sec = 6
return 'V' + str(self.feedrate_format % fr_sec) + ';'
def feedrate_rapid_code(self, p):
fr_sec = p.feedrate_rapid / 60
# valid feedrate for MDX20 is between 0.1mm/sec and 15mm/sec (6mm/min to 900mm/min)
if p.feedrate_rapid >= 900:
fr_sec = 15
if p.feedrate_rapid < 6:
fr_sec = 6
return 'V' + str(self.feedrate_format % fr_sec) + ';'
def spindle_code(self, p):
return '!MC1'
def dwell_code(self, p):
return''
def spindle_stop_code(self,p):
return '!MC0'

View File

137
postprocessors/default.py Normal file
View File

@ -0,0 +1,137 @@
from FlatCAMPostProc import *
class default(FlatCAMPostProc):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
def start_code(self, p):
units = ' ' + str(p['units']).lower()
coords_xy = p['toolchange_xy']
gcode = ''
if str(p['options']['type']) == 'Geometry':
gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
if str(p['options']['type']) == 'Geometry':
gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
if str(p['options']['type']) == 'Geometry':
if p['multidepth'] is True:
gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
else:
gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
gcode += 'G90\n'
gcode += 'G94\n'
return gcode
def startz_code(self, p):
if p.startz is not None:
return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz)
else:
return ''
def lift_code(self, p):
return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
def down_code(self, p):
return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
def toolchange_code(self, p):
toolchangez = p.toolchangez
toolchangexy = p.toolchange_xy
toolchangex = toolchangexy[0]
toolchangey = toolchangexy[1]
no_drills = 1
if int(p.tool) == 1 and p.startz is not None:
toolchangez = p.startz
if p.units.upper() == 'MM':
toolC_formatted = format(p.toolC, '.2f')
else:
toolC_formatted = format(p.toolC, '.4f')
if str(p['options']['type']) == 'Excellon':
for i in p['options']['Tools_in_use']:
if i[0] == p.tool:
no_drills = i[2]
return """G00 Z{toolchangez}
T{tool}
M5
M6
(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
t_drills=no_drills,
toolC=toolC_formatted)
else:
return """G00 Z{toolchangez}
T{tool}
M5
M6
(MSG, Change to Tool Dia = {toolC})
M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
toolC=toolC_formatted)
def up_to_zero_code(self, p):
return 'G01 Z0'
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G00 ' + self.position_code(p)).format(**p)
def linear_code(self, p):
return ('G01 ' + self.position_code(p)).format(**p)
def end_code(self, p):
coords_xy = p['toolchange_xy']
gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
return gcode
def feedrate_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
def feedrate_z_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
def spindle_code(self, p):
if p.spindlespeed:
return 'M03 S' + str(p.spindlespeed)
else:
return 'M03'
def dwell_code(self, p):
if p.dwelltime:
return 'G4 P' + str(p.dwelltime)
def spindle_stop_code(self,p):
return 'M05'

134
postprocessors/grbl_11.py Normal file
View File

@ -0,0 +1,134 @@
from FlatCAMPostProc import *
class grbl_11(FlatCAMPostProc):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
def start_code(self, p):
units = ' ' + str(p['units']).lower()
coords_xy = p['toolchange_xy']
gcode = ''
if str(p['options']['type']) == 'Geometry':
gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n' + '\n'
gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
if str(p['options']['type']) == 'Geometry':
gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
if str(p['options']['type']) == 'Geometry':
if p['multidepth'] is True:
gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
else:
gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
gcode += '(Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + ')\n' + '\n'
gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
gcode += 'G90\n'
gcode += 'G94\n'
gcode += 'G17\n'
return gcode
def startz_code(self, p):
if p.startz is not None:
return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.startz)
else:
return ''
def lift_code(self, p):
return 'G00 Z' + self.coordinate_format%(p.coords_decimals, p.z_move)
def down_code(self, p):
return 'G01 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut)
def toolchange_code(self, p):
toolchangez = p.toolchangez
if int(p.tool) == 1 and p.startz is not None:
toolchangez = p.startz
if p.units.upper() == 'MM':
toolC_formatted = format(p.toolC, '.2f')
else:
toolC_formatted = format(p.toolC, '.4f')
for i in p['options']['Tools_in_use']:
if i[0] == p.tool:
no_drills = i[2]
if str(p['options']['type']) == 'Excellon':
return """G00 Z{toolchangez}
T{tool}
M5
M6
(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
t_drills=no_drills,
toolC=toolC_formatted)
else:
return """G00 Z{toolchangez}
T{tool}
M5
M6
(MSG, Change to Tool Dia = {toolC})
M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
toolC=toolC_formatted)
def up_to_zero_code(self, p):
return 'G01 Z0'
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G00 ' + self.position_code(p)).format(**p)
def linear_code(self, p):
return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
def end_code(self, p):
coords_xy = p['toolchange_xy']
gcode = ('G00 Z' + self.feedrate_format % (p.fr_decimals, p.endz) + "\n")
gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
return gcode
def feedrate_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
def feedrate_z_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
def spindle_code(self,p):
if p.spindlespeed:
return 'M03 S%d' % p.spindlespeed
else:
return 'M03'
def dwell_code(self, p):
if p.dwelltime:
return 'G4 P' + str(p.dwelltime)
def spindle_stop_code(self,p):
return 'M05'

View File

@ -0,0 +1,74 @@
from FlatCAMPostProc import *
# This post processor is configured to output code that
# is compatible with almost any version of Grbl.
class grbl_laser(FlatCAMPostProc):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
def start_code(self, p):
units = ' ' + str(p['units']).lower()
gcode = ''
gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
else:
gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
gcode += 'G90\n'
gcode += 'G94\n'
gcode += 'G17\n'
return gcode
def startz_code(self, p):
return ''
def lift_code(self, p):
return 'M05'
def down_code(self, p):
return 'M03'
def toolchange_code(self, p):
return ''
def up_to_zero_code(self, p):
return 'M05'
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G00 ' + self.position_code(p)).format(**p)
def linear_code(self, p):
return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
def end_code(self, p):
gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
gcode += 'G00 X0Y0'
return gcode
def feedrate_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
def spindle_code(self,p):
if p.spindlespeed:
return 'S%d' % p.spindlespeed
def dwell_code(self, p):
return ''
def spindle_stop_code(self,p):
return 'M05'

View File

@ -0,0 +1,152 @@
from FlatCAMPostProc import *
class manual_toolchange(FlatCAMPostProc):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
def start_code(self, p):
units = ' ' + str(p['units']).lower()
coords_xy = p['toolchange_xy']
gcode = ''
if str(p['options']['type']) == 'Geometry':
gcode += '(TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + ')\n'
gcode += '(Feedrate: ' + str(p['feedrate']) + units + '/min' + ')\n'
if str(p['options']['type']) == 'Geometry':
gcode += '(Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + ')\n'
gcode += '(Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + ')\n' + '\n'
gcode += '(Z_Cut: ' + str(p['z_cut']) + units + ')\n'
if str(p['options']['type']) == 'Geometry':
if p['multidepth'] is True:
gcode += '(DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + ')\n'
gcode += '(Z_Move: ' + str(p['z_move']) + units + ')\n'
gcode += '(Z Toolchange: ' + str(p['toolchangez']) + units + ')\n'
gcode += '(X,Y Toolchange: ' + "%.4f, %.4f" % (coords_xy[0], coords_xy[1]) + units + ')\n'
gcode += '(Z Start: ' + str(p['startz']) + units + ')\n'
gcode += '(Z End: ' + str(p['endz']) + units + ')\n'
gcode += '(Steps per circle: ' + str(p['steps_per_circle']) + ')\n'
if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
gcode += '(Postprocessor Excellon: ' + str(p['pp_excellon_name']) + ')\n'
else:
gcode += '(Postprocessor Geometry: ' + str(p['pp_geometry_name']) + ')\n'
gcode += '(Spindle Speed: %s RPM)\n' % str(p['spindlespeed'])
gcode += ('G20\n' if p.units.upper() == 'IN' else 'G21\n')
gcode += 'G90\n'
gcode += 'G94\n'
return gcode
def startz_code(self, p):
if p.startz is not None:
return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.startz)
else:
return ''
def lift_code(self, p):
return 'G00 Z' + self.coordinate_format % (p.coords_decimals, p.z_move)
def down_code(self, p):
return 'G01 Z' + self.coordinate_format % (p.coords_decimals, p.z_cut)
def toolchange_code(self, p):
toolchangez = p.toolchangez
toolchangexy = p.toolchange_xy
toolchangex = toolchangexy[0]
toolchangey = toolchangexy[1]
no_drills = 1
if int(p.tool) == 1 and p.startz is not None:
toolchangez = p.startz
if p.units.upper() == 'MM':
toolC_formatted = format(p.toolC, '.2f')
else:
toolC_formatted = format(p.toolC, '.4f')
for i in p['options']['Tools_in_use']:
if i[0] == p.tool:
no_drills = i[2]
if str(p['options']['type']) == 'Excellon':
return """G00 Z{toolchangez}
T{tool}
M5
G00 X{toolchangex}Y{toolchangey}
(MSG, Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills})
M0
G01 Z0
M0
G00 Z{toolchangez}
M0
""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
toolchangey=self.coordinate_format%(p.coords_decimals, toolchangey),
toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
t_drills=no_drills,
toolC=toolC_formatted)
else:
return """G00 Z{toolchangez}
T{tool}
M5
G00 X{toolchangex}Y{toolchangey}
(MSG, Change to Tool Dia = {toolC})
M0
G01 Z0
M0
G00 Z{toolchangez}
M0
""".format(toolchangex=self.coordinate_format%(p.coords_decimals, toolchangex),
toolchangey=self.coordinate_format%(p.coords_decimals, toolchangey),
toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
toolC=toolC_formatted)
def up_to_zero_code(self, p):
return 'G01 Z0'
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G00 ' + self.position_code(p)).format(**p)
def linear_code(self, p):
return ('G01 ' + self.position_code(p)).format(**p)
def end_code(self, p):
coords_xy = p['toolchange_xy']
gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
gcode += 'G00 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + "\n"
return gcode
def feedrate_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
def feedrate_z_code(self, p):
return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
def spindle_code(self,p):
if p.spindlespeed:
return 'M03 S' + str(p.spindlespeed)
else:
return 'M03'
def dwell_code(self, p):
if p.dwelltime:
return 'G4 P' + str(p.dwelltime)
def spindle_stop_code(self,p):
return 'M05'

135
postprocessors/marlin.py Normal file
View File

@ -0,0 +1,135 @@
from FlatCAMPostProc import *
class marlin(FlatCAMPostProc):
coordinate_format = "%.*f"
feedrate_format = '%.*f'
feedrate_rapid_format = feedrate_format
def start_code(self, p):
units = ' ' + str(p['units']).lower()
gcode = ''
if str(p['options']['type']) == 'Geometry':
gcode += ';TOOL DIAMETER: ' + str(p['options']['tool_dia']) + units + '\n' + '\n'
gcode += ';Feedrate: ' + str(p['feedrate']) + units + '/min' + '\n'
if str(p['options']['type']) == 'Geometry':
gcode += ';Feedrate_Z: ' + str(p['feedrate_z']) + units + '/min' + '\n'
gcode += ';Feedrate rapids ' + str(p['feedrate_rapid']) + units + '/min' + '\n' + '\n'
gcode += ';Z_Cut: ' + str(p['z_cut']) + units + '\n'
if str(p['options']['type']) == 'Geometry':
if p['multidepth'] is True:
gcode += ';DepthPerCut: ' + str(p['depthpercut']) + units + ' <=>' + \
str(math.ceil(abs(p['z_cut']) / p['depthpercut'])) + ' passes' + '\n'
gcode += ';Z_Move: ' + str(p['z_move']) + units + '\n'
gcode += ';Z Toolchange: ' + str(p['toolchangez']) + units + '\n'
gcode += ';Z Start: ' + str(p['startz']) + units + '\n'
gcode += ';Z End: ' + str(p['endz']) + units + '\n'
gcode += ';Steps per circle: ' + str(p['steps_per_circle']) + '\n'
if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
gcode += ';Postprocessor Excellon: ' + str(p['pp_excellon_name']) + '\n'
else:
gcode += ';Postprocessor Geometry: ' + str(p['pp_geometry_name']) + '\n'
gcode += ';Spindle Speed: ' + str(p['spindlespeed']) + ' RPM' + '\n' + '\n'
gcode += ('G20' if p.units.upper() == 'IN' else 'G21') + "\n"
gcode += 'G90\n'
return gcode
def startz_code(self, p):
if p.startz is not None:
return 'G0 Z' + self.coordinate_format % (p.coords_decimals, p.startz)
else:
return ''
def lift_code(self, p):
return 'G0 Z' + self.coordinate_format%(p.coords_decimals, p.z_move) + " " + self.feedrate_rapid_code(p)
def down_code(self, p):
return 'G1 Z' + self.coordinate_format%(p.coords_decimals, p.z_cut) + " " + self.end_feedrate_code(p)
def toolchange_code(self, p):
toolchangez = p.toolchangez
no_drills = 1
if int(p.tool) == 1 and p.startz is not None:
toolchangez = p.startz
if p.units.upper() == 'MM':
toolC_formatted = format(p.toolC, '.2f')
else:
toolC_formatted = format(p.toolC, '.4f')
for i in p['options']['Tools_in_use']:
if i[0] == p.tool:
no_drills = i[2]
if str(p['options']['type']) == 'Excellon':
return """G0 Z{toolchangez}
M5
M0 Change to Tool Dia = {toolC}, Total drills for current tool = {t_drills}
""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
t_drills=no_drills,
toolC=toolC_formatted)
else:
return """G0 Z{toolchangez}
M5
M0 Change to Tool Dia = {toolC}
""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez),
tool=int(p.tool),
toolC=toolC_formatted)
def up_to_zero_code(self, p):
return 'G1 Z0' + " " + self.feedrate_code(p)
def position_code(self, p):
return ('X' + self.coordinate_format + ' Y' + self.coordinate_format) % \
(p.coords_decimals, p.x, p.coords_decimals, p.y)
def rapid_code(self, p):
return ('G0 ' + self.position_code(p)).format(**p) + " " + self.feedrate_rapid_code(p)
def linear_code(self, p):
return ('G1 ' + self.position_code(p)).format(**p) + " " + self.end_feedrate_code(p)
def end_code(self, p):
coords_xy = p['toolchange_xy']
gcode = ('G0 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + " " + self.feedrate_rapid_code(p) + "\n")
gcode += 'G0 X{x}Y{y}'.format(x=coords_xy[0], y=coords_xy[1]) + " " + self.feedrate_rapid_code(p) + "\n"
return gcode
def feedrate_code(self, p):
return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
def end_feedrate_code(self, p):
return 'F' + self.feedrate_format %(p.fr_decimals, p.feedrate)
def feedrate_z_code(self, p):
return 'G1 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
def feedrate_rapid_code(self, p):
return 'F' + self.feedrate_rapid_format % (p.fr_decimals, p.feedrate_rapid)
def spindle_code(self,p):
if p.spindlespeed:
return 'M3 S%d' % p.spindlespeed
else:
return 'M3'
def dwell_code(self, p):
if p.dwelltime:
return 'G4 P' + str(p.dwelltime)
def spindle_stop_code(self,p):
return 'M5'

18
requirements.txt Normal file
View File

@ -0,0 +1,18 @@
# This file contains python only requirements to be installed with pip
# Python pacakges that cannot be installed with pip (e.g. PyQt5, GDAL) are not included.
# Usage: pip install -r requirements.txt
numpy>=1.8
simplejson
rtree
pyopengl
pyopengl-accelerate
vispy
ortools
svg.path
simplejson
shapely>=1.3
freetype-py
fontTools
rasterio
lxml
ezdxf

31
setup_ubuntu.sh Normal file
View File

@ -0,0 +1,31 @@
#!/bin/sh
apt-get install python3-pip
apt-get install python3-pyqt5
apt-get install python3-pyqt5.qtopengl
apt-get install libpng-dev
apt-get install libfreetype6 libfreetype6-dev
apt-get install python3-dev
apt-get install python3-simplejson
apt-get install python3-numpy python3-scipy
apt-get install libgeos-dev
apt-get install python3-shapely
apt-get install python3-rtree
apt-get install python3-tk
apt-get install libspatialindex-dev
apt-get install python3-gdal
apt-get install python3-lxml
apt-get install python3-ezdxf
easy_install3 -U distribute
pip3 install --upgrade Shapely
pip3 install --upgrade vispy
pip3 install --upgrade rtree
pip3 install --upgrade pyopengl
pip3 install --upgrade pyopengl-accelerate
pip3 install --upgrade setuptools
pip3 install --upgrade svg.path
pip3 install --upgrade ortools
pip3 install --upgrade freetype-py
pip3 install --upgrade fontTools
pip3 install --upgrade rasterio
pip3 install --upgrade lxml
pip3 install --upgrade ezdxf

BIN
share/active.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
share/activity2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
share/addarray16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

BIN
share/addarray20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

BIN
share/addarray32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

BIN
share/arc16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

BIN
share/arc24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

BIN
share/arc32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

BIN
share/axis32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

BIN
share/blocked16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
share/bold32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

BIN
share/buffer16-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

BIN
share/buffer16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

BIN
share/buffer20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

BIN
share/buffer24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

BIN
share/bug16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

BIN
share/calculator24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

BIN
share/cancel_edit16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

BIN
share/cancel_edit32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

BIN
share/circle32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

BIN
share/clear_plot16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

BIN
share/clear_plot32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
share/cnc16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

BIN
share/cnc32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

BIN
share/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

BIN
share/convert24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

BIN
share/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

BIN
share/copy16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
share/copy32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

BIN
share/copy_geo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

BIN
share/corner32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

BIN
share/cut16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

BIN
share/cut32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

BIN
share/cutpath16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

BIN
share/cutpath24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

BIN
share/cutpath32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

BIN
share/defaults.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

BIN
share/delete32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
share/deleteshape16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

BIN
share/deleteshape24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

BIN
share/deleteshape32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

BIN
share/doubleside16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

BIN
share/doubleside32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

BIN
share/draw32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

BIN
share/drill16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

BIN
share/drill32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

BIN
share/dxf16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

BIN
share/edit16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

BIN
share/edit32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Some files were not shown because too many files have changed in this diff Show More