Merge branch '8p5'
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import sys, traceback
|
import sys
|
||||||
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
import getopt
|
import getopt
|
||||||
import random
|
import random
|
||||||
@@ -16,6 +17,7 @@ from contextlib import contextmanager
|
|||||||
########################################
|
########################################
|
||||||
## Imports part of FlatCAM ##
|
## Imports part of FlatCAM ##
|
||||||
########################################
|
########################################
|
||||||
|
import FlatCAMVersion
|
||||||
from FlatCAMWorker import Worker
|
from FlatCAMWorker import Worker
|
||||||
from ObjectCollection import *
|
from ObjectCollection import *
|
||||||
from FlatCAMObj import *
|
from FlatCAMObj import *
|
||||||
@@ -63,8 +65,10 @@ class App(QtCore.QObject):
|
|||||||
log.addHandler(handler)
|
log.addHandler(handler)
|
||||||
|
|
||||||
## Version
|
## Version
|
||||||
version = 8.4
|
version = 8.5
|
||||||
version_date = "2015/10"
|
#version_date_str = "2016/7"
|
||||||
|
version_date = (0, 0, 0)
|
||||||
|
version_name = None
|
||||||
|
|
||||||
## URL for update checks and statistics
|
## URL for update checks and statistics
|
||||||
version_url = "http://flatcam.org/version"
|
version_url = "http://flatcam.org/version"
|
||||||
@@ -115,6 +119,13 @@ class App(QtCore.QObject):
|
|||||||
# in the worker task.
|
# in the worker task.
|
||||||
thread_exception = QtCore.pyqtSignal(object)
|
thread_exception = QtCore.pyqtSignal(object)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version_date_str(self):
|
||||||
|
return "{:4d}/{:02d}".format(
|
||||||
|
self.version_date[0],
|
||||||
|
self.version_date[1]
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, user_defaults=True, post_gui=None):
|
def __init__(self, user_defaults=True, post_gui=None):
|
||||||
"""
|
"""
|
||||||
Starts the application.
|
Starts the application.
|
||||||
@@ -123,12 +134,15 @@ class App(QtCore.QObject):
|
|||||||
:rtype: App
|
:rtype: App
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
FlatCAMVersion.setup(self)
|
||||||
|
|
||||||
App.log.info("FlatCAM Starting...")
|
App.log.info("FlatCAM Starting...")
|
||||||
|
|
||||||
###################
|
###################
|
||||||
### OS-specific ###
|
### OS-specific ###
|
||||||
###################
|
###################
|
||||||
|
|
||||||
|
# Folder for user settings.
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
from win32com.shell import shell, shellcon
|
from win32com.shell import shell, shellcon
|
||||||
App.log.debug("Win32!")
|
App.log.debug("Win32!")
|
||||||
@@ -168,8 +182,11 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
# Application directory. Chdir to it. Otherwise, trying to load
|
# Application directory. Chdir to it. Otherwise, trying to load
|
||||||
# GUI icons will fail as thir path is relative.
|
# GUI icons will fail as thir path is relative.
|
||||||
# This will fail under cx_freeze ...
|
if hasattr(sys, "frozen"):
|
||||||
self.app_home = os.path.dirname(os.path.realpath(__file__))
|
# For cx_freeze and sililar.
|
||||||
|
self.app_home = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
self.app_home = os.path.dirname(os.path.realpath(__file__))
|
||||||
App.log.debug("Application path is " + self.app_home)
|
App.log.debug("Application path is " + self.app_home)
|
||||||
App.log.debug("Started in " + os.getcwd())
|
App.log.debug("Started in " + os.getcwd())
|
||||||
os.chdir(self.app_home)
|
os.chdir(self.app_home)
|
||||||
@@ -180,7 +197,7 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
QtCore.QObject.__init__(self)
|
QtCore.QObject.__init__(self)
|
||||||
|
|
||||||
self.ui = FlatCAMGUI(self.version)
|
self.ui = FlatCAMGUI(self.version, name=self.version_name)
|
||||||
self.connect(self.ui,
|
self.connect(self.ui,
|
||||||
QtCore.SIGNAL("geomUpdate(int, int, int, int)"),
|
QtCore.SIGNAL("geomUpdate(int, int, int, int)"),
|
||||||
self.save_geometry)
|
self.save_geometry)
|
||||||
@@ -545,7 +562,11 @@ class App(QtCore.QObject):
|
|||||||
self.shell.setWindowIcon(self.ui.app_icon)
|
self.shell.setWindowIcon(self.ui.app_icon)
|
||||||
self.shell.setWindowTitle("FlatCAM Shell")
|
self.shell.setWindowTitle("FlatCAM Shell")
|
||||||
self.shell.resize(*self.defaults["shell_shape"])
|
self.shell.resize(*self.defaults["shell_shape"])
|
||||||
self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version)
|
self.shell.append_output("FlatCAM {}".format(self.version))
|
||||||
|
if self.version_name:
|
||||||
|
self.shell.append_output(" - {}".format(self.version_name))
|
||||||
|
self.shell.append_output("\n(c) 2014-{} Juan Pablo Caram\n\n".format(
|
||||||
|
self.version_date[0]))
|
||||||
self.shell.append_output("Type help to get started.\n\n")
|
self.shell.append_output("Type help to get started.\n\n")
|
||||||
|
|
||||||
self.init_tcl()
|
self.init_tcl()
|
||||||
@@ -579,7 +600,7 @@ class App(QtCore.QObject):
|
|||||||
App.log.debug("END of constructor. Releasing control.")
|
App.log.debug("END of constructor. Releasing control.")
|
||||||
|
|
||||||
def init_tcl(self):
|
def init_tcl(self):
|
||||||
if hasattr(self,'tcl'):
|
if hasattr(self, 'tcl'):
|
||||||
# self.tcl = None
|
# self.tcl = None
|
||||||
# TODO we need to clean non default variables and procedures here
|
# TODO we need to clean non default variables and procedures here
|
||||||
# new object cannot be used here as it will not remember values created for next passes,
|
# new object cannot be used here as it will not remember values created for next passes,
|
||||||
@@ -789,28 +810,31 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
def exec_command_test(self, text, reraise=True):
|
def exec_command_test(self, text, reraise=True):
|
||||||
"""
|
"""
|
||||||
|
Same as exec_command(...) with additional control over exceptions.
|
||||||
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
||||||
|
|
||||||
:param text: Input command
|
:param text: Input command
|
||||||
:param reraise: raise exception and not hide it, used mainly in unittests
|
:param reraise: Re-raise TclError exceptions in Python (mostly for unitttests).
|
||||||
:return: output if there was any
|
:return: Output from the command
|
||||||
"""
|
"""
|
||||||
|
|
||||||
text = str(text)
|
text = str(text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.shell.open_proccessing()
|
self.shell.open_proccessing() # Disables input box.
|
||||||
result = self.tcl.eval(str(text))
|
result = self.tcl.eval(str(text))
|
||||||
if result != 'None':
|
if result != 'None':
|
||||||
self.shell.append_output(result + '\n')
|
self.shell.append_output(result + '\n')
|
||||||
|
|
||||||
except Tkinter.TclError, e:
|
except Tkinter.TclError, e:
|
||||||
#this will display more precise answer if something in TCL shell fail
|
# This will display more precise answer if something in TCL shell fails
|
||||||
result = self.tcl.eval("set errorInfo")
|
result = self.tcl.eval("set errorInfo")
|
||||||
self.log.error("Exec command Exception: %s" % (result + '\n'))
|
self.log.error("Exec command Exception: %s" % (result + '\n'))
|
||||||
self.shell.append_error('ERROR: ' + result + '\n')
|
self.shell.append_error('ERROR: ' + result + '\n')
|
||||||
#show error in console and just return or in test raise exception
|
# Show error in console and just return or in test raise exception
|
||||||
if reraise:
|
if reraise:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.shell.close_proccessing()
|
self.shell.close_proccessing()
|
||||||
pass
|
pass
|
||||||
@@ -1056,7 +1080,8 @@ class App(QtCore.QObject):
|
|||||||
self.report_usage("on_about")
|
self.report_usage("on_about")
|
||||||
|
|
||||||
version = self.version
|
version = self.version
|
||||||
version_date = self.version_date
|
version_date_str = self.version_date_str
|
||||||
|
version_year = self.version_date[0]
|
||||||
|
|
||||||
class AboutDialog(QtGui.QDialog):
|
class AboutDialog(QtGui.QDialog):
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@@ -1078,12 +1103,16 @@ class App(QtCore.QObject):
|
|||||||
|
|
||||||
title = QtGui.QLabel(
|
title = QtGui.QLabel(
|
||||||
"<font size=8><B>FlatCAM</B></font><BR>"
|
"<font size=8><B>FlatCAM</B></font><BR>"
|
||||||
"Version %s (%s)<BR>"
|
"Version {} ({})<BR>"
|
||||||
"<BR>"
|
"<BR>"
|
||||||
"2D Computer-Aided Printed Circuit Board<BR>"
|
"2D Computer-Aided Printed Circuit Board<BR>"
|
||||||
"Manufacturing.<BR>"
|
"Manufacturing.<BR>"
|
||||||
"<BR>"
|
"<BR>"
|
||||||
"(c) 2014-2015 Juan Pablo Caram" % (version, version_date)
|
"(c) 2014-{} Juan Pablo Caram".format(
|
||||||
|
version,
|
||||||
|
version_date_str,
|
||||||
|
version_year
|
||||||
|
)
|
||||||
)
|
)
|
||||||
layout2.addWidget(title, stretch=1)
|
layout2.addWidget(title, stretch=1)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||||||
# Emitted when persistent window geometry needs to be retained
|
# Emitted when persistent window geometry needs to be retained
|
||||||
geom_update = QtCore.pyqtSignal(int, int, int, int, name='geomUpdate')
|
geom_update = QtCore.pyqtSignal(int, int, int, int, name='geomUpdate')
|
||||||
|
|
||||||
def __init__(self, version):
|
def __init__(self, version, name=None):
|
||||||
super(FlatCAMGUI, self).__init__()
|
super(FlatCAMGUI, self).__init__()
|
||||||
|
|
||||||
# Divine icon pack by Ipapun @ finicons.com
|
# Divine icon pack by Ipapun @ finicons.com
|
||||||
@@ -248,7 +248,10 @@ class FlatCAMGUI(QtGui.QMainWindow):
|
|||||||
self.setWindowIcon(self.app_icon)
|
self.setWindowIcon(self.app_icon)
|
||||||
|
|
||||||
self.setGeometry(100, 100, 1024, 650)
|
self.setGeometry(100, 100, 1024, 650)
|
||||||
self.setWindowTitle('FlatCAM %s - Development Version' % version)
|
title = 'FlatCAM {}'.format(version)
|
||||||
|
if name is not None:
|
||||||
|
title += ' - {}'.format(name)
|
||||||
|
self.setWindowTitle(title)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class FlatCAMObj(QtCore.QObject):
|
|||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
"""
|
"""
|
||||||
|
Constructor.
|
||||||
|
|
||||||
:param name: Name of the object given by the user.
|
:param name: Name of the object given by the user.
|
||||||
:return: FlatCAMObj
|
:return: FlatCAMObj
|
||||||
@@ -57,6 +58,9 @@ class FlatCAMObj(QtCore.QObject):
|
|||||||
``self.options`` is only updated, not overwritten. This ensures that
|
``self.options`` is only updated, not overwritten. This ensures that
|
||||||
options set by the app do not vanish when reading the objects
|
options set by the app do not vanish when reading the objects
|
||||||
from a project file.
|
from a project file.
|
||||||
|
|
||||||
|
:param d: Dictionary with attributes to set.
|
||||||
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for attr in self.ser_attrs:
|
for attr in self.ser_attrs:
|
||||||
|
|||||||
26
FlatCAMVersion.py
Normal file
26
FlatCAMVersion.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#################################################
|
||||||
|
# FlatCAM - Version settings #
|
||||||
|
#################################################
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
version = {
|
||||||
|
"number": 8.5,
|
||||||
|
"date": (2016, 7, 1), # Year, Month, Day
|
||||||
|
"name": None,
|
||||||
|
"release": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.version = version["number"]
|
||||||
|
app.version_date = version["date"]
|
||||||
|
if version["release"]:
|
||||||
|
app.log.setLevel(logging.WARNING)
|
||||||
|
else:
|
||||||
|
app.log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
if version["name"] is None and version["release"] == False:
|
||||||
|
app.version_name = "Development Version"
|
||||||
|
else:
|
||||||
|
app.version_name = version["name"]
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014 Juan Pablo Caram
|
Copyright (c) 2014-2016 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:
|
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:
|
||||||
|
|
||||||
|
|||||||
0
sandbox/prepare_release.py
Normal file
0
sandbox/prepare_release.py
Normal file
@@ -43,6 +43,7 @@ class TclShellTest(unittest.TestCase):
|
|||||||
|
|
||||||
cls.setup = True
|
cls.setup = True
|
||||||
cls.app = QtGui.QApplication(sys.argv)
|
cls.app = QtGui.QApplication(sys.argv)
|
||||||
|
|
||||||
# Create App, keep app defaults (do not load
|
# Create App, keep app defaults (do not load
|
||||||
# user-defined defaults).
|
# user-defined defaults).
|
||||||
cls.fc = App(user_defaults=False)
|
cls.fc = App(user_defaults=False)
|
||||||
@@ -54,6 +55,7 @@ class TclShellTest(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
|
||||||
cls.fc.tcl = None
|
cls.fc.tcl = None
|
||||||
cls.app.closeAllWindows()
|
cls.app.closeAllWindows()
|
||||||
del cls.fc
|
del cls.fc
|
||||||
@@ -61,46 +63,106 @@ class TclShellTest(unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def test_set_get_units(self):
|
def test_set_get_units(self):
|
||||||
|
"""
|
||||||
|
Tests setting and getting units via the ``set_sys`` command,
|
||||||
|
and persistance after ``new`` command.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# MM
|
||||||
self.fc.exec_command_test('set_sys units MM')
|
self.fc.exec_command_test('set_sys units MM')
|
||||||
self.fc.exec_command_test('new')
|
self.fc.exec_command_test('new')
|
||||||
|
|
||||||
|
# IN
|
||||||
self.fc.exec_command_test('set_sys units IN')
|
self.fc.exec_command_test('set_sys units IN')
|
||||||
self.fc.exec_command_test('new')
|
self.fc.exec_command_test('new')
|
||||||
units=self.fc.exec_command_test('get_sys units')
|
|
||||||
|
#----------------------------------------
|
||||||
|
# Units must be IN
|
||||||
|
#----------------------------------------
|
||||||
|
units = self.fc.exec_command_test('get_sys units')
|
||||||
self.assertEquals(units, "IN")
|
self.assertEquals(units, "IN")
|
||||||
|
|
||||||
|
# MM
|
||||||
self.fc.exec_command_test('set_sys units MM')
|
self.fc.exec_command_test('set_sys units MM')
|
||||||
self.fc.exec_command_test('new')
|
self.fc.exec_command_test('new')
|
||||||
units=self.fc.exec_command_test('get_sys units')
|
|
||||||
|
#----------------------------------------
|
||||||
|
# Units must be MM
|
||||||
|
#----------------------------------------
|
||||||
|
units = self.fc.exec_command_test('get_sys units')
|
||||||
self.assertEquals(units, "MM")
|
self.assertEquals(units, "MM")
|
||||||
|
|
||||||
def test_gerber_flow(self):
|
def test_gerber_flow(self):
|
||||||
|
"""
|
||||||
|
Typical workflow from Gerber to GCode.
|
||||||
|
|
||||||
# open gerber files top, bottom and cutout
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name))
|
gbr_cmd = 'open_gerber {path}/{filename} -outname {outname}'
|
||||||
|
|
||||||
|
#-----------------------------------------
|
||||||
|
# Open top layer and check for object type
|
||||||
|
#-----------------------------------------
|
||||||
|
cmd = gbr_cmd.format(
|
||||||
|
path=self.gerber_files,
|
||||||
|
filename=self.copper_top_filename,
|
||||||
|
outname=self.gerber_top_name)
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
|
gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name)
|
||||||
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
|
self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber),
|
||||||
"Expected FlatCAMGerber, instead, %s is %s" %
|
"Expected FlatCAMGerber, instead, %s is %s" %
|
||||||
(self.gerber_top_name, type(gerber_top_obj)))
|
(self.gerber_top_name, type(gerber_top_obj)))
|
||||||
|
|
||||||
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name))
|
#--------------------------------------------
|
||||||
|
# Open bottom layer and check for object type
|
||||||
|
#--------------------------------------------
|
||||||
|
cmd = gbr_cmd.format(
|
||||||
|
path=self.gerber_files,
|
||||||
|
filename=self.copper_bottom_filename,
|
||||||
|
outname=self.gerber_bottom_name)
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
|
gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name)
|
||||||
self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber),
|
self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber),
|
||||||
"Expected FlatCAMGerber, instead, %s is %s" %
|
"Expected FlatCAMGerber, instead, %s is %s" %
|
||||||
(self.gerber_bottom_name, type(gerber_bottom_obj)))
|
(self.gerber_bottom_name, type(gerber_bottom_obj)))
|
||||||
|
|
||||||
self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name))
|
#--------------------------------------------
|
||||||
|
# Open cutout layer and check for object type
|
||||||
|
#--------------------------------------------
|
||||||
|
cmd = gbr_cmd.format(
|
||||||
|
path=self.gerber_files,
|
||||||
|
filename=self.cutout_filename,
|
||||||
|
outname=self.gerber_cutout_name
|
||||||
|
)
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
|
gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name)
|
||||||
self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber),
|
self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber),
|
||||||
"Expected FlatCAMGerber, instead, %s is %s" %
|
"Expected FlatCAMGerber, instead, %s is %s" %
|
||||||
(self.gerber_cutout_name, type(gerber_cutout_obj)))
|
(self.gerber_cutout_name, type(gerber_cutout_obj)))
|
||||||
|
|
||||||
# exteriors delete and join geometries for top layer
|
# exteriors delete and join geometries for top layer
|
||||||
self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter))
|
cmd = 'isolate {objname} -dia {dia}'.format(
|
||||||
self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior'))
|
objname=self.gerber_cutout_name,
|
||||||
self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso'))
|
dia=self.engraver_diameter)
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
|
|
||||||
|
cmd = 'exteriors {objname} -outname {outname}'.format(
|
||||||
|
objname=self.gerber_cutout_name + '_iso',
|
||||||
|
outname=self.gerber_cutout_name + '_iso_exterior')
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
|
|
||||||
|
cmd = 'delete {objname}'.format(
|
||||||
|
objname=self.gerber_cutout_name + '_iso')
|
||||||
|
self.fc.exec_command_test(cmd)
|
||||||
|
|
||||||
|
# TODO: Check deleteb object is gone.
|
||||||
|
|
||||||
|
#--------------------------------------------
|
||||||
|
# Exteriors of cutout layer, check type
|
||||||
|
#--------------------------------------------
|
||||||
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
|
obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior')
|
||||||
self.assertTrue(isinstance(obj, FlatCAMGeometry),
|
self.assertTrue(isinstance(obj, FlatCAMGeometry),
|
||||||
"Expected FlatCAMGeometry, instead, %s is %s" %
|
"Expected FlatCAMGeometry, instead, %s is %s" %
|
||||||
|
|||||||
Reference in New Issue
Block a user