-clean-up before merge
|
@ -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_())
|
||||
|
|
@ -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
|
||||
|
|
@ -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, ())
|
|
@ -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
|
|
@ -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))
|
|
@ -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()
|
||||
|
|
@ -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.")
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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.
|
|
@ -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())
|
|
@ -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
|
|
@ -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]
|
||||
#
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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()))
|
|
@ -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()
|
|
@ -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()))
|
|
@ -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")
|
|
@ -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
|
|
@ -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
|
|
@ -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]})
|
|
@ -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()))
|
|
@ -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> %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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)]
|
||||
)
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 405 B |
After Width: | Height: | Size: 259 B |
After Width: | Height: | Size: 492 B |
After Width: | Height: | Size: 652 B |
After Width: | Height: | Size: 854 B |
After Width: | Height: | Size: 497 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 489 B |
After Width: | Height: | Size: 304 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 328 B |
After Width: | Height: | Size: 382 B |
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 714 B |
After Width: | Height: | Size: 517 B |
After Width: | Height: | Size: 788 B |
After Width: | Height: | Size: 501 B |
After Width: | Height: | Size: 720 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 506 B |
After Width: | Height: | Size: 841 B |
After Width: | Height: | Size: 526 B |
After Width: | Height: | Size: 684 B |
After Width: | Height: | Size: 569 B |
After Width: | Height: | Size: 444 B |
After Width: | Height: | Size: 624 B |
After Width: | Height: | Size: 582 B |
After Width: | Height: | Size: 957 B |
After Width: | Height: | Size: 352 B |
After Width: | Height: | Size: 557 B |
After Width: | Height: | Size: 454 B |
After Width: | Height: | Size: 523 B |
After Width: | Height: | Size: 581 B |
After Width: | Height: | Size: 681 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 542 B |
After Width: | Height: | Size: 679 B |
After Width: | Height: | Size: 886 B |
After Width: | Height: | Size: 364 B |
After Width: | Height: | Size: 655 B |
After Width: | Height: | Size: 550 B |
After Width: | Height: | Size: 385 B |
After Width: | Height: | Size: 687 B |
After Width: | Height: | Size: 403 B |
After Width: | Height: | Size: 526 B |
After Width: | Height: | Size: 997 B |