summaryrefslogtreecommitdiffstats
path: root/share/extensions/other/gcodetools
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/other/gcodetools')
-rw-r--r--share/extensions/other/gcodetools/.darglint3
-rw-r--r--share/extensions/other/gcodetools/.pylintrc374
-rw-r--r--share/extensions/other/gcodetools/LICENSE.txt340
-rw-r--r--share/extensions/other/gcodetools/MANIFEST.in2
-rw-r--r--share/extensions/other/gcodetools/README.md45
-rw-r--r--share/extensions/other/gcodetools/TESTING.md104
-rwxr-xr-xshare/extensions/other/gcodetools/gcodetools.py5930
-rw-r--r--share/extensions/other/gcodetools/gcodetools_about.inx52
-rw-r--r--share/extensions/other/gcodetools/gcodetools_area.inx133
-rw-r--r--share/extensions/other/gcodetools/gcodetools_dxf_points.inx79
-rw-r--r--share/extensions/other/gcodetools/gcodetools_engraving.inx91
-rw-r--r--share/extensions/other/gcodetools/gcodetools_graffiti.inx120
-rw-r--r--share/extensions/other/gcodetools/gcodetools_lathe.inx113
-rw-r--r--share/extensions/other/gcodetools/gcodetools_orientation_points.inx57
-rw-r--r--share/extensions/other/gcodetools/gcodetools_path_to_gcode.inx93
-rw-r--r--share/extensions/other/gcodetools/gcodetools_prepare_path_for_plasma.inx59
-rw-r--r--share/extensions/other/gcodetools/gcodetools_tools_library.inx62
-rwxr-xr-xshare/extensions/other/gcodetools/genpofiles.sh7
-rw-r--r--share/extensions/other/gcodetools/setup.cfg2
-rw-r--r--share/extensions/other/gcodetools/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out41
-rw-r--r--share/extensions/other/gcodetools/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out30
-rw-r--r--share/extensions/other/gcodetools/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out40
-rw-r--r--share/extensions/other/gcodetools/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out0
-rw-r--r--share/extensions/other/gcodetools/tests/data/svg/default-inkscape-SVG.svg37
-rw-r--r--share/extensions/other/gcodetools/tests/data/svg/shapes.svg284
-rw-r--r--share/extensions/other/gcodetools/tests/dev_requirements.txt11
-rw-r--r--share/extensions/other/gcodetools/tests/test_gcodetools.py65
-rw-r--r--share/extensions/other/gcodetools/tests/test_inkex_inx.py114
-rw-r--r--share/extensions/other/gcodetools/tox.ini14
29 files changed, 8302 insertions, 0 deletions
diff --git a/share/extensions/other/gcodetools/.darglint b/share/extensions/other/gcodetools/.darglint
new file mode 100644
index 0000000..4447949
--- /dev/null
+++ b/share/extensions/other/gcodetools/.darglint
@@ -0,0 +1,3 @@
+[darglint]
+docstring_style=google
+strictness=short \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/.pylintrc b/share/extensions/other/gcodetools/.pylintrc
new file mode 100644
index 0000000..a3903f9
--- /dev/null
+++ b/share/extensions/other/gcodetools/.pylintrc
@@ -0,0 +1,374 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+init-hook='import sys; sys.path.append("pythonenv/lib/python3.6/site-packages"); sys.path.append("../pythonenv/lib/python3.6/site-packages"); sys.path.append("../../pythonenv/lib/python3.6/site-packages"); sys.path.append("../../../pythonenv/lib/python3.6/site-packages");'
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=lxml,numpy
+
+# Allow optimization of some AST trees. This will activate a peephole AST
+# optimizer, which will apply various small optimizations. For instance, it can
+# be used to obtain the result of joining multiple strings with the addition
+# operator. Joining a lot of strings can lead to a maximum recursion error in
+# Pylint and this flag can prevent that. It has one side effect, the resulting
+# AST will be different than the one from reality.
+optimize-ast=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,superfluous-parens,missing-super-argument,model-missing-unicode
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[BASIC]
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,input
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,x,y,z,ex,Run,_,__,js,rx,x1,y1,x2,y2
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Regular expression matching correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for function names
+function-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for variable names
+variable-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for attribute names
+attr-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for argument names
+argument-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for method names
+method-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+
+[ELIF]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set). This supports can work
+# with qualified names.
+ignored-classes=SQLObject,WSGIRequest
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_$|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/share/extensions/other/gcodetools/LICENSE.txt b/share/extensions/other/gcodetools/LICENSE.txt
new file mode 100644
index 0000000..b83f24b
--- /dev/null
+++ b/share/extensions/other/gcodetools/LICENSE.txt
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/share/extensions/other/gcodetools/MANIFEST.in b/share/extensions/other/gcodetools/MANIFEST.in
new file mode 100644
index 0000000..2f29cde
--- /dev/null
+++ b/share/extensions/other/gcodetools/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.py
+include *.inx
diff --git a/share/extensions/other/gcodetools/README.md b/share/extensions/other/gcodetools/README.md
new file mode 100644
index 0000000..faf25c9
--- /dev/null
+++ b/share/extensions/other/gcodetools/README.md
@@ -0,0 +1,45 @@
+# Gcodetools
+
+This folder contains gcodetools extension. They require the Inkscape extensions API inkex, see https://gitlab.com/inkscape/extensions.
+
+## Installation
+
+These scripts should be installed with an Inkscape package already (if you have
+installed Inkscape). For packagers or people testing newer releases, you can
+install the *.inx and *.py files into /usr/share/inkscape/extensions or
+~/.config/inkscape/extensions .
+
+## Testing
+
+These extensions are designed to have good test coverage for python 3.6 and above.
+
+You must install the program `pytest` in order to run these tests. You may run all tests by omitting any other parameters or select tests by adding the test filename that you want to run.
+
+ pytest
+ pytest tests/test_gcodetools.py
+
+See TESTING.md for further details.
+
+## Extension description
+
+Each *.inx file describes an extension, listing its name, purpose,
+prerequisites, location within the menu, etc. These files are read by
+Inkscape on launch. Other files are the scripts themselves (Perl,
+Python, and Ruby are supported, as well as shell scripts).
+
+## Development
+
+Development of both the core inkex modules, tests and each of the extensions
+contained within the core inkscape extensions repository should follow these
+basic rules of quality assurance:
+
+ * Use python3.6 or later, no python2 code would be used here.
+ * Use pylint to ensure code is written consistantly
+ * Have tests so that each line of an extension is covered in the coverage report
+ * Not cross streams between extensions, so your extension should import from
+ a module and not from another extension.
+ * Use translations on text for display to users using get text.
+ * Should not require external programs to work (with some exceptions)
+
+Also join the community on chat.inkscape.org channel #inkscape_extensions with any
+doubts or problems. \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/TESTING.md b/share/extensions/other/gcodetools/TESTING.md
new file mode 100644
index 0000000..07158a6
--- /dev/null
+++ b/share/extensions/other/gcodetools/TESTING.md
@@ -0,0 +1,104 @@
+# Why Test Extensions
+
+Previously, Inkscape extensions were not tested for quality or correctness. But since 1.0, the extensions repository is far more strict about requiring tests and requiring tests to pass before changes can be merged in.
+
+You may find yourself being frustrated by the tests, especially if at first it doesn't make sense why they are failing. But these tests are important and I ask that everyone be as kind as they can to make sure the quality of the repository is maintained.
+
+# Running Tests
+
+You must install the program `pytest` in order to run these tests. Both Pytest and Pytest-Coverage are required to run tests.
+
+Usually the best way to install it is:
+
+```shell
+$ pip3 install pytest pytest-cov
+```
+
+You may run all tests by omitting any other parameters or select tests by adding the test filename that you want to run.
+
+```shell
+$ pytest
+$ pytest tests/test_my_extension.py
+```
+
+You can also run tests until the first time they fail, and ask pytest to run the previously failed tests first. This can cut down how long pytest takes to run before hitting a failure.
+
+```shell
+$ pytest -x --ff
+```
+
+More info here: https://docs.pytest.org/en/latest/getting-started.html
+
+# Test Files
+
+Each extension should have its own test file in the tests directory. This test may be a series of function tests or "comparison" tests. The comparison tests will fail whenever the output of an extension changes, so often they will need to be updated to reflect your changes.
+
+Usually the test file will be named `tests/test_{name_of_extension}.py` using the same name as the extension file itself. For tests covering inkex and other modules you may find test files have the format `tests/test_{package}_{module}.py` or similar.
+
+Each test can be run independently as shown in the previous section.
+
+# Test Data
+
+As well as python test files, each test will normally depend on additional data. From source svg files, to output comparison tests and other such things.
+
+This data is always held in `tests/data`, when writing tests, please make sure your data goes into the right directory. If you are updating the comparison test, usually you just need to rename the `export` file generated and remove the `.export` suffix to enable it.
+
+See tests/data/README.md for further information.
+
+# Writing or Updating tests
+
+You need to read the documentation available inside the tester module to learn how to write tests, or what the test code means. From a python3 terminal type:
+
+```python
+from inkex import tester
+help(tester)
+```
+
+# Coverage
+
+Coverage reports tell us how much of an extension is being exercised when tests are run.
+
+The latest coverage report for master branch can be found at
+https://inkscape.gitlab.io/extensions/coverage/.
+
+To run a complete coverage report, you can specify the `--cov=.` option like so:
+
+```shell
+$ pytest --cov=. --cov-report term
+```
+
+For a single extension coverage report, you can limit it further with:
+
+```shell
+$ pytest --cov=my_extension.py --cov-report term
+```
+
+## Testing Options
+
+Tests can be run with these options that are provided as environment variables:
+
+ FAIL_ON_DEPRECATION=1 - Will instantly fail any use of deprecated APIs
+ EXPORT_COMPARE=1 - Generate output files from comparisons. This is useful for manually checking the output as well as updating the comparison data.
+ NO_MOCK_COMMANDS=1 - Instead of using the mock data, actually call commands. This will also generate the msg files similar to export compare.
+ INKSCAPE_COMMAND=/other/inkscape - Use a different Inkscape (for example development version) while running commands. Works outside of tests too.
+ XML_DIFF=1 - Attempt to output an XML diff file, this can be useful for debugging to see differences in context.
+ DEBUG_KEY=1 - Export mock file keys for debugging. This is a highly specialised option for debugging key generation.
+
+For example:
+
+```shell
+$ EXPORT_COMPARE=1 pytest
+```
+
+or
+
+```shell
+export EXPORT_COMPARE=1
+pytest
+```
+
+# Testing custom extensions
+
+The same testing framework can be used in your own extension repositories by requiring the inkex module and using the inkex.tester module set. It should be available with Inkscape or can be installed via pypi.
+
+This is a great way of ensuring you have access to the same tools Inkscape uses to test, and makes it easier for your external extension to make its way to the core repository without resistance.
diff --git a/share/extensions/other/gcodetools/gcodetools.py b/share/extensions/other/gcodetools/gcodetools.py
new file mode 100755
index 0000000..ef8bdd2
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools.py
@@ -0,0 +1,5930 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2005 Aaron Spike, aaron@ekips.org (super paths et al)
+# 2007 hugomatic... (gcode.py)
+# 2009 Nick Drobchenko, nick@cnc-club.ru (main developer)
+# 2011 Chris Lusby Taylor, clusbytaylor@enterprise.net (engraving functions)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+Comments starting "#LT" or "#CLT" are by Chris Lusby Taylor who rewrote the engraving function in 2011.
+History of CLT changes to engraving and other functions it uses:
+9 May 2011 Changed test of tool diameter to square it
+10 May Note that there are many unused functions, including:
+ bound_to_bound_distance, csp_curvature_radius_at_t,
+ csp_special_points, csplength, rebuild_csp, csp_slope,
+ csp_simple_bound_to_point_distance, csp_bound_to_point_distance,
+ bez_at_t, bez_to_point_distance, bez_normalized_slope, matrix_mul, transpose
+ Fixed csp_point_inside_bound() to work if x outside bounds
+20 May Now encoding the bisectors of angles.
+23 May Using r/cos(a) instead of normalised normals for bisectors of angles.
+23 May Note that Z values generated for engraving are in pixels, not mm.
+ Removed the biarc curves - straight lines are better.
+24 May Changed Bezier slope calculation to be less sensitive to tiny differences in points.
+ Added use of self.options.engraving_newton_iterations to control accuracy
+25 May Big restructure and new recursive function.
+ Changed the way I treat corners - I now find if the centre of a proposed circle is
+ within the area bounded by the line being tested and the two angle bisectors at
+ its ends. See get_radius_to_line().
+29 May Eliminating redundant points. If A,B,C colinear, drop B
+30 May Eliminating redundant lines in divided Beziers. Changed subdivision of lines
+ 7Jun Try to show engraving in 3D
+ 8 Jun Displaying in stereo 3D.
+ Fixed a bug in bisect - it could go wrong due to rounding errors if
+ 1+x1.x2+y1.y2<0 which should never happen. BTW, I spotted a non-normalised normal
+ returned by csp_normalized_normal. Need to check for that.
+ 9 Jun Corrected spelling of 'definition' but still match previous 'defention' and 'defenition' if found in file
+ Changed get_tool to find 1.6.04 tools or new tools with corrected spelling
+10 Jun Put 3D into a separate layer called 3D, created unless it already exists
+ Changed csp_normalized_slope to reject lines shorter than 1e-9.
+10 Jun Changed all dimensions seen by user to be mm/inch, not pixels. This includes
+ tool diameter, maximum engraving distance, tool shape and all Z values.
+12 Jun ver 208 Now scales correctly if orientation points moved or stretched.
+12 Jun ver 209. Now detect if engraving toolshape not a function of radius
+ Graphics now indicate Gcode toolpath, limited by min(tool diameter/2,max-dist)
+24 Jan 2017 Removed hard-coded scale values from orientation point calculation
+TODO Change line division to be recursive, depending on what line is touched. See line_divide
+"""
+
+__version__ = '1.7'
+
+import cmath
+import copy
+import math
+import os
+import re
+import sys
+import time
+from functools import partial
+
+import numpy
+
+import inkex
+from inkex.bezier import bezierlength, bezierparameterize, beziertatlength
+from inkex import Transform, PathElement, TextElement, Tspan, Group, Layer, Marker, CubicSuperPath, Style
+
+if sys.version_info[0] > 2:
+ xrange = range
+ unicode = str
+
+def ireplace(self, old, new, count=0):
+ pattern = re.compile(re.escape(old), re.I)
+ return re.sub(pattern, new, self, count)
+
+
+################################################################################
+#
+# Styles and additional parameters
+#
+################################################################################
+
+TAU = math.pi * 2
+STRAIGHT_TOLERANCE = 0.0001
+STRAIGHT_DISTANCE_TOLERANCE = 0.0001
+ENGRAVING_TOLERANCE = 0.0001
+LOFT_LENGTHS_TOLERANCE = 0.0000001
+
+EMC_TOLERANCE_EQUAL = 0.00001
+
+options = {}
+defaults = {
+ 'header': """%
+(Header)
+(Generated by gcodetools from Inkscape.)
+(Using default header. To add your own header create file "header" in the output dir.)
+M3
+(Header end.)
+""",
+ 'footer': """
+(Footer)
+M5
+G00 X0.0000 Y0.0000
+M2
+(Using default footer. To add your own footer create file "footer" in the output dir.)
+(end)
+%"""
+}
+
+INTERSECTION_RECURSION_DEPTH = 10
+INTERSECTION_TOLERANCE = 0.00001
+
+def marker_style(stroke, marker='DrawCurveMarker', width=1):
+ """Set a marker style with some basic defaults"""
+ return Style(stroke=stroke, fill='none', stroke_width=width,
+ marker_end='url(#{})'.format(marker))
+
+MARKER_STYLE = {
+ "in_out_path_style": marker_style('#0072a7', 'InOutPathMarker'),
+ "loft_style": {
+ 'main curve': marker_style('#88f', 'Arrow2Mend'),
+ },
+ "biarc_style": {
+ 'biarc0': marker_style('#88f'),
+ 'biarc1': marker_style('#8f8'),
+ 'line': marker_style('#f88'),
+ 'area': marker_style('#777', width=0.1),
+ },
+ "biarc_style_dark": {
+ 'biarc0': marker_style('#33a'),
+ 'biarc1': marker_style('#3a3'),
+ 'line': marker_style('#a33'),
+ 'area': marker_style('#222', width=0.3),
+ },
+ "biarc_style_dark_area": {
+ 'biarc0': marker_style('#33a', width=0.1),
+ 'biarc1': marker_style('#3a3', width=0.1),
+ 'line': marker_style('#a33', width=0.1),
+ 'area': marker_style('#222', width=0.3),
+ },
+ "biarc_style_i": {
+ 'biarc0': marker_style('#880'),
+ 'biarc1': marker_style('#808'),
+ 'line': marker_style('#088'),
+ 'area': marker_style('#999', width=0.3),
+ },
+ "biarc_style_dark_i": {
+ 'biarc0': marker_style('#dd5'),
+ 'biarc1': marker_style('#d5d'),
+ 'line': marker_style('#5dd'),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_feed": {
+ 'biarc0': marker_style('#07f', width=0.4),
+ 'biarc1': marker_style('#0f7', width=0.4),
+ 'line': marker_style('#f44', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_passing feed": {
+ 'biarc0': marker_style('#07f', width=0.4),
+ 'biarc1': marker_style('#0f7', width=0.4),
+ 'line': marker_style('#f44', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_fine feed": {
+ 'biarc0': marker_style('#7f0', width=0.4),
+ 'biarc1': marker_style('#f70', width=0.4),
+ 'line': marker_style('#744', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "area artefact": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
+ "area artefact arrow": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
+ "dxf_points": Style(stroke="#ff0000", fill="#ff0000"),
+}
+
+
+################################################################################
+# Gcode additional functions
+################################################################################
+
+def gcode_comment_str(s, replace_new_line=False):
+ if replace_new_line:
+ s = re.sub(r"[\n\r]+", ".", s)
+ res = ""
+ if s[-1] == "\n":
+ s = s[:-1]
+ for a in s.split("\n"):
+ if a != "":
+ res += "(" + re.sub(r"[\(\)\\\n\r]", ".", a) + ")\n"
+ else:
+ res += "\n"
+ return res
+
+
+################################################################################
+# Cubic Super Path additional functions
+################################################################################
+
+
+def csp_from_polyline(line):
+ return [[[point[:] for _ in range(3)] for point in subline] for subline in line]
+
+
+def csp_remove_zero_segments(csp, tolerance=1e-7):
+ res = []
+ for subpath in csp:
+ if len(subpath) > 0:
+ res.append([subpath[0]])
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ if point_to_point_d2(sp1[1], sp2[1]) <= tolerance and point_to_point_d2(sp1[2], sp2[1]) <= tolerance and point_to_point_d2(sp1[1], sp2[0]) <= tolerance:
+ res[-1][-1][2] = sp2[2]
+ else:
+ res[-1].append(sp2)
+ return res
+
+
+def point_inside_csp(p, csp, on_the_path=True):
+ # we'll do the raytracing and see how many intersections are there on the ray's way.
+ # if number of intersections is even then point is outside.
+ # ray will be x=p.x and y=>p.y
+ # you can assign any value to on_the_path, by default if point is on the path
+ # function will return thai it's inside the path.
+ x, y = p
+ ray_intersections_count = 0
+ for subpath in csp:
+
+ for i in range(1, len(subpath)):
+ sp1 = subpath[i - 1]
+ sp2 = subpath[i]
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ if ax == 0 and bx == 0 and cx == 0 and dx == x:
+ # we've got a special case here
+ b = csp_true_bounds([[sp1, sp2]])
+ if b[1][1] <= y <= b[3][1]:
+ # points is on the path
+ return on_the_path
+ else:
+ # we can skip this segment because it won't influence the answer.
+ pass
+ else:
+ for t in csp_line_intersection([x, y], [x, y + 5], sp1, sp2):
+ if t == 0 or t == 1:
+ # we've got another special case here
+ x1, y1 = csp_at_t(sp1, sp2, t)
+ if y1 == y:
+ # the point is on the path
+ return on_the_path
+ # if t == 0 we should have considered this case previously.
+ if t == 1:
+ # we have to check the next segment if it is on the same side of the ray
+ st_d = csp_normalized_slope(sp1, sp2, 1)[0]
+ if st_d == 0:
+ st_d = csp_normalized_slope(sp1, sp2, 0.99)[0]
+
+ for j in range(1, len(subpath) + 1):
+ if (i + j) % len(subpath) == 0:
+ continue # skip the closing segment
+ sp11 = subpath[(i - 1 + j) % len(subpath)]
+ sp22 = subpath[(i + j) % len(subpath)]
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
+ if ax1 == 0 and bx1 == 0 and cx1 == 0 and dx1 == x:
+ continue # this segment parallel to the ray, so skip it
+ en_d = csp_normalized_slope(sp11, sp22, 0)[0]
+ if en_d == 0:
+ en_d = csp_normalized_slope(sp11, sp22, 0.01)[0]
+ if st_d * en_d <= 0:
+ ray_intersections_count += 1
+ break
+ else:
+ x1, y1 = csp_at_t(sp1, sp2, t)
+ if y1 == y:
+ # the point is on the path
+ return on_the_path
+ else:
+ if y1 > y and 3 * ax * t ** 2 + 2 * bx * t + cx != 0: # if it's 0 the path only touches the ray
+ ray_intersections_count += 1
+ return ray_intersections_count % 2 == 1
+
+
+def csp_close_all_subpaths(csp, tolerance=0.000001):
+ for i in range(len(csp)):
+ if point_to_point_d2(csp[i][0][1], csp[i][-1][1]) > tolerance ** 2:
+ csp[i][-1][2] = csp[i][-1][1][:]
+ csp[i] += [[csp[i][0][1][:] for _ in range(3)]]
+ else:
+ if csp[i][0][1] != csp[i][-1][1]:
+ csp[i][-1][1] = csp[i][0][1][:]
+ return csp
+
+
+def csp_simple_bound(csp):
+ minx = None
+ miny = None
+ maxx = None
+ maxy = None
+
+ for subpath in csp:
+ for sp in subpath:
+ for p in sp:
+ minx = min(minx, p[0]) if minx is not None else p[0]
+ miny = min(miny, p[1]) if miny is not None else p[1]
+ maxx = max(maxx, p[0]) if maxx is not None else p[0]
+ maxy = max(maxy, p[1]) if maxy is not None else p[1]
+ return minx, miny, maxx, maxy
+
+
+def csp_segment_to_bez(sp1, sp2):
+ return sp1[1:] + sp2[:2]
+
+
+def csp_to_point_distance(csp, p, dist_bounds=(0, 1e100)):
+ min_dist = [1e100, 0, 0, 0]
+ for j in range(len(csp)):
+ for i in range(1, len(csp[j])):
+ d = csp_seg_to_point_distance(csp[j][i - 1], csp[j][i], p, sample_points=5)
+ if d[0] < dist_bounds[0]:
+ return [d[0], j, i, d[1]]
+ else:
+ if d[0] < min_dist[0]:
+ min_dist = [d[0], j, i, d[1]]
+ return min_dist
+
+
+def csp_seg_to_point_distance(sp1, sp2, p, sample_points=5):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ dx = dx - p[0]
+ dy = dy - p[1]
+ if sample_points < 2:
+ sample_points = 2
+ d = min([(p[0] - sp1[1][0]) ** 2 + (p[1] - sp1[1][1]) ** 2, 0.], [(p[0] - sp2[1][0]) ** 2 + (p[1] - sp2[1][1]) ** 2, 1.])
+ for k in range(sample_points):
+ t = float(k) / (sample_points - 1)
+ i = 0
+ while i == 0 or abs(f) > 0.000001 and i < 20:
+ t2 = t ** 2
+ t3 = t ** 3
+ f = (ax * t3 + bx * t2 + cx * t + dx) * (3 * ax * t2 + 2 * bx * t + cx) + (ay * t3 + by * t2 + cy * t + dy) * (3 * ay * t2 + 2 * by * t + cy)
+ df = (6 * ax * t + 2 * bx) * (ax * t3 + bx * t2 + cx * t + dx) + (3 * ax * t2 + 2 * bx * t + cx) ** 2 + (6 * ay * t + 2 * by) * (ay * t3 + by * t2 + cy * t + dy) + (3 * ay * t2 + 2 * by * t + cy) ** 2
+ if df != 0:
+ t = t - f / df
+ else:
+ break
+ i += 1
+ if 0 <= t <= 1:
+ p1 = csp_at_t(sp1, sp2, t)
+ d1 = (p1[0] - p[0]) ** 2 + (p1[1] - p[1]) ** 2
+ if d1 < d[0]:
+ d = [d1, t]
+ return d
+
+
+def csp_seg_to_csp_seg_distance(sp1, sp2, sp3, sp4, dist_bounds=(0, 1e100), sample_points=5, tolerance=.01):
+ # check the ending points first
+ dist = csp_seg_to_point_distance(sp1, sp2, sp3[1], sample_points)
+ dist += [0.]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp1, sp2, sp4[1], sample_points)
+ if d[0] < dist[0]:
+ dist = d + [1.]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp3, sp4, sp1[1], sample_points)
+ if d[0] < dist[0]:
+ dist = [d[0], 0., d[1]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp3, sp4, sp2[1], sample_points)
+ if d[0] < dist[0]:
+ dist = [d[0], 1., d[1]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ sample_points -= 2
+ if sample_points < 1:
+ sample_points = 1
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
+ ax2, ay2, bx2, by2, cx2, cy2, dx2, dy2 = csp_parameterize(sp3, sp4)
+ # try to find closes points using Newtons method
+ for k in range(sample_points):
+ for j in range(sample_points):
+ t1 = float(k + 1) / (sample_points + 1)
+ t2 = float(j) / (sample_points + 1)
+
+ t12 = t1 * t1
+ t13 = t1 * t1 * t1
+ t22 = t2 * t2
+ t23 = t2 * t2 * t2
+ i = 0
+
+ F1 = [0, 0]
+ F2 = [[0, 0], [0, 0]]
+ F = 1e100
+ x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
+ y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
+ while i < 2 or abs(F - Flast) > tolerance and i < 30:
+ f1x = 3 * ax1 * t12 + 2 * bx1 * t1 + cx1
+ f1y = 3 * ay1 * t12 + 2 * by1 * t1 + cy1
+ f2x = 3 * ax2 * t22 + 2 * bx2 * t2 + cx2
+ f2y = 3 * ay2 * t22 + 2 * by2 * t2 + cy2
+ F1[0] = 2 * f1x * x + 2 * f1y * y
+ F1[1] = -2 * f2x * x - 2 * f2y * y
+ F2[0][0] = 2 * (6 * ax1 * t1 + 2 * bx1) * x + 2 * f1x * f1x + 2 * (6 * ay1 * t1 + 2 * by1) * y + 2 * f1y * f1y
+ F2[0][1] = -2 * f1x * f2x - 2 * f1y * f2y
+ F2[1][0] = -2 * f2x * f1x - 2 * f2y * f1y
+ F2[1][1] = -2 * (6 * ax2 * t2 + 2 * bx2) * x + 2 * f2x * f2x - 2 * (6 * ay2 * t2 + 2 * by2) * y + 2 * f2y * f2y
+ F2 = inv_2x2(F2)
+ if F2 is not None:
+ t1 -= (F2[0][0] * F1[0] + F2[0][1] * F1[1])
+ t2 -= (F2[1][0] * F1[0] + F2[1][1] * F1[1])
+ t12 = t1 * t1
+ t13 = t1 * t1 * t1
+ t22 = t2 * t2
+ t23 = t2 * t2 * t2
+ x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
+ y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
+ Flast = F
+ F = x * x + y * y
+ else:
+ break
+ i += 1
+ if F < dist[0] and 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ dist = [F, t1, t2]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ return dist
+
+
+def csp_to_csp_distance(csp1, csp2, dist_bounds=(0, 1e100), tolerance=.01):
+ dist = [1e100, 0, 0, 0, 0, 0, 0]
+ for i1 in range(len(csp1)):
+ for j1 in range(1, len(csp1[i1])):
+ for i2 in range(len(csp2)):
+ for j2 in range(1, len(csp2[i2])):
+ d = csp_seg_bound_to_csp_seg_bound_max_min_distance(csp1[i1][j1 - 1], csp1[i1][j1], csp2[i2][j2 - 1], csp2[i2][j2])
+ if d[0] >= dist_bounds[1]:
+ continue
+ if d[1] < dist_bounds[0]:
+ return [d[1], i1, j1, 1, i2, j2, 1]
+ d = csp_seg_to_csp_seg_distance(csp1[i1][j1 - 1], csp1[i1][j1], csp2[i2][j2 - 1], csp2[i2][j2], dist_bounds, tolerance=tolerance)
+ if d[0] < dist[0]:
+ dist = [d[0], i1, j1, d[1], i2, j2, d[2]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ if dist[0] >= dist_bounds[1]:
+ return dist
+ return dist
+
+
+def csp_split(sp1, sp2, t=.5):
+ [x1, y1] = sp1[1]
+ [x2, y2] = sp1[2]
+ [x3, y3] = sp2[0]
+ [x4, y4] = sp2[1]
+ x12 = x1 + (x2 - x1) * t
+ y12 = y1 + (y2 - y1) * t
+ x23 = x2 + (x3 - x2) * t
+ y23 = y2 + (y3 - y2) * t
+ x34 = x3 + (x4 - x3) * t
+ y34 = y3 + (y4 - y3) * t
+ x1223 = x12 + (x23 - x12) * t
+ y1223 = y12 + (y23 - y12) * t
+ x2334 = x23 + (x34 - x23) * t
+ y2334 = y23 + (y34 - y23) * t
+ x = x1223 + (x2334 - x1223) * t
+ y = y1223 + (y2334 - y1223) * t
+ return [sp1[0], sp1[1], [x12, y12]], [[x1223, y1223], [x, y], [x2334, y2334]], [[x34, y34], sp2[1], sp2[2]]
+
+
+def csp_true_bounds(csp):
+ # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t)
+ minx = [float("inf"), 0, 0, 0]
+ maxx = [float("-inf"), 0, 0, 0]
+ miny = [float("inf"), 0, 0, 0]
+ maxy = [float("-inf"), 0, 0, 0]
+ for i in range(len(csp)):
+ for j in range(1, len(csp[i])):
+ ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize((csp[i][j - 1][1], csp[i][j - 1][2], csp[i][j][0], csp[i][j][1]))
+ roots = cubic_solver(0, 3 * ax, 2 * bx, cx) + [0, 1]
+ for root in roots:
+ if type(root) is complex and abs(root.imag) < 1e-10:
+ root = root.real
+ if type(root) is not complex and 0 <= root <= 1:
+ y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
+ x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
+ maxx = max([x, y, i, j, root], maxx)
+ minx = min([x, y, i, j, root], minx)
+
+ roots = cubic_solver(0, 3 * ay, 2 * by, cy) + [0, 1]
+ for root in roots:
+ if type(root) is complex and root.imag == 0:
+ root = root.real
+ if type(root) is not complex and 0 <= root <= 1:
+ y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
+ x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
+ maxy = max([y, x, i, j, root], maxy)
+ miny = min([y, x, i, j, root], miny)
+ maxy[0], maxy[1] = maxy[1], maxy[0]
+ miny[0], miny[1] = miny[1], miny[0]
+
+ return minx, miny, maxx, maxy
+
+
+############################################################################
+# csp_segments_intersection(sp1,sp2,sp3,sp4)
+#
+# Returns array containing all intersections between two segments of cubic
+# super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"]
+# where ta, tb are values of t for the intersection point.
+############################################################################
+def csp_segments_intersection(sp1, sp2, sp3, sp4):
+ a = csp_segment_to_bez(sp1, sp2)
+ b = csp_segment_to_bez(sp3, sp4)
+
+ def polish_intersection(a, b, ta, tb, tolerance=INTERSECTION_TOLERANCE):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(a)
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = bezierparameterize(b)
+ i = 0
+ F = [.0, .0]
+ F1 = [[.0, .0], [.0, .0]]
+ while i == 0 or (abs(F[0]) ** 2 + abs(F[1]) ** 2 > tolerance and i < 10):
+ ta3 = ta ** 3
+ ta2 = ta ** 2
+ tb3 = tb ** 3
+ tb2 = tb ** 2
+ F[0] = ax * ta3 + bx * ta2 + cx * ta + dx - ax1 * tb3 - bx1 * tb2 - cx1 * tb - dx1
+ F[1] = ay * ta3 + by * ta2 + cy * ta + dy - ay1 * tb3 - by1 * tb2 - cy1 * tb - dy1
+ F1[0][0] = 3 * ax * ta2 + 2 * bx * ta + cx
+ F1[0][1] = -3 * ax1 * tb2 - 2 * bx1 * tb - cx1
+ F1[1][0] = 3 * ay * ta2 + 2 * by * ta + cy
+ F1[1][1] = -3 * ay1 * tb2 - 2 * by1 * tb - cy1
+ det = F1[0][0] * F1[1][1] - F1[0][1] * F1[1][0]
+ if det != 0:
+ F1 = [[F1[1][1] / det, -F1[0][1] / det], [-F1[1][0] / det, F1[0][0] / det]]
+ ta = ta - (F1[0][0] * F[0] + F1[0][1] * F[1])
+ tb = tb - (F1[1][0] * F[0] + F1[1][1] * F[1])
+ else:
+ break
+ i += 1
+
+ return ta, tb
+
+ def recursion(a, b, ta0, ta1, tb0, tb1, depth_a, depth_b):
+ global bezier_intersection_recursive_result
+ if a == b:
+ bezier_intersection_recursive_result += [[ta0, tb0, ta1, tb1, "Overlap"]]
+ return
+ tam = (ta0 + ta1) / 2
+ tbm = (tb0 + tb1) / 2
+ if depth_a > 0 and depth_b > 0:
+ a1, a2 = bez_split(a, 0.5)
+ b1, b2 = bez_split(b, 0.5)
+ if bez_bounds_intersect(a1, b1):
+ recursion(a1, b1, ta0, tam, tb0, tbm, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a2, b1):
+ recursion(a2, b1, tam, ta1, tb0, tbm, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a1, b2):
+ recursion(a1, b2, ta0, tam, tbm, tb1, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a2, b2):
+ recursion(a2, b2, tam, ta1, tbm, tb1, depth_a - 1, depth_b - 1)
+ elif depth_a > 0:
+ a1, a2 = bez_split(a, 0.5)
+ if bez_bounds_intersect(a1, b):
+ recursion(a1, b, ta0, tam, tb0, tb1, depth_a - 1, depth_b)
+ if bez_bounds_intersect(a2, b):
+ recursion(a2, b, tam, ta1, tb0, tb1, depth_a - 1, depth_b)
+ elif depth_b > 0:
+ b1, b2 = bez_split(b, 0.5)
+ if bez_bounds_intersect(a, b1):
+ recursion(a, b1, ta0, ta1, tb0, tbm, depth_a, depth_b - 1)
+ if bez_bounds_intersect(a, b2):
+ recursion(a, b2, ta0, ta1, tbm, tb1, depth_a, depth_b - 1)
+ else: # Both segments have been subdivided enough. Let's get some intersections :).
+ intersection, t1, t2 = straight_segments_intersection([a[0]] + [a[3]], [b[0]] + [b[3]])
+ if intersection:
+ if intersection == "Overlap":
+ t1 = (max(0, min(1, t1[0])) + max(0, min(1, t1[1]))) / 2
+ t2 = (max(0, min(1, t2[0])) + max(0, min(1, t2[1]))) / 2
+ bezier_intersection_recursive_result += [[ta0 + t1 * (ta1 - ta0), tb0 + t2 * (tb1 - tb0)]]
+
+ global bezier_intersection_recursive_result
+ bezier_intersection_recursive_result = []
+ recursion(a, b, 0., 1., 0., 1., INTERSECTION_RECURSION_DEPTH, INTERSECTION_RECURSION_DEPTH)
+ intersections = bezier_intersection_recursive_result
+ for i in range(len(intersections)):
+ if len(intersections[i]) < 5 or intersections[i][4] != "Overlap":
+ intersections[i] = polish_intersection(a, b, intersections[i][0], intersections[i][1])
+ return intersections
+
+
+def csp_segments_true_intersection(sp1, sp2, sp3, sp4):
+ intersections = csp_segments_intersection(sp1, sp2, sp3, sp4)
+ res = []
+ for intersection in intersections:
+ if (
+ (len(intersection) == 5 and intersection[4] == "Overlap" and (0 <= intersection[0] <= 1 or 0 <= intersection[1] <= 1) and (0 <= intersection[2] <= 1 or 0 <= intersection[3] <= 1))
+ or (0 <= intersection[0] <= 1 and 0 <= intersection[1] <= 1)
+ ):
+ res += [intersection]
+ return res
+
+
+def csp_get_t_at_curvature(sp1, sp2, c, sample_points=16):
+ # returns a list containing [t1,t2,t3,...,tn], 0<=ti<=1...
+ if sample_points < 2:
+ sample_points = 2
+ tolerance = .0000000001
+ res = []
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ for k in range(sample_points):
+ t = float(k) / (sample_points - 1)
+ i = 0
+ F = 1e100
+ while i < 2 or abs(F) > tolerance and i < 17:
+ try: # some numerical calculation could exceed the limits
+ t2 = t * t
+ # slopes...
+ f1x = 3 * ax * t2 + 2 * bx * t + cx
+ f1y = 3 * ay * t2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ f3x = 6 * ax
+ f3y = 6 * ay
+ d = (f1x ** 2 + f1y ** 2) ** 1.5
+ F1 = (
+ ((f1x * f3y - f3x * f1y) * d - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * ((f1x ** 2 + f1y ** 2) ** .5)) /
+ ((f1x ** 2 + f1y ** 2) ** 3)
+ )
+ F = (f1x * f2y - f1y * f2x) / d - c
+ t -= F / F1
+ except:
+ break
+ i += 1
+ if 0 <= t <= 1 and F <= tolerance:
+ if len(res) == 0:
+ res.append(t)
+ for i in res:
+ if abs(t - i) <= 0.001:
+ break
+ if not abs(t - i) <= 0.001:
+ res.append(t)
+ return res
+
+
+def csp_max_curvature(sp1, sp2):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ tolerance = .0001
+ F = 0.
+ i = 0
+ while i < 2 or F - Flast < tolerance and i < 10:
+ t = .5
+ f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
+ f1y = 3 * ay * t ** 2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ f3x = 6 * ax
+ f3y = 6 * ay
+ d = pow(f1x ** 2 + f1y ** 2, 1.5)
+ if d != 0:
+ Flast = F
+ F = (f1x * f2y - f1y * f2x) / d
+ F1 = (
+ (d * (f1x * f3y - f3x * f1y) - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * pow(f1x ** 2 + f1y ** 2, .5)) /
+ (f1x ** 2 + f1y ** 2) ** 3
+ )
+ i += 1
+ if F1 != 0:
+ t -= F / F1
+ else:
+ break
+ else:
+ break
+ return t
+
+
+def csp_curvature_at_t(sp1, sp2, t, depth=3):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(csp_segment_to_bez(sp1, sp2))
+
+ # curvature = (x'y''-y'x'') / (x'^2+y'^2)^1.5
+
+ f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
+ f1y = 3 * ay * t ** 2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ d = (f1x ** 2 + f1y ** 2) ** 1.5
+ if d != 0:
+ return (f1x * f2y - f1y * f2x) / d
+ else:
+ t1 = f1x * f2y - f1y * f2x
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ # Use the Lapitals rule to solve 0/0 problem for 2 times...
+ t1 = 2 * (bx * ay - ax * by) * t + (ay * cx - ax * cy)
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ t1 = bx * ay - ax * by
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ if depth > 0:
+ # little hack ;^) hope it won't influence anything...
+ return csp_curvature_at_t(sp1, sp2, t * 1.004, depth - 1)
+ return 1e100
+
+
+def csp_subpath_ccw(subpath):
+ # Remove all zero length segments
+ s = 0
+ if (P(subpath[-1][1]) - P(subpath[0][1])).l2() > 1e-10:
+ subpath[-1][2] = subpath[-1][1]
+ subpath[0][0] = subpath[0][1]
+ subpath += [[subpath[0][1], subpath[0][1], subpath[0][1]]]
+ pl = subpath[-1][2]
+ for sp1 in subpath:
+ for p in sp1:
+ s += (p[0] - pl[0]) * (p[1] + pl[1])
+ pl = p
+ return s < 0
+
+
+def csp_at_t(sp1, sp2, t):
+ ax = sp1[1][0]
+ bx = sp1[2][0]
+ cx = sp2[0][0]
+ dx = sp2[1][0]
+
+ ay = sp1[1][1]
+ by = sp1[2][1]
+ cy = sp2[0][1]
+ dy = sp2[1][1]
+
+ x1 = ax + (bx - ax) * t
+ y1 = ay + (by - ay) * t
+
+ x2 = bx + (cx - bx) * t
+ y2 = by + (cy - by) * t
+
+ x3 = cx + (dx - cx) * t
+ y3 = cy + (dy - cy) * t
+
+ x4 = x1 + (x2 - x1) * t
+ y4 = y1 + (y2 - y1) * t
+
+ x5 = x2 + (x3 - x2) * t
+ y5 = y2 + (y3 - y2) * t
+
+ x = x4 + (x5 - x4) * t
+ y = y4 + (y5 - y4) * t
+
+ return [x, y]
+
+
+def csp_at_length(sp1, sp2, l=0.5, tolerance=0.01):
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ t = beziertatlength(bez, l, tolerance)
+ return csp_at_t(sp1, sp2, t)
+
+
+def cspseglength(sp1, sp2, tolerance=0.01):
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ return bezierlength(bez, tolerance)
+
+
+def csp_line_intersection(l1, l2, sp1, sp2):
+ dd = l1[0]
+ cc = l2[0] - l1[0]
+ bb = l1[1]
+ aa = l2[1] - l1[1]
+ if aa == cc == 0:
+ return []
+ if aa:
+ coef1 = cc / aa
+ coef2 = 1
+ else:
+ coef1 = 1
+ coef2 = aa / cc
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
+ a = coef1 * ay - coef2 * ax
+ b = coef1 * by - coef2 * bx
+ c = coef1 * cy - coef2 * cx
+ d = coef1 * (y0 - bb) - coef2 * (x0 - dd)
+ roots = cubic_solver(a, b, c, d)
+ retval = []
+ for i in roots:
+ if type(i) is complex and abs(i.imag) < 1e-7:
+ i = i.real
+ if type(i) is not complex and -1e-10 <= i <= 1. + 1e-10:
+ retval.append(i)
+ return retval
+
+
+def csp_split_by_two_points(sp1, sp2, t1, t2):
+ if t1 > t2:
+ t1, t2 = t2, t1
+ if t1 == t2:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ return [sp1, sp2, sp2, sp3]
+ elif t1 <= 1e-10 and t2 >= 1. - 1e-10:
+ return [sp1, sp1, sp2, sp2]
+ elif t1 <= 1e-10:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t2)
+ return [sp1, sp1, sp2, sp3]
+ elif t2 >= 1. - 1e-10:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ return [sp1, sp2, sp3, sp3]
+ else:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ sp2, sp3, sp4 = csp_split(sp2, sp3, (t2 - t1) / (1 - t1))
+ return [sp1, sp2, sp3, sp4]
+
+
+def csp_seg_split(sp1, sp2, points):
+ # points is float=t or list [t1, t2, ..., tn]
+ if type(points) is float:
+ points = [points]
+ points.sort()
+ res = [sp1, sp2]
+ last_t = 0
+ for t in points:
+ if 1e-10 < t < 1. - 1e-10:
+ sp3, sp4, sp5 = csp_split(res[-2], res[-1], (t - last_t) / (1 - last_t))
+ last_t = t
+ res[-2:] = [sp3, sp4, sp5]
+ return res
+
+
+def csp_subpath_split_by_points(subpath, points):
+ # points are [[i,t]...] where i-segment's number
+ points.sort()
+ points = [[1, 0.]] + points + [[len(subpath) - 1, 1.]]
+ parts = []
+ for int1, int2 in zip(points, points[1:]):
+ if int1 == int2:
+ continue
+ if int1[1] == 1.:
+ int1[0] += 1
+ int1[1] = 0.
+ if int1 == int2:
+ continue
+ if int2[1] == 0.:
+ int2[0] -= 1
+ int2[1] = 1.
+ if int1[0] == 0 and int2[0] == len(subpath) - 1: # and small(int1[1]) and small(int2[1]-1) :
+ continue
+ if int1[0] == int2[0]: # same segment
+ sp = csp_split_by_two_points(subpath[int1[0] - 1], subpath[int1[0]], int1[1], int2[1])
+ if sp[1] != sp[2]:
+ parts += [[sp[1], sp[2]]]
+ else:
+ sp5, sp1, sp2 = csp_split(subpath[int1[0] - 1], subpath[int1[0]], int1[1])
+ sp3, sp4, sp5 = csp_split(subpath[int2[0] - 1], subpath[int2[0]], int2[1])
+ if int1[0] == int2[0] - 1:
+ parts += [[sp1, [sp2[0], sp2[1], sp3[2]], sp4]]
+ else:
+ parts += [[sp1, sp2] + subpath[int1[0] + 1:int2[0] - 1] + [sp3, sp4]]
+ return parts
+
+
+def arc_from_s_r_n_l(s, r, n, l):
+ if abs(n[0] ** 2 + n[1] ** 2 - 1) > 1e-10:
+ n = normalize(n)
+ return arc_from_c_s_l([s[0] + n[0] * r, s[1] + n[1] * r], s, l)
+
+
+def arc_from_c_s_l(c, s, l):
+ r = point_to_point_d(c, s)
+ if r == 0:
+ return []
+ alpha = l / r
+ cos_ = math.cos(alpha)
+ sin_ = math.sin(alpha)
+ e = [c[0] + (s[0] - c[0]) * cos_ - (s[1] - c[1]) * sin_, c[1] + (s[0] - c[0]) * sin_ + (s[1] - c[1]) * cos_]
+ n = [c[0] - s[0], c[1] - s[1]]
+ slope = rotate_cw(n) if l > 0 else rotate_ccw(n)
+ return csp_from_arc(s, e, c, r, slope)
+
+
+def csp_from_arc(start, end, center, r, slope_st):
+ # Creates csp that approximise specified arc
+ r = abs(r)
+ alpha = (atan2(end[0] - center[0], end[1] - center[1]) - atan2(start[0] - center[0], start[1] - center[1])) % TAU
+
+ sectors = int(abs(alpha) * 2 / math.pi) + 1
+ alpha_start = atan2(start[0] - center[0], start[1] - center[1])
+ cos_ = math.cos(alpha_start)
+ sin_ = math.sin(alpha_start)
+ k = (4. * math.tan(alpha / sectors / 4.) / 3.)
+ if dot(slope_st, [- sin_ * k * r, cos_ * k * r]) < 0:
+ if alpha > 0:
+ alpha -= TAU
+ else:
+ alpha += TAU
+ if abs(alpha * r) < 0.001:
+ return []
+
+ sectors = int(abs(alpha) * 2 / math.pi) + 1
+ k = (4. * math.tan(alpha / sectors / 4.) / 3.)
+ result = []
+ for i in range(sectors + 1):
+ cos_ = math.cos(alpha_start + alpha * i / sectors)
+ sin_ = math.sin(alpha_start + alpha * i / sectors)
+ sp = [[], [center[0] + cos_ * r, center[1] + sin_ * r], []]
+ sp[0] = [sp[1][0] + sin_ * k * r, sp[1][1] - cos_ * k * r]
+ sp[2] = [sp[1][0] - sin_ * k * r, sp[1][1] + cos_ * k * r]
+ result += [sp]
+ result[0][0] = result[0][1][:]
+ result[-1][2] = result[-1][1]
+
+ return result
+
+
+def point_to_arc_distance(p, arc):
+ # Distance calculattion from point to arc
+ P0, P2, c, a = arc
+ p = P(p)
+ r = (P0 - c).mag()
+ if r > 0:
+ i = c + (p - c).unit() * r
+ alpha = ((i - c).angle() - (P0 - c).angle())
+ if a * alpha < 0:
+ if alpha > 0:
+ alpha = alpha - TAU
+ else:
+ alpha = TAU + alpha
+ if between(alpha, 0, a) or min(abs(alpha), abs(alpha - a)) < STRAIGHT_TOLERANCE:
+ return (p - i).mag(), [i.x, i.y]
+ else:
+ d1 = (p - P0).mag()
+ d2 = (p - P2).mag()
+ if d1 < d2:
+ return d1, [P0.x, P0.y]
+ else:
+ return d2, [P2.x, P2.y]
+
+
+def csp_to_arc_distance(sp1, sp2, arc1, arc2, tolerance=0.01): # arc = [start,end,center,alpha]
+ n = 10
+ i = 0
+ d = (0, [0, 0])
+ d1 = (0, [0, 0])
+ dl = 0
+ while i < 1 or (abs(d1[0] - dl[0]) > tolerance and i < 4):
+ i += 1
+ dl = d1 * 1
+ for j in range(n + 1):
+ t = float(j) / n
+ p = csp_at_t(sp1, sp2, t)
+ d = min(point_to_arc_distance(p, arc1), point_to_arc_distance(p, arc2))
+ d1 = max(d1, d)
+ n = n * 2
+ return d1[0]
+
+
+def csp_point_inside_bound(sp1, sp2, p):
+ bez = [sp1[1], sp1[2], sp2[0], sp2[1]]
+ x, y = p
+ c = 0
+ # CLT added test of x in range
+ xmin = 1e100
+ xmax = -1e100
+ for i in range(4):
+ [x0, y0] = bez[i - 1]
+ [x1, y1] = bez[i]
+ xmin = min(xmin, x0)
+ xmax = max(xmax, x0)
+ if x0 - x1 != 0 and (y - y0) * (x1 - x0) >= (x - x0) * (y1 - y0) and x > min(x0, x1) and x <= max(x0, x1):
+ c += 1
+ return xmin <= x <= xmax and c % 2 == 0
+
+
+def line_line_intersect(p1, p2, p3, p4): # Return only true intersection.
+ if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
+ return False
+ x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
+ if x == 0: # Lines are parallel
+ if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
+ if p3[0] != p4[0]:
+ t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
+ t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
+ t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
+ t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
+ else:
+ t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
+ t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
+ t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
+ t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
+ return "Overlap" if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1) else False
+ else:
+ return False
+ else:
+ return (
+ 0 <= ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x <= 1 and
+ 0 <= ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x <= 1)
+
+
+def line_line_intersection_points(p1, p2, p3, p4): # Return only points [ (x,y) ]
+ if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
+ return []
+ x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
+ if x == 0: # Lines are parallel
+ if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
+ if p3[0] != p4[0]:
+ t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
+ t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
+ t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
+ t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
+ else:
+ t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
+ t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
+ t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
+ t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
+ res = []
+ if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1):
+ if 0 <= t11 <= 1:
+ res += [p1]
+ if 0 <= t12 <= 1:
+ res += [p2]
+ if 0 <= t21 <= 1:
+ res += [p3]
+ if 0 <= t22 <= 1:
+ res += [p4]
+ return res
+ else:
+ return []
+ else:
+ t1 = ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x
+ t2 = ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x
+ if 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ return [[p1[0] * (1 - t1) + p2[0] * t1, p1[1] * (1 - t1) + p2[1] * t1]]
+ else:
+ return []
+
+
+def point_to_point_d2(a, b):
+ return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
+
+
+def point_to_point_d(a, b):
+ return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
+
+
+def point_to_line_segment_distance_2(p1, p2, p3):
+ # p1 - point, p2,p3 - line segment
+ # draw_pointer(p1)
+ w0 = [p1[0] - p2[0], p1[1] - p2[1]]
+ v = [p3[0] - p2[0], p3[1] - p2[1]]
+ c1 = w0[0] * v[0] + w0[1] * v[1]
+ if c1 <= 0:
+ return w0[0] * w0[0] + w0[1] * w0[1]
+ c2 = v[0] * v[0] + v[1] * v[1]
+ if c2 <= c1:
+ return (p1[0] - p3[0]) ** 2 + (p1[1] - p3[1]) ** 2
+ return (p1[0] - p2[0] - v[0] * c1 / c2) ** 2 + (p1[1] - p2[1] - v[1] * c1 / c2)
+
+
+def line_to_line_distance_2(p1, p2, p3, p4):
+ if line_line_intersect(p1, p2, p3, p4):
+ return 0
+ return min(
+ point_to_line_segment_distance_2(p1, p3, p4),
+ point_to_line_segment_distance_2(p2, p3, p4),
+ point_to_line_segment_distance_2(p3, p1, p2),
+ point_to_line_segment_distance_2(p4, p1, p2))
+
+
+def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1, sp2, sp3, sp4):
+ bez1 = csp_segment_to_bez(sp1, sp2)
+ bez2 = csp_segment_to_bez(sp3, sp4)
+ min_dist = 1e100
+ max_dist = 0.
+ for i in range(4):
+ if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]):
+ min_dist = 0.
+ break
+ for i in range(4):
+ for j in range(4):
+ d = line_to_line_distance_2(bez1[i - 1], bez1[i], bez2[j - 1], bez2[j])
+ if d < min_dist:
+ min_dist = d
+ d = (bez2[j][0] - bez1[i][0]) ** 2 + (bez2[j][1] - bez1[i][1]) ** 2
+ if max_dist < d:
+ max_dist = d
+ return min_dist, max_dist
+
+
+def csp_reverse(csp):
+ for i in range(len(csp)):
+ n = []
+ for j in csp[i]:
+ n = [[j[2][:], j[1][:], j[0][:]]] + n
+ csp[i] = n[:]
+ return csp
+
+
+def csp_normalized_slope(sp1, sp2, t):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize((sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:]))
+ if sp1[1] == sp2[1] == sp1[2] == sp2[0]:
+ return [1., 0.]
+ f1x = 3 * ax * t * t + 2 * bx * t + cx
+ f1y = 3 * ay * t * t + 2 * by * t + cy
+ if abs(f1x * f1x + f1y * f1y) > 1e-9: # LT changed this from 1e-20, which caused problems
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+
+ if t == 0:
+ f1x = sp2[0][0] - sp1[1][0]
+ f1y = sp2[0][1] - sp1[1][1]
+ if abs(f1x * f1x + f1y * f1y) > 1e-9: # LT changed this from 1e-20, which caused problems
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ f1x = sp2[1][0] - sp1[1][0]
+ f1y = sp2[1][1] - sp1[1][1]
+ if f1x * f1x + f1y * f1y != 0:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ elif t == 1:
+ f1x = sp2[1][0] - sp1[2][0]
+ f1y = sp2[1][1] - sp1[2][1]
+ if abs(f1x * f1x + f1y * f1y) > 1e-9:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ f1x = sp2[1][0] - sp1[1][0]
+ f1y = sp2[1][1] - sp1[1][1]
+ if f1x * f1x + f1y * f1y != 0:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ return [1., 0.]
+
+
+def csp_normalized_normal(sp1, sp2, t):
+ nx, ny = csp_normalized_slope(sp1, sp2, t)
+ return [-ny, nx]
+
+
+def csp_parameterize(sp1, sp2):
+ return bezierparameterize(csp_segment_to_bez(sp1, sp2))
+
+
+def csp_concat_subpaths(*s):
+ def concat(s1, s2):
+ if not s1:
+ return s2
+ if not s2:
+ return s1
+ if (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2 > 0.00001:
+ return s1[:-1] + [[s1[-1][0], s1[-1][1], s1[-1][1]], [s2[0][1], s2[0][1], s2[0][2]]] + s2[1:]
+ else:
+ return s1[:-1] + [[s1[-1][0], s2[0][1], s2[0][2]]] + s2[1:]
+
+ if len(s) == 0:
+ return []
+ if len(s) == 1:
+ return s[0]
+ result = s[0]
+ for s1 in s[1:]:
+ result = concat(result, s1)
+ return result
+
+
+def csp_subpaths_end_to_start_distance2(s1, s2):
+ return (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2
+
+
+def csp_clip_by_line(csp, l1, l2):
+ result = []
+ for i in range(len(csp)):
+ s = csp[i]
+ intersections = []
+ for j in range(1, len(s)):
+ intersections += [[j, int_] for int_ in csp_line_intersection(l1, l2, s[j - 1], s[j])]
+ splitted_s = csp_subpath_split_by_points(s, intersections)
+ for s in splitted_s[:]:
+ clip = False
+ for p in csp_true_bounds([s]):
+ if (l1[1] - l2[1]) * p[0] + (l2[0] - l1[0]) * p[1] + (l1[0] * l2[1] - l2[0] * l1[1]) < -0.01:
+ clip = True
+ break
+ if clip:
+ splitted_s.remove(s)
+ result += splitted_s
+ return result
+
+
+def csp_subpath_line_to(subpath, points, prepend=False):
+ # Appends subpath with line or polyline.
+ if len(points) > 0:
+ if not prepend:
+ if len(subpath) > 0:
+ subpath[-1][2] = subpath[-1][1][:]
+ if type(points[0]) == type([1, 1]):
+ for p in points:
+ subpath += [[p[:], p[:], p[:]]]
+ else:
+ subpath += [[points, points, points]]
+ else:
+ if len(subpath) > 0:
+ subpath[0][0] = subpath[0][1][:]
+ if type(points[0]) == type([1, 1]):
+ for p in points:
+ subpath = [[p[:], p[:], p[:]]] + subpath
+ else:
+ subpath = [[points, points, points]] + subpath
+ return subpath
+
+
+def csp_join_subpaths(csp):
+ result = csp[:]
+ done_smf = True
+ joined_result = []
+ while done_smf:
+ done_smf = False
+ while len(result) > 0:
+ s1 = result[-1][:]
+ del (result[-1])
+ j = 0
+ joined_smf = False
+ while j < len(joined_result):
+ if csp_subpaths_end_to_start_distance2(joined_result[j], s1) < 0.000001:
+ joined_result[j] = csp_concat_subpaths(joined_result[j], s1)
+ done_smf = True
+ joined_smf = True
+ break
+ if csp_subpaths_end_to_start_distance2(s1, joined_result[j]) < 0.000001:
+ joined_result[j] = csp_concat_subpaths(s1, joined_result[j])
+ done_smf = True
+ joined_smf = True
+ break
+ j += 1
+ if not joined_smf:
+ joined_result += [s1[:]]
+ if done_smf:
+ result = joined_result[:]
+ joined_result = []
+ return joined_result
+
+
+def triangle_cross(a, b, c):
+ return (a[0] - b[0]) * (c[1] - b[1]) - (c[0] - b[0]) * (a[1] - b[1])
+
+
+def csp_segment_convex_hull(sp1, sp2):
+ a = sp1[1][:]
+ b = sp1[2][:]
+ c = sp2[0][:]
+ d = sp2[1][:]
+
+ abc = triangle_cross(a, b, c)
+ abd = triangle_cross(a, b, d)
+ bcd = triangle_cross(b, c, d)
+ cad = triangle_cross(c, a, d)
+ if abc == 0 and abd == 0:
+ return [min(a, b, c, d), max(a, b, c, d)]
+ if abc == 0:
+ return [d, min(a, b, c), max(a, b, c)]
+ if abd == 0:
+ return [c, min(a, b, d), max(a, b, d)]
+ if bcd == 0:
+ return [a, min(b, c, d), max(b, c, d)]
+ if cad == 0:
+ return [b, min(c, a, d), max(c, a, d)]
+
+ m1 = abc * abd > 0
+ m2 = abc * bcd > 0
+ m3 = abc * cad > 0
+
+ if m1 and m2 and m3:
+ return [a, b, c]
+ if m1 and m2 and not m3:
+ return [a, b, c, d]
+ if m1 and not m2 and m3:
+ return [a, b, d, c]
+ if not m1 and m2 and m3:
+ return [a, d, b, c]
+ if m1 and not (m2 and m3):
+ return [a, b, d]
+ if not (m1 and m2) and m3:
+ return [c, a, d]
+ if not (m1 and m3) and m2:
+ return [b, c, d]
+
+ raise ValueError("csp_segment_convex_hull happened which is something that shouldn't happen!")
+
+
+################################################################################
+# Bezier additional functions
+################################################################################
+
+def bez_bounds_intersect(bez1, bez2):
+ return bounds_intersect(bez_bound(bez2), bez_bound(bez1))
+
+
+def bez_bound(bez):
+ return [
+ min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
+ min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
+ max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
+ max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
+ ]
+
+
+def bounds_intersect(a, b):
+ return not ((a[0] > b[2]) or (b[0] > a[2]) or (a[1] > b[3]) or (b[1] > a[3]))
+
+
+def tpoint(xy1, xy2, t):
+ (x1, y1) = xy1
+ (x2, y2) = xy2
+ return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]
+
+
+def bez_split(a, t=0.5):
+ a1 = tpoint(a[0], a[1], t)
+ at = tpoint(a[1], a[2], t)
+ b2 = tpoint(a[2], a[3], t)
+ a2 = tpoint(a1, at, t)
+ b1 = tpoint(b2, at, t)
+ a3 = tpoint(a2, b1, t)
+ return [a[0], a1, a2, a3], [a3, b1, b2, a[3]]
+
+
+################################################################################
+# Some vector functions
+################################################################################
+
+def normalize(xy):
+ (x, y) = xy
+ l = math.sqrt(x ** 2 + y ** 2)
+ if l == 0:
+ return [0., 0.]
+ else:
+ return [x / l, y / l]
+
+
+def cross(a, b):
+ return a[1] * b[0] - a[0] * b[1]
+
+
+def dot(a, b):
+ return a[0] * b[0] + a[1] * b[1]
+
+
+def rotate_ccw(d):
+ return [-d[1], d[0]]
+
+
+def rotate_cw(d):
+ return [d[1], -d[0]]
+
+
+def vectors_ccw(a, b):
+ return a[0] * b[1] - b[0] * a[1] < 0
+
+
+################################################################################
+# Common functions
+################################################################################
+
+def inv_2x2(a): # invert matrix 2x2
+ det = a[0][0] * a[1][1] - a[1][0] * a[0][1]
+ if det == 0:
+ return None
+ return [
+ [a[1][1] / det, -a[0][1] / det],
+ [-a[1][0] / det, a[0][0] / det]
+ ]
+
+
+def small(a):
+ global small_tolerance
+ return abs(a) < small_tolerance
+
+
+def atan2(*arg):
+ if len(arg) == 1 and (type(arg[0]) == type([0., 0.]) or type(arg[0]) == type((0., 0.))):
+ return (math.pi / 2 - math.atan2(arg[0][0], arg[0][1])) % TAU
+ elif len(arg) == 2:
+ return (math.pi / 2 - math.atan2(arg[0], arg[1])) % TAU
+ else:
+ raise ValueError("Bad argumets for atan! ({})".format(*arg))
+
+
+def draw_text(text, x, y, group=None, style=None, font_size=10, gcodetools_tag=None):
+ if style is None:
+ style = "font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;"
+ style += "font-size:{:f}px;".format(font_size)
+ attributes = {'x': str(x), 'y': str(y), 'style': style}
+ if gcodetools_tag is not None:
+ attributes["gcodetools"] = str(gcodetools_tag)
+
+ if group is None:
+ group = options.doc_root
+
+ text_elem = group.add(TextElement(**attributes))
+ text_elem.set("xml:space", "preserve")
+ text = str(text).split("\n")
+ for string in text:
+ span = text_elem.add(Tspan(x=str(x), y=str(y)))
+ span.set('sodipodi:role', 'line')
+ y += font_size
+ span.text = str(string)
+
+
+def draw_csp(csp, stroke="#f00", fill="none", comment="", width=0.354, group=None, style=None):
+ if group is None:
+ group = options.doc_root
+ node = group.add(PathElement())
+
+ node.style = style if style is not None else \
+ {'fill': fill, 'fill-opacity': 1, 'stroke': stroke, 'stroke-width': width}
+
+ node.path = CubicSuperPath(csp)
+
+ if comment != '':
+ node.set('comment', comment)
+
+ return node
+
+
+def draw_pointer(x, color="#f00", figure="cross", group=None, comment="", fill=None, width=.1, size=10., text=None, font_size=None, pointer_type=None, attrib=None):
+ size = size / 2
+ if attrib is None:
+ attrib = {}
+ if pointer_type is None:
+ pointer_type = "Pointer"
+ attrib["gcodetools"] = pointer_type
+ if group is None:
+ group = options.self.svg.get_current_layer()
+ if text is not None:
+ if font_size is None:
+ font_size = 7
+ group = group.add(Group(gcodetools=pointer_type + " group"))
+ draw_text(text, x[0] + size * 2.2, x[1] - size, group=group, font_size=font_size)
+ if figure == "line":
+ s = ""
+ for i in range(1, len(x) / 2):
+ s += " {}, {} ".format(x[i * 2], x[i * 2 + 1])
+ attrib.update({"d": "M {},{} L {}".format(x[0], x[1], s), "style": "fill:none;stroke:{};stroke-width:{:f};".format(color, width), "comment": str(comment)})
+ elif figure == "arrow":
+ if fill is None:
+ fill = "#12b3ff"
+ fill_opacity = "0.8"
+ d = "m {},{} ".format(x[0], x[1]) + re.sub("([0-9\\-.e]+)", (lambda match: str(float(match.group(1)) * size * 2.)), "0.88464,-0.40404 c -0.0987,-0.0162 -0.186549,-0.0589 -0.26147,-0.1173 l 0.357342,-0.35625 c 0.04631,-0.039 0.0031,-0.13174 -0.05665,-0.12164 -0.0029,-1.4e-4 -0.0058,-1.4e-4 -0.0087,0 l -2.2e-5,2e-5 c -0.01189,0.004 -0.02257,0.0119 -0.0305,0.0217 l -0.357342,0.35625 c -0.05818,-0.0743 -0.102813,-0.16338 -0.117662,-0.26067 l -0.409636,0.88193 z")
+ attrib.update({"d": d, "style": "fill:{};stroke:none;fill-opacity:{};".format(fill, fill_opacity), "comment": str(comment)})
+ else:
+ attrib.update({"d": "m {},{} l {:f},{:f} {:f},{:f} {:f},{:f} {:f},{:f} , {:f},{:f}".format(x[0], x[1], size, size, -2 * size, -2 * size, size, size, size, -size, -2 * size, 2 * size), "style": "fill:none;stroke:{};stroke-width:{:f};".format(color, width), "comment": str(comment)})
+ group.add(PathElement(**attrib))
+
+
+def straight_segments_intersection(a, b, true_intersection=True): # (True intersection means check ta and tb are in [0,1])
+ ax = a[0][0]
+ bx = a[1][0]
+ cx = b[0][0]
+ dx = b[1][0]
+ ay = a[0][1]
+ by = a[1][1]
+ cy = b[0][1]
+ dy = b[1][1]
+ if (ax == bx and ay == by) or (cx == dx and cy == dy):
+ return False, 0, 0
+ if (bx - ax) * (dy - cy) - (by - ay) * (dx - cx) == 0: # Lines are parallel
+ ta = (ax - cx) / (dx - cx) if cx != dx else (ay - cy) / (dy - cy)
+ tb = (bx - cx) / (dx - cx) if cx != dx else (by - cy) / (dy - cy)
+ tc = (cx - ax) / (bx - ax) if ax != bx else (cy - ay) / (by - ay)
+ td = (dx - ax) / (bx - ax) if ax != bx else (dy - ay) / (by - ay)
+ return ("Overlap" if 0 <= ta <= 1 or 0 <= tb <= 1 or 0 <= tc <= 1 or 0 <= td <= 1 or not true_intersection else False), (ta, tb), (tc, td)
+ else:
+ ta = ((ay - cy) * (dx - cx) - (ax - cx) * (dy - cy)) / ((bx - ax) * (dy - cy) - (by - ay) * (dx - cx))
+ tb = (ax - cx + ta * (bx - ax)) / (dx - cx) if dx != cx else (ay - cy + ta * (by - ay)) / (dy - cy)
+ return (0 <= ta <= 1 and 0 <= tb <= 1 or not true_intersection), ta, tb
+
+
+def between(c, x, y):
+ return x - STRAIGHT_TOLERANCE <= c <= y + STRAIGHT_TOLERANCE or y - STRAIGHT_TOLERANCE <= c <= x + STRAIGHT_TOLERANCE
+
+
+def cubic_solver_real(a, b, c, d):
+ # returns only real roots of a cubic equation.
+ roots = cubic_solver(a, b, c, d)
+ res = []
+ for root in roots:
+ if type(root) is complex:
+ if -1e-10 < root.imag < 1e-10:
+ res.append(root.real)
+ else:
+ res.append(root)
+ return res
+
+
+def cubic_solver(a, b, c, d):
+ if a != 0:
+ # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
+ a, b, c = (b / a, c / a, d / a)
+ m = 2 * a ** 3 - 9 * a * b + 27 * c
+ k = a ** 2 - 3 * b
+ n = m ** 2 - 4 * k ** 3
+ w1 = -.5 + .5 * cmath.sqrt(3) * 1j
+ w2 = -.5 - .5 * cmath.sqrt(3) * 1j
+ if n >= 0:
+ t = m + math.sqrt(n)
+ m1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
+ t = m - math.sqrt(n)
+ n1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
+ else:
+ m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1. / 3)
+ n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1. / 3)
+ x1 = -1. / 3 * (a + m1 + n1)
+ x2 = -1. / 3 * (a + w1 * m1 + w2 * n1)
+ x3 = -1. / 3 * (a + w2 * m1 + w1 * n1)
+ return [x1, x2, x3]
+ elif b != 0:
+ det = c ** 2 - 4 * b * d
+ if det > 0:
+ return [(-c + math.sqrt(det)) / (2 * b), (-c - math.sqrt(det)) / (2 * b)]
+ elif d == 0:
+ return [-c / (b * b)]
+ else:
+ return [(-c + cmath.sqrt(det)) / (2 * b), (-c - cmath.sqrt(det)) / (2 * b)]
+ elif c != 0:
+ return [-d / c]
+ else:
+ return []
+
+
+################################################################################
+# print_ prints any arguments into specified log file
+################################################################################
+
+def print_(*arg):
+ with open(options.log_filename, "ab") as f:
+ for s in arg:
+ s = unicode(s).encode('unicode_escape') + b" "
+ f.write(s)
+ f.write(b"\n")
+
+
+################################################################################
+# Point (x,y) operations
+################################################################################
+class P(object):
+ def __init__(self, x, y=None):
+ if not y is None:
+ self.x = float(x)
+ self.y = float(y)
+ else:
+ self.x = float(x[0])
+ self.y = float(x[1])
+
+ def __add__(self, other):
+ return P(self.x + other.x, self.y + other.y)
+
+ def __sub__(self, other):
+ return P(self.x - other.x, self.y - other.y)
+
+ def __neg__(self):
+ return P(-self.x, -self.y)
+
+ def __mul__(self, other):
+ if isinstance(other, P):
+ return self.x * other.x + self.y * other.y
+ return P(self.x * other, self.y * other)
+
+ __rmul__ = __mul__
+
+ def __div__(self, other):
+ return P(self.x / other, self.y / other)
+
+ def __truediv__(self, other):
+ return self.__div__(other)
+
+ def mag(self):
+ return math.hypot(self.x, self.y)
+
+ def unit(self):
+ h_mag = self.mag()
+ if h_mag:
+ return self / h_mag
+ return P(0, 0)
+
+ def dot(self, other):
+ return self.x * other.x + self.y * other.y
+
+ def rot(self, theta):
+ c = math.cos(theta)
+ s = math.sin(theta)
+ return P(self.x * c - self.y * s, self.x * s + self.y * c)
+
+ def angle(self):
+ return math.atan2(self.y, self.x)
+
+ def __repr__(self):
+ return '{:f},{:f}'.format(self.x, self.y)
+
+ def pr(self):
+ return "{:.2f},{:.2f}".format(self.x, self.y)
+
+ def to_list(self):
+ return [self.x, self.y]
+
+ def ccw(self):
+ return P(-self.y, self.x)
+
+ def l2(self):
+ return self.x * self.x + self.y * self.y
+
+
+class Line(object):
+ def __init__(self, st, end):
+ if st.__class__ == P:
+ st = st.to_list()
+ if end.__class__ == P:
+ end = end.to_list()
+ self.st = P(st)
+ self.end = P(end)
+ self.l = self.length()
+ if self.l != 0:
+ self.n = ((self.end - self.st) / self.l).ccw()
+ else:
+ self.n = [0, 1]
+
+ def offset(self, r):
+ self.st -= self.n * r
+ self.end -= self.n * r
+
+ def l2(self):
+ return (self.st - self.end).l2()
+
+ def length(self):
+ return (self.st - self.end).mag()
+
+ def draw(self, group, style, layer, transform, num=0, reverse_angle=1):
+ st = gcodetools.transform(self.st.to_list(), layer, True)
+ end = gcodetools.transform(self.end.to_list(), layer, True)
+
+ attr = {'style': style['line'],
+ 'd': 'M {},{} L {},{}'.format(st[0], st[1], end[0], end[1]),
+ "gcodetools": "Preview",
+ }
+ if transform:
+ attr["transform"] = transform
+ group.add(PathElement(**attr))
+
+ def intersect(self, b):
+ if b.__class__ == Line:
+ if self.l < 10e-8 or b.l < 10e-8:
+ return []
+ v1 = self.end - self.st
+ v2 = b.end - b.st
+ x = v1.x * v2.y - v2.x * v1.y
+ if x == 0:
+ # lines are parallel
+ res = []
+
+ if (self.st.x - b.st.x) * v1.y - (self.st.y - b.st.y) * v1.x == 0:
+ # lines are the same
+ if v1.x != 0:
+ if 0 <= (self.st.x - b.st.x) / v2.x <= 1:
+ res.append(self.st)
+ if 0 <= (self.end.x - b.st.x) / v2.x <= 1:
+ res.append(self.end)
+ if 0 <= (b.st.x - self.st.x) / v1.x <= 1:
+ res.append(b.st)
+ if 0 <= (b.end.x - b.st.x) / v1.x <= 1:
+ res.append(b.end)
+ else:
+ if 0 <= (self.st.y - b.st.y) / v2.y <= 1:
+ res.append(self.st)
+ if 0 <= (self.end.y - b.st.y) / v2.y <= 1:
+ res.append(self.end)
+ if 0 <= (b.st.y - self.st.y) / v1.y <= 1:
+ res.append(b.st)
+ if 0 <= (b.end.y - b.st.y) / v1.y <= 1:
+ res.append(b.end)
+ return res
+ else:
+ t1 = (-v1.x * (b.end.y - self.end.y) + v1.y * (b.end.x - self.end.x)) / x
+ t2 = (-v1.y * (self.st.x - b.st.x) + v1.x * (self.st.y - b.st.y)) / x
+
+ gcodetools.error(str((x, t1, t2)))
+ if 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ return [self.st + v1 * t1]
+ else:
+ return []
+ else:
+ return []
+
+
+################################################################################
+#
+# Offset function
+#
+# This function offsets given cubic super path.
+# It's based on src/livarot/PathOutline.cpp from Inkscape's source code.
+#
+#
+################################################################################
+def csp_offset(csp, r):
+ offset_tolerance = 0.05
+ offset_subdivision_depth = 10
+ time_ = time.time()
+ time_start = time_
+ print_("Offset start at {}".format(time_))
+ print_("Offset radius {}".format(r))
+
+ def csp_offset_segment(sp1, sp2, r):
+ result = []
+ t = csp_get_t_at_curvature(sp1, sp2, 1 / r)
+ if len(t) == 0:
+ t = [0., 1.]
+ t.sort()
+ if t[0] > .00000001:
+ t = [0.] + t
+ if t[-1] < .99999999:
+ t.append(1.)
+ for st, end in zip(t, t[1:]):
+ c = csp_curvature_at_t(sp1, sp2, (st + end) / 2)
+ sp = csp_split_by_two_points(sp1, sp2, st, end)
+ if sp[1] != sp[2]:
+ if c > 1 / r and r < 0 or c < 1 / r and r > 0:
+ offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
+ else: # This part will be clipped for sure... TODO Optimize it...
+ offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
+
+ if not result:
+ result = offset[:]
+ else:
+ if csp_subpaths_end_to_start_distance2(result, offset) < 0.0001:
+ result = csp_concat_subpaths(result, offset)
+ else:
+
+ intersection = csp_get_subapths_last_first_intersection(result, offset)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(result[i - 1], result[i], t1)
+ result = result[:i - 1] + [sp1_, sp2_]
+ sp1_, sp2_, sp3_ = csp_split(offset[j - 1], offset[j], t2)
+ result = csp_concat_subpaths(result, [sp2_, sp3_] + offset[j + 1:])
+ else:
+ pass # ???
+ return result
+
+ def create_offset_segment(sp1, sp2, r):
+ # See Gernot Hoffmann "Bezier Curves" p.34 -> 7.1 Bezier Offset Curves
+ p0 = P(sp1[1])
+ p1 = P(sp1[2])
+ p2 = P(sp2[0])
+ p3 = P(sp2[1])
+
+ s0 = p1 - p0
+ s1 = p2 - p1
+ s3 = p3 - p2
+
+ n0 = s0.ccw().unit() if s0.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 0))
+ n3 = s3.ccw().unit() if s3.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 1))
+ n1 = s1.ccw().unit() if s1.l2() != 0 else (n0.unit() + n3.unit()).unit()
+
+ q0 = p0 + r * n0
+ q3 = p3 + r * n3
+ c = csp_curvature_at_t(sp1, sp2, 0)
+ q1 = q0 + (p1 - p0) * (1 - (r * c if abs(c) < 100 else 0))
+ c = csp_curvature_at_t(sp1, sp2, 1)
+ q2 = q3 + (p2 - p3) * (1 - (r * c if abs(c) < 100 else 0))
+
+ return [[q0.to_list(), q0.to_list(), q1.to_list()], [q2.to_list(), q3.to_list(), q3.to_list()]]
+
+ def csp_get_subapths_last_first_intersection(s1, s2):
+ _break = False
+ for i in range(1, len(s1)):
+ sp11 = s1[-i - 1]
+ sp12 = s1[-i]
+ for j in range(1, len(s2)):
+ sp21 = s2[j - 1]
+ sp22 = s2[j]
+ intersection = csp_segments_true_intersection(sp11, sp12, sp21, sp22)
+ if intersection:
+ _break = True
+ break
+ if _break:
+ break
+ if _break:
+ intersection = max(intersection)
+ return [len(s1) - i, intersection[0], j, intersection[1]]
+ else:
+ return []
+
+ def csp_join_offsets(prev, next, sp1, sp2, sp1_l, sp2_l, r):
+ if len(next) > 1:
+ if (P(prev[-1][1]) - P(next[0][1])).l2() < 0.001:
+ return prev, [], next
+ intersection = csp_get_subapths_last_first_intersection(prev, next)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
+ return prev[:i - 1] + [sp1_, sp2_], [], [sp4_, sp5_] + next[j + 1:]
+
+ # Offsets do not intersect... will add an arc...
+ start = (P(csp_at_t(sp1_l, sp2_l, 1.)) + r * P(csp_normalized_normal(sp1_l, sp2_l, 1.))).to_list()
+ end = (P(csp_at_t(sp1, sp2, 0.)) + r * P(csp_normalized_normal(sp1, sp2, 0.))).to_list()
+ arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l, sp2_l, 1.))
+ if not arc:
+ return prev, [], next
+ else:
+ # Clip prev by arc
+ if csp_subpaths_end_to_start_distance2(prev, arc) > 0.00001:
+ intersection = csp_get_subapths_last_first_intersection(prev, arc)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(arc[j - 1], arc[j], t2)
+ prev = prev[:i - 1] + [sp1_, sp2_]
+ arc = [sp4_, sp5_] + arc[j + 1:]
+ # Clip next by arc
+ if not next:
+ return prev, [], arc
+ if csp_subpaths_end_to_start_distance2(arc, next) > 0.00001:
+ intersection = csp_get_subapths_last_first_intersection(arc, next)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(arc[i - 1], arc[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
+ arc = arc[:i - 1] + [sp1_, sp2_]
+ next = [sp4_, sp5_] + next[j + 1:]
+
+ return prev, arc, next
+
+ def offset_segment_recursion(sp1, sp2, r, depth, tolerance):
+ sp1_r, sp2_r = create_offset_segment(sp1, sp2, r)
+ err = max(
+ csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .25)) + P(csp_normalized_normal(sp1, sp2, .25)) * r).to_list())[0],
+ csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .50)) + P(csp_normalized_normal(sp1, sp2, .50)) * r).to_list())[0],
+ csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .75)) + P(csp_normalized_normal(sp1, sp2, .75)) * r).to_list())[0],
+ )
+
+ if err > tolerance ** 2 and depth > 0:
+ if depth > offset_subdivision_depth - 2:
+ t = csp_max_curvature(sp1, sp2)
+ t = max(.1, min(.9, t))
+ else:
+ t = .5
+ sp3, sp4, sp5 = csp_split(sp1, sp2, t)
+ r1 = offset_segment_recursion(sp3, sp4, r, depth - 1, tolerance)
+ r2 = offset_segment_recursion(sp4, sp5, r, depth - 1, tolerance)
+ return r1[:-1] + [[r1[-1][0], r1[-1][1], r2[0][2]]] + r2[1:]
+ else:
+ return [sp1_r, sp2_r]
+
+ ############################################################################
+ # Some small definitions
+ ############################################################################
+ csp_len = len(csp)
+
+ ############################################################################
+ # Prepare the path
+ ############################################################################
+ # Remove all small segments (segment length < 0.001)
+
+ for i in xrange(len(csp)):
+ for j in xrange(len(csp[i])):
+ sp = csp[i][j]
+ if (P(sp[1]) - P(sp[0])).mag() < 0.001:
+ csp[i][j][0] = sp[1]
+ if (P(sp[2]) - P(sp[0])).mag() < 0.001:
+ csp[i][j][2] = sp[1]
+ for i in xrange(len(csp)):
+ for j in xrange(1, len(csp[i])):
+ if cspseglength(csp[i][j - 1], csp[i][j]) < 0.001:
+ csp[i] = csp[i][:j] + csp[i][j + 1:]
+ if cspseglength(csp[i][-1], csp[i][0]) > 0.001:
+ csp[i][-1][2] = csp[i][-1][1]
+ csp[i] += [[csp[i][0][1], csp[i][0][1], csp[i][0][1]]]
+
+ # TODO Get rid of self intersections.
+
+ original_csp = csp[:]
+ # Clip segments which has curvature>1/r. Because their offset will be self-intersecting and very nasty.
+
+ print_("Offset prepared the path in {}".format(time.time() - time_))
+ print_("Path length = {}".format(sum([len(i) for i in csp])))
+ time_ = time.time()
+
+ ############################################################################
+ # Offset
+ ############################################################################
+ # Create offsets for all segments in the path. And join them together inside each subpath.
+ unclipped_offset = [[] for i in xrange(csp_len)]
+
+ intersection = [[] for i in xrange(csp_len)]
+ for i in xrange(csp_len):
+ subpath = csp[i]
+ subpath_offset = []
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ segment_offset = csp_offset_segment(sp1, sp2, r)
+ if not subpath_offset:
+ subpath_offset = segment_offset
+
+ prev_l = len(subpath_offset)
+ else:
+ prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], segment_offset, sp1, sp2, sp1_l, sp2_l, r)
+
+ subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc, next)
+ prev_l = len(next)
+ sp1_l = sp1[:]
+ sp2_l = sp2[:]
+
+ # Join last and first offsets togother to close the curve
+
+ prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l, sp2_l, r)
+ subpath_offset[:2] = next[:]
+ subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc)
+
+ # Collect subpath's offset and save it to unclipped offset list.
+ unclipped_offset[i] = subpath_offset[:]
+
+ print_("Offsetted path in {}".format(time.time() - time_))
+ time_ = time.time()
+
+ ############################################################################
+ # Now to the clipping.
+ ############################################################################
+ # First of all find all intersection's between all segments of all offset subpaths, including self intersections.
+
+ # TODO define offset tolerance here
+ global small_tolerance
+ small_tolerance = 0.01
+ summ = 0
+ summ1 = 0
+ for subpath_i in xrange(csp_len):
+ for subpath_j in xrange(subpath_i, csp_len):
+ subpath = unclipped_offset[subpath_i]
+ subpath1 = unclipped_offset[subpath_j]
+ for i in xrange(1, len(subpath)):
+ # If subpath_i==subpath_j we are looking for self intersections, so
+ # we'll need search intersections only for xrange(i,len(subpath1))
+ for j in (xrange(i, len(subpath1)) if subpath_i == subpath_j else xrange(len(subpath1))):
+ if subpath_i == subpath_j and j == i:
+ # Find self intersections of a segment
+ sp1, sp2, sp3 = csp_split(subpath[i - 1], subpath[i], .5)
+ intersections = csp_segments_intersection(sp1, sp2, sp2, sp3)
+ summ += 1
+ for t in intersections:
+ summ1 += 1
+ if not (small(t[0] - 1) and small(t[1])) and 0 <= t[0] <= 1 and 0 <= t[1] <= 1:
+ intersection[subpath_i] += [[i, t[0] / 2], [j, t[1] / 2 + .5]]
+ else:
+ intersections = csp_segments_intersection(subpath[i - 1], subpath[i], subpath1[j - 1], subpath1[j])
+ summ += 1
+ for t in intersections:
+ summ1 += 1
+ # TODO tolerance dependence to cpsp_length(t)
+ if len(t) == 2 and 0 <= t[0] <= 1 and 0 <= t[1] <= 1 and not (
+ subpath_i == subpath_j and (
+ (j - i - 1) % (len(subpath) - 1) == 0 and small(t[0] - 1) and small(t[1]) or
+ (i - j - 1) % (len(subpath) - 1) == 0 and small(t[1] - 1) and small(t[0]))):
+ intersection[subpath_i] += [[i, t[0]]]
+ intersection[subpath_j] += [[j, t[1]]]
+
+ elif len(t) == 5 and t[4] == "Overlap":
+ intersection[subpath_i] += [[i, t[0]], [i, t[1]]]
+ intersection[subpath_j] += [[j, t[1]], [j, t[3]]]
+
+ print_("Intersections found in {}".format(time.time() - time_))
+ print_("Examined {} segments".format(summ))
+ print_("found {} intersections".format(summ1))
+ time_ = time.time()
+
+ ########################################################################
+ # Split unclipped offset by intersection points into splitted_offset
+ ########################################################################
+ splitted_offset = []
+ for i in xrange(csp_len):
+ subpath = unclipped_offset[i]
+ if len(intersection[i]) > 0:
+ parts = csp_subpath_split_by_points(subpath, intersection[i])
+ # Close parts list to close path (The first and the last parts are joined together)
+ if [1, 0.] not in intersection[i]:
+ parts[0][0][0] = parts[-1][-1][0]
+ parts[0] = csp_concat_subpaths(parts[-1], parts[0])
+ splitted_offset += parts[:-1]
+ else:
+ splitted_offset += parts[:]
+ else:
+ splitted_offset += [subpath[:]]
+
+ print_("Split in {}".format(time.time() - time_))
+ time_ = time.time()
+
+ ########################################################################
+ # Clipping
+ ########################################################################
+ result = []
+ for subpath_i in range(len(splitted_offset)):
+ clip = False
+ s1 = splitted_offset[subpath_i]
+ for subpath_j in range(len(splitted_offset)):
+ s2 = splitted_offset[subpath_j]
+ if (P(s1[0][1]) - P(s2[-1][1])).l2() < 0.0001 and ((subpath_i + 1) % len(splitted_offset) != subpath_j):
+ if dot(csp_normalized_normal(s2[-2], s2[-1], 1.), csp_normalized_slope(s1[0], s1[1], 0.)) * r < -0.0001:
+ clip = True
+ break
+ if (P(s2[0][1]) - P(s1[-1][1])).l2() < 0.0001 and ((subpath_j + 1) % len(splitted_offset) != subpath_i):
+ if dot(csp_normalized_normal(s2[0], s2[1], 0.), csp_normalized_slope(s1[-2], s1[-1], 1.)) * r > 0.0001:
+ clip = True
+ break
+
+ if not clip:
+ result += [s1[:]]
+ elif options.offset_draw_clippend_path:
+ draw_csp([s1], width=.1)
+ draw_pointer(csp_at_t(s2[-2], s2[-1], 1.) +
+ (P(csp_at_t(s2[-2], s2[-1], 1.)) + P(csp_normalized_normal(s2[-2], s2[-1], 1.)) * 10).to_list(), "Green", "line")
+ draw_pointer(csp_at_t(s1[0], s1[1], 0.) +
+ (P(csp_at_t(s1[0], s1[1], 0.)) + P(csp_normalized_slope(s1[0], s1[1], 0.)) * 10).to_list(), "Red", "line")
+
+ # Now join all together and check closure and orientation of result
+ joined_result = csp_join_subpaths(result)
+ # Check if each subpath from joined_result is closed
+
+ for s in joined_result[:]:
+ if csp_subpaths_end_to_start_distance2(s, s) > 0.001:
+ # Remove open parts
+ if options.offset_draw_clippend_path:
+ draw_csp([s], width=1)
+ draw_pointer(s[0][1], comment=csp_subpaths_end_to_start_distance2(s, s))
+ draw_pointer(s[-1][1], comment=csp_subpaths_end_to_start_distance2(s, s))
+ joined_result.remove(s)
+ else:
+ # Remove small parts
+ minx, miny, maxx, maxy = csp_true_bounds([s])
+ if (minx[0] - maxx[0]) ** 2 + (miny[1] - maxy[1]) ** 2 < 0.1:
+ joined_result.remove(s)
+ print_("Clipped and joined path in {}".format(time.time() - time_))
+
+ ########################################################################
+ # Now to the Dummy clipping: remove parts from split offset if their
+ # centers are closer to the original path than offset radius.
+ ########################################################################
+
+ if abs(r * .01) < 1:
+ r1 = (0.99 * r) ** 2
+ r2 = (1.01 * r) ** 2
+ else:
+ r1 = (abs(r) - 1) ** 2
+ r2 = (abs(r) + 1) ** 2
+
+ for s in joined_result[:]:
+ dist = csp_to_point_distance(original_csp, s[int(len(s) / 2)][1], dist_bounds=[r1, r2])
+ if not r1 < dist[0] < r2:
+ joined_result.remove(s)
+ if options.offset_draw_clippend_path:
+ draw_csp([s], comment=math.sqrt(dist[0]))
+ draw_pointer(csp_at_t(csp[dist[1]][dist[2] - 1], csp[dist[1]][dist[2]], dist[3]) + s[int(len(s) / 2)][1], "blue", "line", comment=[math.sqrt(dist[0]), i, j, sp])
+
+ print_("-----------------------------")
+ print_("Total offset time {}".format(time.time() - time_start))
+ print_()
+ return joined_result
+
+
+################################################################################
+#
+# Biarc function
+#
+# Calculates biarc approximation of cubic super path segment
+# splits segment if needed or approximates it with straight line
+#
+################################################################################
+def biarc(sp1, sp2, z1, z2, depth=0):
+ def biarc_split(sp1, sp2, z1, z2, depth):
+ if depth < options.biarc_max_split_depth:
+ sp1, sp2, sp3 = csp_split(sp1, sp2)
+ l1 = cspseglength(sp1, sp2)
+ l2 = cspseglength(sp2, sp3)
+ if l1 + l2 == 0:
+ zm = z1
+ else:
+ zm = z1 + (z2 - z1) * l1 / (l1 + l2)
+ return biarc(sp1, sp2, z1, zm, depth + 1) + biarc(sp2, sp3, zm, z2, depth + 1)
+ else:
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ P0 = P(sp1[1])
+ P4 = P(sp2[1])
+ TS = (P(sp1[2]) - P0)
+ TE = -(P(sp2[0]) - P4)
+ v = P0 - P4
+ tsa = TS.angle()
+ tea = TE.angle()
+ va = v.angle()
+ if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE and TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ # Both tangents are zero - line straight
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+ if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ TE = -(TS + v).unit()
+ r = TS.mag() / v.mag() * 2
+ elif TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ TS = -(TE + v).unit()
+ r = 1 / (TE.mag() / v.mag() * 2)
+ else:
+ r = TS.mag() / TE.mag()
+ TS = TS.unit()
+ TE = TE.unit()
+ tang_are_parallel = ((tsa - tea) % math.pi < STRAIGHT_TOLERANCE or math.pi - (tsa - tea) % math.pi < STRAIGHT_TOLERANCE)
+ if (tang_are_parallel and
+ ((v.mag() < STRAIGHT_DISTANCE_TOLERANCE or TE.mag() < STRAIGHT_DISTANCE_TOLERANCE or TS.mag() < STRAIGHT_DISTANCE_TOLERANCE) or
+ 1 - abs(TS * v / (TS.mag() * v.mag())) < STRAIGHT_TOLERANCE)):
+ # Both tangents are parallel and start and end are the same - line straight
+ # or one of tangents still smaller then tolerance
+
+ # Both tangents and v are parallel - line straight
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ c = v * v
+ b = 2 * v * (r * TS + TE)
+ a = 2 * r * (TS * TE - 1)
+ if v.mag() == 0:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ asmall = abs(a) < 10 ** -10
+ bsmall = abs(b) < 10 ** -10
+ csmall = abs(c) < 10 ** -10
+ if asmall and b != 0:
+ beta = -c / b
+ elif csmall and a != 0:
+ beta = -b / a
+ elif not asmall:
+ discr = b * b - 4 * a * c
+ if discr < 0:
+ raise ValueError(a, b, c, discr)
+ disq = discr ** .5
+ beta1 = (-b - disq) / 2 / a
+ beta2 = (-b + disq) / 2 / a
+ if beta1 * beta2 > 0:
+ raise ValueError(a, b, c, disq, beta1, beta2)
+ beta = max(beta1, beta2)
+ elif asmall and bsmall:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ alpha = beta * r
+ ab = alpha + beta
+ P1 = P0 + alpha * TS
+ P3 = P4 - beta * TE
+ P2 = (beta / ab) * P1 + (alpha / ab) * P3
+
+ def calculate_arc_params(P0, P1, P2):
+ D = (P0 + P2) / 2
+ if (D - P1).mag() == 0:
+ return None, None
+ R = D - ((D - P0).mag() ** 2 / (D - P1).mag()) * (P1 - D).unit()
+ p0a = (P0 - R).angle() % (2 * math.pi)
+ p1a = (P1 - R).angle() % (2 * math.pi)
+ p2a = (P2 - R).angle() % (2 * math.pi)
+ alpha = (p2a - p0a) % (2 * math.pi)
+ if (p0a < p2a and (p1a < p0a or p2a < p1a)) or (p2a < p1a < p0a):
+ alpha = -2 * math.pi + alpha
+ if abs(R.x) > 1000000 or abs(R.y) > 1000000 or (R - P0).mag() < options.min_arc_radius ** 2:
+ return None, None
+ else:
+ return R, alpha
+
+ R1, a1 = calculate_arc_params(P0, P1, P2)
+ R2, a2 = calculate_arc_params(P2, P3, P4)
+ if R1 is None or R2 is None or (R1 - P0).mag() < STRAIGHT_TOLERANCE or (R2 - P2).mag() < STRAIGHT_TOLERANCE:
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ d = csp_to_arc_distance(sp1, sp2, [P0, P2, R1, a1], [P2, P4, R2, a2])
+ if d > options.biarc_tolerance and depth < options.biarc_max_split_depth:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ else:
+ if R2.mag() * a2 == 0:
+ zm = z2
+ else:
+ zm = z1 + (z2 - z1) * (abs(R1.mag() * a1)) / (abs(R2.mag() * a2) + abs(R1.mag() * a1))
+
+ l = (P0 - P2).l2()
+ if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R1.l2() / 100:
+ # arc should be straight otherwise it could be treated as full circle
+ arc1 = [sp1[1], 'line', 0, 0, [P2.x, P2.y], [z1, zm]]
+ else:
+ arc1 = [sp1[1], 'arc', [R1.x, R1.y], a1, [P2.x, P2.y], [z1, zm]]
+
+ l = (P4 - P2).l2()
+ if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R2.l2() / 100:
+ # arc should be straight otherwise it could be treated as full circle
+ arc2 = [[P2.x, P2.y], 'line', 0, 0, [P4.x, P4.y], [zm, z2]]
+ else:
+ arc2 = [[P2.x, P2.y], 'arc', [R2.x, R2.y], a2, [P4.x, P4.y], [zm, z2]]
+
+ return [arc1, arc2]
+
+
+class Postprocessor(object):
+ def __init__(self, error_function_handler):
+ self.error = error_function_handler
+ self.functions = {
+ "remap": self.remap,
+ "remapi": self.remapi,
+ "scale": self.scale,
+ "move": self.move,
+ "flip": self.flip_axis,
+ "flip_axis": self.flip_axis,
+ "round": self.round_coordinates,
+ "parameterize": self.parameterize,
+ "regex": self.re_sub_on_gcode_lines
+ }
+
+ def process(self, command):
+ command = re.sub(r"\\\\", ":#:#:slash:#:#:", command)
+ command = re.sub(r"\\;", ":#:#:semicolon:#:#:", command)
+ command = command.split(";")
+ for s in command:
+ s = re.sub(":#:#:slash:#:#:", "\\\\", s)
+ s = re.sub(":#:#:semicolon:#:#:", "\\;", s)
+ s = s.strip()
+ if s != "":
+ self.parse_command(s)
+
+ def parse_command(self, command):
+ r = re.match(r"([A-Za-z0-9_]+)\s*\(\s*(.*)\)", command)
+ if not r:
+ self.error("Parse error while postprocessing.\n(Command: '{}')".format(command), "error")
+ function = r.group(1).lower()
+ parameters = r.group(2)
+ if function in self.functions:
+ print_("Postprocessor: executing function {}({})".format(function, parameters))
+ self.functions[function](parameters)
+ else:
+ self.error("Unrecognized function '{}' while postprocessing.\n(Command: '{}')".format(function, command), "error")
+
+ def re_sub_on_gcode_lines(self, parameters):
+ gcode = self.gcode.split("\n")
+ self.gcode = ""
+ try:
+ for line in gcode:
+ self.gcode += eval("re.sub({},line)".format(parameters)) + "\n"
+
+ except Exception as ex:
+ self.error("Bad parameters for regexp. "
+ "They should be as re.sub pattern and replacement parameters! "
+ "For example: r\"G0(\\d)\", r\"G\\1\" \n"
+ "(Parameters: '{}')\n {}".format(parameters, ex), "error")
+
+ def remapi(self, parameters):
+ self.remap(parameters, case_sensitive=True)
+
+ def remap(self, parameters, case_sensitive=False):
+ # remap parameters should be like "x->y,y->x"
+ parameters = parameters.replace("\\,", ":#:#:coma:#:#:")
+ parameters = parameters.split(",")
+ pattern = []
+ remap = []
+ for s in parameters:
+ s = s.replace(":#:#:coma:#:#:", "\\,")
+ r = re.match("""\\s*(\'|\")(.*)\\1\\s*->\\s*(\'|\")(.*)\\3\\s*""", s)
+ if not r:
+ self.error("Bad parameters for remap.\n(Parameters: '{}')".format(parameters), "error")
+ pattern += [r.group(2)]
+ remap += [r.group(4)]
+
+ for i in range(len(pattern)):
+ if case_sensitive:
+ self.gcode = ireplace(self.gcode, pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
+ else:
+ self.gcode = self.gcode.replace(pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
+
+ for i in range(len(remap)):
+ self.gcode = self.gcode.replace(":#:#:remap_pattern{}:#:#:".format(i), remap[i])
+
+ def transform(self, move, scale):
+ axis = ["xi", "yj", "zk", "a"]
+ flip = scale[0] * scale[1] * scale[2] < 0
+ gcode = ""
+ warned = []
+ r_scale = scale[0]
+ plane = "g17"
+ for s in self.gcode.split("\n"):
+ # get plane selection:
+ s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
+ r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
+ if r:
+ plane = r.group(1).lower()
+ if plane == "g17":
+ r_scale = scale[0] # plane XY -> scale x
+ if plane == "g18":
+ r_scale = scale[0] # plane XZ -> scale x
+ if plane == "g19":
+ r_scale = scale[1] # plane YZ -> scale y
+ # Raise warning if scale factors are not the game for G02 and G03
+ if plane not in warned:
+ r = re.search(r"(?i)(G02|G03)", s_wo_comments)
+ if r:
+ if plane == "g17" and scale[0] != scale[1]:
+ self.error("Post-processor: Scale factors for X and Y axis are not the same. G02 and G03 codes will be corrupted.")
+ if plane == "g18" and scale[0] != scale[2]:
+ self.error("Post-processor: Scale factors for X and Z axis are not the same. G02 and G03 codes will be corrupted.")
+ if plane == "g19" and scale[1] != scale[2]:
+ self.error("Post-processor: Scale factors for Y and Z axis are not the same. G02 and G03 codes will be corrupted.")
+ warned += [plane]
+ # Transform
+ for i in range(len(axis)):
+ if move[i] != 0 or scale[i] != 1:
+ for a in axis[i]:
+ r = re.search(r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)", s)
+ if r and r.group(3) != "":
+ s = re.sub(r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)", r"\1 {:f}".format(float(r.group(2) + r.group(3)) * scale[i] + (move[i] if a not in ["i", "j", "k"] else 0)), s)
+ # scale radius R
+ if r_scale != 1:
+ r = re.search(r"(?i)(r)\s*(-?\s*(\d*\.?\d*))", s)
+ if r and r.group(3) != "":
+ try:
+ s = re.sub(r"(?i)(r)\s*(-?)\s*(\d*\.?\d*)", r"\1 {:f}".format(float(r.group(2) + r.group(3)) * r_scale), s)
+ except:
+ pass
+
+ gcode += s + "\n"
+
+ self.gcode = gcode
+ if flip:
+ self.remapi("'G02'->'G03', 'G03'->'G02'")
+
+ def parameterize(self, parameters):
+ planes = []
+ feeds = {}
+ coords = []
+ gcode = ""
+ coords_def = {"x": "x", "y": "y", "z": "z", "i": "x", "j": "y", "k": "z", "a": "a"}
+ for s in self.gcode.split("\n"):
+ s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
+ # get Planes
+ r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
+ if r:
+ plane = r.group(1).lower()
+ if plane not in planes:
+ planes += [plane]
+ # get Feeds
+ r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
+ if r:
+ feed = float(r.group(2) + r.group(3))
+ if feed not in feeds:
+ feeds[feed] = "#" + str(len(feeds) + 20)
+
+ # Coordinates
+ for c in "xyzijka":
+ r = re.search(r"(?i)(" + c + r")\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
+ if r:
+ c = coords_def[r.group(1).lower()]
+ if c not in coords:
+ coords += [c]
+ # Add offset parametrization
+ offset = {"x": "#6", "y": "#7", "z": "#8", "a": "#9"}
+ for c in coords:
+ gcode += "{} = 0 ({} axis offset)\n".format(offset[c], c.upper())
+
+ # Add scale parametrization
+ if not planes:
+ planes = ["g17"]
+ if len(planes) > 1: # have G02 and G03 in several planes scale_x = scale_y = scale_z required
+ gcode += "#10 = 1 (Scale factor)\n"
+ scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
+ else:
+ gcode += "#10 = 1 ({} Scale factor)\n".format({"g17": "XY", "g18": "XZ", "g19": "YZ"}[planes[0]])
+ gcode += "#11 = 1 ({} Scale factor)\n".format({"g17": "Z", "g18": "Y", "g19": "X"}[planes[0]])
+ scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
+ if "g17" in planes:
+ scale["z"] = "#11"
+ scale["k"] = "#11"
+ if "g18" in planes:
+ scale["y"] = "#11"
+ scale["j"] = "#11"
+ if "g19" in planes:
+ scale["x"] = "#11"
+ scale["i"] = "#11"
+ # Add a scale
+ if "a" in coords:
+ gcode += "#12 = 1 (A axis scale)\n"
+ scale["a"] = "#12"
+
+ # Add feed parametrization
+ for f in feeds:
+ gcode += "{} = {:f} (Feed definition)\n".format(feeds[f], f)
+
+ # Parameterize Gcode
+ for s in self.gcode.split("\n"):
+ # feed replace :
+ r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s)
+ if r and len(r.group(3)) > 0:
+ s = re.sub(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", "F [{}]".format(feeds[float(r.group(2) + r.group(3))]), s)
+ # Coords XYZA replace
+ for c in "xyza":
+ r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
+ if r and len(r.group(4)) > 0:
+ s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}+{}]".format(scale[c], offset[c]), s)
+
+ # Coords IJKR replace
+ for c in "ijkr":
+ r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
+ if r and len(r.group(4)) > 0:
+ s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}]".format(scale[c]), s)
+
+ gcode += s + "\n"
+
+ self.gcode = gcode
+
+ def round_coordinates(self, parameters):
+ try:
+ round_ = int(parameters)
+ except:
+ self.error("Bad parameters for round. Round should be an integer! \n(Parameters: '{}')".format(parameters), "error")
+ gcode = ""
+ for s in self.gcode.split("\n"):
+ for a in "xyzijkaf":
+ r = re.search(r"(?i)(" + a + r")\s*(-?\s*(\d*\.?\d*))", s)
+ if r:
+
+ if r.group(2) != "":
+ s = re.sub(
+ r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)",
+ (r"\1 %0." + str(round_) + "f" if round_ > 0 else r"\1 %d") % round(float(r.group(2)), round_),
+ s)
+ gcode += s + "\n"
+ self.gcode = gcode
+
+ def scale(self, parameters):
+ parameters = parameters.split(",")
+ scale = [1., 1., 1., 1.]
+ try:
+ for i in range(len(parameters)):
+ if float(parameters[i]) == 0:
+ self.error("Bad parameters for scale. Scale should not be 0 at any axis! \n(Parameters: '{}')".format(parameters), "error")
+ scale[i] = float(parameters[i])
+ except:
+ self.error("Bad parameters for scale.\n(Parameters: '{}')".format(parameters), "error")
+ self.transform([0, 0, 0, 0], scale)
+
+ def move(self, parameters):
+ parameters = parameters.split(",")
+ move = [0., 0., 0., 0.]
+ try:
+ for i in range(len(parameters)):
+ move[i] = float(parameters[i])
+ except:
+ self.error("Bad parameters for move.\n(Parameters: '{}')".format(parameters), "error")
+ self.transform(move, [1., 1., 1., 1.])
+
+ def flip_axis(self, parameters):
+ parameters = parameters.lower()
+ axis = {"x": 1., "y": 1., "z": 1., "a": 1.}
+ for p in parameters:
+ if p in [",", " ", " ", "\r", "'", '"']:
+ continue
+ if p not in ["x", "y", "z", "a"]:
+ self.error("Bad parameters for flip_axis. Parameter should be string consists of 'xyza' \n(Parameters: '{}')".format(parameters), "error")
+ axis[p] = -axis[p]
+ self.scale("{:f},{:f},{:f},{:f}".format(axis["x"], axis["y"], axis["z"], axis["a"]))
+
+
+################################################################################
+# Polygon class
+################################################################################
+class Polygon(object):
+ def __init__(self, polygon=None):
+ self.polygon = [] if polygon is None else polygon[:]
+
+ def move(self, x, y):
+ for i in range(len(self.polygon)):
+ for j in range(len(self.polygon[i])):
+ self.polygon[i][j][0] += x
+ self.polygon[i][j][1] += y
+
+ def bounds(self):
+ minx = 1e400
+ miny = 1e400
+ maxx = -1e400
+ maxy = -1e400
+ for poly in self.polygon:
+ for p in poly:
+ if minx > p[0]:
+ minx = p[0]
+ if miny > p[1]:
+ miny = p[1]
+ if maxx < p[0]:
+ maxx = p[0]
+ if maxy < p[1]:
+ maxy = p[1]
+ return minx * 1, miny * 1, maxx * 1, maxy * 1
+
+ def width(self):
+ b = self.bounds()
+ return b[2] - b[0]
+
+ def rotate_(self, sin, cos):
+ self.polygon = [
+ [
+ [point[0] * cos - point[1] * sin, point[0] * sin + point[1] * cos] for point in subpoly
+ ]
+ for subpoly in self.polygon
+ ]
+
+ def rotate(self, a):
+ cos = math.cos(a)
+ sin = math.sin(a)
+ self.rotate_(sin, cos)
+
+ def drop_into_direction(self, direction, surface):
+ # Polygon is a list of simple polygons
+ # Surface is a polygon + line y = 0
+ # Direction is [dx,dy]
+ if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
+ return
+ if direction[0] ** 2 + direction[1] ** 2 < 1e-10:
+ return
+ direction = normalize(direction)
+ sin = direction[0]
+ cos = -direction[1]
+ self.rotate_(-sin, cos)
+ surface.rotate_(-sin, cos)
+ self.drop_down(surface, zerro_plane=False)
+ self.rotate_(sin, cos)
+ surface.rotate_(sin, cos)
+
+ def centroid(self):
+ centroids = []
+ sa = 0
+ for poly in self.polygon:
+ cx = 0
+ cy = 0
+ a = 0
+ for i in range(len(poly)):
+ [x1, y1] = poly[i - 1]
+ [x2, y2] = poly[i]
+ cx += (x1 + x2) * (x1 * y2 - x2 * y1)
+ cy += (y1 + y2) * (x1 * y2 - x2 * y1)
+ a += (x1 * y2 - x2 * y1)
+ a *= 3.
+ if abs(a) > 0:
+ cx /= a
+ cy /= a
+ sa += abs(a)
+ centroids += [[cx, cy, a]]
+ if sa == 0:
+ return [0., 0.]
+ cx = 0
+ cy = 0
+ for c in centroids:
+ cx += c[0] * c[2]
+ cy += c[1] * c[2]
+ cx /= sa
+ cy /= sa
+ return [cx, cy]
+
+ def drop_down(self, surface, zerro_plane=True):
+ # Polygon is a list of simple polygons
+ # Surface is a polygon + line y = 0
+ # Down means min y (0,-1)
+ if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
+ return
+ # Get surface top point
+ top = surface.bounds()[3]
+ if zerro_plane:
+ top = max(0, top)
+ # Get polygon bottom point
+ bottom = self.bounds()[1]
+ self.move(0, top - bottom + 10)
+ # Now get shortest distance from surface to polygon in positive x=0 direction
+ # Such distance = min(distance(vertex, edge)...) where edge from surface and
+ # vertex from polygon and vice versa...
+ dist = 1e300
+ for poly in surface.polygon:
+ for i in range(len(poly)):
+ for poly1 in self.polygon:
+ for i1 in range(len(poly1)):
+ st = poly[i - 1]
+ end = poly[i]
+ vertex = poly1[i1]
+ if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
+ if st[0] == end[0]:
+ d = min(vertex[1] - st[1], vertex[1] - end[1])
+ else:
+ d = vertex[1] - st[1] - (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
+ if dist > d:
+ dist = d
+ # and vice versa just change the sign because vertex now under the edge
+ st = poly1[i1 - 1]
+ end = poly1[i1]
+ vertex = poly[i]
+ if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
+ if st[0] == end[0]:
+ d = min(- vertex[1] + st[1], -vertex[1] + end[1])
+ else:
+ d = - vertex[1] + st[1] + (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
+ if dist > d:
+ dist = d
+
+ if zerro_plane and dist > 10 + top:
+ dist = 10 + top
+ self.move(0, -dist)
+
+ def draw(self, color="#075", width=.1, group=None):
+ csp = [csp_subpath_line_to([], poly + [poly[0]]) for poly in self.polygon]
+ draw_csp(csp, width=width, group=group)
+
+ def add(self, add):
+ if type(add) == type([]):
+ self.polygon += add[:]
+ else:
+ self.polygon += add.polygon[:]
+
+ def point_inside(self, p):
+ inside = False
+ for poly in self.polygon:
+ for i in range(len(poly)):
+ st = poly[i - 1]
+ end = poly[i]
+ if p == st or p == end:
+ return True # point is a vertex = point is on the edge
+ if st[0] > end[0]:
+ st, end = end, st # This will be needed to check that edge if open only at right end
+ c = (p[1] - st[1]) * (end[0] - st[0]) - (end[1] - st[1]) * (p[0] - st[0])
+ if st[0] <= p[0] < end[0]:
+ if c < 0:
+ inside = not inside
+ elif c == 0:
+ return True # point is on the edge
+ elif st[0] == end[0] == p[0] and (st[1] <= p[1] <= end[1] or end[1] <= p[1] <= st[1]): # point is on the edge
+ return True
+ return inside
+
+ def hull(self):
+ # Add vertices at all self intersection points.
+ hull = []
+ for i1 in range(len(self.polygon)):
+ poly1 = self.polygon[i1]
+ poly_ = []
+ for j1 in range(len(poly1)):
+ s = poly1[j1 - 1]
+ e = poly1[j1]
+ poly_ += [s]
+
+ # Check self intersections
+ for j2 in range(j1 + 1, len(poly1)):
+ s1 = poly1[j2 - 1]
+ e1 = poly1[j2]
+ int_ = line_line_intersection_points(s, e, s1, e1)
+ for p in int_:
+ if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
+ poly_ += [p]
+ # Check self intersections with other polys
+ for i2 in range(len(self.polygon)):
+ if i1 == i2:
+ continue
+ poly2 = self.polygon[i2]
+ for j2 in range(len(poly2)):
+ s1 = poly2[j2 - 1]
+ e1 = poly2[j2]
+ int_ = line_line_intersection_points(s, e, s1, e1)
+ for p in int_:
+ if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
+ poly_ += [p]
+ hull += [poly_]
+ # Create the dictionary containing all edges in both directions
+ edges = {}
+ for poly in self.polygon:
+ for i in range(len(poly)):
+ s = tuple(poly[i - 1])
+ e = tuple(poly[i])
+ if point_to_point_d2(e, s) < 0.000001:
+ continue
+ break_s = False
+ break_e = False
+ for p in edges:
+ if point_to_point_d2(p, s) < 0.000001:
+ break_s = True
+ s = p
+ if point_to_point_d2(p, e) < 0.000001:
+ break_e = True
+ e = p
+ if break_s and break_e:
+ break
+ l = point_to_point_d(s, e)
+ if not break_s and not break_e:
+ edges[s] = [[s, e, l]]
+ edges[e] = [[e, s, l]]
+ else:
+ if e in edges:
+ for edge in edges[e]:
+ if point_to_point_d2(edge[1], s) < 0.000001:
+ break
+ if point_to_point_d2(edge[1], s) > 0.000001:
+ edges[e] += [[e, s, l]]
+ else:
+ edges[e] = [[e, s, l]]
+ if s in edges:
+ for edge in edges[s]:
+ if point_to_point_d2(edge[1], e) < 0.000001:
+ break
+ if point_to_point_d2(edge[1], e) > 0.000001:
+ edges[s] += [[s, e, l]]
+ else:
+ edges[s] = [[s, e, l]]
+
+ def angle_quadrant(sin, cos):
+ # quadrants are (0,pi/2], (pi/2,pi], (pi,3*pi/2], (3*pi/2, 2*pi], i.e. 0 is in the 4-th quadrant
+ if sin > 0 and cos >= 0:
+ return 1
+ if sin >= 0 and cos < 0:
+ return 2
+ if sin < 0 and cos <= 0:
+ return 3
+ if sin <= 0 and cos > 0:
+ return 4
+
+ def angle_is_less(sin, cos, sin1, cos1):
+ # 0 = 2*pi is the largest angle
+ if [sin1, cos1] == [0, 1]:
+ return True
+ if [sin, cos] == [0, 1]:
+ return False
+ if angle_quadrant(sin, cos) > angle_quadrant(sin1, cos1):
+ return False
+ if angle_quadrant(sin, cos) < angle_quadrant(sin1, cos1):
+ return True
+ if sin >= 0 and cos > 0:
+ return sin < sin1
+ if sin > 0 and cos <= 0:
+ return sin > sin1
+ if sin <= 0 and cos < 0:
+ return sin > sin1
+ if sin < 0 and cos >= 0:
+ return sin < sin1
+
+ def get_closes_edge_by_angle(edges, last):
+ # Last edge is normalized vector of the last edge.
+ min_angle = [0, 1]
+ next = last
+ last_edge = [(last[0][0] - last[1][0]) / last[2], (last[0][1] - last[1][1]) / last[2]]
+ for p in edges:
+
+ cur = [(p[1][0] - p[0][0]) / p[2], (p[1][1] - p[0][1]) / p[2]]
+ cos = dot(cur, last_edge)
+ sin = cross(cur, last_edge)
+
+ if angle_is_less(sin, cos, min_angle[0], min_angle[1]):
+ min_angle = [sin, cos]
+ next = p
+
+ return next
+
+ # Join edges together into new polygon cutting the vertexes inside new polygon
+ self.polygon = []
+ len_edges = sum([len(edges[p]) for p in edges])
+ loops = 0
+
+ while len(edges) > 0:
+ poly = []
+ if loops > len_edges:
+ raise ValueError("Hull error")
+ loops += 1
+ # Find left most vertex.
+ start = (1e100, 1)
+ for edge in edges:
+ start = min(start, min(edges[edge]))
+ last = [(start[0][0] - 1, start[0][1]), start[0], 1]
+ first_run = True
+ loops1 = 0
+ while last[1] != start[0] or first_run:
+ first_run = False
+ if loops1 > len_edges:
+ raise ValueError("Hull error")
+ loops1 += 1
+ next = get_closes_edge_by_angle(edges[last[1]], last)
+
+ last = next
+ poly += [list(last[0])]
+ self.polygon += [poly]
+ # Remove all edges that are intersects new poly (any vertex inside new poly)
+ poly_ = Polygon([poly])
+ for p in edges.keys()[:]:
+ if poly_.point_inside(list(p)):
+ del edges[p]
+ self.draw(color="Green", width=1)
+
+
+################################################################################
+#
+# Gcodetools class
+#
+################################################################################
+
+class Gcodetools(inkex.EffectExtension):
+ multi_inx = True # XXX Remove this after refactoring
+
+ def export_gcode(self, gcode, no_headers=False):
+ if self.options.postprocessor != "" or self.options.postprocessor_custom != "":
+ postprocessor = Postprocessor(self.error)
+ postprocessor.gcode = gcode
+ if self.options.postprocessor != "":
+ postprocessor.process(self.options.postprocessor)
+ if self.options.postprocessor_custom != "":
+ postprocessor.process(self.options.postprocessor_custom)
+
+ if not no_headers:
+ postprocessor.gcode = self.header + postprocessor.gcode + self.footer
+
+ with open(os.path.join(self.options.directory, self.options.file), "w") as f:
+ f.write(postprocessor.gcode)
+
+ ################################################################################
+ # In/out paths:
+ # TODO move it to the bottom
+ ################################################################################
+ def tab_plasma_prepare_path(self):
+ self.get_info_plus()
+
+ def add_arc(sp1, sp2, end=False, l=10., r=10.):
+ if not end:
+ n = csp_normalized_normal(sp1, sp2, 0.)
+ return csp_reverse([arc_from_s_r_n_l(sp1[1], r, n, -l)])[0]
+ else:
+ n = csp_normalized_normal(sp1, sp2, 1.)
+ return arc_from_s_r_n_l(sp2[1], r, n, l)
+
+ def add_normal(sp1, sp2, end=False, l=10., r=10.):
+ # r is needed only for be compatible with add_arc
+ if not end:
+ n = csp_normalized_normal(sp1, sp2, 0.)
+ p = [n[0] * l + sp1[1][0], n[1] * l + sp1[1][1]]
+ return csp_subpath_line_to([], [p, sp1[1]])
+ else:
+ n = csp_normalized_normal(sp1, sp2, 1.)
+ p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
+ return csp_subpath_line_to([], [sp2[1], p])
+
+ def add_tangent(sp1, sp2, end=False, l=10., r=10.):
+ # r is needed only for be compatible with add_arc
+ if not end:
+ n = csp_normalized_slope(sp1, sp2, 0.)
+ p = [-n[0] * l + sp1[1][0], -n[1] * l + sp1[1][1]]
+ return csp_subpath_line_to([], [p, sp1[1]])
+ else:
+ n = csp_normalized_slope(sp1, sp2, 1.)
+ p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
+ return csp_subpath_line_to([], [sp2[1], p])
+
+ if not self.options.in_out_path and not self.options.plasma_prepare_corners and self.options.in_out_path_do_not_add_reference_point:
+ self.error("Warning! Extension is not said to do anything! Enable one of Create in-out paths or Prepare corners checkboxes or disable Do not add in-out reference point!")
+ return
+
+ # Add in-out-reference point if there is no one yet.
+ if ((len(self.in_out_reference_points) == 0 and self.options.in_out_path
+ or not self.options.in_out_path and not self.options.plasma_prepare_corners)
+ and not self.options.in_out_path_do_not_add_reference_point):
+ self.options.orientation_points_count = "in-out reference point"
+ #self.orientation()
+
+ if self.options.in_out_path or self.options.plasma_prepare_corners:
+ self.set_markers()
+ add_func = {"Round": add_arc, "Perpendicular": add_normal, "Tangent": add_tangent}[self.options.in_out_path_type]
+ if self.options.in_out_path_type == "Round" and self.options.in_out_path_len > self.options.in_out_path_radius * 3 / 2 * math.pi:
+ self.error("In-out len is to big for in-out radius will cropp it to be r*3/2*pi!")
+
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ self.selected_paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+
+ if self.selected_paths == {}:
+ self.error("Nothing is selected. Please select something.")
+ a = self.options.plasma_prepare_corners_tolerance
+ corner_tolerance = cross([1., 0.], [math.cos(a), math.sin(a)])
+
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ max_dist = self.transform_scalar(self.options.in_out_path_point_max_dist, layer, reverse=True)
+ l = self.transform_scalar(self.options.in_out_path_len, layer, reverse=True)
+ plasma_l = self.transform_scalar(self.options.plasma_prepare_corners_distance, layer, reverse=True)
+ r = self.transform_scalar(self.options.in_out_path_radius, layer, reverse=True)
+ l = min(l, r * 3 / 2 * math.pi)
+
+ for path in self.selected_paths[layer]:
+ csp = self.apply_transforms(path, path.path.to_superpath())
+ csp = csp_remove_zero_segments(csp)
+ res = []
+
+ for subpath in csp:
+ # Find closes point to in-out reference point
+ # If subpath is open skip this step
+ if self.options.in_out_path:
+ # split and reverse path for further add in-out points
+ if point_to_point_d2(subpath[0][1], subpath[-1][1]) < 1.e-10:
+ d = [1e100, 1, 1, 1.]
+ for p in self.in_out_reference_points:
+ d1 = csp_to_point_distance([subpath], p, dist_bounds=[0, max_dist])
+ if d1[0] < d[0]:
+ d = d1[:]
+ p_ = p
+ if d[0] < max_dist ** 2:
+ # Lets find is there any angles near this point to put in-out path in
+ # the angle if it's possible
+ # remove last node to make iterations easier
+ subpath[0][0] = subpath[-1][0]
+ del subpath[-1]
+ max_cross = [-1e100, None]
+ for j in range(len(subpath)):
+ sp1 = subpath[j - 2]
+ sp2 = subpath[j - 1]
+ sp3 = subpath[j]
+ if point_to_point_d2(sp2[1], p_) < max_dist ** 2:
+ s1 = csp_normalized_slope(sp1, sp2, 1.)
+ s2 = csp_normalized_slope(sp2, sp3, 0.)
+ max_cross = max(max_cross, [cross(s1, s2), j - 1])
+ # return back last point
+ subpath.append(subpath[0])
+ if max_cross[1] is not None and max_cross[0] > corner_tolerance:
+ # there's an angle near the point
+ j = max_cross[1]
+ if j < 0:
+ j -= 1
+ if j != 0:
+ subpath = csp_concat_subpaths(subpath[j:], subpath[:j + 1])
+ else:
+ # have to cut path's segment
+ d, i, j, t = d
+ sp1, sp2, sp3 = csp_split(subpath[j - 1], subpath[j], t)
+ subpath = csp_concat_subpaths([sp2, sp3], subpath[j:], subpath[:j], [sp1, sp2])
+
+ if self.options.plasma_prepare_corners:
+ # prepare corners
+ # find corners and add some nodes
+ # corner at path's start/end is ignored
+ res_ = [subpath[0]]
+ for sp2, sp3 in zip(subpath[1:], subpath[2:]):
+ sp1 = res_[-1]
+ s1 = csp_normalized_slope(sp1, sp2, 1.)
+ s2 = csp_normalized_slope(sp2, sp3, 0.)
+ if cross(s1, s2) > corner_tolerance:
+ # got a corner to process
+ S1 = P(s1)
+ S2 = P(s2)
+ N = (S1 - S2).unit() * plasma_l
+ SP2 = P(sp2[1])
+ P1 = (SP2 + N)
+ res_ += [
+ [sp2[0], sp2[1], (SP2 + S1 * plasma_l).to_list()],
+ [(P1 - N.ccw() / 2).to_list(), P1.to_list(), (P1 + N.ccw() / 2).to_list()],
+ [(SP2 - S2 * plasma_l).to_list(), sp2[1], sp2[2]]
+ ]
+ else:
+ res_ += [sp2]
+ res_ += [sp3]
+ subpath = res_
+ if self.options.in_out_path:
+ # finally add let's add in-out paths...
+ subpath = csp_concat_subpaths(
+ add_func(subpath[0], subpath[1], False, l, r),
+ subpath,
+ add_func(subpath[-2], subpath[-1], True, l, r)
+ )
+
+ res += [subpath]
+
+ if self.options.in_out_path_replace_original_path:
+ path.path = CubicSuperPath(self.apply_transforms(path, res, True))
+ else:
+ draw_csp(res, width=1, style=MARKER_STYLE["in_out_path_style"])
+
+ def add_arguments(self, pars):
+ add_argument = pars.add_argument
+ add_argument("-d", "--directory", default="/home/", help="Directory for gcode file")
+ add_argument("-f", "--filename", dest="file", default="-1.0", help="File name")
+ add_argument("--add-numeric-suffix-to-filename", type=inkex.Boolean, default=True, help="Add numeric suffix to filename")
+ add_argument("--Zscale", type=float, default="1.0", help="Scale factor Z")
+ add_argument("--Zoffset", type=float, default="0.0", help="Offset along Z")
+ add_argument("-s", "--Zsafe", type=float, default="0.5", help="Z above all obstacles")
+ add_argument("-z", "--Zsurface", type=float, default="0.0", help="Z of the surface")
+ add_argument("-c", "--Zdepth", type=float, default="-0.125", help="Z depth of cut")
+ add_argument("--Zstep", type=float, default="-0.125", help="Z step of cutting")
+ add_argument("-p", "--feed", type=float, default="4.0", help="Feed rate in unit/min")
+
+ add_argument("--biarc-tolerance", type=float, default="1", help="Tolerance used when calculating biarc interpolation.")
+ add_argument("--biarc-max-split-depth", type=int, default="4", help="Defines maximum depth of splitting while approximating using biarcs.")
+ add_argument("--path-to-gcode-order", default="path by path", help="Defines cutting order path by path or layer by layer.")
+ add_argument("--path-to-gcode-depth-function", default="zd", help="Path to gcode depth function.")
+ add_argument("--path-to-gcode-sort-paths", type=inkex.Boolean, default=True, help="Sort paths to reduce rapid distance.")
+ add_argument("--comment-gcode", default="", help="Comment Gcode")
+ add_argument("--comment-gcode-from-properties", type=inkex.Boolean, default=False, help="Get additional comments from Object Properties")
+
+ add_argument("--tool-diameter", type=float, default="3", help="Tool diameter used for area cutting")
+ add_argument("--max-area-curves", type=int, default="100", help="Maximum area curves for each area")
+ add_argument("--area-inkscape-radius", type=float, default="0", help="Area curves overlapping (depends on tool diameter [0, 0.9])")
+ add_argument("--area-tool-overlap", type=float, default="-10", help="Radius for preparing curves using inkscape")
+ add_argument("--unit", default="G21 (All units in mm)", help="Units")
+ add_argument("--active-tab", type=self.arg_method('tab'), default=self.tab_help, help="Defines which tab is active")
+
+ add_argument("--area-fill-angle", type=float, default="0", help="Fill area with lines heading this angle")
+ add_argument("--area-fill-shift", type=float, default="0", help="Shift the lines by tool d * shift")
+ add_argument("--area-fill-method", default="zig-zag", help="Filling method either zig-zag or spiral")
+
+ add_argument("--area-find-artefacts-diameter", type=float, default="1", help="Artefacts seeking radius")
+ add_argument("--area-find-artefacts-action", default="mark with an arrow", help="Artefacts action type")
+
+ add_argument("--auto_select_paths", type=inkex.Boolean, default=True, help="Select all paths if nothing is selected.")
+
+ add_argument("--loft-distances", default="10", help="Distances between paths.")
+ add_argument("--loft-direction", default="crosswise", help="Direction of loft's interpolation.")
+ add_argument("--loft-interpolation-degree", type=float, default="2", help="Which interpolation use to loft the paths smooth interpolation or staright.")
+
+ add_argument("--min-arc-radius", type=float, default=".1", help="All arc having radius less than minimum will be considered as straight line")
+
+ add_argument("--engraving-sharp-angle-tollerance", type=float, default="150", help="All angles thar are less than engraving-sharp-angle-tollerance will be thought sharp")
+ add_argument("--engraving-max-dist", type=float, default="10", help="Distance from original path where engraving is not needed (usually it's cutting tool diameter)")
+ add_argument("--engraving-newton-iterations", type=int, default="4", help="Number of sample points used to calculate distance")
+ add_argument("--engraving-draw-calculation-paths", type=inkex.Boolean, default=False, help="Draw additional graphics to debug engraving path")
+ add_argument("--engraving-cutter-shape-function", default="w", help="Cutter shape function z(w). Ex. cone: w. ")
+
+ add_argument("--lathe-width", type=float, default=10., help="Lathe width")
+ add_argument("--lathe-fine-cut-width", type=float, default=1., help="Fine cut width")
+ add_argument("--lathe-fine-cut-count", type=int, default=1., help="Fine cut count")
+ add_argument("--lathe-create-fine-cut-using", default="Move path", help="Create fine cut using")
+ add_argument("--lathe-x-axis-remap", default="X", help="Lathe X axis remap")
+ add_argument("--lathe-z-axis-remap", default="Z", help="Lathe Z axis remap")
+
+ add_argument("--lathe-rectangular-cutter-width", type=float, default="4", help="Rectangular cutter width")
+
+ add_argument("--create-log", type=inkex.Boolean, dest="log_create_log", default=False, help="Create log files")
+ add_argument("--log-filename", default='', help="Create log files")
+
+ add_argument("--orientation-points-count", default="2", help="Orientation points count")
+ add_argument("--tools-library-type", default='cylinder cutter', help="Create tools definition")
+
+ add_argument("--dxfpoints-action", default='replace', help="dxfpoint sign toggle")
+
+ add_argument("--help-language", default='http://www.cnc-club.ru/forum/viewtopic.php?f=33&t=35', help="Open help page in webbrowser.")
+
+ add_argument("--offset-radius", type=float, default=10., help="Offset radius")
+ add_argument("--offset-step", type=float, default=10., help="Offset step")
+ add_argument("--offset-draw-clippend-path", type=inkex.Boolean, default=False, help="Draw clipped path")
+ add_argument("--offset-just-get-distance", type=inkex.Boolean, default=False, help="Don't do offset just get distance")
+
+ add_argument("--postprocessor", default='', help="Postprocessor command.")
+ add_argument("--postprocessor-custom", default='', help="Postprocessor custom command.")
+
+ add_argument("--graffiti-max-seg-length", type=float, default=1., help="Graffiti maximum segment length.")
+ add_argument("--graffiti-min-radius", type=float, default=10., help="Graffiti minimal connector's radius.")
+ add_argument("--graffiti-start-pos", default="(0;0)", help="Graffiti Start position (x;y).")
+ add_argument("--graffiti-create-linearization-preview", type=inkex.Boolean, default=True, help="Graffiti create linearization preview.")
+ add_argument("--graffiti-create-preview", type=inkex.Boolean, default=True, help="Graffiti create preview.")
+ add_argument("--graffiti-preview-size", type=int, default=800, help="Graffiti preview's size.")
+ add_argument("--graffiti-preview-emmit", type=int, default=800, help="Preview's paint emmit (pts/s).")
+
+ add_argument("--in-out-path", type=inkex.Boolean, default=True, help="Create in-out paths")
+ add_argument("--in-out-path-do-not-add-reference-point", type=inkex.Boolean, default=False, help="Just add reference in-out point")
+ add_argument("--in-out-path-point-max-dist", type=float, default=10., help="In-out path max distance to reference point")
+ add_argument("--in-out-path-type", default="Round", help="In-out path type")
+ add_argument("--in-out-path-len", type=float, default=10., help="In-out path length")
+ add_argument("--in-out-path-replace-original-path", type=inkex.Boolean, default=False, help="Replace original path")
+ add_argument("--in-out-path-radius", type=float, default=10., help="In-out path radius for round path")
+
+ add_argument("--plasma-prepare-corners", type=inkex.Boolean, default=True, help="Prepare corners")
+ add_argument("--plasma-prepare-corners-distance", type=float, default=10., help="Stepout distance for corners")
+ add_argument("--plasma-prepare-corners-tolerance", type=float, default=10., help="Maximum angle for corner (0-180 deg)")
+
+ def __init__(self):
+ super(Gcodetools, self).__init__()
+ self.default_tool = {
+ "name": "Default tool",
+ "id": "default tool",
+ "diameter": 10.,
+ "shape": "10",
+ "penetration angle": 90.,
+ "penetration feed": 100.,
+ "depth step": 1.,
+ "feed": 400.,
+ "in trajectotry": "",
+ "out trajectotry": "",
+ "gcode before path": "",
+ "gcode after path": "",
+ "sog": "",
+ "spinlde rpm": "",
+ "CW or CCW": "",
+ "tool change gcode": " ",
+ "4th axis meaning": " ",
+ "4th axis scale": 1.,
+ "4th axis offset": 0.,
+ "passing feed": "800",
+ "fine feed": "800",
+ }
+ self.tools_field_order = [
+ 'name',
+ 'id',
+ 'diameter',
+ 'feed',
+ 'shape',
+ 'penetration angle',
+ 'penetration feed',
+ "passing feed",
+ 'depth step',
+ "in trajectotry",
+ "out trajectotry",
+ "gcode before path",
+ "gcode after path",
+ "sog",
+ "spinlde rpm",
+ "CW or CCW",
+ "tool change gcode",
+ ]
+
+ def parse_curve(self, p, layer, w=None, f=None):
+ c = []
+ if len(p) == 0:
+ return []
+ p = self.transform_csp(p, layer)
+
+ # Sort to reduce Rapid distance
+ k = list(range(1, len(p)))
+ keys = [0]
+ while len(k) > 0:
+ end = p[keys[-1]][-1][1]
+ dist = None
+ for i in range(len(k)):
+ start = p[k[i]][0][1]
+ dist = max((-((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2), i), dist)
+ keys += [k[dist[1]]]
+ del k[dist[1]]
+ for k in keys:
+ subpath = p[k]
+ c += [[[subpath[0][1][0], subpath[0][1][1]], 'move', 0, 0]]
+ for i in range(1, len(subpath)):
+ sp1 = [[subpath[i - 1][j][0], subpath[i - 1][j][1]] for j in range(3)]
+ sp2 = [[subpath[i][j][0], subpath[i][j][1]] for j in range(3)]
+ c += biarc(sp1, sp2, 0, 0) if w is None else biarc(sp1, sp2, -f(w[k][i - 1]), -f(w[k][i]))
+ c += [[[subpath[-1][1][0], subpath[-1][1][1]], 'end', 0, 0]]
+ return c
+
+ ################################################################################
+ # Draw csp
+ ################################################################################
+
+ def draw_csp(self, csp, layer=None, group=None, fill='none', stroke='#178ade', width=0.354, style=None):
+ if layer is not None:
+ csp = self.transform_csp(csp, layer, reverse=True)
+ if group is None and layer is None:
+ group = self.document.getroot()
+ elif group is None and layer is not None:
+ group = layer
+ csp = self.apply_transforms(group, csp, reverse=True)
+ if style is not None:
+ return draw_csp(csp, group=group, style=style)
+ else:
+ return draw_csp(csp, group=group, fill=fill, stroke=stroke, width=width)
+
+ def draw_curve(self, curve, layer, group=None, style=MARKER_STYLE["biarc_style"]):
+ self.set_markers()
+
+ for i in [0, 1]:
+ sid = 'biarc{}_r'.format(i)
+ style[sid] = style['biarc{}'.format(i)].copy()
+ style[sid]["marker-start"] = "url(#DrawCurveMarker_r)"
+ del style[sid]["marker-end"]
+
+ if group is None:
+ group = self.layers[min(1, len(self.layers) - 1)].add(Group(gcodetools="Preview group"))
+ if not hasattr(self, "preview_groups"):
+ self.preview_groups = {layer: group}
+ elif layer not in self.preview_groups:
+ self.preview_groups[layer] = group
+ group = self.preview_groups[layer]
+
+ s = ''
+ arcn = 0
+
+ transform = self.get_transforms(group)
+ if transform:
+ transform = self.reverse_transform(transform)
+ transform = str(Transform(transform))
+
+ a = [0., 0.]
+ b = [1., 0.]
+ c = [0., 1.]
+ k = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
+ a = self.transform(a, layer, True)
+ b = self.transform(b, layer, True)
+ c = self.transform(c, layer, True)
+ if ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])) * k > 0:
+ reverse_angle = 1
+ else:
+ reverse_angle = -1
+ for sk in curve:
+ si = sk[:]
+ si[0] = self.transform(si[0], layer, True)
+ si[2] = self.transform(si[2], layer, True) if type(si[2]) == type([]) and len(si[2]) == 2 else si[2]
+
+ if s != '':
+ if s[1] == 'line':
+ elem = group.add(PathElement(gcodetools="Preview"))
+ elem.transform = transform
+ elem.style = style['line']
+ elem.path = 'M {},{} L {},{}'.format(s[0][0], s[0][1], si[0][0], si[0][1])
+ elif s[1] == 'arc':
+ arcn += 1
+ sp = s[0]
+ c = s[2]
+ s[3] = s[3] * reverse_angle
+
+ a = ((P(si[0]) - P(c)).angle() - (P(s[0]) - P(c)).angle()) % TAU # s[3]
+ if s[3] * a < 0:
+ if a > 0:
+ a = a - TAU
+ else:
+ a = TAU + a
+ r = math.sqrt((sp[0] - c[0]) ** 2 + (sp[1] - c[1]) ** 2)
+ a_st = (math.atan2(sp[0] - c[0], - (sp[1] - c[1])) - math.pi / 2) % (math.pi * 2)
+ if a > 0:
+ a_end = a_st + a
+ st = style['biarc{}'.format(arcn % 2)]
+ else:
+ a_end = a_st * 1
+ a_st = a_st + a
+ st = style['biarc{}_r'.format(arcn % 2)]
+
+ elem = group.add(PathElement.arc(c, r, start=a_st, end=a_end,
+ open=True, gcodetools="Preview"))
+ elem.transform = transform
+ elem.style = st
+
+ s = si
+
+ def check_dir(self):
+ print_("Checking directory: '{}'".format(self.options.directory))
+ if os.path.isdir(self.options.directory):
+ if os.path.isfile(os.path.join(self.options.directory, 'header')):
+ with open(os.path.join(self.options.directory, 'header')) as f:
+ self.header = f.read()
+ else:
+ self.header = defaults['header']
+ if os.path.isfile(os.path.join(self.options.directory, 'footer')):
+ with open(os.path.join(self.options.directory, 'footer')) as f:
+ self.footer = f.read()
+ else:
+ self.footer = defaults['footer']
+ self.header += self.options.unit + "\n"
+ else:
+ self.error("Directory does not exist! Please specify existing directory at Preferences tab!", "error")
+ return False
+
+ if self.options.add_numeric_suffix_to_filename:
+ dir_list = os.listdir(self.options.directory)
+ if "." in self.options.file:
+ r = re.match(r"^(.*)(\..*)$", self.options.file)
+ ext = r.group(2)
+ name = r.group(1)
+ else:
+ ext = ""
+ name = self.options.file
+ max_n = 0
+ for s in dir_list:
+ r = re.match(r"^{}_0*(\d+){}$".format(re.escape(name), re.escape(ext)), s)
+ if r:
+ max_n = max(max_n, int(r.group(1)))
+ filename = name + "_" + ("0" * (4 - len(str(max_n + 1))) + str(max_n + 1)) + ext
+ self.options.file = filename
+
+ try:
+ with open(os.path.join(self.options.directory, self.options.file), "w") as f:
+ pass
+ except:
+ self.error("Can not write to specified file!\n{}".format(os.path.join(self.options.directory, self.options.file)), "error")
+ return False
+ return True
+
+ ################################################################################
+ #
+ # Generate Gcode
+ # Generates Gcode on given curve.
+ #
+ # Curve definition [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]]
+ #
+ ################################################################################
+ def generate_gcode(self, curve, layer, depth):
+ Zauto_scale = self.Zauto_scale[layer]
+ tool = self.tools[layer][0]
+ g = ""
+
+ def c(c):
+ c = [c[i] if i < len(c) else None for i in range(6)]
+ if c[5] == 0:
+ c[5] = None
+ s = [" X", " Y", " Z", " I", " J", " K"]
+ s1 = ["", "", "", "", "", ""]
+ m = [1, 1, self.options.Zscale * Zauto_scale, 1, 1, self.options.Zscale * Zauto_scale]
+ a = [0, 0, self.options.Zoffset, 0, 0, 0]
+ r = ''
+ for i in range(6):
+ if c[i] is not None:
+ r += s[i] + ("{:f}".format(c[i] * m[i] + a[i])) + s1[i]
+ return r
+
+ def calculate_angle(a, current_a):
+ return min(
+ [abs(a - current_a % TAU + TAU), a + current_a - current_a % TAU + TAU],
+ [abs(a - current_a % TAU - TAU), a + current_a - current_a % TAU - TAU],
+ [abs(a - current_a % TAU), a + current_a - current_a % TAU])[1]
+
+ if len(curve) == 0:
+ return ""
+
+ try:
+ self.last_used_tool is None
+ except:
+ self.last_used_tool = None
+ print_("working on curve")
+ print_(curve)
+
+ if tool != self.last_used_tool:
+ g += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", tool["name"]))) + tool["tool change gcode"] + "\n"
+
+ lg = 'G00'
+ zs = self.options.Zsafe
+ f = " F{:f}".format(tool['feed'])
+ current_a = 0
+ go_to_safe_distance = "G00" + c([None, None, zs]) + "\n"
+ penetration_feed = " F{}".format(tool['penetration feed'])
+ for i in range(1, len(curve)):
+ # Creating Gcode for curve between s=curve[i-1] and si=curve[i] start at s[0] end at s[4]=si[0]
+ s = curve[i - 1]
+ si = curve[i]
+ feed = f if lg not in ['G01', 'G02', 'G03'] else ''
+ if s[1] == 'move':
+ g += go_to_safe_distance + "G00" + c(si[0]) + "\n" + tool['gcode before path'] + "\n"
+ lg = 'G00'
+ elif s[1] == 'end':
+ g += go_to_safe_distance + tool['gcode after path'] + "\n"
+ lg = 'G00'
+ elif s[1] == 'line':
+ if tool['4th axis meaning'] == "tangent knife":
+ a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1])
+ a = calculate_angle(a, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ if lg == "G00":
+ g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
+ g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
+ lg = 'G01'
+ elif s[1] == 'arc':
+ r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
+ if tool['4th axis meaning'] == "tangent knife":
+ if s[3] < 0: # CW
+ a1 = atan2(s[2][1] - s[0][1], -s[2][0] + s[0][0]) + math.pi
+ else: # CCW
+ a1 = atan2(-s[2][1] + s[0][1], s[2][0] - s[0][0]) + math.pi
+ a = calculate_angle(a1, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ axis4 = " A{}".format((current_a + s[3]) * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = current_a + s[3]
+ else:
+ axis4 = ""
+ if lg == "G00":
+ g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
+ if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
+ r1 = (P(s[0]) - P(s[2]))
+ r2 = (P(si[0]) - P(s[2]))
+ if abs(r1.mag() - r2.mag()) < 0.001:
+ g += ("G02" if s[3] < 0 else "G03") + c(si[0] + [s[5][1] + depth, (s[2][0] - s[0][0]), (s[2][1] - s[0][1])]) + feed + axis4 + "\n"
+ else:
+ r = (r1.mag() + r2.mag()) / 2
+ g += ("G02" if s[3] < 0 else "G03") + c(si[0] + [s[5][1] + depth]) + " R{:f}".format(r) + feed + axis4 + "\n"
+ lg = 'G02'
+ else:
+ if tool['4th axis meaning'] == "tangent knife":
+ a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1]) + math.pi
+ a = calculate_angle(a, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
+ lg = 'G01'
+ if si[1] == 'end':
+ g += go_to_safe_distance + tool['gcode after path'] + "\n"
+ return g
+
+ def get_transforms(self, g):
+ root = self.document.getroot()
+ trans = []
+ while g != root:
+ if 'transform' in g.keys():
+ t = g.get('transform')
+ t = Transform(t).matrix
+ trans = (Transform(t) * Transform(trans)).matrix if trans != [] else t
+
+ print_(trans)
+ g = g.getparent()
+ return trans
+
+ def reverse_transform(self, transform):
+ trans = numpy.array(transform + ([0, 0, 1],))
+ if numpy.linalg.det(trans) != 0:
+ trans = numpy.linalg.inv(trans).tolist()[:2]
+ return trans
+ else:
+ return transform
+
+ def apply_transforms(self, g, csp, reverse=False):
+ trans = self.get_transforms(g)
+ if trans:
+ if not reverse:
+ # TODO: This was applyTransformToPath but was deprecated. Candidate for refactoring.
+ for comp in csp:
+ for ctl in comp:
+ for pt in ctl:
+ pt[0], pt[1] = Transform(trans).apply_to_point(pt)
+
+ else:
+ # TODO: This was applyTransformToPath but was deprecated. Candidate for refactoring.
+ for comp in csp:
+ for ctl in comp:
+ for pt in ctl:
+ pt[0], pt[1] = Transform(self.reverse_transform(trans)).apply_to_point(pt)
+ return csp
+
+ def transform_scalar(self, x, layer, reverse=False):
+ return self.transform([x, 0], layer, reverse)[0] - self.transform([0, 0], layer, reverse)[0]
+
+ def transform(self, source_point, layer, reverse=False):
+ if layer not in self.transform_matrix:
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.orientation_points:
+ break
+ if self.layers[i] not in self.orientation_points:
+ self.error(f"Orientation points for '{layer.label}' layer have not been found! Please add orientation points using Orientation tab!", "error")
+ elif self.layers[i] in self.transform_matrix:
+ self.transform_matrix[layer] = self.transform_matrix[self.layers[i]]
+ self.Zcoordinates[layer] = self.Zcoordinates[self.layers[i]]
+ else:
+ orientation_layer = self.layers[i]
+ if len(self.orientation_points[orientation_layer]) > 1:
+ self.error(f"There are more than one orientation point groups in '{orientation_layer.label}' layer")
+ points = self.orientation_points[orientation_layer][0]
+ if len(points) == 2:
+ points += [[[(points[1][0][1] - points[0][0][1]) + points[0][0][0], -(points[1][0][0] - points[0][0][0]) + points[0][0][1]], [-(points[1][1][1] - points[0][1][1]) + points[0][1][0], points[1][1][0] - points[0][1][0] + points[0][1][1]]]]
+ if len(points) == 3:
+ print_("Layer '{orientation_layer.label}' Orientation points: ")
+ for point in points:
+ print_(point)
+ # Zcoordinates definition taken from Orientatnion point 1 and 2
+ self.Zcoordinates[layer] = [max(points[0][1][2], points[1][1][2]), min(points[0][1][2], points[1][1][2])]
+ matrix = numpy.array([
+ [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1],
+ [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1],
+ [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1]
+ ])
+
+ if numpy.linalg.det(matrix) != 0:
+ m = numpy.linalg.solve(matrix,
+ numpy.array(
+ [[points[0][1][0]], [points[0][1][1]], [1], [points[1][1][0]], [points[1][1][1]], [1], [points[2][1][0]], [points[2][1][1]], [1]]
+ )
+ ).tolist()
+ self.transform_matrix[layer] = [[m[j * 3 + i][0] for i in range(3)] for j in range(3)]
+
+ else:
+ self.error("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)", "error")
+ else:
+ self.error("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)", "error")
+
+ self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist()
+ print_(f"\n Layer '{layer.label}' transformation matrixes:")
+ print_(self.transform_matrix)
+ print_(self.transform_matrix_reverse)
+
+ # Zautoscale is obsolete
+ self.Zauto_scale[layer] = 1
+ print_("Z automatic scale = {} (computed according orientation points)".format(self.Zauto_scale[layer]))
+
+ x = source_point[0]
+ y = source_point[1]
+ if not reverse:
+ t = self.transform_matrix[layer]
+ else:
+ t = self.transform_matrix_reverse[layer]
+ return [t[0][0] * x + t[0][1] * y + t[0][2], t[1][0] * x + t[1][1] * y + t[1][2]]
+
+ def transform_csp(self, csp_, layer, reverse=False):
+ csp = [[[csp_[i][j][0][:], csp_[i][j][1][:], csp_[i][j][2][:]] for j in range(len(csp_[i]))] for i in range(len(csp_))]
+ for i in xrange(len(csp)):
+ for j in xrange(len(csp[i])):
+ for k in xrange(len(csp[i][j])):
+ csp[i][j][k] = self.transform(csp[i][j][k], layer, reverse)
+ return csp
+
+ def error(self, s, msg_type="warning"):
+ """
+ Errors handling function
+ warnings are printed into log file and warning message is displayed but
+ extension continues working,
+ errors causes log and execution is halted
+ """
+ if msg_type == "warning":
+ print_(s)
+ inkex.errormsg(s + "\n")
+
+ elif msg_type == "error":
+ print_(s)
+ raise inkex.AbortExtension(s)
+
+ else:
+ print_("Unknown message type: {}".format(msg_type))
+ print_(s)
+ raise inkex.AbortExtension(s)
+
+ ################################################################################
+ # Set markers
+ ################################################################################
+ def set_markers(self):
+ """Make sure all markers are available"""
+ def ensure_marker(elem_id, x=-4, polA='', polB='-', fill='#000044'):
+ if self.svg.getElementById(elem_id) is None:
+ marker = self.svg.defs.add(Marker(
+ id=elem_id, orient="auto", refX=str(x), refY="-1.687441",
+ style="overflow:visible"))
+ path = marker.add(PathElement(
+ d="m {0}4.588864,-1.687441 0.0,0.0 L {0}9.177728,0.0 "\
+ "c {1}0.73311,-0.996261 {1}0.728882,-2.359329 0.0,-3.374882"\
+ .format(polA, polB)))
+ path.style = "fill:{};fill-rule:evenodd;stroke:none;".format(fill)
+
+ ensure_marker("CheckToolsAndOPMarker")
+ ensure_marker("DrawCurveMarker")
+ ensure_marker("DrawCurveMarker_r", x=4, polA='-', polB='')
+ ensure_marker("InOutPathMarker", fill='#0072a7')
+
+ def get_info(self):
+ """Get Gcodetools info from the svg"""
+ self.selected_paths = {}
+ self.paths = {}
+ self.tools = {}
+ self.orientation_points = {}
+ self.graffiti_reference_points = {}
+ self.layers = [self.document.getroot()]
+ self.Zcoordinates = {}
+ self.transform_matrix = {}
+ self.transform_matrix_reverse = {}
+ self.Zauto_scale = {}
+ self.in_out_reference_points = []
+ self.my3Dlayer = None
+
+ def recursive_search(g, layer, selected=False):
+ items = g.getchildren()
+ items.reverse()
+ for i in items:
+ if selected:
+ self.svg.selected[i.get("id")] = i
+ if isinstance(i, Layer):
+ if i.label == '3D':
+ self.my3Dlayer = i
+ else:
+ self.layers += [i]
+ recursive_search(i, i)
+
+ elif i.get('gcodetools') == "Gcodetools orientation group":
+ points = self.get_orientation_points(i)
+ if points is not None:
+ self.orientation_points[layer] = self.orientation_points[layer] + [points[:]] if layer in self.orientation_points else [points[:]]
+ print_(f"Found orientation points in '{layer.label}' layer: {points}")
+ else:
+ self.error(f"Warning! Found bad orientation points in '{layer.label}' layer. Resulting Gcode could be corrupt!")
+
+ # Need to recognise old files ver 1.6.04 and earlier
+ elif i.get("gcodetools") == "Gcodetools tool definition" or i.get("gcodetools") == "Gcodetools tool definition":
+ tool = self.get_tool(i)
+ self.tools[layer] = self.tools[layer] + [tool.copy()] if layer in self.tools else [tool.copy()]
+ print_(f"Found tool in '{layer.label}' layer: {tool}")
+
+ elif i.get("gcodetools") == "Gcodetools graffiti reference point":
+ point = self.get_graffiti_reference_points(i)
+ if point:
+ self.graffiti_reference_points[layer] = self.graffiti_reference_points[layer] + [point[:]] if layer in self.graffiti_reference_points else [point]
+ else:
+ self.error(f"Warning! Found bad graffiti reference point in '{layer.label}' layer. Resulting Gcode could be corrupt!")
+
+ elif isinstance(i, inkex.PathElement):
+ if "gcodetools" not in i.keys():
+ self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i]
+ if i.get("id") in self.svg.selected.ids:
+ self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i]
+
+ elif i.get("gcodetools") == "In-out reference point group":
+ items_ = i.getchildren()
+ items_.reverse()
+ for j in items_:
+ if j.get("gcodetools") == "In-out reference point":
+ self.in_out_reference_points.append(self.apply_transforms(j, j.path.to_superpath())[0][0][1])
+
+ elif isinstance(i, inkex.Group):
+ recursive_search(i, layer, (i.get("id") in self.svg.selected))
+
+ elif i.get("id") in self.svg.selected:
+ # xgettext:no-pango-format
+ self.error("This extension works with Paths and Dynamic Offsets and groups of them only! "
+ "All other objects will be ignored!\n"
+ "Solution 1: press Path->Object to path or Shift+Ctrl+C.\n"
+ "Solution 2: Path->Dynamic offset or Ctrl+J.\n"
+ "Solution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file.")
+
+ recursive_search(self.document.getroot(), self.document.getroot())
+
+ if len(self.layers) == 1:
+ self.error("Document has no layers! Add at least one layer using layers panel (Ctrl+Shift+L)", "error")
+ root = self.document.getroot()
+
+ if root in self.selected_paths or root in self.paths:
+ self.error("Warning! There are some paths in the root of the document, but not in any layer! Using bottom-most layer for them.")
+
+ if root in self.selected_paths:
+ if self.layers[-1] in self.selected_paths:
+ self.selected_paths[self.layers[-1]] += self.selected_paths[root][:]
+ else:
+ self.selected_paths[self.layers[-1]] = self.selected_paths[root][:]
+ del self.selected_paths[root]
+
+ if root in self.paths:
+ if self.layers[-1] in self.paths:
+ self.paths[self.layers[-1]] += self.paths[root][:]
+ else:
+ self.paths[self.layers[-1]] = self.paths[root][:]
+ del self.paths[root]
+
+ def get_orientation_points(self, g):
+ items = g.getchildren()
+ items.reverse()
+ p2 = []
+ p3 = []
+ p = None
+ for i in items:
+ if isinstance(i, inkex.Group):
+ if i.get("gcodetools") == "Gcodetools orientation point (2 points)":
+ p2 += [i]
+ if i.get("gcodetools") == "Gcodetools orientation point (3 points)":
+ p3 += [i]
+ if len(p2) == 2:
+ p = p2
+ elif len(p3) == 3:
+ p = p3
+ if p is None:
+ return None
+ points = []
+ for i in p:
+ point = [[], []]
+ for node in i:
+ if node.get('gcodetools') == "Gcodetools orientation point arrow":
+ csp = node.path.transform(node.composed_transform()).to_superpath()
+ point[0] = csp[0][0][1]
+ if node.get('gcodetools') == "Gcodetools orientation point text":
+ inkex.errormsg(node.get_text())
+ r = re.match(r'(?i)\s*\(\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*\)\s*', node.get_text())
+ point[1] = [float(r.group(1)), float(r.group(2)), float(r.group(3))]
+ if point[0] != [] and point[1] != []:
+ points += [point]
+ if len(points) == len(p2) == 2 or len(points) == len(p3) == 3:
+ return points
+ else:
+ return None
+
+ def get_graffiti_reference_points(self, g):
+ point = [[], '']
+ for node in g:
+ if node.get('gcodetools') == "Gcodetools graffiti reference point arrow":
+ point[0] = self.apply_transforms(node, node.path.to_superpath())[0][0][1]
+ if node.get('gcodetools') == "Gcodetools graffiti reference point text":
+ point[1] = node.get_text()
+ if point[0] != [] and point[1] != '':
+ return point
+ else:
+ return []
+
+ def get_tool(self, g):
+ tool = self.default_tool.copy()
+ tool["self_group"] = g
+ for i in g:
+ # Get parameters
+ if i.get("gcodetools") == "Gcodetools tool background":
+ tool["style"] = dict(i.style)
+ elif i.get("gcodetools") == "Gcodetools tool parameter":
+ key = None
+ value = None
+ for j in i:
+ # need to recognise old tools from ver 1.6.04
+ if j.get("gcodetools") == "Gcodetools tool definition field name" or j.get("gcodetools") == "Gcodetools tool defention field name":
+ key = j.get_text()
+ if j.get("gcodetools") == "Gcodetools tool definition field value" or j.get("gcodetools") == "Gcodetools tool defention field value":
+ value = j.get_text()
+ if value == "(None)":
+ value = ""
+ if value is None or key is None:
+ continue
+ if key in self.default_tool.keys():
+ try:
+ tool[key] = type(self.default_tool[key])(value)
+ except:
+ tool[key] = self.default_tool[key]
+ self.error("Warning! Tool's and default tool's parameter's ({}) types are not the same ( type('{}') != type('{}') ).".format(key, value, self.default_tool[key]))
+ else:
+ tool[key] = value
+ self.error("Warning! Tool has parameter that default tool has not ( '{}': '{}' ).".format(key, value))
+ return tool
+
+ def set_tool(self, layer):
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.tools:
+ break
+ if self.layers[i] in self.tools:
+ if self.layers[i] != layer:
+ self.tools[layer] = self.tools[self.layers[i]]
+ if len(self.tools[layer]) > 1:
+ label = self.layers[i].label
+ self.error(f"Layer '{label}' contains more than one tool!")
+ return self.tools[layer]
+ else:
+ self.error(f"Can not find tool for '{layer.label}' layer! Please add one with Tools library tab!", "error")
+
+ ################################################################################
+ #
+ # Path to Gcode
+ #
+ ################################################################################
+ def tab_path_to_gcode(self):
+ self.get_info_plus()
+ def get_boundaries(points):
+ minx = None
+ miny = None
+ maxx = None
+ maxy = None
+ out = [[], [], [], []]
+ for p in points:
+ if minx == p[0]:
+ out[0] += [p]
+ if minx is None or p[0] < minx:
+ minx = p[0]
+ out[0] = [p]
+
+ if miny == p[1]:
+ out[1] += [p]
+ if miny is None or p[1] < miny:
+ miny = p[1]
+ out[1] = [p]
+
+ if maxx == p[0]:
+ out[2] += [p]
+ if maxx is None or p[0] > maxx:
+ maxx = p[0]
+ out[2] = [p]
+
+ if maxy == p[1]:
+ out[3] += [p]
+ if maxy is None or p[1] > maxy:
+ maxy = p[1]
+ out[3] = [p]
+ return out
+
+ def remove_duplicates(points):
+ i = 0
+ out = []
+ for p in points:
+ for j in xrange(i, len(points)):
+ if p == points[j]:
+ points[j] = [None, None]
+ if p != [None, None]:
+ out += [p]
+ i += 1
+ return out
+
+ def get_way_len(points):
+ l = 0
+ for i in xrange(1, len(points)):
+ l += math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2)
+ return l
+
+ def sort_dxfpoints(points):
+ points = remove_duplicates(points)
+ ways = [
+ # l=0, d=1, r=2, u=3
+ [3, 0], # ul
+ [3, 2], # ur
+ [1, 0], # dl
+ [1, 2], # dr
+ [0, 3], # lu
+ [0, 1], # ld
+ [2, 3], # ru
+ [2, 1], # rd
+ ]
+ minimal_way = []
+ minimal_len = None
+ for w in ways:
+ tpoints = points[:]
+ cw = []
+ for j in xrange(0, len(points)):
+ p = get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]]
+ tpoints.remove(p[0])
+ cw += p
+ curlen = get_way_len(cw)
+ if minimal_len is None or curlen < minimal_len:
+ minimal_len = curlen
+ minimal_way = cw
+
+ return minimal_way
+
+ def sort_lines(lines):
+ if len(lines) == 0:
+ return []
+ lines = [[key] + lines[key] for key in range(len(lines))]
+ keys = [0]
+ end_point = lines[0][3:]
+ print_("!!!", lines, "\n", end_point)
+ del lines[0]
+ while len(lines) > 0:
+ dist = [[point_to_point_d2(end_point, lines[i][1:3]), i] for i in range(len(lines))]
+ i = min(dist)[1]
+ keys.append(lines[i][0])
+ end_point = lines[i][3:]
+ del lines[i]
+ return keys
+
+ def sort_curves(curves):
+ lines = []
+ for curve in curves:
+ lines += [curve[0][0][0] + curve[-1][-1][0]]
+ return sort_lines(lines)
+
+ def print_dxfpoints(points):
+ gcode = ""
+ for point in points:
+ gcode += "(drilling dxfpoint)\nG00 Z{:f}\nG00 X{:f} Y{:f}\nG01 Z{:f} F{:f}\nG04 P{:f}\nG00 Z{:f}\n".format(self.options.Zsafe, point[0], point[1], self.Zcoordinates[layer][1], self.tools[layer][0]["penetration feed"], 0.2, self.options.Zsafe)
+ return gcode
+
+ def get_path_properties(node):
+ res = {}
+ done = False
+ while not done and node != self.svg:
+ for i in node.getchildren():
+ if isinstance(i, inkex.Desc):
+ res["Description"] = i.text
+ elif isinstance(i, inkex.Title):
+ res["Title"] = i.text
+ done = True
+ node = node.getparent()
+ return res
+
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ self.check_dir()
+ gcode = ""
+
+ parent = list(self.selected_paths)[0] if self.selected_paths else self.layers[0]
+ biarc_group = parent.add(Group())
+ print_(("self.layers=", self.layers))
+ print_(("paths=", paths))
+ colors = {}
+ for layer in self.layers:
+ if layer in paths:
+ print_(("layer", layer))
+ # transform simple path to get all var about orientation
+ self.transform_csp([[[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]]]], layer)
+
+ self.set_tool(layer)
+ curves = []
+ dxfpoints = []
+
+ try:
+ depth_func = eval('lambda c,d,s: ' + self.options.path_to_gcode_depth_function.strip('"'))
+ except:
+ self.error("Bad depth function! Enter correct function at Path to Gcode tab!")
+
+ for path in paths[layer]:
+ if "d" not in path.keys():
+ self.error("Warning: One or more paths do not have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!")
+ continue
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ id_ = path.get("id")
+
+ def set_comment(match, path):
+ if match.group(1) in path.keys():
+ return path.get(match.group(1))
+ else:
+ return "None"
+
+ if self.options.comment_gcode != "":
+ comment = re.sub("\\[([A-Za-z_\\-\\:]+)\\]", partial(set_comment, path=path), self.options.comment_gcode)
+ comment = comment.replace(":newline:", "\n")
+ comment = gcode_comment_str(comment)
+ else:
+ comment = ""
+ if self.options.comment_gcode_from_properties:
+ tags = get_path_properties(path)
+ for tag in tags:
+ comment += gcode_comment_str("{}: {}".format(tag, tags[tag]))
+
+ stroke = path.style('stroke')
+ colors[id_] = inkex.Color(stroke if stroke != None else "#000").to_rgb()
+ if path.get("dxfpoint") == "1":
+ tmp_curve = self.transform_csp(csp, layer)
+ x = tmp_curve[0][0][0][0]
+ y = tmp_curve[0][0][0][1]
+ print_("got dxfpoint (scaled) at ({:f},{:f})".format(x, y))
+ dxfpoints += [[x, y]]
+ else:
+
+ zd = self.Zcoordinates[layer][1]
+ zs = self.Zcoordinates[layer][0]
+ c = 1. - float(sum(colors[id_])) / 255 / 3
+ curves += [
+ [
+ [id_, depth_func(c, zd, zs), comment],
+ [self.parse_curve([subpath], layer) for subpath in csp]
+ ]
+ ]
+ dxfpoints = sort_dxfpoints(dxfpoints)
+ gcode += print_dxfpoints(dxfpoints)
+
+ for curve in curves:
+ for subcurve in curve[1]:
+ self.draw_curve(subcurve, layer)
+
+ if self.options.path_to_gcode_order == 'subpath by subpath':
+ curves_ = []
+ for curve in curves:
+ curves_ += [[curve[0], [subcurve]] for subcurve in curve[1]]
+ curves = curves_
+
+ self.options.path_to_gcode_order = 'path by path'
+
+ if self.options.path_to_gcode_order == 'path by path':
+ if self.options.path_to_gcode_sort_paths:
+ keys = sort_curves([curve[1] for curve in curves])
+ else:
+ keys = range(len(curves))
+ for key in keys:
+ d = curves[key][0][1]
+ for step in range(0, int(math.ceil(abs((zs - d) / self.tools[layer][0]["depth step"])))):
+ z = max(d, zs - abs(self.tools[layer][0]["depth step"] * (step + 1)))
+
+ gcode += gcode_comment_str("\nStart cutting path id: {}".format(curves[key][0][0]))
+ if curves[key][0][2] != "()":
+ gcode += curves[key][0][2] # add comment
+
+ for curve in curves[key][1]:
+ gcode += self.generate_gcode(curve, layer, z)
+
+ gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
+
+ else: # pass by pass
+ mind = min([curve[0][1] for curve in curves])
+ for step in range(0, 1 + int(math.ceil(abs((zs - mind) / self.tools[layer][0]["depth step"])))):
+ z = zs - abs(self.tools[layer][0]["depth step"] * step)
+ curves_ = []
+ for curve in curves:
+ if curve[0][1] < z:
+ curves_.append(curve)
+
+ z = zs - abs(self.tools[layer][0]["depth step"] * (step + 1))
+ gcode += "\n(Pass at depth {})\n".format(z)
+
+ if self.options.path_to_gcode_sort_paths:
+ keys = sort_curves([curve[1] for curve in curves_])
+ else:
+ keys = range(len(curves_))
+ for key in keys:
+
+ gcode += gcode_comment_str("Start cutting path id: {}".format(curves[key][0][0]))
+ if curves[key][0][2] != "()":
+ gcode += curves[key][0][2] # add comment
+
+ for subcurve in curves_[key][1]:
+ gcode += self.generate_gcode(subcurve, layer, max(z, curves_[key][0][1]))
+
+ gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
+
+ self.export_gcode(gcode)
+
+ ################################################################################
+ #
+ # dxfpoints
+ #
+ ################################################################################
+ def tab_dxfpoints(self):
+ self.get_info_plus()
+ if self.selected_paths == {}:
+ self.error("Nothing is selected. Please select something to convert to drill point (dxfpoint) or clear point sign.")
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ for path in self.selected_paths[layer]:
+ if self.options.dxfpoints_action == 'replace':
+
+ path.set("dxfpoint", "1")
+ r = re.match("^\\s*.\\s*(\\S+)", path.get("d"))
+ if r is not None:
+ print_(("got path=", r.group(1)))
+ path.set("d", "m {} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z".format(r.group(1)))
+ path.set("style", MARKER_STYLE["dxf_points"])
+
+ if self.options.dxfpoints_action == 'save':
+ path.set("dxfpoint", "1")
+
+ if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1":
+ path.set("dxfpoint", "0")
+
+ ################################################################################
+ #
+ # Artefacts
+ #
+ ################################################################################
+ def tab_area_artefacts(self):
+ self.get_info_plus()
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ for layer in paths:
+ for path in paths[layer]:
+ parent = path.getparent()
+ if "d" not in path.keys():
+ self.error("Warning: One or more paths do not have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!")
+ continue
+ csp = path.path.to_superpath()
+ remove = []
+ for i in range(len(csp)):
+ subpath = [[point[:] for point in points] for points in csp[i]]
+ subpath = self.apply_transforms(path, [subpath])[0]
+ bounds = csp_simple_bound([subpath])
+ if (bounds[2] - bounds[0]) ** 2 + (bounds[3] - bounds[1]) ** 2 < self.options.area_find_artefacts_diameter ** 2:
+ if self.options.area_find_artefacts_action == "mark with an arrow":
+ arrow = Path('m {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z'.format(subpath[0][1][0], subpath[0][1][1])).to_superpath()
+ arrow = self.apply_transforms(path, arrow, True)
+ node = parent.add(PathElement())
+ node.path = CubicSuperPath(arrow)
+ node.style = MARKER_STYLE["area artefact arrow"]
+ node.set('gcodetools', 'area artefact arrow')
+ elif self.options.area_find_artefacts_action == "mark with style":
+ node = parent.add(PathElement())
+ node.path = CubicSuperPath(csp[i])
+ node.style = MARKER_STYLE["area artefact"]
+ remove.append(i)
+ elif self.options.area_find_artefacts_action == "delete":
+ remove.append(i)
+ print_("Deleted artefact {}".format(subpath))
+ remove.reverse()
+ for i in remove:
+ del csp[i]
+ if len(csp) == 0:
+ parent.remove(path)
+ else:
+ path.path = CubicSuperPath(csp)
+
+ return
+
+ def tab_area(self):
+ """Calculate area curves"""
+ self.get_info_plus()
+ if len(self.selected_paths) <= 0:
+ self.error("This extension requires at least one selected path.")
+ return
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ self.set_tool(layer)
+ if self.tools[layer][0]['diameter'] <= 0:
+ self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
+
+ for path in self.selected_paths[layer]:
+ print_(("doing path", path.get("style"), path.get("d")))
+
+ area_group = path.getparent().add(Group())
+
+ csp = path.path.to_superpath()
+ print_(csp)
+ if not csp:
+ print_("omitting non-path")
+ self.error("Warning: omitting non-path")
+ continue
+
+ if path.get('sodipodi:type') != "inkscape:offset":
+ print_("Path {} is not an offset. Preparation started.".format(path.get("id")))
+ # Path is not offset. Preparation will be needed.
+ # Finding top most point in path (min y value)
+
+ min_x, min_y, min_i, min_j, min_t = csp_true_bounds(csp)[1]
+
+ # Reverse path if needed.
+ if min_y != float("-inf"):
+ # Move outline subpath to the beginning of csp
+ subp = csp[min_i]
+ del csp[min_i]
+ j = min_j
+ # Split by the topmost point and join again
+ if min_t in [0, 1]:
+ if min_t == 0:
+ j = j - 1
+ subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
+ subp = [[subp[j][1], subp[j][1], subp[j][2]]] + subp[j + 1:] + subp[:j] + [[subp[j][0], subp[j][1], subp[j][1]]]
+ else:
+ sp1, sp2, sp3 = csp_split(subp[j - 1], subp[j], min_t)
+ subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
+ subp = [[sp2[1], sp2[1], sp2[2]]] + [sp3] + subp[j + 1:] + subp[:j - 1] + [sp1] + [[sp2[0], sp2[1], sp2[1]]]
+ csp = [subp] + csp
+ # reverse path if needed
+ if csp_subpath_ccw(csp[0]):
+ for i in range(len(csp)):
+ n = []
+ for j in csp[i]:
+ n = [[j[2][:], j[1][:], j[0][:]]] + n
+ csp[i] = n[:]
+
+ # What the absolute fudge is this doing? Closing paths? Ugh.
+ # Not sure but it most be at this level and not in the if statement, or it will not work with dynamic offsets
+ d = str(CubicSuperPath(csp))
+ print_(("original d=", d))
+ d = re.sub(r'(?i)(m[^mz]+)', r'\1 Z ', d)
+ d = re.sub(r'(?i)\s*z\s*z\s*', r' Z ', d)
+ d = re.sub(r'(?i)\s*([A-Za-z])\s*', r' \1 ', d)
+ print_(("formatted d=", d))
+ p0 = self.transform([0, 0], layer)
+ p1 = self.transform([0, 1], layer)
+ scale = (P(p0) - P(p1)).mag()
+ if scale == 0:
+ scale = 1.
+ else:
+ scale = 1. / scale
+ print_(scale)
+ tool_d = self.tools[layer][0]['diameter'] * scale
+ r = self.options.area_inkscape_radius * scale
+ sign = 1 if r > 0 else -1
+ print_("Tool diameter = {}, r = {}".format(tool_d, r))
+
+ # avoiding infinite loops
+ if self.options.area_tool_overlap > 0.9:
+ self.options.area_tool_overlap = .9
+
+ for i in range(self.options.max_area_curves):
+ radius = - tool_d * (i * (1 - self.options.area_tool_overlap) + 0.5) * sign
+ if abs(radius) > abs(r):
+ radius = -r
+
+ elem = area_group.add(PathElement(style=str(MARKER_STYLE["biarc_style_i"]['area'])))
+ elem.set('sodipodi:type', 'inkscape:offset')
+ elem.set('inkscape:radius', radius)
+ elem.set('inkscape:original', d)
+ print_(("adding curve", area_group, d, str(MARKER_STYLE["biarc_style_i"]['area'])))
+ if radius == -r:
+ break
+
+ def tab_area_fill(self):
+ """Fills area with lines"""
+ self.get_info_plus()
+ # convert degrees into rad
+ self.options.area_fill_angle = self.options.area_fill_angle * math.pi / 180
+ if len(self.selected_paths) <= 0:
+ self.error("This extension requires at least one selected path.")
+ return
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ self.set_tool(layer)
+ if self.tools[layer][0]['diameter'] <= 0:
+ self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
+ tool = self.tools[layer][0]
+ for path in self.selected_paths[layer]:
+ lines = []
+ print_(("doing path", path.get("style"), path.get("d")))
+ area_group = path.getparent().add(Group())
+ csp = path.path.to_superpath()
+ if not csp:
+ print_("omitting non-path")
+ self.error("Warning: omitting non-path")
+ continue
+ csp = self.apply_transforms(path, csp)
+ csp = csp_close_all_subpaths(csp)
+ csp = self.transform_csp(csp, layer)
+
+ # rotate the path to get bounds in defined direction.
+ a = - self.options.area_fill_angle
+ rotated_path = [[[[point[0] * math.cos(a) - point[1] * math.sin(a), point[0] * math.sin(a) + point[1] * math.cos(a)] for point in sp] for sp in subpath] for subpath in csp]
+ bounds = csp_true_bounds(rotated_path)
+
+ # Draw the lines
+ # Get path's bounds
+ b = [0.0, 0.0, 0.0, 0.0] # [minx,miny,maxx,maxy]
+ for k in range(4):
+ i = bounds[k][2]
+ j = bounds[k][3]
+ t = bounds[k][4]
+
+ b[k] = csp_at_t(rotated_path[i][j - 1], rotated_path[i][j], t)[k % 2]
+
+ # Zig-zag
+ r = tool['diameter'] * (1 - self.options.area_tool_overlap)
+ if r <= 0:
+ self.error('Tools diameter must be greater than 0!', 'error')
+ return
+
+ lines += [[]]
+
+ if self.options.area_fill_method == 'zig-zag':
+ i = b[0] - self.options.area_fill_shift * r
+ top = True
+ last_one = True
+ while i < b[2] or last_one:
+ if i >= b[2]:
+ last_one = False
+ if not lines[-1]:
+ lines[-1] += [[i, b[3]]]
+
+ if top:
+ lines[-1] += [[i, b[1]], [i + r, b[1]]]
+
+ else:
+ lines[-1] += [[i, b[3]], [i + r, b[3]]]
+
+ top = not top
+ i += r
+ else:
+
+ w = b[2] - b[0] + self.options.area_fill_shift * r
+ h = b[3] - b[1] + self.options.area_fill_shift * r
+ x = b[0] - self.options.area_fill_shift * r
+ y = b[1] - self.options.area_fill_shift * r
+ lines[-1] += [[x, y]]
+ stage = 0
+ start = True
+ while w > 0 and h > 0:
+ stage = (stage + 1) % 4
+ if stage == 0:
+ y -= h
+ h -= r
+ elif stage == 1:
+ x += w
+ if not start:
+ w -= r
+ start = False
+ elif stage == 2:
+ y += h
+ h -= r
+ elif stage == 3:
+ x -= w
+ w -= r
+
+ lines[-1] += [[x, y]]
+
+ stage = (stage + 1) % 4
+ if w <= 0 and h > 0:
+ y = y - h if stage == 0 else y + h
+ if h <= 0 and w > 0:
+ x = x - w if stage == 3 else x + w
+ lines[-1] += [[x, y]]
+ # Rotate created paths back
+ a = self.options.area_fill_angle
+ lines = [[[point[0] * math.cos(a) - point[1] * math.sin(a), point[0] * math.sin(a) + point[1] * math.cos(a)] for point in subpath] for subpath in lines]
+
+ # get the intersection points
+
+ splitted_line = [[lines[0][0]]]
+ intersections = {}
+ for l1, l2, in zip(lines[0], lines[0][1:]):
+ ints = []
+
+ if l1[0] == l2[0] and l1[1] == l2[1]:
+ continue
+ for i in range(len(csp)):
+ for j in range(1, len(csp[i])):
+ sp1 = csp[i][j - 1]
+ sp2 = csp[i][j]
+ roots = csp_line_intersection(l1, l2, sp1, sp2)
+ for t in roots:
+ p = tuple(csp_at_t(sp1, sp2, t))
+ if l1[0] == l2[0]:
+ t1 = (p[1] - l1[1]) / (l2[1] - l1[1])
+ else:
+ t1 = (p[0] - l1[0]) / (l2[0] - l1[0])
+ if 0 <= t1 <= 1:
+ ints += [[t1, p[0], p[1], i, j, t]]
+ if p in intersections:
+ intersections[p] += [[i, j, t]]
+ else:
+ intersections[p] = [[i, j, t]]
+
+ ints.sort()
+ for i in ints:
+ splitted_line[-1] += [[i[1], i[2]]]
+ splitted_line += [[[i[1], i[2]]]]
+ splitted_line[-1] += [l2]
+ i = 0
+ print_(splitted_line)
+ while i < len(splitted_line):
+ # check if the middle point of the first lines segment is inside the path.
+ # and remove the subline if not.
+ l1 = splitted_line[i][0]
+ l2 = splitted_line[i][1]
+ p = [(l1[0] + l2[0]) / 2, (l1[1] + l2[1]) / 2]
+ if not point_inside_csp(p, csp):
+ del splitted_line[i]
+ else:
+ i += 1
+
+ # and apply back transrormations to draw them
+ csp_line = csp_from_polyline(splitted_line)
+ csp_line = self.transform_csp(csp_line, layer, True)
+
+ self.draw_csp(csp_line, group=area_group)
+
+ ################################################################################
+ #
+ # Engraving
+ #
+ # LT Notes to self: See wiki.inkscape.org/wiki/index.php/PythonEffectTutorial
+ # To create anything in the Inkscape document, look at the XML editor for
+ # details of how such an element looks in XML, then follow this model.
+ # layer number n appears in XML as <svg:g id="layern" inkscape:label="layername">
+ #
+ # to create it, use
+ # Mylayer = self.svg.add(Layer.new('layername'))
+ #
+ # group appears in XML as <svg:g id="gnnnnn"> where nnnnn is a number
+ #
+ # to create it, use
+ # Mygroup = parent.add(Group(gcodetools="My group label")
+ # where parent may be the layer or a parent group. To get the parent group, you can use
+ # parent = self.selected_paths[layer][0].getparent()
+ ################################################################################
+ def tab_engraving(self):
+ self.get_info_plus()
+ global cspm
+ global wl
+ global nlLT
+ global i
+ global j
+ global gcode_3Dleft
+ global gcode_3Dright
+ global max_dist # minimum of tool radius and user's requested maximum distance
+ global eye_dist
+ eye_dist = 100 # 3D constant. Try varying it for your eyes
+
+ def bisect(nxy1, nxy2):
+ """LT Find angle bisecting the normals n1 and n2
+
+ Parameters: Normalised normals
+ Returns: nx - Normal of bisector, normalised to 1/cos(a)
+ ny -
+ sinBis2 - sin(angle turned/2): positive if turning in
+ Note that bisect(n1,n2) and bisect(n2,n1) give opposite sinBis2 results
+ If sinturn is less than the user's requested angle tolerance, I return 0
+ """
+ (nx1, ny1) = nxy1
+ (nx2, ny2) = nxy2
+ cosBis = math.sqrt(max(0, (1.0 + nx1 * nx2 - ny1 * ny2) / 2.0))
+ # We can get correct sign of the sin, assuming cos is positive
+ if (abs(ny1 - ny2) < ENGRAVING_TOLERANCE) or (abs(cosBis) < ENGRAVING_TOLERANCE):
+ if abs(nx1 - nx2) < ENGRAVING_TOLERANCE:
+ return nx1, ny1, 0.0
+ sinBis = math.copysign(1, ny1)
+ else:
+ sinBis = cosBis * (nx2 - nx1) / (ny1 - ny2)
+ # We can correct signs by noting that the dot product
+ # of bisector and either normal must be >0
+ costurn = cosBis * nx1 + sinBis * ny1
+ if costurn == 0:
+ return ny1 * 100, -nx1 * 100, 1 # Path doubles back on itself
+ sinturn = sinBis * nx1 - cosBis * ny1
+ if costurn < 0:
+ sinturn = -sinturn
+ if 0 < sinturn * 114.6 < (180 - self.options.engraving_sharp_angle_tollerance):
+ sinturn = 0 # set to zero if less than the user wants to see.
+ return cosBis / costurn, sinBis / costurn, sinturn
+ # end bisect
+
+ def get_radius_to_line(xy1, n_xy1, n_xy2, xy2, n_xy23, xy3, n_xy3):
+ """LT find biggest circle we can engrave here, if constrained by line 2-3
+
+ Parameters:
+ x1,y1,nx1,ny1 coordinates and normal of the line we're currently engraving
+ nx2,ny2 angle bisector at point 2
+ x2,y2 coordinates of first point of line 2-3
+ nx23,ny23 normal to the line 2-3
+ x3,y3 coordinates of the other end
+ nx3,ny3 angle bisector at point 3
+ Returns:
+ radius or self.options.engraving_max_dist if line doesn't limit radius
+ This function can be used in three ways:
+ - With nx1=ny1=0 it finds circle centred at x1,y1
+ - with nx1,ny1 normalised, it finds circle tangential at x1,y1
+ - with nx1,ny1 scaled by 1/cos(a) it finds circle centred on an angle bisector
+ where a is the angle between the bisector and the previous/next normals
+
+ If the centre of the circle tangential to the line 2-3 is outside the
+ angle bisectors at its ends, ignore this line.
+
+ Note that it handles corners in the conventional manner of letter cutting
+ by mitering, not rounding.
+ Algorithm uses dot products of normals to find radius
+ and hence coordinates of centre
+ """
+ (x1, y1) = xy1
+ (nx1, ny1) = n_xy1
+ (nx2, ny2) = n_xy2
+ (x2, y2) = xy2
+ (nx23, ny23) = n_xy23
+ (x3, y3) = xy3
+ (nx3, ny3) = n_xy3
+ global max_dist
+
+ # Start by converting coordinates to be relative to x1,y1
+ x2, y2 = x2 - x1, y2 - y1
+ x3, y3 = x3 - x1, y3 - y1
+
+ # The logic uses vector arithmetic.
+ # The dot product of two vectors gives the product of their lengths
+ # multiplied by the cos of the angle between them.
+ # So, the perpendicular distance from x1y1 to the line 2-3
+ # is equal to the dot product of its normal and x2y2 or x3y3
+ # It is also equal to the projection of x1y1-xcyc on the line's normal
+ # plus the radius. But, as the normal faces inside the path we must negate it.
+
+ # Make sure the line in question is facing x1,y1 and vice versa
+ dist = -x2 * nx23 - y2 * ny23
+ if dist < 0:
+ return max_dist
+ denom = 1. - nx23 * nx1 - ny23 * ny1
+ if denom < ENGRAVING_TOLERANCE:
+ return max_dist
+
+ # radius and centre are:
+ r = dist / denom
+ cx = r * nx1
+ cy = r * ny1
+ # if c is not between the angle bisectors at the ends of the line, ignore
+ # Use vector cross products. Not sure if I need the .0001 safety margins:
+ if (x2 - cx) * ny2 > (y2 - cy) * nx2 + 0.0001:
+ return max_dist
+ if (x3 - cx) * ny3 < (y3 - cy) * nx3 - 0.0001:
+ return max_dist
+ return min(r, max_dist)
+ # end of get_radius_to_line
+
+ def get_radius_to_point(xy1, n_xy, xy2):
+ """LT find biggest circle we can engrave here, constrained by point x2,y2
+
+ This function can be used in three ways:
+ - With nx=ny=0 it finds circle centred at x1,y1
+ - with nx,ny normalised, it finds circle tangential at x1,y1
+ - with nx,ny scaled by 1/cos(a) it finds circle centred on an angle bisector
+ where a is the angle between the bisector and the previous/next normals
+
+ Note that I wrote this to replace find_cutter_centre. It is far less
+ sophisticated but, I hope, far faster.
+ It turns out that finding a circle touching a point is harder than a circle
+ touching a line.
+ """
+ (x1, y1) = xy1
+ (nx, ny) = n_xy
+ (x2, y2) = xy2
+ global max_dist
+
+ # Start by converting coordinates to be relative to x1,y1
+ x2 = x2 - x1
+ y2 = y2 - y1
+ denom = nx ** 2 + ny ** 2 - 1
+ if denom <= ENGRAVING_TOLERANCE: # Not a corner bisector
+ if denom == -1: # Find circle centre x1,y1
+ return math.sqrt(x2 ** 2 + y2 ** 2)
+ # if x2,y2 not in front of the normal...
+ if x2 * nx + y2 * ny <= 0:
+ return max_dist
+ return (x2 ** 2 + y2 ** 2) / (2 * (x2 * nx + y2 * ny))
+ # It is a corner bisector, so..
+ discriminator = (x2 * nx + y2 * ny) ** 2 - denom * (x2 ** 2 + y2 ** 2)
+ if discriminator < 0:
+ return max_dist # this part irrelevant
+ r = (x2 * nx + y2 * ny - math.sqrt(discriminator)) / denom
+ return min(r, max_dist)
+ # end of get_radius_to_point
+
+ def bez_divide(a, b, c, d):
+ """LT recursively divide a Bezier.
+
+ Divides until difference between each
+ part and a straight line is less than some limit
+ Note that, as simple as this code is, it is mathematically correct.
+ Parameters:
+ a,b,c and d are each a list of x,y real values
+ Bezier end points a and d, control points b and c
+ Returns:
+ a list of Beziers.
+ Each Bezier is a list with four members,
+ each a list holding a coordinate pair
+ Note that the final point of one member is the same as
+ the first point of the next, and the control points
+ there are smooth and symmetrical. I use this fact later.
+ """
+ bx = b[0] - a[0]
+ by = b[1] - a[1]
+ cx = c[0] - a[0]
+ cy = c[1] - a[1]
+ dx = d[0] - a[0]
+ dy = d[1] - a[1]
+ limit = 8 * math.hypot(dx, dy) / self.options.engraving_newton_iterations
+ # LT This is the only limit we get from the user currently
+ if abs(dx * by - bx * dy) < limit and abs(dx * cy - cx * dy) < limit:
+ return [[a, b, c, d]]
+ abx = (a[0] + b[0]) / 2.0
+ aby = (a[1] + b[1]) / 2.0
+ bcx = (b[0] + c[0]) / 2.0
+ bcy = (b[1] + c[1]) / 2.0
+ cdx = (c[0] + d[0]) / 2.0
+ cdy = (c[1] + d[1]) / 2.0
+ abcx = (abx + bcx) / 2.0
+ abcy = (aby + bcy) / 2.0
+ bcdx = (bcx + cdx) / 2.0
+ bcdy = (bcy + cdy) / 2.0
+ m = [(abcx + bcdx) / 2.0, (abcy + bcdy) / 2.0]
+ return bez_divide(a, [abx, aby], [abcx, abcy], m) + bez_divide(m, [bcdx, bcdy], [cdx, cdy], d)
+ # end of bez_divide
+
+ def get_biggest(nxy1, nxy2):
+ """LT Find biggest circle we can draw inside path at point x1,y1 normal nx,ny
+
+ Parameters:
+ point - either on a line or at a reflex corner
+ normal - normalised to 1 if on a line, to 1/cos(a) at a corner
+ Returns:
+ tuple (j,i,r)
+ ..where j and i are indices of limiting segment, r is radius
+ """
+ (x1, y1) = nxy1
+ (nx, ny) = nxy2
+ global max_dist
+ global nlLT
+ global i
+ global j
+
+ n1 = nlLT[j][i - 1] # current node
+ jjmin = -1
+ iimin = -1
+ r = max_dist
+ # set limits within which to look for lines
+ xmin = x1 + r * nx - r
+ xmax = x1 + r * nx + r
+ ymin = y1 + r * ny - r
+ ymax = y1 + r * ny + r
+ for jj in xrange(0, len(nlLT)): # for every subpath of this object
+ for ii in xrange(0, len(nlLT[jj])): # for every point and line
+ if nlLT[jj][ii - 1][2]: # if a point
+ if jj == j: # except this one
+ if abs(ii - i) < 3 or abs(ii - i) > len(nlLT[j]) - 3:
+ continue
+ t1 = get_radius_to_point((x1, y1), (nx, ny), nlLT[jj][ii - 1][0])
+ else: # doing a line
+ if jj == j: # except this one
+ if abs(ii - i) < 2 or abs(ii - i) == len(nlLT[j]) - 1:
+ continue
+ if abs(ii - i) == 2 and nlLT[j][(ii + i) / 2 - 1][3] <= 0:
+ continue
+ if (abs(ii - i) == len(nlLT[j]) - 2) and nlLT[j][-1][3] <= 0:
+ continue
+ nx2, ny2 = nlLT[jj][ii - 2][1]
+ x2, y2 = nlLT[jj][ii - 1][0]
+ nx23, ny23 = nlLT[jj][ii - 1][1]
+ x3, y3 = nlLT[jj][ii][0]
+ nx3, ny3 = nlLT[jj][ii][1]
+ if nlLT[jj][ii - 2][3] > 0: # acute, so use normal, not bisector
+ nx2 = nx23
+ ny2 = ny23
+ if nlLT[jj][ii][3] > 0: # acute, so use normal, not bisector
+ nx3 = nx23
+ ny3 = ny23
+ x23min = min(x2, x3)
+ x23max = max(x2, x3)
+ y23min = min(y2, y3)
+ y23max = max(y2, y3)
+ # see if line in range
+ if n1[2] == False and (x23max < xmin or x23min > xmax or y23max < ymin or y23min > ymax):
+ continue
+ t1 = get_radius_to_line((x1, y1), (nx, ny), (nx2, ny2), (x2, y2), (nx23, ny23), (x3, y3), (nx3, ny3))
+ if 0 <= t1 < r:
+ r = t1
+ iimin = ii
+ jjmin = jj
+ xmin = x1 + r * nx - r
+ xmax = x1 + r * nx + r
+ ymin = y1 + r * ny - r
+ ymax = y1 + r * ny + r
+ # next ii
+ # next jj
+ return jjmin, iimin, r
+ # end of get_biggest
+
+ def line_divide(xy0, j0, i0, xy1, j1, i1, n_xy, length):
+ """LT recursively divide a line as much as necessary
+
+ NOTE: This function is not currently used
+ By noting which other path segment is touched by the circles at each end,
+ we can see if anything is to be gained by a further subdivision, since
+ if they touch the same bit of path we can move linearly between them.
+ Also, we can handle points correctly.
+ Parameters:
+ end points and indices of limiting path, normal, length
+ Returns:
+ list of toolpath points
+ each a list of 3 reals: x, y coordinates, radius
+
+ """
+ (x0, y0) = xy0
+ (x1, y1) = xy1
+ (nx, ny) = n_xy
+ global nlLT
+ global i
+ global j
+ global lmin
+ x2 = (x0 + x1) / 2
+ y2 = (y0 + y1) / 2
+ j2, i2, r2 = get_biggest((x2, y2), (nx, ny))
+ if length < lmin:
+ return [[x2, y2, r2]]
+ if j2 == j0 and i2 == i0: # Same as left end. Don't subdivide this part any more
+ return [[x2, y2, r2], line_divide((x2, y2), j2, i2, (x1, y1), j1, i1, (nx, ny), length / 2)]
+ if j2 == j1 and i2 == i1: # Same as right end. Don't subdivide this part any more
+ return [line_divide((x0, y0), j0, i0, (x2, y2), j2, i2, (nx, ny), length / 2), [x2, y2, r2]]
+ return [line_divide((x0, y0), j0, i0, (x2, y2), j2, i2, (nx, ny), length / 2), line_divide((x2, y2), j2, i2, (x1, y1), j1, i1, (nx, ny), length / 2)]
+ # end of line_divide()
+
+ def save_point(xy, w, i, j, ii, jj):
+ """LT Save this point and delete previous one if linear
+
+ The point is, we generate tons of points but many may be in a straight 3D line.
+ There is no benefit in saving the intermediate points.
+ """
+ (x, y) = xy
+ global wl
+ global cspm
+ x = round(x, 4) # round to 4 decimals
+ y = round(y, 4) # round to 4 decimals
+ w = round(w, 4) # round to 4 decimals
+ if len(cspm) > 1:
+ xy1a, xy1, xy1b, i1, j1, ii1, jj1 = cspm[-1]
+ w1 = wl[-1]
+ if i == i1 and j == j1 and ii == ii1 and jj == jj1: # one match
+ xy1a, xy2, xy1b, i1, j1, ii1, jj1 = cspm[-2]
+ w2 = wl[-2]
+ if i == i1 and j == j1 and ii == ii1 and jj == jj1: # two matches. Now test linearity
+ length1 = math.hypot(xy1[0] - x, xy1[1] - y)
+ length2 = math.hypot(xy2[0] - x, xy2[1] - y)
+ length12 = math.hypot(xy2[0] - xy1[0], xy2[1] - xy1[1])
+ # get the xy distance of point 1 from the line 0-2
+ if length2 > length1 and length2 > length12: # point 1 between them
+ xydist = abs((xy2[0] - x) * (xy1[1] - y) - (xy1[0] - x) * (xy2[1] - y)) / length2
+ if xydist < ENGRAVING_TOLERANCE: # so far so good
+ wdist = w2 + (w - w2) * length1 / length2 - w1
+ if abs(wdist) < ENGRAVING_TOLERANCE:
+ cspm.pop()
+ wl.pop()
+ cspm += [[[x, y], [x, y], [x, y], i, j, ii, jj]]
+ wl += [w]
+ # end of save_point
+
+ def draw_point(xy0, xy, w, t):
+ """LT Draw this point as a circle with a 1px dot in the middle (x,y)
+ and a 3D line from (x0,y0) down to x,y. 3D line thickness should be t/2
+
+ Note that points that are subsequently erased as being unneeded do get
+ displayed, but this helps the user see the total area covered.
+ """
+ (x0, y0) = xy0
+ (x, y) = xy
+ global gcode_3Dleft
+ global gcode_3Dright
+ if self.options.engraving_draw_calculation_paths:
+ elem = engraving_group.add(PathElement.arc((x, y), 1))
+ elem.set('gcodetools', "Engraving calculation toolpath")
+ elem.style = "fill:#ff00ff; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
+
+ # Don't draw zero radius circles
+ if w:
+ elem = engraving_group.add(PathElement.arc((x, y), w))
+ elem.set('gcodetools', "Engraving calculation paths")
+ elem.style = "fill:none; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
+
+ # Find slope direction for shading
+ s = math.atan2(y - y0, x - x0) # -pi to pi
+ # convert to 2 hex digits as a shade of red
+ s2 = "#{0:x}0000".format(int(101 * (1.5 - math.sin(s + 0.5))))
+ style = "stroke:{}; stroke-opacity:1;stroke-width:{};fill:none".format(s2, t/2)
+ right = gcode_3Dleft.add(PathElement(style=style, gcodetools="Gcode G1R"))
+ right.path = "M {:f},{:f} L {:f},{:f}".format(
+ x0 - eye_dist, y0, x - eye_dist - 0.14 * w, y)
+ left = gcode_3Dright.add(PathElement(style=style, gcodetools="Gcode G1L"))
+ left.path = "M {:f},{:f} L {:f},{:f}".format(
+ x0 + eye_dist, y0, x + eye_dist + 0.14 * r, y)
+
+ # end of subfunction definitions. engraving() starts here:
+ gcode = ''
+ r = 0 # theoretical and tool-radius-limited radii in pixels
+ w = 0
+ wmax = 0
+ cspe = []
+ we = []
+ if not self.selected_paths:
+ self.error("Please select at least one path to engrave and run again.")
+ return
+ if not self.check_dir():
+ return
+ # Find what units the user uses
+ unit = " mm"
+ if self.options.unit == "G20 (All units in inches)":
+ unit = " inches"
+ elif self.options.unit != "G21 (All units in mm)":
+ self.error("Unknown unit selected. mm assumed")
+ print_("engraving_max_dist mm/inch", self.options.engraving_max_dist)
+
+ # LT See if we can use this parameter for line and Bezier subdivision:
+ bitlen = 20 / self.options.engraving_newton_iterations
+
+ for layer in self.layers:
+ if layer in self.selected_paths and layer in self.orientation_points:
+ # Calculate scale in pixels per user unit (mm or inch)
+ p1 = self.orientation_points[layer][0][0]
+ p2 = self.orientation_points[layer][0][1]
+ ol = math.hypot(p1[0][0] - p2[0][0], p1[0][1] - p2[0][1])
+ oluu = math.hypot(p1[1][0] - p2[1][0], p1[1][1] - p2[1][1])
+ print_("Orientation2 p1 p2 ol oluu", p1, p2, ol, oluu)
+ orientation_scale = ol / oluu
+
+ self.set_tool(layer)
+ shape = self.tools[layer][0]['shape']
+ if re.search('w', shape):
+ toolshape = eval('lambda w: ' + shape.strip('"'))
+ else:
+ self.error("Tool '{}' has no shape. 45 degree cone assumed!".format(self.tools[layer][0]['name']))
+ toolshape = lambda w: w
+ # Get tool radius in pixels
+ toolr = self.tools[layer][0]['diameter'] * orientation_scale / 2
+ print_("tool radius in pixels=", toolr)
+ # max dist from path to engrave in user's units
+ max_distuu = min(self.tools[layer][0]['diameter'] / 2, self.options.engraving_max_dist)
+ max_dist = max_distuu * orientation_scale
+ print_("max_dist pixels", max_dist)
+
+ engraving_group = self.selected_paths[layer][0].getparent().add(Group())
+ if self.options.engraving_draw_calculation_paths and (self.my3Dlayer is None):
+ self.svg.add(Layer.new("3D"))
+ # Create groups for left and right eyes
+ if self.options.engraving_draw_calculation_paths:
+ gcode_3Dleft = self.my3Dlayer.add(Group(gcodetools="Gcode 3D L"))
+ gcode_3Dright = self.my3Dlayer.add(Group(gcodetools="Gcode 3D R"))
+
+ for node in self.selected_paths[layer]:
+ if isinstance(node, inkex.PathElement):
+ cspi = node.path.to_superpath()
+ # LT: Create my own list. n1LT[j] is for subpath j
+ nlLT = []
+ for j in xrange(len(cspi)): # LT For each subpath...
+ # Remove zero length segments, assume closed path
+ i = 0 # LT was from i=1
+ while i < len(cspi[j]):
+ if abs(cspi[j][i - 1][1][0] - cspi[j][i][1][0]) < ENGRAVING_TOLERANCE and abs(cspi[j][i - 1][1][1] - cspi[j][i][1][1]) < ENGRAVING_TOLERANCE:
+ cspi[j][i - 1][2] = cspi[j][i][2]
+ del cspi[j][i]
+ else:
+ i += 1
+ for csp in cspi: # LT6a For each subpath...
+ # Create copies in 3D layer
+ print_("csp is zz ", csp)
+ cspl = []
+ cspr = []
+ # create list containing lines and points, starting with a point
+ # line members: [x,y],[nx,ny],False,i
+ # x,y is start of line. Normal on engraved side.
+ # Normal is normalised (unit length)
+ # Note that Y axis increases down the page
+ # corner members: [x,y],[nx,ny],True,sin(halfangle)
+ # if halfangle>0: radius 0 here. normal is bisector
+ # if halfangle<0. reflex angle. normal is bisector
+ # corner normals are divided by cos(halfangle)
+ # so that they will engrave correctly
+ print_("csp is", csp)
+ nlLT.append([])
+ for i in range(0, len(csp)): # LT for each point
+ sp0 = csp[i - 2]
+ sp1 = csp[i - 1]
+ sp2 = csp[i]
+ if self.options.engraving_draw_calculation_paths:
+ # Copy it to 3D layer objects
+ spl = []
+ spr = []
+ for j in range(0, 3):
+ pl = [sp2[j][0] - eye_dist, sp2[j][1]]
+ pr = [sp2[j][0] + eye_dist, sp2[j][1]]
+ spl += [pl]
+ spr += [pr]
+ cspl += [spl]
+ cspr += [spr]
+ # LT find angle between this and previous segment
+ x0, y0 = sp1[1]
+ nx1, ny1 = csp_normalized_normal(sp1, sp2, 0)
+ # I don't trust this function, so test result
+ if abs(1 - math.hypot(nx1, ny1)) > 0.00001:
+ print_("csp_normalised_normal error t=0", nx1, ny1, sp1, sp2)
+ self.error("csp_normalised_normal error. See log.")
+
+ nx0, ny0 = csp_normalized_normal(sp0, sp1, 1)
+ if abs(1 - math.hypot(nx0, ny0)) > 0.00001:
+ print_("csp_normalised_normal error t=1", nx0, ny0, sp1, sp2)
+ self.error("csp_normalised_normal error. See log.")
+ bx, by, s = bisect((nx0, ny0), (nx1, ny1))
+ # record x,y,normal,ifCorner, sin(angle-turned/2)
+ nlLT[-1] += [[[x0, y0], [bx, by], True, s]]
+
+ # LT now do the line
+ if sp1[1] == sp1[2] and sp2[0] == sp2[1]: # straightline
+ nlLT[-1] += [[sp1[1], [nx1, ny1], False, i]]
+ else: # Bezier. First, recursively cut it up:
+ nn = bez_divide(sp1[1], sp1[2], sp2[0], sp2[1])
+ first = True # Flag entry to divided Bezier
+ for bLT in nn: # save as two line segments
+ for seg in range(3):
+ if seg > 0 or first:
+ nx1 = bLT[seg][1] - bLT[seg + 1][1]
+ ny1 = bLT[seg + 1][0] - bLT[seg][0]
+ l1 = math.hypot(nx1, ny1)
+ if l1 < ENGRAVING_TOLERANCE:
+ continue
+ nx1 = nx1 / l1 # normalise them
+ ny1 = ny1 / l1
+ nlLT[-1] += [[bLT[seg], [nx1, ny1], False, i]]
+ first = False
+ if seg < 2: # get outgoing bisector
+ nx0 = nx1
+ ny0 = ny1
+ nx1 = bLT[seg + 1][1] - bLT[seg + 2][1]
+ ny1 = bLT[seg + 2][0] - bLT[seg + 1][0]
+ l1 = math.hypot(nx1, ny1)
+ if l1 < ENGRAVING_TOLERANCE:
+ continue
+ nx1 = nx1 / l1 # normalise them
+ ny1 = ny1 / l1
+ # bisect
+ bx, by, s = bisect((nx0, ny0), (nx1, ny1))
+ nlLT[-1] += [[bLT[seg + 1], [bx, by], True, 0.]]
+ # LT for each segment - ends here.
+ print_(("engraving_draw_calculation_paths=", self.options.engraving_draw_calculation_paths))
+ if self.options.engraving_draw_calculation_paths:
+ # Copy complete paths to 3D layer
+ cspl += [cspl[0]] # Close paths
+ cspr += [cspr[0]] # Close paths
+ style = "stroke:#808080; stroke-opacity:1; stroke-width:0.6; fill:none"
+ elem = gcode_3Dleft.add(PathElement(style=style, gcodetools="G1L outline"))
+ elem.path = CubicSuperPath([cspl])
+ elem = gcode_3Dright.add(Pathelement(style=style, gcodetools="G1R outline"))
+ elem.path = CubicSuperPath([cspr])
+
+ for p in nlLT[-1]: # For last sub-path
+ if p[2]:
+ elem = engraving_group.add(PathElement(gcodetools="Engraving normals"))
+ elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
+ p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
+ elem.style = "stroke:#f000af; stroke-opacity:0.46; stroke-width:0.1; fill:none"
+ else:
+ elem = engraving_group.add(PathElement(gcodetools="Engraving bisectors"))
+ elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
+ p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
+ elem.style = "stroke:#0000ff; stroke-opacity:0.46; stroke-width:0.1; fill:none"
+
+ # LT6a build nlLT[j] for each subpath - ends here
+ # Calculate offset points
+ reflex = False
+ for j in xrange(len(nlLT)): # LT6b for each subpath
+ cspm = [] # Will be my output. List of csps.
+ wl = [] # Will be my w output list
+ w = r = 0 # LT initial, as first point is an angle
+ for i in xrange(len(nlLT[j])): # LT for each node
+ # LT Note: Python enables wrapping of array indices
+ # backwards to -1, -2, but not forwards. Hence:
+ n0 = nlLT[j][i - 2] # previous node
+ n1 = nlLT[j][i - 1] # current node
+ n2 = nlLT[j][i] # next node
+ # if n1[2] == True and n1[3]==0 : # A straight angle
+ # continue
+ x1a, y1a = n1[0] # this point/start of this line
+ nx, ny = n1[1]
+ x1b, y1b = n2[0] # next point/end of this line
+ if n1[2]: # We're at a corner
+ bits = 1
+ bit0 = 0
+ # lastr=r #Remember r from last line
+ lastw = w # Remember w from last line
+ w = max_dist
+ if n1[3] > 0: # acute. Limit radius
+ len1 = math.hypot((n0[0][0] - n1[0][0]), (n0[0][1] - n1[0][1]))
+ if i < (len(nlLT[j]) - 1):
+ len2 = math.hypot((nlLT[j][i + 1][0][0] - n1[0][0]), (nlLT[j][i + 1][0][1] - n1[0][1]))
+ else:
+ len2 = math.hypot((nlLT[j][0][0][0] - n1[0][0]), (nlLT[j][0][0][1] - n1[0][1]))
+ # set initial r value, not to be exceeded
+ w = math.sqrt(min(len1, len2)) / n1[3]
+ else: # line. Cut it up if long.
+ if n0[3] > 0 and not self.options.engraving_draw_calculation_paths:
+ bit0 = r * n0[3] # after acute corner
+ else:
+ bit0 = 0.0
+ length = math.hypot((x1b - x1a), (y1a - y1b))
+ bit0 = (min(length, bit0))
+ bits = int((length - bit0) / bitlen)
+ # split excess evenly at both ends
+ bit0 += (length - bit0 - bitlen * bits) / 2
+ for b in xrange(bits): # divide line into bits
+ x1 = x1a + ny * (b * bitlen + bit0)
+ y1 = y1a - nx * (b * bitlen + bit0)
+ jjmin, iimin, w = get_biggest((x1, y1), (nx, ny))
+ print_("i,j,jjmin,iimin,w", i, j, jjmin, iimin, w)
+ wmax = max(wmax, w)
+ if reflex: # just after a reflex corner
+ reflex = False
+ if w < lastw: # need to adjust it
+ draw_point((x1, y1), (n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, (lastw - w) / 2)
+ save_point((n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, i, j, iimin, jjmin)
+ if n1[2]: # We're at a corner
+ if n1[3] > 0: # acute
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+ draw_point((x1, y1), (x1, y1), 0, 0)
+ save_point((x1, y1), 0, i, j, iimin, jjmin)
+ elif n1[3] < 0: # reflex
+ if w > lastw:
+ draw_point((x1, y1), (x1 + nx * lastw, y1 + ny * lastw), w, (w - lastw) / 2)
+ wmax = max(wmax, w)
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+ elif b > 0 and n2[3] > 0 and not self.options.engraving_draw_calculation_paths: # acute corner coming up
+ if jjmin == j and iimin == i + 2:
+ break
+ draw_point((x1, y1), (x1 + nx * w, y1 + ny * w), w, bitlen)
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+
+ # LT end of for each bit of this line
+ if n1[2] == True and n1[3] < 0: # reflex angle
+ reflex = True
+ lastw = w # remember this w
+ # LT next i
+ cspm += [cspm[0]]
+ print_("cspm", cspm)
+ wl += [wl[0]]
+ print_("wl", wl)
+ # Note: Original csp_points was a list, each element
+ # being 4 points, with the first being the same as the
+ # last of the previous set.
+ # Each point is a list of [cx,cy,r,w]
+ # I have flattened it to a flat list of points.
+
+ if self.options.engraving_draw_calculation_paths:
+ node = engraving_group.add(PathElement(
+ gcodetools="Engraving calculation paths",
+ style=MARKER_STYLE["biarc_style_i"]['biarc1']))
+ node.path = CubicSuperPath([cspm])
+ for i in xrange(len(cspm)):
+ elem = engraving_group.add(PathElement.arc(cspm[i][1], wl[i]))
+ elem.set('gcodetools', "Engraving calculation paths")
+ elem.style = "fill:none;fill-opacity:0.46;stroke:#000000;stroke-width:0.1;"
+ cspe += [cspm]
+ wluu = [] # width list in user units: mm/inches
+ for w in wl:
+ wluu += [w / orientation_scale]
+ print_("wl in pixels", wl)
+ print_("wl in user units", wluu)
+ # LT previously, we was in pixels so gave wrong depth
+ we += [wluu]
+ # LT6b For each subpath - ends here
+ # LT5 if it is a path - ends here
+ # LT4 for each selected object in this layer - ends here
+
+ if cspe:
+ curve = self.parse_curve(cspe, layer, we, toolshape) # convert to lines
+ self.draw_curve(curve, layer, engraving_group)
+ gcode += self.generate_gcode(curve, layer, self.options.Zsurface)
+
+ # LT3 for layers loop ends here
+ if gcode != '':
+ self.header += "(Tool diameter should be at least " + str(2 * wmax / orientation_scale) + unit + ")\n"
+ self.header += "(Depth, as a function of radius w, must be " + self.tools[layer][0]['shape'] + ")\n"
+ self.header += "(Rapid feeds use safe Z=" + str(self.options.Zsafe) + unit + ")\n"
+ self.header += "(Material surface at Z=" + str(self.options.Zsurface) + unit + ")\n"
+ self.export_gcode(gcode)
+ else:
+ self.error("No need to engrave sharp angles.")
+
+ ################################################################################
+ #
+ # Orientation
+ #
+ ################################################################################
+ def tab_orientation(self, layer=None):
+ self.get_info()
+ Zsurface = f"{self.options.Zsurface:.5f}"
+ Zdepth = f"{self.options.Zdepth:.5f}"
+ if layer is None:
+ layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
+
+ transform = self.get_transforms(layer)
+ if transform:
+ transform = self.reverse_transform(transform)
+ transform = str(Transform(transform))
+
+ if self.options.orientation_points_count == "graffiti":
+ print_(self.graffiti_reference_points)
+ print_("Inserting graffiti points")
+ if layer in self.graffiti_reference_points:
+ graffiti_reference_points_count = len(self.graffiti_reference_points[layer])
+ else:
+ graffiti_reference_points_count = 0
+ axis = ["X", "Y", "Z", "A"][graffiti_reference_points_count % 4]
+ attr = {'gcodetools': "Gcodetools graffiti reference point"}
+ if transform:
+ attr["transform"] = transform
+ group = layer.add(Group(**attr))
+ elem = group.add(PathElement(style="stroke:none;fill:#00ff00;"))
+ elem.set('gcodetools', "Gcodetools graffiti reference point arrow")
+ elem.path = 'm {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,'\
+ '-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.8125000000'\
+ '01 z z'.format(graffiti_reference_points_count * 100, 0)
+
+ draw_text(axis, graffiti_reference_points_count * 100 + 10, -10, group=group, gcodetools_tag="Gcodetools graffiti reference point text")
+
+ elif self.options.orientation_points_count == "in-out reference point":
+ draw_pointer(group=self.svg.get_current_layer(), x=self.svg.namedview.center, figure="arrow", pointer_type="In-out reference point", text="In-out point")
+
+ else:
+ print_("Inserting orientation points")
+
+ if layer in self.orientation_points:
+ self.error("Active layer already has orientation points! Remove them or select another layer!", "error")
+
+ attr = {"gcodetools": "Gcodetools orientation group"}
+ if transform:
+ attr["transform"] = transform
+
+ orientation_group = layer.add(Group(**attr))
+ doc_height = self.svg.unittouu(self.document.getroot().get('height'))
+ if self.document.getroot().get('height') == "100%":
+ doc_height = 1052.3622047
+ print_("Overriding height from 100 percents to {}".format(doc_height))
+ if self.options.unit == "G21 (All units in mm)":
+ points = [[0., 0., Zsurface], [100., 0., Zdepth], [0., 100., 0.]]
+ elif self.options.unit == "G20 (All units in inches)":
+ points = [[0., 0., Zsurface], [5., 0., Zdepth], [0., 5., 0.]]
+ if self.options.orientation_points_count == "2":
+ points = points[:2]
+ for i in points:
+ name = "Gcodetools orientation point ({} points)".format(
+ self.options.orientation_points_count)
+ grp = orientation_group.add(Group(gcodetools=name))
+ elem = grp.add(PathElement(style="stroke:none;fill:#000000;"))
+ elem.set('gcodetools', "Gcodetools orientation point arrow")
+ elem.path = 'm {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,'\
+ '-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000'\
+ '001 z'.format(i[0], -i[1] + doc_height)
+
+ draw_text("({}; {}; {})".format(i[0], i[1], i[2]), (i[0] + 10), (-i[1] - 10 + doc_height), group=grp, gcodetools_tag="Gcodetools orientation point text")
+
+ ################################################################################
+ #
+ # Tools library
+ #
+ ################################################################################
+ def tab_tools_library(self, layer=None):
+ self.get_info()
+
+ if self.options.tools_library_type == "check":
+ return self.check_tools_and_op()
+
+ # Add a tool to the drawing
+ if layer is None:
+ layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
+ if layer in self.tools:
+ self.error("Active layer already has a tool! Remove it or select another layer!", "error")
+
+ if self.options.tools_library_type == "cylinder cutter":
+ tool = {
+ "name": "Cylindrical cutter",
+ "id": "Cylindrical cutter 0001",
+ "diameter": 10,
+ "penetration angle": 90,
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "lathe cutter":
+ tool = {
+ "name": "Lathe cutter",
+ "id": "Lathe cutter 0001",
+ "diameter": 10,
+ "penetration angle": 90,
+ "feed": "400",
+ "passing feed": "800",
+ "fine feed": "100",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "cone cutter":
+ tool = {
+ "name": "Cone cutter",
+ "id": "Cone cutter 0001",
+ "diameter": 10,
+ "shape": "w",
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "tangent knife":
+ tool = {
+ "name": "Tangent knife",
+ "id": "Tangent knife 0001",
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "100",
+ "4th axis meaning": "tangent knife",
+ "4th axis scale": 1.,
+ "4th axis offset": 0,
+ "tool change gcode": " "
+ }
+
+ elif self.options.tools_library_type == "plasma cutter":
+ tool = {
+ "name": "Plasma cutter",
+ "id": "Plasma cutter 0001",
+ "diameter": 10,
+ "penetration feed": 100,
+ "feed": 400,
+ "gcode before path": """G31 Z-100 F500 (find metal)
+G92 Z0 (zero z)
+G00 Z10 F500 (going up)
+M03 (turn on plasma)
+G04 P0.2 (pause)
+G01 Z1 (going to cutting z)\n""",
+ "gcode after path": "M05 (turn off plasma)\n",
+ }
+ elif self.options.tools_library_type == "graffiti":
+ tool = {
+ "name": "Graffiti",
+ "id": "Graffiti 0001",
+ "diameter": 10,
+ "penetration feed": 100,
+ "feed": 400,
+ "gcode before path": """M03 S1(Turn spray on)\n """,
+ "gcode after path": "M05 (Turn spray off)\n ",
+ "tool change gcode": "(Add G00 here to change sprayer if needed)\n",
+
+ }
+
+ else:
+ tool = self.default_tool
+
+ tool_num = sum([len(self.tools[i]) for i in self.tools])
+ colors = ["00ff00", "0000ff", "ff0000", "fefe00", "00fefe", "fe00fe", "fe7e00", "7efe00", "00fe7e", "007efe", "7e00fe", "fe007e"]
+
+ tools_group = layer.add(Group(gcodetools="Gcodetools tool definition"))
+ bg = tools_group.add(PathElement(gcodetools="Gcodetools tool background"))
+ bg.style = "fill-opacity:0.5;stroke:#444444;"
+ bg.style['fill'] = "#" + colors[tool_num % len(colors)]
+
+ y = 0
+ keys = []
+ for key in self.tools_field_order:
+ if key in tool:
+ keys += [key]
+ for key in tool:
+ if key not in keys:
+ keys += [key]
+ for key in keys:
+ g = tools_group.add(Group(gcodetools="Gcodetools tool parameter"))
+ draw_text(key, 0, y, group=g, gcodetools_tag="Gcodetools tool definition field name", font_size=10 if key != 'name' else 20)
+ param = tool[key]
+ if type(param) == str and re.match("^\\s*$", param):
+ param = "(None)"
+ draw_text(param, 150, y, group=g, gcodetools_tag="Gcodetools tool definition field value", font_size=10 if key != 'name' else 20)
+ v = str(param).split("\n")
+ y += 15 * len(v) if key != 'name' else 20 * len(v)
+
+ bg.set('d', "m -20,-20 l 400,0 0,{:f} -400,0 z ".format(y + 50))
+ tools_group.transform.add_translate(*self.svg.namedview.center)
+ tools_group.transform.add_translate(-150, 0)
+
+ ################################################################################
+ #
+ # Check tools and OP assignment
+ #
+ ################################################################################
+ def check_tools_and_op(self):
+ if len(self.svg.selected) <= 0:
+ self.error("Selection is empty! Will compute whole drawing.")
+ paths = self.paths
+ else:
+ paths = self.selected_paths
+ # Set group
+ parent = self.selected_paths.keys()[0] if len(self.selected_paths.keys()) > 0 else self.layers[0]
+ group = parent.add(Group())
+ trans_ = [[1, 0.3, 0], [0, 0.5, 0]]
+
+ self.set_markers()
+
+ bounds = [float('inf'), float('inf'), float('-inf'), float('-inf')]
+ tools_bounds = {}
+ for layer in self.layers:
+ if layer in paths:
+ self.set_tool(layer)
+ tool = self.tools[layer][0]
+ tools_bounds[layer] = tools_bounds[layer] if layer in tools_bounds else [float("inf"), float("-inf")]
+ for path in paths[layer]:
+ group.insert(0, PathElement(**path.attrib))
+ new = group.getchildren()[0]
+ new.style = Style(
+ stroke='#000044', stroke_width=1,
+ marker_mid='url(#CheckToolsAndOPMarker)',
+ fill=tool["style"].get('fill', '#00ff00'),
+ fill_opacity=tool["style"].get('fill-opacity', 0.5))
+
+ trans = trans_ * self.get_transforms(path)
+ csp = path.path.transform(trans).to_superpath()
+
+ path_bounds = csp_simple_bound(csp)
+ trans = str(Transform(trans))
+ bounds = [min(bounds[0], path_bounds[0]), min(bounds[1], path_bounds[1]), max(bounds[2], path_bounds[2]), max(bounds[3], path_bounds[3])]
+ tools_bounds[layer] = [min(tools_bounds[layer][0], path_bounds[1]), max(tools_bounds[layer][1], path_bounds[3])]
+
+ new.set("transform", trans)
+ trans_[1][2] += 20
+ trans_[1][2] += 100
+
+ for layer in self.layers:
+ if layer in self.tools:
+ if layer in tools_bounds:
+ tool = self.tools[layer][0]
+ g = copy.deepcopy(tool["self_group"])
+ g.attrib["gcodetools"] = "Check tools and OP assignment"
+ trans = [[1, 0.3, bounds[2]], [0, 0.5, tools_bounds[layer][0]]]
+ g.set("transform", str(Transform(trans)))
+ group.insert(0, g)
+
+ ################################################################################
+ # TODO Launch browser on help tab
+ ################################################################################
+ def tab_help(self):
+ self.error("Switch to another tab to run the extensions.\n"
+ "No changes are made if the preferences or help tabs are active.\n\n"
+ "Tutorials, manuals and support can be found at\n"
+ " English support forum:\n"
+ " http://www.cnc-club.ru/gcodetools\n"
+ "and Russian support forum:\n"
+ " http://www.cnc-club.ru/gcodetoolsru")
+ return
+
+ def tab_about(self):
+ return self.tab_help()
+
+ def tab_preferences(self):
+ return self.tab_help()
+
+ def tab_options(self):
+ return self.tab_help()
+
+
+ ################################################################################
+ # Lathe
+ ################################################################################
+ def generate_lathe_gcode(self, subpath, layer, feed_type):
+ if len(subpath) < 2:
+ return ""
+ feed = " F {:f}".format(self.tool[feed_type])
+ x = self.options.lathe_x_axis_remap
+ z = self.options.lathe_z_axis_remap
+ flip_angle = -1 if x.lower() + z.lower() in ["xz", "yx", "zy"] else 1
+ alias = {"X": "I", "Y": "J", "Z": "K", "x": "i", "y": "j", "z": "k"}
+ i_ = alias[x]
+ k_ = alias[z]
+ c = [[subpath[0][1], "move", 0, 0, 0]]
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ c += biarc(sp1, sp2, 0, 0)
+ for i in range(1, len(c)): # Just in case check end point of each segment
+ c[i - 1][4] = c[i][0][:]
+ c += [[subpath[-1][1], "end", 0, 0, 0]]
+ self.draw_curve(c, layer, style=MARKER_STYLE["biarc_style_lathe_{}".format(feed_type)])
+
+ gcode = ("G01 {} {:f} {} {:f}".format(x, c[0][4][0], z, c[0][4][1])) + feed + "\n" # Just in case move to the start...
+ for s in c:
+ if s[1] == 'line':
+ gcode += ("G01 {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1])) + feed + "\n"
+ elif s[1] == 'arc':
+ r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
+ if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
+ r1 = (P(s[0]) - P(s[2]))
+ r2 = (P(s[4]) - P(s[2]))
+ if abs(r1.mag() - r2.mag()) < 0.001:
+ gcode += ("G02" if s[3] * flip_angle < 0 else "G03") + (" {} {:f} {} {:f} {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1], i_, (s[2][0] - s[0][0]), k_, (s[2][1] - s[0][1]))) + feed + "\n"
+ else:
+ r = (r1.mag() + r2.mag()) / 2
+ gcode += ("G02" if s[3] * flip_angle < 0 else "G03") + (" {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1])) + " R{:f}".format(r) + feed + "\n"
+ return gcode
+
+ def tab_lathe(self):
+ self.get_info_plus()
+ if not self.check_dir():
+ return
+ x = self.options.lathe_x_axis_remap
+ z = self.options.lathe_z_axis_remap
+ x = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", x)
+ z = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", z)
+ if x not in ["X", "Y", "Z", "x", "y", "z"] or z not in ["X", "Y", "Z", "x", "y", "z"]:
+ self.error("Lathe X and Z axis remap should be 'X', 'Y' or 'Z'. Exiting...")
+ return
+ if x.lower() == z.lower():
+ self.error("Lathe X and Z axis remap should be the same. Exiting...")
+ return
+ if x.lower() + z.lower() in ["xy", "yx"]:
+ gcode_plane_selection = "G17 (Using XY plane)\n"
+ if x.lower() + z.lower() in ["xz", "zx"]:
+ gcode_plane_selection = "G18 (Using XZ plane)\n"
+ if x.lower() + z.lower() in ["zy", "yz"]:
+ gcode_plane_selection = "G19 (Using YZ plane)\n"
+ self.options.lathe_x_axis_remap = x
+ self.options.lathe_z_axis_remap = z
+
+ paths = self.selected_paths
+ self.tool = []
+ gcode = ""
+ for layer in self.layers:
+ if layer in paths:
+ self.set_tool(layer)
+ if self.tool != self.tools[layer][0]:
+ self.tool = self.tools[layer][0]
+ self.tool["passing feed"] = float(self.tool["passing feed"] if "passing feed" in self.tool else self.tool["feed"])
+ self.tool["feed"] = float(self.tool["feed"])
+ self.tool["fine feed"] = float(self.tool["fine feed"] if "fine feed" in self.tool else self.tool["feed"])
+ gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
+
+ for path in paths[layer]:
+ csp = self.transform_csp(path.path.to_superpath(), layer)
+
+ for subpath in csp:
+ # Offset the path if fine cut is defined.
+ fine_cut = subpath[:]
+ if self.options.lathe_fine_cut_width > 0:
+ r = self.options.lathe_fine_cut_width
+ if self.options.lathe_create_fine_cut_using == "Move path":
+ subpath = [[[i2[0], i2[1] + r] for i2 in i1] for i1 in subpath]
+ else:
+ # Close the path to make offset correct
+ bound = csp_simple_bound([subpath])
+ minx, miny, maxx, maxy = csp_true_bounds([subpath])
+ offsetted_subpath = csp_subpath_line_to(subpath[:], [[subpath[-1][1][0], miny[1] - r * 10], [subpath[0][1][0], miny[1] - r * 10], [subpath[0][1][0], subpath[0][1][1]]])
+ left = subpath[-1][1][0]
+ right = subpath[0][1][0]
+ if left > right:
+ left, right = right, left
+ offsetted_subpath = csp_offset([offsetted_subpath], r if not csp_subpath_ccw(offsetted_subpath) else -r)
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
+ # Join offsetted_subpath together
+ # Hope there won't be any circles
+ subpath = csp_join_subpaths(offsetted_subpath)[0]
+
+ # Create solid object from path and lathe_width
+ bound = csp_simple_bound([subpath])
+ top_start = [subpath[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ top_end = [subpath[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+
+ gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+
+ subpath = csp_concat_subpaths(csp_subpath_line_to([], [top_start, subpath[0][1]]), subpath)
+ subpath = csp_subpath_line_to(subpath, [top_end, top_start])
+
+ width = max(0, self.options.lathe_width - max(0, bound[1]))
+ step = self.tool['depth step']
+ steps = int(math.ceil(width / step))
+ for i in range(steps + 1):
+ current_width = self.options.lathe_width - step * i
+ intersections = []
+ for j in range(1, len(subpath)):
+ sp1 = subpath[j - 1]
+ sp2 = subpath[j]
+ intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width], [bound[2] + 10, current_width], sp1, sp2)]
+ intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width + step], [bound[2] + 10, current_width + step], sp1, sp2)]
+ parts = csp_subpath_split_by_points(subpath, intersections)
+ for part in parts:
+ minx, miny, maxx, maxy = csp_true_bounds([part])
+ y = (maxy[1] + miny[1]) / 2
+ if y > current_width + step:
+ gcode += self.generate_lathe_gcode(part, layer, "passing feed")
+ elif current_width <= y <= current_width + step:
+ gcode += self.generate_lathe_gcode(part, layer, "feed")
+ else:
+ # full step cut
+ part = csp_subpath_line_to([], [part[0][1], part[-1][1]])
+ gcode += self.generate_lathe_gcode(part, layer, "feed")
+
+ top_start = [fine_cut[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ top_end = [fine_cut[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ gcode += "\n(Fine cutting start)\n(Calculating fine cut using {})\n".format(self.options.lathe_create_fine_cut_using)
+ for i in range(int(self.options.lathe_fine_cut_count)):
+ width = self.options.lathe_fine_cut_width * (1 - float(i + 1) / self.options.lathe_fine_cut_count)
+ if width == 0:
+ current_pass = fine_cut
+ else:
+ if self.options.lathe_create_fine_cut_using == "Move path":
+ current_pass = [[[i2[0], i2[1] + width] for i2 in i1] for i1 in fine_cut]
+ else:
+ minx, miny, maxx, maxy = csp_true_bounds([fine_cut])
+ offsetted_subpath = csp_subpath_line_to(fine_cut[:], [[fine_cut[-1][1][0], miny[1] - r * 10], [fine_cut[0][1][0], miny[1] - r * 10], [fine_cut[0][1][0], fine_cut[0][1][1]]])
+ left = fine_cut[-1][1][0]
+ right = fine_cut[0][1][0]
+ if left > right:
+ left, right = right, left
+ offsetted_subpath = csp_offset([offsetted_subpath], width if not csp_subpath_ccw(offsetted_subpath) else -width)
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
+ current_pass = csp_join_subpaths(offsetted_subpath)[0]
+
+ gcode += "\n(Fine cut {:d}-th cicle start)\n".format(i + 1)
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, current_pass[0][1][0], z, current_pass[0][1][1] + self.options.lathe_fine_cut_width, self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, current_pass[0][1][0], z, current_pass[0][1][1], self.tool["fine feed"]))
+
+ gcode += self.generate_lathe_gcode(current_pass, layer, "fine feed")
+ gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+
+ self.export_gcode(gcode)
+
+ ################################################################################
+ #
+ # Lathe modify path
+ # Modifies path to fit current cutter. As for now straight rect cutter.
+ #
+ ################################################################################
+
+ def tab_lathe_modify_path(self):
+ self.get_info()
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+
+ for layer in self.layers:
+ if layer in paths:
+ width = self.options.lathe_rectangular_cutter_width
+ for path in paths[layer]:
+ csp = self.transform_csp(path.path.to_superpath(), layer)
+ new_csp = []
+ for subpath in csp:
+ orientation = subpath[-1][1][0] > subpath[0][1][0]
+ new_subpath = []
+
+ # Split segment at x' and y' == 0
+ for sp1, sp2 in zip(subpath[:], subpath[1:]):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ roots = cubic_solver_real(0, 3 * ax, 2 * bx, cx)
+ roots += cubic_solver_real(0, 3 * ay, 2 * by, cy)
+ new_subpath = csp_concat_subpaths(new_subpath, csp_seg_split(sp1, sp2, roots))
+ subpath = new_subpath
+ new_subpath = []
+ first_seg = True
+ for sp1, sp2 in zip(subpath[:], subpath[1:]):
+ n = csp_normalized_normal(sp1, sp2, 0)
+ a = math.atan2(n[0], n[1])
+ if a == 0 or a == math.pi:
+ n = csp_normalized_normal(sp1, sp2, 1)
+ a = math.atan2(n[0], n[1])
+ if a != 0 and a != math.pi:
+ o = 0 if 0 < a <= math.pi / 2 or -math.pi < a < -math.pi / 2 else 1
+ if not orientation:
+ o = 1 - o
+
+ # Add first horizontal straight line if needed
+ if not first_seg and new_subpath == []:
+ new_subpath = [[[subpath[0][i][0] - width * o, subpath[0][i][1]] for i in range(3)]]
+
+ new_subpath = csp_concat_subpaths(
+ new_subpath,
+ [
+ [[sp1[i][0] - width * o, sp1[i][1]] for i in range(3)],
+ [[sp2[i][0] - width * o, sp2[i][1]] for i in range(3)]
+ ]
+ )
+ first_seg = False
+
+ # Add last horizontal straight line if needed
+ if a == 0 or a == math.pi:
+ new_subpath += [[[subpath[-1][i][0] - width * o, subpath[-1][i][1]] for i in range(3)]]
+
+ new_csp += [new_subpath]
+ self.draw_csp(new_csp, layer)
+
+ ################################################################################
+ # Graffiti function generates Gcode for graffiti drawer
+ ################################################################################
+ def tab_graffiti(self):
+ self.get_info_plus()
+ # Get reference points.
+
+ def get_gcode_coordinates(point, layer):
+ gcode = ''
+ pos = []
+ for ref_point in self.graffiti_reference_points[layer]:
+ c = math.sqrt((point[0] - ref_point[0][0]) ** 2 + (point[1] - ref_point[0][1]) ** 2)
+ gcode += " {} {:f}".format(ref_point[1], c)
+ pos += [c]
+ return pos, gcode
+
+ def graffiti_preview_draw_point(x1, y1, color, radius=.5):
+ self.graffiti_preview = self.graffiti_preview
+ r, g, b, a_ = color
+ for x in range(int(x1 - 1 - math.ceil(radius)), int(x1 + 1 + math.ceil(radius) + 1)):
+ for y in range(int(y1 - 1 - math.ceil(radius)), int(y1 + 1 + math.ceil(radius) + 1)):
+ if x >= 0 and y >= 0 and y < len(self.graffiti_preview) and x * 4 < len(self.graffiti_preview[0]):
+ d = math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2)
+ a = float(a_) * (max(0, (1 - (d - radius))) if d > radius else 1) / 256
+ self.graffiti_preview[y][x * 4] = int(r * a + (1 - a) * self.graffiti_preview[y][x * 4])
+ self.graffiti_preview[y][x * 4 + 1] = int(g * a + (1 - a) * self.graffiti_preview[y][x * 4 + 1])
+ self.graffiti_preview[y][x * 4 + 2] = int(g * b + (1 - a) * self.graffiti_preview[y][x * 4 + 2])
+ self.graffiti_preview[y][x * 4 + 3] = min(255, int(self.graffiti_preview[y][x * 4 + 3] + a * 256))
+
+ def graffiti_preview_transform(x, y):
+ tr = self.graffiti_preview_transform
+ d = max(tr[2] - tr[0] + 2, tr[3] - tr[1] + 2)
+ return [(x - tr[0] + 1) * self.options.graffiti_preview_size / d, self.options.graffiti_preview_size - (y - tr[1] + 1) * self.options.graffiti_preview_size / d]
+
+ def draw_graffiti_segment(layer, start, end, feed, color=(0, 255, 0, 40), emmit=1000):
+ # Emit = dots per second
+ l = math.sqrt(sum([(start[i] - end[i]) ** 2 for i in range(len(start))]))
+ time_ = l / feed
+ c1 = self.graffiti_reference_points[layer][0][0]
+ c2 = self.graffiti_reference_points[layer][1][0]
+ d = math.sqrt((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2)
+ if d == 0:
+ raise ValueError("Error! Reference points should not be the same!")
+ for i in range(int(time_ * emmit + 1)):
+ t = i / (time_ * emmit)
+ r1 = start[0] * (1 - t) + end[0] * t
+ r2 = start[1] * (1 - t) + end[1] * t
+ a = (r1 ** 2 - r2 ** 2 + d ** 2) / (2 * d)
+ h = math.sqrt(r1 ** 2 - a ** 2)
+ xa = c1[0] + a * (c2[0] - c1[0]) / d
+ ya = c1[1] + a * (c2[1] - c1[1]) / d
+
+ x1 = xa + h * (c2[1] - c1[1]) / d
+ x2 = xa - h * (c2[1] - c1[1]) / d
+ y1 = ya - h * (c2[0] - c1[0]) / d
+ y2 = ya + h * (c2[0] - c1[0]) / d
+
+ x = x1 if y1 < y2 else x2
+ y = min(y1, y2)
+ x, y = graffiti_preview_transform(x, y)
+ graffiti_preview_draw_point(x, y, color)
+
+ def create_connector(p1, p2, t1, t2):
+ P1 = P(p1)
+ P2 = P(p2)
+ N1 = P(rotate_ccw(t1))
+ N2 = P(rotate_ccw(t2))
+ r = self.options.graffiti_min_radius
+ C1 = P1 + N1 * r
+ C2 = P2 + N2 * r
+ # Get closest possible centers of arcs, also we define that arcs are both ccw or both not.
+ dc, N1, N2, m = (
+ (
+ (((P2 - N1 * r) - (P1 - N2 * r)).l2(), -N1, -N2, 1)
+ if vectors_ccw(t1, t2) else
+ (((P2 + N1 * r) - (P1 + N2 * r)).l2(), N1, N2, -1)
+ )
+ if vectors_ccw((P1 - C1).to_list(), t1) == vectors_ccw((P2 - C2).to_list(), t2) else
+ (
+ (((P2 + N1 * r) - (P1 - N2 * r)).l2(), N1, -N2, 1)
+ if vectors_ccw(t1, t2) else
+ (((P2 - N1 * r) - (P1 + N2 * r)).l2(), -N1, N2, 1)
+ )
+ )
+ dc = math.sqrt(dc)
+ C1 = P1 + N1 * r
+ C2 = P2 + N2 * r
+ Dc = C2 - C1
+
+ if dc == 0:
+ # can be joined by one arc
+ return csp_from_arc(p1, p2, C1.to_list(), r, t1)
+
+ cos = Dc.x / dc
+ sin = Dc.y / dc
+
+ p1_end = [C1.x - r * sin * m, C1.y + r * cos * m]
+ p2_st = [C2.x - r * sin * m, C2.y + r * cos * m]
+ if point_to_point_d2(p1, p1_end) < 0.0001 and point_to_point_d2(p2, p2_st) < 0.0001:
+ return [[p1, p1, p1], [p2, p2, p2]]
+
+ arc1 = csp_from_arc(p1, p1_end, C1.to_list(), r, t1)
+ arc2 = csp_from_arc(p2_st, p2, C2.to_list(), r, [cos, sin])
+ return csp_concat_subpaths(arc1, arc2)
+
+ if not self.check_dir():
+ return
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ self.tool = []
+ gcode = """(Header)
+(Generated by gcodetools from Inkscape.)
+(Using graffiti extension.)
+(Header end.)"""
+
+ minx = float("inf")
+ miny = float("inf")
+ maxx = float("-inf")
+ maxy = float("-inf")
+ # Get all reference points and path's bounds to make preview
+
+ for layer in self.layers:
+ if layer in paths:
+ # Set reference points
+ if layer not in self.graffiti_reference_points:
+ reference_points = None
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.graffiti_reference_points:
+ reference_points = self.graffiti_reference_points[self.layers[i]]
+ self.graffiti_reference_points[layer] = self.graffiti_reference_points[self.layers[i]]
+ break
+ if reference_points is None:
+ self.error('There are no graffiti reference points for layer {}'.format(layer), "error")
+
+ # Transform reference points
+ for i in range(len(self.graffiti_reference_points[layer])):
+ self.graffiti_reference_points[layer][i][0] = self.transform(self.graffiti_reference_points[layer][i][0], layer)
+ point = self.graffiti_reference_points[layer][i]
+ gcode += "(Reference point {:f};{:f} for {} axis)\n".format(point[0][0], point[0][1], point[1])
+
+ if self.options.graffiti_create_preview:
+ for point in self.graffiti_reference_points[layer]:
+ minx = min(minx, point[0][0])
+ miny = min(miny, point[0][1])
+ maxx = max(maxx, point[0][0])
+ maxy = max(maxy, point[0][1])
+ for path in paths[layer]:
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ csp = self.transform_csp(csp, layer)
+ bounds = csp_simple_bound(csp)
+ minx = min(minx, bounds[0])
+ miny = min(miny, bounds[1])
+ maxx = max(maxx, bounds[2])
+ maxy = max(maxy, bounds[3])
+
+ if self.options.graffiti_create_preview:
+ self.graffiti_preview = list([[255] * (4 * self.options.graffiti_preview_size) for _ in range(self.options.graffiti_preview_size)])
+ self.graffiti_preview_transform = [minx, miny, maxx, maxy]
+
+ for layer in self.layers:
+ if layer in paths:
+
+ r = re.match("\\s*\\(\\s*([0-9\\-,.]+)\\s*;\\s*([0-9\\-,.]+)\\s*\\)\\s*", self.options.graffiti_start_pos)
+ if r:
+ start_point = [float(r.group(1)), float(r.group(2))]
+ else:
+ start_point = [0., 0.]
+ last_sp1 = [[start_point[0], start_point[1] - 10] for _ in range(3)]
+ last_sp2 = [start_point for _ in range(3)]
+
+ self.set_tool(layer)
+ self.tool = self.tools[layer][0]
+ # Change tool every layer. (Probably layer = color so it'll be
+ # better to change it even if the tool has not been changed)
+ gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
+
+ subpaths = []
+ for path in paths[layer]:
+ # Rebuild the paths to polyline.
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ csp = self.transform_csp(csp, layer)
+ subpaths += csp
+ polylines = []
+ while len(subpaths) > 0:
+ i = min([(point_to_point_d2(last_sp2[1], subpaths[i][0][1]), i) for i in range(len(subpaths))])[1]
+ subpath = subpaths[i][:]
+ del subpaths[i]
+ polylines += [
+ ['connector', create_connector(
+ last_sp2[1],
+ subpath[0][1],
+ csp_normalized_slope(last_sp1, last_sp2, 1.),
+ csp_normalized_slope(subpath[0], subpath[1], 0.),
+ )]
+ ]
+ polyline = []
+ spl = None
+
+ # remove zerro length segments
+ i = 0
+ while i < len(subpath) - 1:
+ if cspseglength(subpath[i], subpath[i + 1]) < 0.00000001:
+ subpath[i][2] = subpath[i + 1][2]
+ del subpath[i + 1]
+ else:
+ i += 1
+
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ if spl is not None and abs(cross(csp_normalized_slope(spl, sp1, 1.), csp_normalized_slope(sp1, sp2, 0.))) > 0.1: # TODO add coefficient into inx
+ # We've got sharp angle at sp1.
+ polyline += [sp1]
+ polylines += [['draw', polyline[:]]]
+ polylines += [
+ ['connector', create_connector(
+ sp1[1],
+ sp1[1],
+ csp_normalized_slope(spl, sp1, 1.),
+ csp_normalized_slope(sp1, sp2, 0.),
+ )]
+ ]
+ polyline = []
+ # max_segment_length
+ polyline += [sp1]
+ print_(polyline)
+ print_(sp1)
+
+ spl = sp1
+ polyline += [sp2]
+ polylines += [['draw', polyline[:]]]
+
+ last_sp1 = sp1
+ last_sp2 = sp2
+
+ # Add return to start_point
+ if not polylines:
+ continue
+ polylines += [["connect1", [[polylines[-1][1][-1][1] for _ in range(3)], [start_point for _ in range(3)]]]]
+
+ # Make polylines from polylines. They are still csp.
+ for i in range(len(polylines)):
+ polyline = []
+ l = 0
+ print_("polylines", polylines)
+ print_(polylines[i])
+ for sp1, sp2 in zip(polylines[i][1], polylines[i][1][1:]):
+ print_(sp1, sp2)
+ l = cspseglength(sp1, sp2)
+ if l > 0.00000001:
+ polyline += [sp1[1]]
+ parts = int(math.ceil(l / self.options.graffiti_max_seg_length))
+ for j in range(1, parts):
+ polyline += [csp_at_length(sp1, sp2, float(j) / parts)]
+ if l > 0.00000001:
+ polyline += [sp2[1]]
+ print_(i)
+ polylines[i][1] = polyline
+
+ t = 0
+ last_state = None
+ for polyline_ in polylines:
+ polyline = polyline_[1]
+ # Draw linearization
+ if self.options.graffiti_create_linearization_preview:
+ t += 1
+ csp = [[polyline[i], polyline[i], polyline[i]] for i in range(len(polyline))]
+ draw_csp(self.transform_csp([csp], layer, reverse=True))
+
+ # Export polyline to gcode
+ # we are making transform from XYZA coordinates to R1...Rn
+ # where R1...Rn are radius vectors from graffiti reference points
+ # to current (x,y) point. Also we need to assign custom feed rate
+ # for each segment. And we'll use only G01 gcode.
+ last_real_pos, g = get_gcode_coordinates(polyline[0], layer)
+ last_pos = polyline[0]
+ if polyline_[0] == "draw" and last_state != "draw":
+ gcode += self.tool['gcode before path'] + "\n"
+ for point in polyline:
+ real_pos, g = get_gcode_coordinates(point, layer)
+ real_l = sum([(real_pos[i] - last_real_pos[i]) ** 2 for i in range(len(last_real_pos))])
+ l = (last_pos[0] - point[0]) ** 2 + (last_pos[1] - point[1]) ** 2
+ if l != 0:
+ feed = self.tool['feed'] * math.sqrt(real_l / l)
+ gcode += "G01 " + g + " F {:f}\n".format(feed)
+ if self.options.graffiti_create_preview:
+ draw_graffiti_segment(layer, real_pos, last_real_pos, feed, color=(0, 0, 255, 200) if polyline_[0] == "draw" else (255, 0, 0, 200), emmit=self.options.graffiti_preview_emmit)
+ last_real_pos = real_pos
+ last_pos = point[:]
+ if polyline_[0] == "draw" and last_state != "draw":
+ gcode += self.tool['gcode after path'] + "\n"
+ last_state = polyline_[0]
+ self.export_gcode(gcode, no_headers=True)
+ if self.options.graffiti_create_preview:
+ try:
+ # Draw reference points
+ for layer in self.graffiti_reference_points:
+ for point in self.graffiti_reference_points[layer]:
+ x, y = graffiti_preview_transform(point[0][0], point[0][1])
+ graffiti_preview_draw_point(x, y, (0, 255, 0, 255), radius=5)
+
+ import png
+ writer = png.Writer(width=self.options.graffiti_preview_size, height=self.options.graffiti_preview_size, size=None, greyscale=False, alpha=True, bitdepth=8, palette=None, transparent=None, background=None, gamma=None, compression=None, interlace=False, bytes_per_sample=None, planes=None, colormap=None, maxval=None, chunk_limit=1048576)
+ with open(os.path.join(self.options.directory, self.options.file + ".png"), 'wb') as f:
+ writer.write(f, self.graffiti_preview)
+
+ except:
+ self.error("Png module have not been found!")
+
+ def get_info_plus(self):
+ """Like get_info(), but checks some of the values"""
+ self.get_info()
+ if self.orientation_points == {}:
+ self.error("Orientation points have not been defined! A default set of orientation points has been automatically added.")
+ self.tab_orientation(self.layers[min(1, len(self.layers) - 1)])
+ self.get_info()
+ if self.tools == {}:
+ self.error("Cutting tool has not been defined! A default tool has been automatically added.")
+ self.options.tools_library_type = "default"
+ self.tab_tools_library(self.layers[min(1, len(self.layers) - 1)])
+ self.get_info()
+
+ ################################################################################
+ #
+ # Effect
+ #
+ # Main function of Gcodetools class
+ #
+ ################################################################################
+ def effect(self):
+ start_time = time.time()
+ global options
+ options = self.options
+ options.self = self
+ options.doc_root = self.document.getroot()
+
+ # define print_ function
+ global print_
+ if self.options.log_create_log:
+ try:
+ if os.path.isfile(self.options.log_filename):
+ os.remove(self.options.log_filename)
+ with open(self.options.log_filename, "a") as fhl:
+ fhl.write("""Gcodetools log file.
+Started at {}.
+{}
+""".format(time.strftime("%d.%m.%Y %H:%M:%S"), options.log_filename))
+ except:
+ print_ = lambda *x: None
+ else:
+ print_ = lambda *x: None
+
+ # This automatically calls any `tab_{tab_name_in_inx}` which in this
+ # extension is A LOT of different functions. So see all method prefixed
+ # with tab_ to find out what's supported here.
+ self.options.active_tab()
+
+ print_("------------------------------------------")
+ print_("Done in {:f} seconds".format(time.time() - start_time))
+ print_("End at {}.".format(time.strftime("%d.%m.%Y %H:%M:%S")))
+
+
+ def tab_offset(self):
+ self.get_info()
+ if self.options.offset_just_get_distance:
+ for layer in self.selected_paths:
+ if len(self.selected_paths[layer]) == 2:
+ csp1 = self.selected_paths[layer][0].path.to_superpath()
+ csp2 = self.selected_paths[layer][1].path.to_superpath()
+ dist = csp_to_csp_distance(csp1, csp2)
+ print_(dist)
+ draw_pointer(list(csp_at_t(csp1[dist[1]][dist[2] - 1], csp1[dist[1]][dist[2]], dist[3]))
+ + list(csp_at_t(csp2[dist[4]][dist[5] - 1], csp2[dist[4]][dist[5]], dist[6])), "red", "line", comment=math.sqrt(dist[0]))
+ return
+ if self.options.offset_step == 0:
+ self.options.offset_step = self.options.offset_radius
+ if self.options.offset_step * self.options.offset_radius < 0:
+ self.options.offset_step *= -1
+ time_ = time.time()
+ offsets_count = 0
+ for layer in self.selected_paths:
+ for path in self.selected_paths[layer]:
+
+ offset = self.options.offset_step / 2
+ while abs(offset) <= abs(self.options.offset_radius):
+ offset_ = csp_offset(path.path.to_superpath(), offset)
+ offsets_count += 1
+ if offset_:
+ for iii in offset_:
+ draw_csp([iii], width=1)
+ else:
+ print_("------------Reached empty offset at radius {}".format(offset))
+ break
+ offset += self.options.offset_step
+ print_()
+ print_("-----------------------------------------------------------------------------------")
+ print_("-----------------------------------------------------------------------------------")
+ print_("-----------------------------------------------------------------------------------")
+ print_()
+ print_("Done in {}".format(time.time() - time_))
+ print_("Total offsets count {}".format(offsets_count))
+
+
+if __name__ == '__main__':
+ Gcodetools().run()
diff --git a/share/extensions/other/gcodetools/gcodetools_about.inx b/share/extensions/other/gcodetools/gcodetools_about.inx
new file mode 100644
index 0000000..72f2b99
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_about.inx
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>About</name>
+ <id>ru.cnc-club.filter.gcodetools_about_no_options_no_preferences</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='about' gui-text='About'>
+ <label xml:space="preserve">Gcodetools was developed to make simple Gcode from Inkscape's paths. Gcode is a special format which is used in most of CNC machines. So Gcodetools allows you to use Inkscape as CAM program.
+
+It can be used with a lot of machine types:
+ Mills
+ Lathes
+ Laser and Plasma cutters and engravers
+ Mill engravers
+ Plotters
+ etc.
+
+To get more info visit developers page at http://www.cnc-club.ru/gcodetools</label>
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_area.inx b/share/extensions/other/gcodetools/gcodetools_area.inx
new file mode 100644
index 0000000..6a500e2
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_area.inx
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Area</name>
+ <id>ru.cnc-club.filter.gcodetools_area_area_fill_area_artefacts_ptg</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='area' gui-text='Area'>
+ <param name="max-area-curves" type="int" min="0" max="1000" gui-text="Maximum area cutting curves:">100</param>
+ <param name="area-inkscape-radius" type="float" min="-1000" max="1000" gui-text="Area width:">-10</param>
+ <param name="area-tool-overlap" type="float" min="0" max="1" gui-text="Area tool overlap (0..0.9):">0</param>
+
+ <label xml:space="preserve">
+"Create area offset": creates several Inkscape path offsets to fill original path's area up to "Area radius" value.
+
+Outlines start from "1/2 D" up to "Area width" total width with "D" steps where D is taken from the nearest tool definition ("Tool diameter" value).
+Only one offset will be created if the "Area width" is equal to "1/2 D".
+ </label>
+ </page>
+
+ <page name='area_fill' gui-text='Fill area'>
+ <param name="area-fill-angle" type="float" min="-360" max="360" gui-text="Area fill angle">0</param>
+ <param name="area-fill-shift" type="float" min="-1" max="1" gui-text="Area fill shift">0</param>
+ <param name="area-fill-method" type="float" min="-1" max="1" gui-text="Area fill shift">0</param>
+ <param name="area-fill-method" gui-text="Filling method" type="optiongroup" appearance="radio">
+ <option value="zig-zag">Zig zag</option>
+ <option value="spiral">Spiral</option>
+ </param>
+ </page>
+
+ <page name='area_artefacts' gui-text='Area artifacts'>
+ <param name="area-find-artefacts-diameter" type="float" min="0.01" max="1000" gui-text="Artifact diameter:">5.0</param>
+ <param name="area-find-artefacts-action" type="optiongroup" appearance="radio" gui-text="Action:">
+ <option value="mark with an arrow">mark with an arrow</option>
+ <option value="mark with style">mark with style</option>
+ <option value="delete">delete</option>
+ </param>
+ <label xml:space="preserve">
+Usage:
+1. Select all Area Offsets (gray outlines)
+2. Object/Ungroup (Shift+Ctrl+G)
+3. Press Apply
+
+Suspected small objects will be marked out by colored arrows.
+ </label>
+ </page>
+
+ <page name='path-to-gcode' gui-text='Path to Gcode'>
+ <param name="biarc-tolerance" type='float' precision="5" gui-text='Biarc interpolation tolerance:'>1</param>
+ <param name="biarc-max-split-depth" type="int" gui-text="Maximum splitting depth:">4</param>
+ <param name="path-to-gcode-order" gui-text="Cutting order:" type="optiongroup" appearance="combo">
+ <option value="subpath by subpath">Subpath by subpath</option>
+ <option value="path by path">Path by path</option>
+ <option value="pass by pass">Pass by Pass</option>
+ </param>
+
+ <param name="path-to-gcode-depth-function" type="string" gui-text="Depth function:">d</param>
+ <param name="path-to-gcode-sort-paths" type="bool" gui-text="Sort paths to reduce rapid distance">true</param>
+
+ <label xml:space="preserve">
+Biarc interpolation tolerance is the maximum distance between path and its approximation.
+The segment will be split into two segments if the distance between path's segment and its approximation exceeds biarc interpolation tolerance.
+For depth function c=color intensity from 0.0 (white) to 1.0 (black), d is the depth defined by orientation points, s - surface defined by orientation points.
+</label>
+ </page>
+
+ <page name='options' gui-text='Options'>
+ <param name="Zscale" type="float" precision="5" min="-100000" max="100000" gui-text="Scale along Z axis:">1</param>
+ <param name="Zoffset" type="float" precision="5" min="-100000" max="100000" gui-text="Offset along Z axis:">0.0</param>
+ <param name="auto_select_paths" type="bool" gui-text="Select all paths if nothing is selected">true</param>
+ <param name="min-arc-radius" type="float" precision="5" min="-1000" max="1000" gui-text="Minimum arc radius:">0.05</param>
+ <param name="comment-gcode" type="string" gui-text="Comment Gcode:"></param>
+ <param name="comment-gcode-from-properties" type="bool" gui-text="Get additional comments from object's properties">false</param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_dxf_points.inx b/share/extensions/other/gcodetools/gcodetools_dxf_points.inx
new file mode 100644
index 0000000..8cd782a
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_dxf_points.inx
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>DXF Points</name>
+ <id>ru.cnc-club.filter.gcodetools_dxfpoints_no_options</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='dxfpoints' gui-text='DXF points'>
+ <label xml:space="preserve">
+
+Convert selected objects to drill points (as dxf_import plugin does). Also you can save original shape. Only the start point of each curve will be used.
+
+Also you can manually select object, open XML editor (Shift+Ctrl+X) and add or remove XML tag 'dxfpoint' with any value.
+ </label>
+ <param type='optiongroup' appearance="radio" name='dxfpoints-action' gui-text="Convert selection:">
+ <option value='save'>set as dxfpoint and save shape</option>
+ <option value='replace'>set as dxfpoint and draw arrow</option>
+ <option value='clear'>clear dxfpoint sign</option>
+ </param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_engraving.inx b/share/extensions/other/gcodetools/gcodetools_engraving.inx
new file mode 100644
index 0000000..daf2d4a
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_engraving.inx
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Engraving</name>
+ <id>ru.cnc-club.filter.gcodetools_engraving</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='engraving' gui-text='Engraving'>
+ <param name="engraving-sharp-angle-tollerance" type="float" precision="5" min="150" max="180" gui-text="Smooth convex corners between this value and 180 degrees:">175</param>
+ <param name="engraving-max-dist" type="float" precision="5" min="0" max="1000" gui-text="Maximum distance for engraving (mm/inch):">10</param>
+ <param name="engraving-newton-iterations" type="int" min="2" max="10" gui-text="Accuracy factor (2 low to 10 high):">4</param>
+ <param name="engraving-draw-calculation-paths" type="bool" gui-text="Draw additional graphics to see engraving path">false</param>
+
+ <label xml:space="preserve">
+This function creates path to engrave letters or any shape with sharp angles.
+Cutter's depth as a function of radius is defined by the tool.
+Depth may be any Python expression. For instance:
+
+cone....(45 degrees)......................: w
+cone....(height/diameter=10/3)..: 10*w/3
+sphere..(radius r)...........................: math.sqrt(max(0,r**2-w**2))
+ellipse.(minor axis r, major 4r).....: math.sqrt(max(0,r**2-w**2))*4</label>
+ </page>
+
+ <page name='options' gui-text='Options'>
+ <param name="Zscale" type="float" precision="5" min="-100000" max="100000" gui-text="Scale along Z axis:">1</param>
+ <param name="Zoffset" type="float" precision="5" min="-100000" max="100000" gui-text="Offset along Z axis:">0.0</param>
+ <param name="auto_select_paths" type="bool" gui-text="Select all paths if nothing is selected">true</param>
+ <param name="min-arc-radius" type="float" precision="5" min="-1000" max="1000" gui-text="Minimum arc radius:">0.05</param>
+ <param name="comment-gcode" type="string" gui-text="Comment Gcode:"></param>
+ <param name="comment-gcode-from-properties" type="bool" gui-text="Get additional comments from object's properties">false</param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_graffiti.inx b/share/extensions/other/gcodetools/gcodetools_graffiti.inx
new file mode 100644
index 0000000..ea428b7
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_graffiti.inx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Graffiti</name>
+ <id>ru.cnc-club.filter.gcodetools_graffiti_orientation</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='graffiti' gui-text='Graffiti'>
+ <param name="graffiti-max-seg-length" type="float" precision="5" min="0" max="1000" gui-text="Maximum segment length:">10</param>
+ <param name="graffiti-min-radius" type="float" precision="5" min="0" max="1000" gui-text="Minimal connector radius:">10</param>
+ <param name="graffiti-start-pos" type="string" gui-text="Start position (x;y):">(0.0;0.0)</param>
+ <param name="graffiti-create-preview" type="bool" gui-text="Create preview">true</param>
+ <param name="graffiti-create-linearization-preview" type="bool" gui-text="Create linearization preview">true</param>
+ <param name="graffiti-preview-size" type="int" min="100" max="10000" gui-text="Preview's size (px):">800</param>
+ <param name="graffiti-preview-emmit" type="int" min="100" max="10000" gui-text="Preview's paint emmit (pts/s):">1000</param>
+ </page>
+
+ <page name='orientation' gui-text='Orientation'>
+
+ <param name="orientation-points-count" type="optiongroup" appearance="radio" gui-text="Orientation type:">
+ <option value="2">2-points mode
+ (move and rotate,
+ maintained aspect ratio X/Y)
+ </option>
+ <option value="3">3-points mode
+ (move, rotate and mirror,
+ different X/Y scale)
+ </option>
+ <option value="graffiti">graffiti points</option>
+ <option value="in-out reference point">in-out reference point</option>
+
+ </param>
+ <param name="Zsurface" type="float" precision="5" min="-1000" max="1000" gui-text="Z surface:">0</param>
+ <param name="Zdepth" type="float" precision="5" min="-1000" max="1000" gui-text="Z depth:">-1</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+
+ <label xml:space="preserve">
+Orientation points are used to calculate transformation (offset,scale,mirror,rotation in XY plane) of the path.
+3-points mode only: do not put all three into one line (use 2-points mode instead).
+
+You can modify Z surface, Z depth values later using text tool (3rd coordinates).
+
+If there are no orientation points inside current layer they are taken from the upper layer.
+
+Do not ungroup orientation points! You can select them using double click to enter the group or by Ctrl+Click.
+
+Now press apply to create control points (independent set for each layer).
+ </label>
+ </page>
+
+ <page name='options' gui-text='Options'>
+ <param name="Zscale" type="float" precision="5" min="-100000" max="100000" gui-text="Scale along Z axis:">1</param>
+ <param name="Zoffset" type="float" precision="5" min="-100000" max="100000" gui-text="Offset along Z axis:">0.0</param>
+ <param name="auto_select_paths" type="bool" gui-text="Select all paths if nothing is selected">true</param>
+ <param name="min-arc-radius" type="float" precision="5" min="-1000" max="1000" gui-text="Minimum arc radius:">0.05</param>
+ <param name="comment-gcode" type="string" gui-text="Comment Gcode:"></param>
+ <param name="comment-gcode-from-properties" type="bool" gui-text="Get additional comments from object's properties">false</param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_lathe.inx b/share/extensions/other/gcodetools/gcodetools_lathe.inx
new file mode 100644
index 0000000..ac486bb
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_lathe.inx
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Lathe</name>
+ <id>ru.cnc-club.filter.gcodetools_lathe_lathe_modify_path_ptg</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='lathe' gui-text='Lathe'>
+ <param name="lathe-width" type="float" precision="5" min="0" max="1000" gui-text="Lathe width:">10</param>
+ <param name="lathe-fine-cut-width" type="float" precision="5" min="0" max="1000" gui-text="Fine cut width:">1</param>
+ <param name="lathe-fine-cut-count" type="int" min="0" max="1000" gui-text="Fine cut count:">1</param>
+ <param name="lathe-create-fine-cut-using" gui-text="Create fine cut using:" type="optiongroup" appearance="combo">
+ <option value="Move path">Move path</option>
+ <option value="Offset path">Offset path</option>
+ </param>
+ <param name="lathe-x-axis-remap" type="string" gui-text="Lathe X axis remap:">X</param>
+ <param name="lathe-z-axis-remap" type="string" gui-text="Lathe Z axis remap:">Z</param>
+ </page>
+
+ <page name='lathe_modify_path' gui-text='Lathe modify path'>
+ <label xml:space="preserve">
+ This function modifies path so it will be possible to be cut it with a rectangular cutter.
+ </label>
+ <param name="lathe-rectangular-cutter-width" type="float" precision="5" min="0" max="1000" gui-text="Lathe width:">4</param>
+ </page>
+
+
+ <page name='path-to-gcode' gui-text='Path to Gcode'>
+ <param name="biarc-tolerance" type='float' precision="5" gui-text='Biarc interpolation tolerance:'>1</param>
+ <param name="biarc-max-split-depth" type="int" gui-text="Maximum splitting depth:">4</param>
+ <param name="path-to-gcode-order" gui-text="Cutting order:" type="optiongroup" appearance="combo">
+ <option value="subpath by subpath">Subpath by subpath</option>
+ <option value="path by path">Path by path</option>
+ <option value="pass by pass">Pass by Pass</option>
+ </param>
+
+ <param name="path-to-gcode-depth-function" type="string" gui-text="Depth function:">d</param>
+ <param name="path-to-gcode-sort-paths" type="bool" gui-text="Sort paths to reduce rapid distance">true</param>
+
+ <label xml:space="preserve">
+Biarc interpolation tolerance is the maximum distance between path and its approximation.
+The segment will be split into two segments if the distance between path's segment and its approximation exceeds biarc interpolation tolerance.
+For depth function c=color intensity from 0.0 (white) to 1.0 (black), d is the depth defined by orientation points, s - surface defined by orientation points.
+</label>
+ </page>
+
+ <page name='options' gui-text='Options'>
+ <param name="Zscale" type="float" precision="5" min="-100000" max="100000" gui-text="Scale along Z axis:">1</param>
+ <param name="Zoffset" type="float" precision="5" min="-100000" max="100000" gui-text="Offset along Z axis:">0.0</param>
+ <param name="auto_select_paths" type="bool" gui-text="Select all paths if nothing is selected">true</param>
+ <param name="min-arc-radius" type="float" precision="5" min="-1000" max="1000" gui-text="Minimum arc radius:">0.05</param>
+ <param name="comment-gcode" type="string" gui-text="Comment Gcode:"></param>
+ <param name="comment-gcode-from-properties" type="bool" gui-text="Get additional comments from object's properties">false</param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_orientation_points.inx b/share/extensions/other/gcodetools/gcodetools_orientation_points.inx
new file mode 100644
index 0000000..41aeeb6
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_orientation_points.inx
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Orientation points</name>
+ <id>ru.cnc-club.filter.gcodetools_orientation_no_options_no_preferences</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='orientation' gui-text='Orientation'>
+ <param name="orientation-points-count" type="optiongroup" appearance="radio" gui-text="Orientation type:">
+ <option value="2">2-points mode (move and rotate, maintained aspect ratio X/Y)</option>
+ <option value="3">3-points mode (move, rotate and mirror, different X/Y scale)</option>
+ <option value="graffiti">graffiti points</option>
+ <option value="in-out reference point">in-out reference point</option>
+ </param>
+ <param name="Zsurface" type="float" precision="5" min="-1000" max="1000" gui-text="Z surface:">0</param>
+ <param name="Zdepth" type="float" precision="5" min="-1000" max="1000" gui-text="Z depth:">-1</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Orientation points are used to calculate transformation (offset,scale,mirror,rotation in XY plane) of the path.
+3-points mode only: do not put all three into one line (use 2-points mode instead).
+You can modify Z surface, Z depth values later using text tool (3rd coordinates).
+If there are no orientation points inside current layer, they are taken from the upper layer.
+Do not ungroup orientation points!
+You can select them using double click to enter the group or by Ctrl+Click. Now press apply to create control points (independent set for each layer).
+ </label>
+ </page>
+
+ <page name='about' gui-text='About'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters. This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at English support forum: http://www.cnc-club.ru/gcodetools and Russian support forum: http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+ </label>
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_path_to_gcode.inx b/share/extensions/other/gcodetools/gcodetools_path_to_gcode.inx
new file mode 100644
index 0000000..9c6365c
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_path_to_gcode.inx
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Path to Gcode</name>
+ <id>ru.cnc-club.filter.gcodetools_ptg</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='path-to-gcode' gui-text='Path to Gcode'>
+ <param name="biarc-tolerance" type='float' precision="5" gui-text='Biarc interpolation tolerance:'>1</param>
+ <param name="biarc-max-split-depth" type="int" gui-text="Maximum splitting depth:">4</param>
+ <param name="path-to-gcode-order" gui-text="Cutting order:" type="optiongroup" appearance="combo">
+ <option value="subpath by subpath">Subpath by subpath</option>
+ <option value="path by path">Path by path</option>
+ <option value="pass by pass">Pass by Pass</option>
+ </param>
+
+ <param name="path-to-gcode-depth-function" type="string" gui-text="Depth function:">d</param>
+ <param name="path-to-gcode-sort-paths" type="bool" gui-text="Sort paths to reduce rapid distance">true</param>
+
+ <label xml:space="preserve">
+Biarc interpolation tolerance is the maximum distance between path and its approximation.
+The segment will be split into two segments if the distance between path's segment and its approximation exceeds biarc interpolation tolerance.
+For depth function c=color intensity from 0.0 (white) to 1.0 (black), d is the depth defined by orientation points, s - surface defined by orientation points.
+</label>
+ </page>
+
+ <page name='options' gui-text='Options'>
+ <param name="Zscale" type="float" precision="5" min="-100000" max="100000" gui-text="Scale along Z axis:">1</param>
+ <param name="Zoffset" type="float" precision="5" min="-100000" max="100000" gui-text="Offset along Z axis:">0.0</param>
+ <param name="auto_select_paths" type="bool" gui-text="Select all paths if nothing is selected">true</param>
+ <param name="min-arc-radius" type="float" precision="5" min="-1000" max="1000" gui-text="Minimum arc radius:">0.05</param>
+ <param name="comment-gcode" type="string" gui-text="Comment Gcode:"></param>
+ <param name="comment-gcode-from-properties" type="bool" gui-text="Get additional comments from object's properties">false</param>
+
+ </page>
+
+ <page name='preferences' gui-text='Preferences'>
+ <param name="filename" type="string" gui-text="File:">output.ngc</param>
+ <param name="add-numeric-suffix-to-filename" type="bool" gui-text="Add numeric suffix to filename">true</param>
+
+ <param name="directory" type="string" gui-text="Directory:">/home</param>
+
+ <param name="Zsafe" type="float" precision="5" min="-1000" max="1000" gui-text="Z safe height for G00 move over blank:">5</param>
+ <param name="unit" type="optiongroup" appearance="combo" gui-text="Units (mm or in):">
+ <option value="G21 (All units in mm)">mm</option>
+ <option value="G20 (All units in inches)">in</option>
+ </param>
+ <param name="postprocessor" type="optiongroup" appearance="combo" gui-text="Post-processor:">
+ <option context="GCode postprocessor" value=" ">None</option>
+ <option value="parameterize();">Parameterize Gcode</option>
+ <option value="flip(y);parameterize();">Flip y axis and parameterize Gcode</option>
+ <option value="round(4);">Round all values to 4 digits</option>
+ <option value='regex("G01 Z([0-9\.\-]+).*\(Penetrate\)", lambda match: "G00 Z%f (Fast pre-penetrate)\n%s" %(float(match.group(1))+5, match.group(0)));'>Fast pre-penetrate</option>
+ </param>
+ <param name="postprocessor-custom" type="string" gui-text="Additional post-processor:"></param>
+
+
+ <param name="create-log" type="bool" gui-text="Generate log file">false</param>
+ <param name="log-filename" type="string" gui-text="Full path to log file:"></param>
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_prepare_path_for_plasma.inx b/share/extensions/other/gcodetools/gcodetools_prepare_path_for_plasma.inx
new file mode 100644
index 0000000..53e6035
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_prepare_path_for_plasma.inx
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Prepare path for plasma</name>
+ <id>ru.cnc-club.filter.gcodetools_plasma-prepare-path_no_options_no_preferences</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='plasma-prepare-path' gui-text='Prepare path for plasma or laser cutters'>
+ <param name='in-out-path' type="bool" gui-text="Create in-out paths">true</param>
+ <param name='in-out-path-len' type="float" precision="5" min="0" max="1000000" gui-text='In-out path length:'>10</param>
+ <param name='in-out-path-point-max-dist' type="float" precision="5" min="0" max="1000000" gui-text='In-out path max distance to reference point:'>10</param>
+ <param name="in-out-path-type" gui-text="In-out path type:" type="optiongroup" appearance="combo">
+ <option value="Round">Round</option>
+ <option value="Perpendicular">Perpendicular</option>
+ <option value="Tangent">Tangent</option>
+ </param>
+ <param name='in-out-path-radius' type="float" precision="5" min="0" max="1000000" gui-text='In-out path radius for round path:'>10</param>
+ <param name='in-out-path-replace-original-path' type="bool" gui-text="Replace original path">false</param>
+ <param name='in-out-path-do-not-add-reference-point' type="bool" gui-text="Do not add in-out reference points">false</param>
+
+ <label>-------------------------------------------------</label>
+ <param name='plasma-prepare-corners' type="bool" gui-text="Prepare corners">true</param>
+ <param name='plasma-prepare-corners-distance' type="float" precision="5" min="0" max="1000000" gui-text='Stepout distance for corners:'>10</param>
+ <param name='plasma-prepare-corners-tolerance' type="float" precision="5" min="0" max="180" gui-text='Maximum angle for corner (0-180 deg):'>140</param>
+
+
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in:
+Converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/gcodetools_tools_library.inx b/share/extensions/other/gcodetools/gcodetools_tools_library.inx
new file mode 100644
index 0000000..ca78a0c
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools_tools_library.inx
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Tools library</name>
+ <id>ru.cnc-club.filter.gcodetools_tools_library_no_options_no_preferences</id>
+ <param name='active-tab' type="notebook">
+
+ <page name='tools_library' gui-text='Tools library'>
+
+ <param type='optiongroup' appearance="radio" name='tools-library-type' gui-text="Tools type:">
+ <option value='default tool'>default</option>
+ <option value='cylinder cutter'>cylinder</option>
+ <option value='cone cutter'>cone</option>
+ <option value='plasma cutter'>plasma</option>
+ <option value='tangent knife'>tangent knife</option>
+ <option value='lathe cutter'>lathe cutter</option>
+ <option value='graffiti'>graffiti</option>
+
+
+ <option value='check'>Just check tools</option>
+
+ </param>
+
+ <label xml:space="preserve">
+Selected tool type fills appropriate default values. You can change these values using the Text tool later on.
+
+The topmost (z order) tool in the active layer is used. If there is no tool inside the current layer it is taken from the upper layer.
+
+Press Apply to create new tool.
+ </label>
+ </page>
+
+ <page name='help' gui-text='Help'>
+ <label xml:space="preserve">
+Gcodetools plug-in: converts paths to Gcode (using circular interpolation), makes offset paths and engraves sharp corners using cone cutters.
+This plug-in calculates Gcode for paths using circular interpolation or linear motion when needed.
+
+Tutorials, manuals and support can be found at
+English support forum:
+ http://www.cnc-club.ru/gcodetools
+
+and Russian support forum:
+ http://www.cnc-club.ru/gcodetoolsru
+
+Credits: Nick Drobchenko, Vladimir Kalyaev, John Brooker, Henry Nicolas, Chris Lusby Taylor.
+
+Gcodetools ver. 1.7
+</label>
+
+ </page>
+
+ </param>
+ <effect>
+ <object-type>path</object-type>
+ <effects-menu>
+ <submenu name="Gcodetools"/>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">gcodetools.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/other/gcodetools/genpofiles.sh b/share/extensions/other/gcodetools/genpofiles.sh
new file mode 100755
index 0000000..5106c52
--- /dev/null
+++ b/share/extensions/other/gcodetools/genpofiles.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+OLDPATH=`pwd`
+
+cd ../..
+find share/extensions -name "*.inx" | sort | xargs -n 1 printf "[type: gettext/xml] %s\n"
+cd ${OLDPATH}
diff --git a/share/extensions/other/gcodetools/setup.cfg b/share/extensions/other/gcodetools/setup.cfg
new file mode 100644
index 0000000..b7e4789
--- /dev/null
+++ b/share/extensions/other/gcodetools/setup.cfg
@@ -0,0 +1,2 @@
+[aliases]
+test=pytest
diff --git a/share/extensions/other/gcodetools/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out
new file mode 100644
index 0000000..61a62cc
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out
@@ -0,0 +1,41 @@
+<!-- Created with Inkscape (http://www.inkscape.org/) --><svg xmlns:ns1="https://launchpad.net/jessyink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="1000" height="1000" viewBox="0 0 1000 1000" version="1.1" id="svg8" inkscape:version="0.92.4 5da689c313, 2019-01-14" sodipodi:docname="test.svg">
+ <defs id="defs33">
+ <marker inkscape:stockid="Arrow2Lstart" orient="auto" refY="0.0" refX="0.0" id="Arrow2Lstart" style="overflow:visible" inkscape:isstock="true">
+ <path id="path859" style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z " transform="scale(1.1) translate(1,0)"/>
+ </marker>
+ </defs>
+ <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1" objecttolerance="10" gridtolerance="10" guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-width="1920" inkscape:window-height="1017" id="base" showgrid="true" inkscape:snap-text-baseline="true" inkscape:zoom="0.6675088" inkscape:cx="202.7349" inkscape:cy="481.16986" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="webslicer-layer">
+ <inkscape:grid type="xygrid" id="grid27" spacingx="10" spacingy="10" empspacing="10" color="#8080ff" opacity="0.25098039" empcolor="#0000ff" empopacity="0.25098039"/>
+ </sodipodi:namedview>
+ <g inkscape:groupmode="layer" id="webslicer-layer" inkscape:label="Slide3" style="display:inline">
+ <rect height="201.49811" width="248.69112" y="353.92661" x="651.3089" id="slicerect1" style="opacity:0.5;fill:#ff0000;stroke-width:1.11927199">
+ <desc id="desc51">format: png
+dpi: 96
+layout-disposition: bg-el-norepeat
+layout-position-anchor: tl</desc>
+ </rect>
+ <svg:g gcodetools="Gcodetools orientation group"><g gcodetools="Gcodetools orientation point (3 points)"><svg:path style="stroke:none;fill:#000000;" gcodetools="Gcodetools orientation point arrow" d="m 0 1000 l 2.9375 -6.34375 l 0.8125 1.90625 l 6.84375 -6.84375 l 0 0 l 0.6875 0.6875 l -6.84375 6.84375 l 1.90625 0.8125 z"/><svg:text x="10.0" y="990.0" style="font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;font-size:10.000000px;" gcodetools="Gcodetools orientation point text" xml:space="preserve"><tspan x="10.0" y="990.0" sodipodi:role="line">(0.0; 0.0; 0.00000)</tspan></svg:text></g><g gcodetools="Gcodetools orientation point (3 points)"><svg:path style="stroke:none;fill:#000000;" gcodetools="Gcodetools orientation point arrow" d="m 100 1000 l 2.9375 -6.34375 l 0.8125 1.90625 l 6.84375 -6.84375 l 0 0 l 0.6875 0.6875 l -6.84375 6.84375 l 1.90625 0.8125 z"/><svg:text x="110.0" y="990.0" style="font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;font-size:10.000000px;" gcodetools="Gcodetools orientation point text" xml:space="preserve"><tspan x="110.0" y="990.0" sodipodi:role="line">(100.0; 0.0; -0.00000)</tspan></svg:text></g><g gcodetools="Gcodetools orientation point (3 points)"><svg:path style="stroke:none;fill:#000000;" gcodetools="Gcodetools orientation point arrow" d="m 0 900 l 2.9375 -6.34375 l 0.8125 1.90625 l 6.84375 -6.84375 l 0 0 l 0.6875 0.6875 l -6.84375 6.84375 l 1.90625 0.8125 z"/><svg:text x="10.0" y="890.0" style="font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;font-size:10.000000px;" gcodetools="Gcodetools orientation point text" xml:space="preserve"><tspan x="10.0" y="890.0" sodipodi:role="line">(0.0; 100.0; 0.0)</tspan></svg:text></g></svg:g></g>
+ <g inkscape:groupmode="layer" id="layer2" inkscape:label="Slide2" style="display:inline">
+ <circle style="display:inline;fill:#000080;stroke:none" id="c1" cx="150" cy="450" r="50" inkscape:label="#path3736"/>
+ <ellipse style="display:inline;fill:none;stroke:#ff0000;stroke-width:16" id="c2" cx="400" cy="450" rx="100" ry="50" inkscape:label="#path3738"/>
+ <path style="display:inline;fill:#ffff00;stroke:#008000;stroke-width:16" id="c3" sodipodi:type="arc" sodipodi:cx="700" sodipodi:cy="450" sodipodi:rx="100" sodipodi:ry="50" sodipodi:start="0.59013865" sodipodi:end="5.6484511" d="m 783.08635,477.82381 a 100,50 0 0 1 -111.09848,20.17442 100,50 0 0 1 -71.96301,-46.88343 100,50 0 0 1 67.71127,-48.44091 100,50 0 0 1 112.7868,17.67793 L 700,450 Z" inkscape:label="#path3740"/>
+ <path style="display:inline;fill:none;stroke:#000000;stroke-width:10;marker-start:url(#Arrow2Lstart)" d="M 100,600 200,700 300,600 400,700" id="p1" inkscape:connector-curvature="0" inkscape:label="#path3746"/>
+ <path style="display:inline;fill:none;stroke:#000000;stroke-width:10" d="m 500,600 c 0,0 0,100 100,100 100,0 0,-100 100,-100 100,0 100,100 100,100" id="p2" inkscape:connector-curvature="0" inkscape:label="#path3748"/>
+ <path sodipodi:type="star" style="display:inline;fill:#ffff00;stroke:#008000;stroke-width:10" id="s1" sodipodi:sides="5" sodipodi:cx="189.03001" sodipodi:cy="847.93945" sodipodi:r1="69.364868" sodipodi:r2="34.682434" sodipodi:arg1="0.63598373" sodipodi:arg2="1.2643023" inkscape:flatsided="false" inkscape:rounded="0" inkscape:randomized="0" d="m 244.8332,889.14005 -45.33887,-8.13446 -32.40428,32.73753 -6.27415,-45.63352 -41.14872,-20.70184 41.46124,-20.06861 6.97297,-45.53197 31.89861,33.23044 45.45824,-7.43847 -21.74681,40.60615 z" inkscape:transform-center-x="6.4673011" inkscape:transform-center-y="-0.16430137"/>
+ <use style="display:inline" x="0" y="0" xlink:href="#s1" inkscape:transform-center-x="6.4673011" inkscape:transform-center-y="-0.16430137" id="u1" transform="translate(200,2.9962152)" width="100%" height="100%" inkscape:label="#use3808"/>
+ </g>
+ <g inkscape:label="Slide1" inkscape:groupmode="layer" id="layer1" style="display:inline">
+ <text xml:space="preserve" style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" x="100" y="100" id="t1" inkscape:label="#text12"><tspan sodipodi:role="line" id="tspan10" x="100" y="100">Hello World</tspan></text>
+ <flowRoot xml:space="preserve" id="t4" style="font-size:40px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" transform="matrix(0.26458333,0,0,0.26458333,372.02961,28.904505)" inkscape:label="#flowRoot14"><flowRegion id="flowRegion16"><rect id="rect18" width="264.5675" height="262.16187" x="105.71429" y="219.66254"/></flowRegion><flowPara id="flowPara20" style="font-size:55.43307114px">flow text which wraps</flowPara></flowRoot> <text xml:space="preserve" style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" x="200" y="100" id="t2" inkscape:label="#text3727"><tspan sodipodi:role="line" id="tspan3725" x="200" y="100" style="font-size:14.66666698px">UPPER</tspan></text>
+ <text xml:space="preserve" style="font-size:10.58333302px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" x="300" y="100" id="t3" inkscape:label="#text3735"><tspan sodipodi:role="line" id="tspan3733" x="300" y="100" style="font-size:14.66666698px">Multi line</tspan><tspan sodipodi:role="line" x="300" y="118.33334" id="tspan3737" style="font-size:14.66666698px">text</tspan><tspan sodipodi:role="line" x="300" y="136.66667" id="tspan3739" style="font-size:14.66666698px">FOO</tspan></text>
+ <g id="t5" transform="translate(445.71038,-129.64807)" inkscape:label="#g3772">
+ <text id="text3762" y="229.64807" x="54.289616" style="font-size:10.58333302px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" xml:space="preserve"><tspan y="229.64807" x="54.289616" id="tspan3760" sodipodi:role="line" style="font-size:14.66666698px">Grouped</tspan></text>
+ <text id="text3766" y="259.64807" x="54.289619" style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none" xml:space="preserve"><tspan y="259.64807" x="54.289619" id="tspan3764" sodipodi:role="line" style="font-size:14.66666698px">text</tspan></text>
+ </g>
+ <rect style="fill:#000080;stroke:none" id="r1" width="100" height="100" x="100" y="200" inkscape:label="#rect3732"/>
+ <rect style="fill:none;stroke:#ff0000;stroke-width:16" id="r2" width="200" height="100" x="300" y="200" inkscape:label="#rect3734"/>
+ <rect style="fill:#ffff00;stroke:#008000;stroke-width:16" id="r3" width="200" height="100" x="600" y="200" ry="38.950798" rx="44.943226" inkscape:label="#rect3744"/>
+ </g>
+ <script ns1:version="1.5.5" id="JessyInk">// dummy
+</script>
+</svg> \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out
new file mode 100644
index 0000000..e2ae9a3
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out
@@ -0,0 +1,30 @@
+%
+(Header)
+(Generated by gcodetools from Inkscape.)
+(Using default header. To add your own header create file "header" in the output dir.)
+M3
+(Header end.)
+G21 (All units in mm)
+
+(Start cutting path id: p1)
+(Change tool to Default tool)
+
+G00 Z5.000000
+G00 X100.000000 Y400.000000
+
+G01 Z-0.125000 F100.0(Penetrate)
+G01 X200.000000 Y300.000000 Z-0.125000 F400.000000
+G01 X300.000000 Y400.000000 Z-0.125000
+G01 X400.000000 Y300.000000 Z-0.125000
+G00 Z5.000000
+
+(End cutting path id: p1)
+
+
+(Footer)
+M5
+G00 X0.0000 Y0.0000
+M2
+(Using default footer. To add your own footer create file "footer" in the output dir.)
+(end)
+% \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out
new file mode 100644
index 0000000..8694e43
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out
@@ -0,0 +1,40 @@
+%
+(Header)
+(Generated by gcodetools from Inkscape.)
+(Using default header. To add your own header create file "header" in the output dir.)
+M3
+(Header end.)
+G21 (All units in mm)
+(Change tool to Default tool)
+
+G01 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 401.000000 F 800.000000
+G01 X 200.000000 Z 301.000000 F 800.000000
+G01 X 300.000000 Z 401.000000 F 800.000000
+G01 X 400.000000 Z 301.000000 F 800.000000
+G01 X 400.000000 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 16.000000 F 800.000000
+
+(Fine cutting start)
+(Calculating fine cut using Move path)
+
+(Fine cut 1-th cicle start)
+G01 X 100.000000 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 401.000000 F 800.000000
+G01 X 100.000000 Z 400.000000 F 800.000000
+G01 X 100.000000 Z 400.000000 F 800.000000
+G01 X 200.000000 Z 300.000000 F 800.000000
+G01 X 300.000000 Z 400.000000 F 800.000000
+G01 X 400.000000 Z 300.000000 F 800.000000
+G01 Z 16.000000 F 800.000000
+G01 X 100.000000 Z 16.000000 F 800.000000
+
+(Footer)
+M5
+G00 X0.0000 Y0.0000
+M2
+(Using default footer. To add your own footer create file "footer" in the output dir.)
+(end)
+% \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out
diff --git a/share/extensions/other/gcodetools/tests/data/svg/default-inkscape-SVG.svg b/share/extensions/other/gcodetools/tests/data/svg/default-inkscape-SVG.svg
new file mode 100644
index 0000000..259e13c
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/svg/default-inkscape-SVG.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ width="793.7007874"
+ height="1122.519685">
+ <defs />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="0.35"
+ inkscape:cx="375"
+ inkscape:cy="520"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1" />
+ <metadata>
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" />
+</svg>
diff --git a/share/extensions/other/gcodetools/tests/data/svg/shapes.svg b/share/extensions/other/gcodetools/tests/data/svg/shapes.svg
new file mode 100644
index 0000000..eb9caed
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/data/svg/shapes.svg
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:ns1="https://launchpad.net/jessyink"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1000"
+ height="1000"
+ viewBox="0 0 1000 1000"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"
+ sodipodi:docname="test.svg">
+ <defs
+ id="defs33">
+ <marker
+ inkscape:stockid="Arrow2Lstart"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Lstart"
+ style="overflow:visible"
+ inkscape:isstock="true">
+ <path
+ id="path859"
+ style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(1.1) translate(1,0)" />
+ </marker>
+ </defs>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1017"
+ id="base"
+ showgrid="true"
+ inkscape:snap-text-baseline="true"
+ inkscape:zoom="0.6675088"
+ inkscape:cx="202.7349"
+ inkscape:cy="481.16986"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="webslicer-layer">
+ <inkscape:grid
+ type="xygrid"
+ id="grid27"
+ spacingx="10"
+ spacingy="10"
+ empspacing="10"
+ color="#8080ff"
+ opacity="0.25098039"
+ empcolor="#0000ff"
+ empopacity="0.25098039" />
+ </sodipodi:namedview>
+ <g
+ inkscape:groupmode="layer"
+ id="webslicer-layer"
+ inkscape:label="Slide3"
+ style="display:inline">
+ <rect
+ height="201.49811"
+ width="248.69112"
+ y="353.92661"
+ x="651.3089"
+ id="slicerect1"
+ style="opacity:0.5;fill:#ff0000;stroke-width:1.11927199">
+ <desc
+ id="desc51">format: png
+dpi: 96
+layout-disposition: bg-el-norepeat
+layout-position-anchor: tl</desc>
+ </rect>
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="Slide2"
+ style="display:inline">
+ <circle
+ style="display:inline;fill:#000080;stroke:none"
+ id="c1"
+ cx="150"
+ cy="450"
+ r="50"
+ inkscape:label="#path3736" />
+ <ellipse
+ style="display:inline;fill:none;stroke:#ff0000;stroke-width:16"
+ id="c2"
+ cx="400"
+ cy="450"
+ rx="100"
+ ry="50"
+ inkscape:label="#path3738" />
+ <path
+ style="display:inline;fill:#ffff00;stroke:#008000;stroke-width:16"
+ id="c3"
+ sodipodi:type="arc"
+ sodipodi:cx="700"
+ sodipodi:cy="450"
+ sodipodi:rx="100"
+ sodipodi:ry="50"
+ sodipodi:start="0.59013865"
+ sodipodi:end="5.6484511"
+ d="m 783.08635,477.82381 a 100,50 0 0 1 -111.09848,20.17442 100,50 0 0 1 -71.96301,-46.88343 100,50 0 0 1 67.71127,-48.44091 100,50 0 0 1 112.7868,17.67793 L 700,450 Z"
+ inkscape:label="#path3740" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:10;marker-start:url(#Arrow2Lstart)"
+ d="M 100,600 200,700 300,600 400,700"
+ id="p1"
+ inkscape:connector-curvature="0"
+ inkscape:label="#path3746" />
+ <path
+ style="display:inline;fill:none;stroke:#000000;stroke-width:10"
+ d="m 500,600 c 0,0 0,100 100,100 100,0 0,-100 100,-100 100,0 100,100 100,100"
+ id="p2"
+ inkscape:connector-curvature="0"
+ inkscape:label="#path3748" />
+ <path
+ sodipodi:type="star"
+ style="display:inline;fill:#ffff00;stroke:#008000;stroke-width:10"
+ id="s1"
+ sodipodi:sides="5"
+ sodipodi:cx="189.03001"
+ sodipodi:cy="847.93945"
+ sodipodi:r1="69.364868"
+ sodipodi:r2="34.682434"
+ sodipodi:arg1="0.63598373"
+ sodipodi:arg2="1.2643023"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 244.8332,889.14005 -45.33887,-8.13446 -32.40428,32.73753 -6.27415,-45.63352 -41.14872,-20.70184 41.46124,-20.06861 6.97297,-45.53197 31.89861,33.23044 45.45824,-7.43847 -21.74681,40.60615 z"
+ inkscape:transform-center-x="6.4673011"
+ inkscape:transform-center-y="-0.16430137" />
+ <use
+ style="display:inline"
+ x="0"
+ y="0"
+ xlink:href="#s1"
+ inkscape:transform-center-x="6.4673011"
+ inkscape:transform-center-y="-0.16430137"
+ id="u1"
+ transform="translate(200,2.9962152)"
+ width="100%"
+ height="100%"
+ inkscape:label="#use3808" />
+ </g>
+ <g
+ inkscape:label="Slide1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ style="display:inline">
+ <text
+ xml:space="preserve"
+ style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ x="100"
+ y="100"
+ id="t1"
+ inkscape:label="#text12"><tspan
+ sodipodi:role="line"
+ id="tspan10"
+ x="100"
+ y="100">Hello World</tspan></text>
+ <flowRoot
+ xml:space="preserve"
+ id="t4"
+ style="font-size:40px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ transform="matrix(0.26458333,0,0,0.26458333,372.02961,28.904505)"
+ inkscape:label="#flowRoot14"><flowRegion
+ id="flowRegion16"><rect
+ id="rect18"
+ width="264.5675"
+ height="262.16187"
+ x="105.71429"
+ y="219.66254" /></flowRegion><flowPara
+ id="flowPara20"
+ style="font-size:55.43307114px">flow text which wraps</flowPara></flowRoot> <text
+ xml:space="preserve"
+ style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ x="200"
+ y="100"
+ id="t2"
+ inkscape:label="#text3727"><tspan
+ sodipodi:role="line"
+ id="tspan3725"
+ x="200"
+ y="100"
+ style="font-size:14.66666698px">UPPER</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:10.58333302px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ x="300"
+ y="100"
+ id="t3"
+ inkscape:label="#text3735"><tspan
+ sodipodi:role="line"
+ id="tspan3733"
+ x="300"
+ y="100"
+ style="font-size:14.66666698px">Multi line</tspan><tspan
+ sodipodi:role="line"
+ x="300"
+ y="118.33334"
+ id="tspan3737"
+ style="font-size:14.66666698px">text</tspan><tspan
+ sodipodi:role="line"
+ x="300"
+ y="136.66667"
+ id="tspan3739"
+ style="font-size:14.66666698px">FOO</tspan></text>
+ <g
+ id="t5"
+ transform="translate(445.71038,-129.64807)"
+ inkscape:label="#g3772">
+ <text
+ id="text3762"
+ y="229.64807"
+ x="54.289616"
+ style="font-size:10.58333302px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ xml:space="preserve"><tspan
+ y="229.64807"
+ x="54.289616"
+ id="tspan3760"
+ sodipodi:role="line"
+ style="font-size:14.66666698px">Grouped</tspan></text>
+ <text
+ id="text3766"
+ y="259.64807"
+ x="54.289619"
+ style="font-size:14.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke:none"
+ xml:space="preserve"><tspan
+ y="259.64807"
+ x="54.289619"
+ id="tspan3764"
+ sodipodi:role="line"
+ style="font-size:14.66666698px">text</tspan></text>
+ </g>
+ <rect
+ style="fill:#000080;stroke:none"
+ id="r1"
+ width="100"
+ height="100"
+ x="100"
+ y="200"
+ inkscape:label="#rect3732" />
+ <rect
+ style="fill:none;stroke:#ff0000;stroke-width:16"
+ id="r2"
+ width="200"
+ height="100"
+ x="300"
+ y="200"
+ inkscape:label="#rect3734" />
+ <rect
+ style="fill:#ffff00;stroke:#008000;stroke-width:16"
+ id="r3"
+ width="200"
+ height="100"
+ x="600"
+ y="200"
+ ry="38.950798"
+ rx="44.943226"
+ inkscape:label="#rect3744" />
+ </g>
+ <script
+ ns1:version="1.5.5"
+ id="JessyInk">// dummy
+</script>
+</svg>
diff --git a/share/extensions/other/gcodetools/tests/dev_requirements.txt b/share/extensions/other/gcodetools/tests/dev_requirements.txt
new file mode 100644
index 0000000..0e358d9
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/dev_requirements.txt
@@ -0,0 +1,11 @@
+# Dev Requirements
+pytest
+pytest-cov
+
+# Code requirements
+typing
+lxml
+numpy
+scour
+pyserial
+cssselect \ No newline at end of file
diff --git a/share/extensions/other/gcodetools/tests/test_gcodetools.py b/share/extensions/other/gcodetools/tests/test_gcodetools.py
new file mode 100644
index 0000000..0575729
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/test_gcodetools.py
@@ -0,0 +1,65 @@
+# coding=utf-8
+
+import sys
+import os
+
+from gcodetools import Gcodetools
+from inkex.tester import ComparisonMixin, InkscapeExtensionTestMixin, TestCase
+from inkex.tester.filters import CompareOrderIndependentBytes
+
+SETTINGS = (
+ '--id=p1', '--max-area-curves=100',
+ '--area-inkscape-radius=-10', '--area-tool-overlap=0',
+ '--area-fill-angle=0', '--area-fill-shift=0', '--area-fill-method=0',
+ '--area-fill-method=0', '--area-find-artefacts-diameter=5',
+ '--area-find-artefacts-action=mark with an arrow',
+ '--biarc-tolerance=1', '--biarc-max-split-depth=4',
+ '--path-to-gcode-order=subpath by subpath',
+ '--path-to-gcode-depth-function=d',
+ '--path-to-gcode-sort-paths=false', '--Zscale=1', '--Zoffset=0',
+ '--auto_select_paths=true', '--min-arc-radius=0.05000000074505806',
+ '--comment-gcode-from-properties=false', '--create-log=false',
+ '--add-numeric-suffix-to-filename=false', '--Zsafe=5',
+ '--unit=G21 (All units in mm)', '--postprocessor= ',
+)
+FILESET = SETTINGS + ('--directory=/home', '--filename=output.ngc',)
+
+class TestGcodetoolsBasic(ComparisonMixin, InkscapeExtensionTestMixin, TestCase):
+ stderr_protect = False
+ effect_class = Gcodetools
+ comparisons = [
+ FILESET + ('--active-tab="area_fill"',),
+ FILESET + ('--active-tab="area"',),
+ FILESET + ('--active-tab="area_artefacts"',),
+ FILESET + ('--active-tab="dxfpoints"',),
+ FILESET + ('--active-tab="orientation"',),
+ FILESET + ('--active-tab="tools_library"',),
+ FILESET + ('--active-tab="lathe_modify_path"',),
+ FILESET + ('--active-tab="offset"',),
+ FILESET + ('--active-tab="plasma-prepare-path"',),
+ ]
+ compare_filters = [CompareOrderIndependentBytes()]
+ compare_file_extension = 'dxf'
+
+ def test_all_comparisons(self):
+ """
+ gcodetools tries to write to a folder and filename specified
+ on the command line, this needs to be handled carefully.
+ """
+ for tab in (
+ ('--active-tab="path-to-gcode"',),
+ #('--active-tab="engraving"',),
+ #('--active-tab="graffiti"',),
+ ('--active-tab="lathe"',),
+ ):
+ args = SETTINGS + tab + (
+ '--directory={}'.format(self.tempdir),
+ '--filename=output.ngc',
+ )
+ outfile = os.path.join(self.tempdir, 'output.ngc')
+ self.assertCompare(self.compare_file, None, args, 'output.ngc')
+
+class TestGcodeToolsOrientationScientific(ComparisonMixin, TestCase):
+ effect_class = Gcodetools
+ compare_file = "svg/shapes.svg"
+ comparisons = [("--active-tab=orientation", "--Zsurface=0.00000000000001e-5", "--Zdepth=-9.71445146547012e-17", "--orientation-points-count=3")]
diff --git a/share/extensions/other/gcodetools/tests/test_inkex_inx.py b/share/extensions/other/gcodetools/tests/test_inkex_inx.py
new file mode 100644
index 0000000..211b746
--- /dev/null
+++ b/share/extensions/other/gcodetools/tests/test_inkex_inx.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+# coding=utf-8
+"""
+Test elements extra logic from svg xml lxml custom classes.
+"""
+
+import os
+from glob import glob
+
+from inkex.utils import PY3
+from inkex.tester import TestCase
+from inkex.tester.inx import InxMixin
+from inkex.inx import InxFile
+
+class InxTestCase(InxMixin, TestCase):
+ """Test INX files"""
+ def test_inx_effect(self):
+ inx = InxFile("""
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>TestOne</name>
+ <id>org.inkscape.test.inx_one</id>
+ <effect needs-live-preview="false">
+ <object-type>all</object-type>
+ <effects-menu>
+ <submenu name="Banana">
+ <submenu name="Ice Cream"/>
+ </submenu>
+ </effects-menu>
+ </effect>
+ <script>
+ <command location="inx" interpreter="python">inx_test.py</command>
+ </script>
+</inkscape-extension>""")
+ self.assertEqual(inx.name, 'TestOne')
+ self.assertEqual(inx.ident, 'org.inkscape.test.inx_one')
+ self.assertEqual(inx.slug, 'InxOne')
+ self.assertEqual(inx.metadata, {'type': 'effect', 'preview': False, 'objects': 'all'})
+ self.assertEqual(inx.menu, ['Banana', 'Ice Cream', 'TestOne'])
+ self.assertEqual(inx.warnings, [])
+
+ def test_inx_output(self):
+ inx = InxFile("""
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <_name>TestTwo</_name>
+ <id>org.inkscape.test.inx_two</id>
+ <output>
+ <extension>.inx</extension>
+ <mimetype>text/xml+inx</mimetype>
+ <filetypename>Extension (*.inx)</filetypename>
+ <_filetypetooltip>The extension extension repention suspension.</_filetypetooltip>
+ <dataloss>true</dataloss>
+ </output>
+</inkscape-extension>""")
+ self.assertEqual(inx.name, 'TestTwo')
+ self.assertEqual(inx.ident, 'org.inkscape.test.inx_two')
+ self.assertEqual(inx.metadata, {
+ 'dataloss': True,
+ 'extension': '.inx',
+ 'mimetype': 'text/xml+inx',
+ 'name': 'Extension (*.inx)',
+ 'tooltip': 'The extension extension repention suspension.',
+ 'type': 'output'})
+ self.assertEqual(inx.warnings, [
+ 'Use of old translation scheme: <_filetypetooltip...>',
+ 'Use of old translation scheme: <_name...>'])
+
+ def test_inx_input(self):
+ inx = InxFile("""<inkscape-extension>
+ <name>TestThree</name>
+ <id>org.inkscape.test.inx_three</id>
+ <input>
+ <extension>.inx</extension>
+ <mimetype>text/xml+inx</mimetype>
+ <filetypename>Extension (*.inx)</filetypename>
+ <filetypetooltip>The extension extension repention suspension.</filetypetooltip>
+ </input>
+</inkscape-extension>""")
+ self.assertEqual(inx.name, 'TestThree')
+ self.assertEqual(inx.metadata, {
+ 'extension': '.inx',
+ 'mimetype': 'text/xml+inx',
+ 'name': 'Extension (*.inx)',
+ 'tooltip': 'The extension extension repention suspension.',
+ 'type': 'input'})
+ self.assertEqual(inx.warnings, ['No inx xml prefix.'])
+
+ def test_inx_template(self):
+ inx = InxFile("""<inkscape-extension xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
+ <name>TestFour</name>
+ <id>org.inkscape.test.inx_four</id>
+ <effect needs-live-preview="false">
+ <object-type>all</object-type>
+ <effects-menu hidden="true" />
+ </effect>
+ <inkscape:templateinfo>
+ <inkscape:name>Magic Number</inkscape:name>
+ <inkscape:author>Donky Oaty</inkscape:author>
+ <inkscape:shortdesc>Something might happen.</inkscape:shortdesc>
+ <inkscape:date>2070-01-01</inkscape:date>
+ <inkscape:keywords>word food strawberry</inkscape:keywords>
+ </inkscape:templateinfo>
+</inkscape-extension>""")
+ self.assertEqual(inx.name, 'TestFour')
+ self.assertEqual(inx.metadata, {'author': 'Donky Oaty', 'desc': 'Something might happen.', 'type': 'template'})
+ self.assertEqual(inx.warnings, ['No inx xml prefix.'])
+
+
+ def test_inx_files(self):
+ """Get all inx files and test each of them"""
+ if not PY3:
+ self.skipTest("No INX testing in python2")
+ return
+ for inx_file in glob(os.path.join(self._testdir(), '..', '*.inx')):
+ self.assertInxIsGood(inx_file)
diff --git a/share/extensions/other/gcodetools/tox.ini b/share/extensions/other/gcodetools/tox.ini
new file mode 100644
index 0000000..a554a8b
--- /dev/null
+++ b/share/extensions/other/gcodetools/tox.ini
@@ -0,0 +1,14 @@
+[tox]
+envlist = py{36,37,38,39,310}-normal
+skipsdist = True
+
+
+
+[testenv]
+setenv = COVERAGE_FILE=.coverage-{env:TOX_ENV_NAME}
+
+commands =
+ pytest --cov=. --cov-report html --cov-report term {posargs}
+
+deps =
+ -rtests/dev_requirements.txt