More fixes to recent 1.2.1 extensions
This commit is contained in:
parent
41a3592340
commit
942b295ed7
@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Cutting Optimizer (Nesting)</name>
|
||||
<id>fablabchemnitz.de.cutting_optimizer</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="tab_general" gui-text="Cutting Optimizer (CutOptim)">
|
||||
<hbox>
|
||||
<vbox>
|
||||
<label appearance="header">Geometry</label>
|
||||
<param name="unit" type="optiongroup" appearance="combo" gui-text="Unit">
|
||||
<option value="mm">mm</option>
|
||||
</param>
|
||||
<param name="distance" type="float" min="0.00" max="10.00" precision="2" gui-text="Min distance between objects" gui-description="This is the size of which polygons will be enlarged. This value must be greater than 0.8mm, the approximation by polygons is not perfect.">0.00</param>
|
||||
<param name="max_length" type="float" min="0" max="1000" gui-text="Max length of single segment" gui-description="the software will try to find a good configuration by positioning vertices on other vertices. It can be interesting in some cases to 'add' vertices to have more possibilities. If an edge is larger than the specified size, it will be broken into multiple segments, with additional vertices. Do not abuse this option, too low a value will slow down the treatment tremendously. Do not go below 100mm in most cases.">1000</param>
|
||||
<param name="optimizing_level" type="int" min="1" max="3" gui-text="Optimizing level" gui-description=" By increasing this parameter, the software will optimize the placement of a group of N polygons. This gives better results, but be careful, it considerably increases the treatment time. Do not exceed 2 or 3">1</param>
|
||||
<param name="firstpos" type="optiongroup" appearance="combo" gui-text="Placement" gui-description="Select option for largest element placement.">
|
||||
<option value="TL">Top Left</option>
|
||||
<option value="TC">Top Center</option>
|
||||
<option value="TR">Top Right</option>
|
||||
<option value="CL">Center Left</option>
|
||||
<option value="CC">Center Center</option>
|
||||
<option value="CR">Center Right</option>
|
||||
<option value="BL">Bottom Left</option>
|
||||
<option value="BC">Bottom Center</option>
|
||||
<option value="BR">Bottom Right</option>
|
||||
</param>
|
||||
<param name="free_rot" type="bool" gui-text="Allow free rotation of paths">true</param>
|
||||
<param name="angle" type="float" min="0.00" max="360.00" precision="2" gui-text="Rotation angle step (°)" gui-description="Try rotation by angle (0 = no rotation allowed)">0.00</param>
|
||||
<param name="nested" type="bool" gui-text="Keep nested paths together" gui-description="If checked, path included in another one will not processed but linked to the larger path">true</param>
|
||||
<param name="use_cache" type="bool" gui-text="Use cache" gui-description="Use cache to speed up processing. Cache operation is currently bugged and should NOT be used.">false</param>
|
||||
<param name="rect_cost" type="float" min="0.00" max="1000.00" precision="2" gui-text="Overall rectangle area * factor" gui-description="Add overall rectangle area * factor to cost function. Default factor is 0.0">0.00</param>
|
||||
</vbox>
|
||||
<vbox>
|
||||
<label appearance="header">Layers</label>
|
||||
<param name="original" type="bool" gui-text="Keep original layer">false</param>
|
||||
<separator/>
|
||||
<param name="layer_output_0" type="bool" gui-text="Placed Layer">true</param>
|
||||
<!--<param name="layer_output_1" type="bool" gui-text="Original (Input) Layer">false</param>-->
|
||||
<param name="layer_output_2" type="bool" gui-text="Polygon layer">false</param>
|
||||
<param name="layer_output_4" type="bool" gui-text="Large polygon layer">false</param>
|
||||
<param name="layer_output_8" type="bool" gui-text="Hull layer">false</param>
|
||||
<param name="layer_output_16" type="bool" gui-text="Placed Polygon layer">false</param>
|
||||
<label appearance="header">Debug / Error handling</label>
|
||||
<param name="debug_file" type="bool" gui-text="Generate and open debug file">false</param>
|
||||
<param name="print_cmd" type="bool" gui-text="Print console cmd only">false</param>
|
||||
<param name="cancel_on_error" type="bool" gui-text="Cancel on errors" gui-description="Will cancel whole transaction if any polygon could not be placed">true</param>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</page>
|
||||
<page name="tab_about" gui-text="About">
|
||||
<label appearance="header">Cutting Optimizer (CutOptim)</label>
|
||||
<label>CutOptim - a nesting tool for Inkscape (written by thierry7100).</label>
|
||||
<label>2020 - 2022 / Wrapper written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Online Documentation</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/cutoptim</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Contributing</label>
|
||||
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2</label>
|
||||
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Third Party Modules</label>
|
||||
<label appearance="url">https://github.com/thierry7100/CutOptim</label>
|
||||
<spacer/>
|
||||
<label appearance="header">MightyScape Extension Collection</label>
|
||||
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
|
||||
</page>
|
||||
<page name="tab_donate" gui-text="Donate">
|
||||
<label appearance="header">Coffee + Pizza</label>
|
||||
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
|
||||
<spacer/>
|
||||
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
|
||||
<spacer/>
|
||||
<label>Thanks for using our extension and helping us!</label>
|
||||
<image>../000_about_fablabchemnitz.svg</image>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Transformations"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">cutting_optimizer.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
202
extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py
Normal file
202
extensions/fablabchemnitz/cutting_optimizer/cutting_optimizer.py
Normal file
@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Extension for InkScape 1.2
|
||||
|
||||
CutOptim OS Wrapper script to make CutOptim work on Windows and Linux systems without duplicating .inx files
|
||||
|
||||
Author: Mario Voigt / FabLab Chemnitz
|
||||
Mail: mario.voigt@stadtfabrikanten.org
|
||||
Date: 31.08.2020
|
||||
Last patch: 03.11.2022
|
||||
License: GNU GPL v3
|
||||
|
||||
"""
|
||||
|
||||
import inkex
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
from lxml import etree
|
||||
from copy import deepcopy
|
||||
import tempfile
|
||||
from inkex.command import inkscape, inkscape_command
|
||||
|
||||
class CuttingOptimizer(inkex.EffectExtension):
|
||||
|
||||
def openDebugFile(self, file):
|
||||
DETACHED_PROCESS = 0x00000008
|
||||
if os.name == 'nt':
|
||||
subprocess.Popen(["explorer", file], close_fds=True, creationflags=DETACHED_PROCESS, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", file], close_fds=True, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).wait()
|
||||
|
||||
def add_arguments(self, pars):
|
||||
args = sys.argv[1:]
|
||||
for arg in args:
|
||||
key=arg.split("=")[0]
|
||||
if len(arg.split("=")) == 2:
|
||||
value=arg.split("=")[1]
|
||||
try:
|
||||
if key != "--id":
|
||||
pars.add_argument(key, default=key)
|
||||
except:
|
||||
pass #ignore duplicate id arg
|
||||
|
||||
def effect(self):
|
||||
extension_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
cmd = []
|
||||
if os.name == "nt":
|
||||
cutoptim = os.path.join(extension_dir, "CutOptim.exe")
|
||||
else:
|
||||
cutoptim = os.path.join(extension_dir, "CutOptim")
|
||||
cmd.append(cutoptim)
|
||||
|
||||
elements = self.svg.selected
|
||||
if len(elements) > 0: #if selection is existing, then we export only selected items to a new svg, which is then going to be processed. Otherwise we process the whole SVG document
|
||||
extra_param = None
|
||||
template = self.svg.copy()
|
||||
for child in template.getchildren():
|
||||
if child.tag == '{http://www.w3.org/2000/svg}defs':
|
||||
continue
|
||||
template.remove(child)
|
||||
group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
|
||||
group.attrib['id'] = 'export_selection_transform'
|
||||
for element in self.svg.selected.values():
|
||||
elem_copy = deepcopy(element)
|
||||
elem_copy.attrib['transform'] = str(element.composed_transform())
|
||||
elem_copy.attrib['style'] = str(element.specified_style())
|
||||
group.append(elem_copy)
|
||||
template.attrib['viewBox'] = self.svg.attrib['viewBox']
|
||||
template.attrib['width'] = self.svg.attrib['width']
|
||||
template.attrib['height'] = self.svg.attrib['height']
|
||||
template.append(group)
|
||||
svg_out = os.path.join(tempfile.gettempdir(), self.svg.get_unique_id("selection") + '.svg')
|
||||
with open(svg_out, 'wb') as fp:
|
||||
fp.write(template.tostring())
|
||||
actions_list=[]
|
||||
actions_list.append("SelectionUnGroup")
|
||||
actions_list.append("export-type:svg")
|
||||
actions_list.append("export-filename:{}".format(svg_out))
|
||||
actions_list.append("export-do")
|
||||
actions = ";".join(actions_list)
|
||||
cli_output = inkscape(svg_out, extra_param, actions=actions) #process recent file
|
||||
if len(cli_output) > 0:
|
||||
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
|
||||
self.msg(cli_output)
|
||||
|
||||
layerSum = 0
|
||||
layer_output_0 = False
|
||||
cancel_on_error = False
|
||||
for arg in vars(self.options):
|
||||
argval = str(getattr(self.options, arg))
|
||||
if arg not in ("tab", "output", "ids", "selected_nodes", "print_cmd"):
|
||||
#inkex.utils.debug(str(arg) + " = " + str(getattr(self.options, arg)))
|
||||
#fix behaviour of "original" arg which does not correctly gets interpreted if set to false
|
||||
if arg == "original" and argval == "false":
|
||||
continue
|
||||
if arg == "input_file":
|
||||
cmd.append("--file")
|
||||
if len(elements) > 0:
|
||||
cmd.append(svg_out)
|
||||
else:
|
||||
cmd.append(argval)
|
||||
elif arg == "layer_output_0":
|
||||
if argval == "true":
|
||||
layerSum += 0
|
||||
layer_output_0 = True
|
||||
#elif arg == "layer_output_1":
|
||||
# if argval == "true": layerSum += 1
|
||||
elif arg == "layer_output_2":
|
||||
if argval == "true": layerSum += 2
|
||||
elif arg == "layer_output_4":
|
||||
if argval == "true": layerSum += 4
|
||||
elif arg == "layer_output_8":
|
||||
if argval == "true": layerSum += 8
|
||||
elif arg == "layer_output_16":
|
||||
if argval == "true": layerSum += 16
|
||||
elif arg == "cancel_on_error":
|
||||
if argval == "true": cancel_on_error = True
|
||||
else:
|
||||
cmd.append("--{}={}".format(arg, argval))
|
||||
cmd.append("--layer_output")
|
||||
cmd.append("{}".format(layerSum))
|
||||
if layerSum == 0 and layer_output_0 is False:
|
||||
inkex.utils.debug("You need to enable at least one type of layer to continue!")
|
||||
output_file = None
|
||||
if os.name == "nt":
|
||||
output_file = "cutoptim.svg"
|
||||
else:
|
||||
output_file = "/tmp/cutoptim.svg"
|
||||
if os.path.exists(output_file):
|
||||
try:
|
||||
os.remove(output_file)
|
||||
except OSError as e:
|
||||
pass
|
||||
|
||||
cmd.append("--output")
|
||||
cmd.append(output_file)
|
||||
|
||||
# run CutOptim with the parameters provided
|
||||
if self.options.print_cmd == "true":
|
||||
inkex.utils.debug("The following command would be executed on shell:\n")
|
||||
inkex.utils.debug(" ".join(cmd))
|
||||
exit(0)
|
||||
else:
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) as cutoptim:
|
||||
cutoptim.wait()
|
||||
stdout, stderr = cutoptim.communicate()
|
||||
#inkex.utils.debug("stdout:\n{}".format(stdout.decode('UTF-8')))
|
||||
errors = stderr.decode('UTF-8')
|
||||
if len(errors) > 0:
|
||||
inkex.utils.debug("Errors occured:\n{}".format(errors))
|
||||
if len(errors) > 0 and cancel_on_error is True:
|
||||
inkex.utils.debug("Maybe enlarge your document size in case not all polygons could be placed and try again! Nesting was cancelled!")
|
||||
exit(1)
|
||||
|
||||
# check output existence
|
||||
try:
|
||||
stream = open(output_file, 'r')
|
||||
except FileNotFoundError as e:
|
||||
inkex.utils.debug("There was no SVG output generated. Cannot continue. Command was:\n")
|
||||
inkex.utils.debug(" ".join(cmd))
|
||||
exit(1)
|
||||
|
||||
if self.options.original == "false": #we need to use string representation of bool
|
||||
for element in self.document.getroot():
|
||||
if isinstance(element, inkex.ShapeElement):
|
||||
element.delete()
|
||||
|
||||
if self.options.debug_file == "true": #we need to use string representation of bool
|
||||
self.openDebugFile("Debug_CutOptim.txt")
|
||||
|
||||
# write the generated SVG into Inkscape's canvas
|
||||
doc = etree.parse(stream, parser=etree.XMLParser(huge_tree=True))
|
||||
stream.close()
|
||||
group = inkex.Group(id="CutOptim")
|
||||
'''
|
||||
0 = Placed_Layer
|
||||
1 = Original_Layer (Input Layer) <> see "Keep original layer"
|
||||
2 = Polygon_Layer
|
||||
4 = Large_Polygon_Layer
|
||||
8 = Hull_Placed_Layer
|
||||
16 = Placed_Polygon_Layer
|
||||
'''
|
||||
if layer_output_0 is False:
|
||||
l0 = None
|
||||
else:
|
||||
l0 = doc.xpath('//svg:g[@inkscape:label="Placed_Layer"]', namespaces=inkex.NSS)
|
||||
#l1 = doc.xpath('//svg:g[@inkscape:label="Original_Layer"]', namespaces=inkex.NSS)
|
||||
l2 = doc.xpath('//svg:g[@inkscape:label="Polygon_Layer"]', namespaces=inkex.NSS)
|
||||
l4 = doc.xpath('//svg:g[@inkscape:label="Large_Polygon_Layer"]', namespaces=inkex.NSS)
|
||||
l8 = doc.xpath('//svg:g[@inkscape:label="Hull_Placed_Layer"]', namespaces=inkex.NSS)
|
||||
l16 = doc.xpath('//svg:g[@inkscape:label="Placed_Polygon_Layer"]', namespaces=inkex.NSS)
|
||||
for layer in (l0, l2, l4, l8, l16): #,l1
|
||||
if layer is not None and len(layer) > 0:
|
||||
for element in layer:#[0].getchildren():
|
||||
group.append(element)
|
||||
self.document.getroot().append(group)
|
||||
|
||||
if __name__ == '__main__':
|
||||
CuttingOptimizer().run()
|
21
extensions/fablabchemnitz/cutting_optimizer/meta.json
Normal file
21
extensions/fablabchemnitz/cutting_optimizer/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "Cutting Optimizer (Nesting)",
|
||||
"id": "fablabchemnitz.de.cutting_optimizer",
|
||||
"path": "cutting_optimizer",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Laser Cutting Optmizer",
|
||||
"original_id": "fr.fablab-lannion.inkscape.cutopimiser",
|
||||
"license": "MIT License",
|
||||
"license_url": "https://github.com/thierry7100/CutOptim/blob/master/LICENSE",
|
||||
"comment": "",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/cutting_optimizer",
|
||||
"fork_url": "https://github.com/thierry7100/CutOptim",
|
||||
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=55018148",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/thierry7100",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Export Selection As ...</name>
|
||||
<id>fablabchemnitz.de.export_selection_as</id>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="tab_settings" gui-text="Settings">
|
||||
<param name="wrap_transform" type="bool" gui-text="Wrap final document in transform">false</param>
|
||||
<param name="border_offset" type="float" min="0.000" max="9999.000" precision="3" gui-text="Add border offset around selection">1.000</param>
|
||||
<param name="border_offset_unit" type="optiongroup" appearance="combo" gui-text="Offset unit">
|
||||
<option value="mm">mm</option>
|
||||
<option value="cm">cm</option>
|
||||
<option value="px">px</option>
|
||||
<option value="pt">pt</option>
|
||||
<option value="pc">pc</option>
|
||||
<option value="in">in</option>
|
||||
</param>
|
||||
<param name="export_dir" type="path" mode="folder" gui-text="Location to save exported documents">./inkscape_export/</param>
|
||||
<param name="opendir" type="bool" gui-text="Open containing output directory after export">false</param>
|
||||
<param name="dxf_exporter_path" type="path" mode="file" filetypes="py" gui-text="Location of dxf_outlines.py" gui-description="Do not use dxf12_outlines.py! This will try to create R12 DXF files, which will fail!">/usr/share/inkscape/extensions/dxf_outlines.py</param>
|
||||
<param name="export_svg" type="bool" gui-text="Export as SVG">true</param>
|
||||
<param name="export_dxf" type="bool" gui-text="Export as DXF R14 file (mm units)">false</param>
|
||||
<param name="export_pdf" type="bool" gui-text="Export as PDF 1.5">false</param>
|
||||
<param name="export_png" type="bool" gui-text="Export as PNG">false</param>
|
||||
<param name="png_dpi" type="float" min="1" max="2400" precision="3" gui-text="PNG DPI (applies for export and replace)" gui-description="default is 96">96</param>
|
||||
<param name="replace_by_png" type="bool" gui-text="Replace by PNG" gui-description="Please convert strokes to paths to keep exact size and prevent cutoffs!">false</param>
|
||||
<param name="newwindow" type="bool" gui-text="Open file in new Inkscape instance">false</param>
|
||||
<param name="skip_errors" type="bool" gui-text="Skip on errors">false</param>
|
||||
<label>Note: If svg/dxf/pdf already existed before, they might get accidently deleted or overwritten. Please take care!</label>
|
||||
</page>
|
||||
<page name="tab_about" gui-text="About">
|
||||
<label appearance="header">Export Selection As ...</label>
|
||||
<label>Extension to export the current selection into different formats like SVG, DXF or PDF.</label>
|
||||
<label>2021 / written by Mario Voigt (Stadtfabrikanten e.V. / FabLab Chemnitz)</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Online Documentation</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/exportselectionas</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Contributing</label>
|
||||
<label appearance="url">https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X</label>
|
||||
<label appearance="url">mailto:mario.voigt@stadtfabrikanten.org</label>
|
||||
<spacer/>
|
||||
<label appearance="header">Based on</label>
|
||||
<label appearance="url">https://github.com/mireq/inkscape-export-selection-as-svg</label>
|
||||
<spacer/>
|
||||
<label appearance="header">MightyScape Extension Collection</label>
|
||||
<label>This piece of software is part of the MightyScape for Inkscape Extension Collection and is licensed under GNU GPL v3</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/mightyscape-overview</label>
|
||||
</page>
|
||||
<page name="tab_donate" gui-text="Donate">
|
||||
<label appearance="header">Coffee + Pizza</label>
|
||||
<label>We are the Stadtfabrikanten, running the FabLab Chemnitz since 2016. A FabLab is an open workshop that gives people access to machines and digital tools like 3D printers, laser cutters and CNC milling machines.</label>
|
||||
<spacer/>
|
||||
<label>You like our work and want to support us? You can donate to our non-profit organization by different ways:</label>
|
||||
<label appearance="url">https://y.stadtfabrikanten.org/donate</label>
|
||||
<spacer/>
|
||||
<label>Thanks for using our extension and helping us!</label>
|
||||
<image>../000_about_fablabchemnitz.svg</image>
|
||||
</page>
|
||||
</param>
|
||||
<effect needs-document="true" needs-live-preview="false">
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Import/Export/Transfer"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
<menu-tip>Export selection to separate files.</menu-tip>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">export_selection_as.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE
|
||||
import inkex
|
||||
from inkex import Rectangle
|
||||
import inkex.command
|
||||
from inkex.command import inkscape, inkscape_command
|
||||
import tempfile
|
||||
from PIL import Image
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import warnings
|
||||
warnings.simplefilter('ignore', Image.DecompressionBombWarning)
|
||||
from lxml import etree
|
||||
from scour.scour import scourString
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DETACHED_PROCESS = 0x00000008
|
||||
GROUP_ID = 'export_selection_transform'
|
||||
|
||||
class ExportObject(inkex.EffectExtension):
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument("--tab")
|
||||
pars.add_argument("--wrap_transform", type=inkex.Boolean, default=False, help="Wrap final document in transform")
|
||||
pars.add_argument("--border_offset", type=float, default=1.000, help="Add border offset around selection")
|
||||
pars.add_argument("--border_offset_unit", default="mm", help="Offset unit")
|
||||
pars.add_argument("--export_dir", default="~/inkscape_export/", help="Location to save exported documents")
|
||||
pars.add_argument("--opendir", type=inkex.Boolean, default=False, help="Open containing output directory after export")
|
||||
pars.add_argument("--dxf_exporter_path", default="/usr/share/inkscape/extensions/dxf_outlines.py", help="Location of dxf_outlines.py")
|
||||
pars.add_argument("--export_svg", type=inkex.Boolean, default=False, help="Create a svg file")
|
||||
pars.add_argument("--export_dxf", type=inkex.Boolean, default=False, help="Create a dxf file")
|
||||
pars.add_argument("--export_pdf", type=inkex.Boolean, default=False, help="Create a pdf file")
|
||||
pars.add_argument("--export_png", type=inkex.Boolean, default=False, help="Create a png file")
|
||||
pars.add_argument("--png_dpi", type=float, default=96, help="PNG DPI (applies for export and replace)")
|
||||
pars.add_argument("--replace_by_png", type=inkex.Boolean, default=False, help="Replace selection by png export")
|
||||
pars.add_argument("--newwindow", type=inkex.Boolean, default=False, help="Open file in new Inkscape window")
|
||||
pars.add_argument("--skip_errors", type=inkex.Boolean, default=False, help="Skip on errors")
|
||||
|
||||
def openExplorer(self, dir):
|
||||
if os.name == 'nt':
|
||||
Popen(["explorer", dir], close_fds=True, creationflags=DETACHED_PROCESS).wait()
|
||||
else:
|
||||
Popen(["xdg-open", dir], close_fds=True, start_new_session=True).wait()
|
||||
|
||||
def spawnIndependentInkscape(self, file): #function to spawn non-blocking inkscape instance. the inkscape command is available because it is added to ENVIRONMENT when Inkscape main instance is started
|
||||
if not os.path.exists(file):
|
||||
inkex.utils.debug("Error. {} does not exist!".format(file))
|
||||
exit(1)
|
||||
warnings.simplefilter('ignore', ResourceWarning) #suppress "enable tracemalloc to get the object allocation traceback"
|
||||
if os.name == 'nt':
|
||||
Popen(["inkscape", file], close_fds=True, creationflags=DETACHED_PROCESS)
|
||||
else:
|
||||
subprocess.Popen(["inkscape", file], start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
warnings.simplefilter("default", ResourceWarning)
|
||||
|
||||
def effect(self):
|
||||
scale_factor = self.svg.unittouu("1px")
|
||||
|
||||
svg_export = self.options.export_svg
|
||||
#extra_param = "--batch-process"
|
||||
extra_param = None
|
||||
|
||||
if self.options.export_svg is False and \
|
||||
self.options.export_dxf is False and \
|
||||
self.options.export_pdf is False and \
|
||||
self.options.export_png is False and \
|
||||
self.options.replace_by_png is False and \
|
||||
self.options.newwindow is False:
|
||||
inkex.utils.debug("You must select at least one option to continue!")
|
||||
return
|
||||
|
||||
if self.options.replace_by_png is True:
|
||||
self.options.border_offset = 0 #override
|
||||
|
||||
if not self.svg.selected:
|
||||
inkex.errormsg("Selection is empty. Please select some objects first!")
|
||||
return
|
||||
|
||||
if self.options.export_dxf is True:
|
||||
#preflight check for DXF input dir
|
||||
if not os.path.exists(self.options.dxf_exporter_path):
|
||||
inkex.utils.debug("Location of dxf_outlines.py does not exist. Please select a proper file and try again.")
|
||||
exit(1)
|
||||
|
||||
export_dir = Path(self.absolute_href(self.options.export_dir))
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
offset = self.svg.unittouu(str(self.options.border_offset) + self.options.border_offset_unit)
|
||||
|
||||
bbox = inkex.BoundingBox()
|
||||
|
||||
selected = self.svg.selected
|
||||
firstId = selected[0].get('id')
|
||||
parent = self.svg.getElementById(firstId).getparent()
|
||||
|
||||
for element in selected.values():
|
||||
transform = inkex.Transform()
|
||||
parent = element.getparent()
|
||||
if parent is not None and isinstance(parent, inkex.ShapeElement):
|
||||
transform = parent.composed_transform()
|
||||
try:
|
||||
'''
|
||||
...rectangles cause some strangle scaling issue, offendingly caused by namedview units.
|
||||
The rectangle attributes are set in px. They ignore the real units from namedview.
|
||||
Strange fact: ellipses, spirals and other primitives work flawlessly.
|
||||
'''
|
||||
if isinstance (element, inkex.Rectangle) or \
|
||||
isinstance (element, inkex.Circle) or \
|
||||
isinstance (element, inkex.Ellipse):
|
||||
bbox += element.bounding_box(transform) * scale_factor
|
||||
elif isinstance (element, inkex.TextElement) or \
|
||||
isinstance (element, inkex.Tspan):
|
||||
if self.options.skip_errors is False:
|
||||
self.msg("Text elements are not supported!")
|
||||
return
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
bbox += element.bounding_box(transform)
|
||||
except Exception:
|
||||
logger.exception("Bounding box not computed")
|
||||
logger.info("Skipping bounding box")
|
||||
transform = element.composed_transform()
|
||||
x1, y1 = transform.apply_to_point([0, 0])
|
||||
x2, y2 = transform.apply_to_point([1, 1])
|
||||
bbox += inkex.BoundingBox((x1, x2), (y1, y2))
|
||||
|
||||
template = self.create_document()
|
||||
svg_filename = None
|
||||
|
||||
group = etree.SubElement(template, '{http://www.w3.org/2000/svg}g')
|
||||
group.attrib['id'] = GROUP_ID
|
||||
group.attrib['transform'] = str(inkex.Transform(((1, 0, -bbox.left), (0, 1, -bbox.top))))
|
||||
|
||||
for element in self.svg.selected.values():
|
||||
if element.tag == inkex.addNS('image', 'svg'):
|
||||
continue #skip images
|
||||
elem_copy = deepcopy(element)
|
||||
elem_copy.attrib['transform'] = str(element.composed_transform())
|
||||
elem_copy.attrib['style'] = str(element.specified_style())
|
||||
group.append(elem_copy)
|
||||
|
||||
template.attrib['viewBox'] = f'{-offset} {-offset} {bbox.width + offset * 2} {bbox.height + offset * 2}'
|
||||
template.attrib['width'] = f'{bbox.width + offset * 2}' + self.svg.unit
|
||||
template.attrib['height'] = f'{bbox.height + offset * 2}' + self.svg.unit
|
||||
|
||||
if svg_filename is None:
|
||||
filename_base = element.attrib.get('id', None).replace(os.sep, '_')
|
||||
if filename_base:
|
||||
svg_filename = filename_base + '.svg'
|
||||
if not filename_base: #should never be the case. Inkscape might crash if the id attribute is empty or not existent due to invalid SVG
|
||||
filename_base = self.svg.get_unique_id("selection")
|
||||
svg_filename = filename_base + '.svg'
|
||||
|
||||
if len(group) == 0:
|
||||
self.msg("Selection does not contain any vector data.")
|
||||
exit(1)
|
||||
|
||||
template.append(group)
|
||||
svg_out = os.path.join(tempfile.gettempdir(), svg_filename)
|
||||
|
||||
if self.options.wrap_transform is False:
|
||||
#self.load(inkscape_command(template.tostring(), select=GROUP_ID, verbs=['SelectionUnGroup;FileSave'])) #fails due to new bug
|
||||
|
||||
#workaround
|
||||
self.save_document(template, svg_out) #export recent file
|
||||
actions_list=[]
|
||||
actions_list.append("SelectionUnGroup")
|
||||
actions_list.append("export-type:svg")
|
||||
actions_list.append("export-filename:{}".format(svg_out))
|
||||
actions_list.append("export-do")
|
||||
actions = ";".join(actions_list)
|
||||
cli_output = inkscape(svg_out, extra_param, actions=actions) #process recent file
|
||||
if len(cli_output) > 0:
|
||||
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
|
||||
self.msg(cli_output)
|
||||
self.load(svg_out) #reload recent file
|
||||
|
||||
template = self.svg
|
||||
for child in template.getchildren():
|
||||
if child.tag == '{http://www.w3.org/2000/svg}metadata':
|
||||
template.remove(child)
|
||||
|
||||
self.save_document(template, svg_out) # save one into temp dir to access for dxf/pdf/new window instance
|
||||
|
||||
if self.options.export_svg is True:
|
||||
self.save_document(template, export_dir / svg_filename)
|
||||
|
||||
if self.options.opendir is True:
|
||||
self.openExplorer(export_dir)
|
||||
|
||||
if self.options.newwindow is True:
|
||||
#inkscape(os.path.join(export_dir, svg_filename)) #blocking cmd
|
||||
self.spawnIndependentInkscape(os.path.join(tempfile.gettempdir(), svg_filename)) #non-blocking
|
||||
|
||||
if self.options.export_dxf is True:
|
||||
#ensure that python command is available #we pass 25.4/96 which stands for unit mm. See inkex.units.UNITS and dxf_outlines.inx
|
||||
cmd = [
|
||||
sys.executable, #the path of the python interpreter which is used for this script
|
||||
self.options.dxf_exporter_path,
|
||||
'--output=' + os.path.join(export_dir, filename_base + '.dxf'),
|
||||
r'--units=25.4/96',
|
||||
os.path.join(tempfile.gettempdir(), svg_filename)
|
||||
]
|
||||
proc = Popen(cmd, shell=False, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
inkex.utils.debug("%d %s %s" % (proc.returncode, stdout, stderr))
|
||||
|
||||
if self.options.export_pdf is True:
|
||||
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions='export-pdf-version:1.5;export-text-to-path;export-filename:{file_name};export-do'.format(file_name=os.path.join(export_dir, filename_base + '.pdf')))
|
||||
if len(cli_output) > 0:
|
||||
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
|
||||
self.msg(cli_output)
|
||||
|
||||
if self.options.export_png is True:
|
||||
png_export=os.path.join(export_dir, filename_base + '.png')
|
||||
try:
|
||||
os.remove(png_export)
|
||||
except OSError as e:
|
||||
#inkex.utils.debug("Error while deleting previously generated output file " + png_export)
|
||||
pass
|
||||
actions_list=[]
|
||||
actions_list.append("export-background:white")
|
||||
actions_list.append("export-type:png")
|
||||
actions_list.append("export-dpi:{}".format(self.options.png_dpi))
|
||||
actions_list.append("export-filename:{}".format(png_export))
|
||||
actions_list.append("export-do")
|
||||
actions = ";".join(actions_list)
|
||||
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions)
|
||||
if len(cli_output) > 0:
|
||||
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
|
||||
self.msg(cli_output)
|
||||
|
||||
if self.options.replace_by_png is True:
|
||||
#export to png file to temp
|
||||
png_export=os.path.join(tempfile.gettempdir(), filename_base + '.png')
|
||||
try:
|
||||
os.remove(png_export)
|
||||
except OSError as e:
|
||||
#inkex.utils.debug("Error while deleting previously generated output file " + png_export)
|
||||
pass
|
||||
actions_list=[]
|
||||
actions_list.append("export-background:white")
|
||||
actions_list.append("export-type:png")
|
||||
actions_list.append("export-dpi:{}".format(self.options.png_dpi))
|
||||
actions_list.append("export-filename:{}".format(png_export))
|
||||
actions_list.append("export-do")
|
||||
actions = ";".join(actions_list)
|
||||
cli_output = inkscape(os.path.join(tempfile.gettempdir(), svg_filename), extra_param, actions=actions)
|
||||
if len(cli_output) > 0:
|
||||
self.msg("Inkscape returned the following output when trying to run the file export; the file export may still have worked:")
|
||||
self.msg(cli_output)
|
||||
#then remove the selection and replace it by png
|
||||
#self.msg(parent.get('id'))
|
||||
for element in selected.values():
|
||||
element.delete()
|
||||
#read png file and get base64 string from it
|
||||
try:
|
||||
img = Image.open(png_export)
|
||||
except Image.DecompressionBombError as e: #we could also increse PIL.Image.MAX_IMAGE_PIXELS = some large int
|
||||
self.msg("Error. Image is too large ({} x {} px). Reduce DPI and try again!".format(self.svg.uutounit(bbox.width), self.svg.uutounit(bbox.height)))
|
||||
exit(1)
|
||||
output_buffer = BytesIO()
|
||||
img.save(output_buffer, format='PNG')
|
||||
byte_data = output_buffer.getvalue()
|
||||
base64_str = base64.b64encode(byte_data).decode('UTF-8')
|
||||
#finally replace the svg:path(s) with svg:image
|
||||
imgReplacement = etree.SubElement(Rectangle(), '{http://www.w3.org/2000/svg}image')
|
||||
imgReplacement.attrib['x'] = str(bbox.left)
|
||||
imgReplacement.attrib['y'] = str(bbox.top)
|
||||
imgReplacement.attrib['width'] = str(bbox.width)
|
||||
imgReplacement.attrib['height'] = str(bbox.height)
|
||||
imgReplacement.attrib['id'] = firstId
|
||||
imgReplacement.attrib['{http://www.w3.org/1999/xlink}href'] = "data:image/png;base64,{}".format(base64_str)
|
||||
parent.append(imgReplacement)
|
||||
if parent.attrib.has_key('transform'):
|
||||
del parent.attrib['transform'] #remove transform
|
||||
|
||||
|
||||
def create_document(self):
|
||||
document = self.svg.copy()
|
||||
for child in document.getchildren():
|
||||
if child.tag == '{http://www.w3.org/2000/svg}defs':
|
||||
continue
|
||||
document.remove(child)
|
||||
return document
|
||||
|
||||
def save_document(self, document, filename):
|
||||
with open(filename, 'wb') as fp:
|
||||
document = document.tostring()
|
||||
fp.write(scourString(document).encode('utf8'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ExportObject().run()
|
21
extensions/fablabchemnitz/export_selection_as/meta.json
Normal file
21
extensions/fablabchemnitz/export_selection_as/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "Export Selection As ...",
|
||||
"id": "fablabchemnitz.de.export_selection_as",
|
||||
"path": "export_selection_as",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Export selection as svg",
|
||||
"original_id": "sk.linuxos.export_selection",
|
||||
"license": "MIT License",
|
||||
"license_url": "https://github.com/mireq/inkscape-export-selection-as-svg/blob/master/LICENSE",
|
||||
"comment": "",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/export_selection_as",
|
||||
"fork_url": "https://github.com/mireq/inkscape-export-selection-as-svg",
|
||||
"documentation_url": "https://stadtfabrikanten.org/pages/viewpage.action?pageId=104923223",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/mireq",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Guilloche Contour</name>
|
||||
<id>fablabchemnitz.de.guilloche_creations.guilloche_contour</id>
|
||||
<param name="contourFunction" type="optiongroup" appearance="combo" gui-text="Function">
|
||||
<option value="line">Line</option>
|
||||
<option value="sin">Sin</option>
|
||||
<option value="cos">Cos</option>
|
||||
<option value="env1">Env1</option>
|
||||
<option value="env2">Env2</option>
|
||||
<option value="env3">Env3</option>
|
||||
<option value="env4">Env4</option>
|
||||
<option value="env5">Env5</option>
|
||||
<option value="env6">Env6</option>
|
||||
<option value="env7">Env7</option>
|
||||
<option value="env8">Env8</option>
|
||||
<option value="env9">Env9</option>
|
||||
<option value="env10">Env10</option>
|
||||
<option value="env11">Env11</option>
|
||||
<option value="env12">Env12</option>
|
||||
</param>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="contour" gui-text="Contour">
|
||||
<param name="frequency" type="int" min="1" max="100" gui-text="Frequency:">10</param>
|
||||
<param name="amplitude" type="int" min="-15" max="15" gui-text="Amplitude:">1</param>
|
||||
<param name="phaseOffset" type="int" min="-100" max="100" gui-text="Phase offset:">0</param>
|
||||
<param name="offset" type="int" min="-100" max="100" gui-text="Offset:">0</param>
|
||||
<param name="nodes" type="int" min="2" max="1000" gui-text="Number of nodes:">20</param>
|
||||
<param name="remove" type="bool" gui-text="Remove control object">false</param>
|
||||
<param name="strokeColor" type="color" gui-text="Stroke color"></param>
|
||||
</page>
|
||||
<page name="function" gui-text="Function">
|
||||
<param name="amplitude1" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 1:">0.0</param>
|
||||
<param name="phase1" type="int" min="-100" max="100" gui-text="Phase offset 1:">0</param>
|
||||
<param name="amplitude2" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 2:">0.0</param>
|
||||
<param name="phase2" type="int" min="-100" max="100" gui-text="Phase offset 2:">0</param>
|
||||
<param name="amplitude3" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 3:">0.0</param>
|
||||
<param name="phase3" type="int" min="-100" max="100" gui-text="Phase offset 3:">0</param>
|
||||
<param name="amplitude4" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 4:">0.0</param>
|
||||
<param name="phase4" type="int" min="-100" max="100" gui-text="Phase offset 4:">0</param>
|
||||
<param name="amplitude5" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 5:">0.0</param>
|
||||
<param name="phase5" type="int" min="-100" max="100" gui-text="Phase offset 5:">0</param>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Modify existing Path(s)"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">guilloche_contour.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,663 @@
|
||||
#! /usr/bin/env python3
|
||||
'''
|
||||
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.
|
||||
|
||||
Quick description:
|
||||
|
||||
'''
|
||||
# standard library
|
||||
from math import *
|
||||
from copy import deepcopy
|
||||
# local library
|
||||
import inkex
|
||||
import pathmodifier
|
||||
import cubicsuperpath
|
||||
from inkex import paths
|
||||
from lxml import etree
|
||||
|
||||
def getColorAndOpacity(longColor):
|
||||
'''
|
||||
Convert the long into a #rrggbb color value
|
||||
Conversion back is A + B*256^1 + G*256^2 + R*256^3
|
||||
'''
|
||||
longColor = int(longColor)
|
||||
|
||||
if longColor < 0:
|
||||
longColor = longColor & 0xFFFFFFFF
|
||||
|
||||
hexColor = hex(longColor)
|
||||
lhc = len(hexColor)
|
||||
|
||||
hexOpacity = hexColor[lhc-2 : ]
|
||||
hexColor = '#' + hexColor[2:-2].rjust(6, '0')
|
||||
|
||||
return (hexColor, hexOpacity)
|
||||
|
||||
def setColorAndOpacity(style, color, opacity):
|
||||
declarations = style.split(';')
|
||||
strokeOpacityInStyle = False
|
||||
newOpacity = round((int(opacity, 16) / 255.0), 8)
|
||||
|
||||
for i,decl in enumerate(declarations):
|
||||
parts = decl.split(':', 2)
|
||||
|
||||
if len(parts) == 2:
|
||||
(prop, val) = parts
|
||||
prop = prop.strip().lower()
|
||||
|
||||
if (prop == 'stroke' and val != color):
|
||||
declarations[i] = prop + ':' + color
|
||||
|
||||
if prop == 'stroke-opacity':
|
||||
if val != newOpacity:
|
||||
declarations[i] = prop + ':' + str(newOpacity)
|
||||
|
||||
strokeOpacityInStyle = True
|
||||
|
||||
if not strokeOpacityInStyle:
|
||||
declarations.append('stroke-opacity' + ':' + str(newOpacity))
|
||||
|
||||
return ";".join(declarations)
|
||||
|
||||
def getSkeletonPath(d, offs):
|
||||
'''
|
||||
Recieves a current skeleton path and offset specified by the user if it's line.
|
||||
Calculates new skeleton path to use for creating contour with given offset.
|
||||
'''
|
||||
|
||||
if offs != 0:
|
||||
comps = d.split()
|
||||
|
||||
if ((comps[2] == 'h' or comps[2] == 'H') and len(comps) == 4):
|
||||
startPt = comps[1].split(',')
|
||||
startX = float(startPt[0])
|
||||
startY = float(startPt[1])
|
||||
|
||||
finalX = float(comps[3]) if comps[2] == 'H' else startX + float(comps[3])
|
||||
|
||||
if startX < finalX:
|
||||
startY -= offs
|
||||
else:
|
||||
startY += offs
|
||||
|
||||
comps[1] = startPt[0] + ',' + str(startY)
|
||||
elif ((comps[2] == 'v' or comps[2] == 'V') and len(comps) == 4):
|
||||
startPt = comps[1].split(',')
|
||||
startX = float(startPt[0])
|
||||
startY = float(startPt[1])
|
||||
|
||||
finalY = float(comps[3]) if comps[2] == 'V' else startY + float(comps[3])
|
||||
|
||||
if startY < finalY:
|
||||
startX += offs
|
||||
else:
|
||||
startX -= offs
|
||||
|
||||
comps[1] = str(startX) + ',' + startPt[1]
|
||||
elif (comps[0] == 'M' and len(comps) == 3):
|
||||
startPt = comps[1].split(',')
|
||||
startX = float(startPt[0])
|
||||
startY = float(startPt[1])
|
||||
|
||||
finalPt = comps[2].split(',')
|
||||
finalX = float(finalPt[0])
|
||||
finalY = float(finalPt[1])
|
||||
|
||||
if startX < finalX:
|
||||
if (startY > finalY):
|
||||
startX -= offs
|
||||
finalX -= offs
|
||||
else:
|
||||
startX += offs
|
||||
finalX += offs
|
||||
startY -= offs
|
||||
finalY -= offs
|
||||
else:
|
||||
if startY > finalY:
|
||||
startX -= offs
|
||||
finalX -= offs
|
||||
else:
|
||||
startX += offs
|
||||
finalX += offs
|
||||
startY += offs
|
||||
finalY += offs
|
||||
|
||||
comps[1] = str(startX) + ',' + str(startY)
|
||||
comps[2] = str(finalX) + ',' + str(finalY)
|
||||
elif (comps[0] == 'm' and len(comps) == 3):
|
||||
startPt = comps[1].split(',')
|
||||
startX = float(startPt[0])
|
||||
startY = float(startPt[1])
|
||||
|
||||
finalPt = comps[2].split(',')
|
||||
dx = float(finalPt[0])
|
||||
dy = float(finalPt[1])
|
||||
finalX = startX + dx
|
||||
finalY = startY + dy
|
||||
|
||||
if startX < finalX:
|
||||
if startY > finalY:
|
||||
startX -= offs
|
||||
else:
|
||||
startX += offs
|
||||
startY -= offs
|
||||
else:
|
||||
if startY > finalY:
|
||||
startX -= offs
|
||||
else:
|
||||
startX += offs
|
||||
startY += offs
|
||||
|
||||
comps[1] = str(startX) + ',' + str(startY)
|
||||
comps[2] = str(dx) + ',' + str(dy)
|
||||
|
||||
return paths.CubicSuperPath(paths.Path(' '.join(comps)))
|
||||
|
||||
return paths.CubicSuperPath(paths.Path(d))
|
||||
|
||||
def modifySkeletonPath(skelPath):
|
||||
|
||||
resPath = []
|
||||
l = len(skelPath)
|
||||
resPath += skelPath[0]
|
||||
|
||||
if l > 1:
|
||||
for i in range(1, l):
|
||||
|
||||
if skelPath[i][0][1] == resPath[-1][1]:
|
||||
skelPath[i][0][0] = resPath[-1][0]
|
||||
del resPath[-1]
|
||||
|
||||
resPath += skelPath[i]
|
||||
|
||||
return resPath
|
||||
|
||||
def linearize(p, tolerance=0.001):
|
||||
'''
|
||||
This function receives a component of a 'cubicsuperpath' and returns two things:
|
||||
The path subdivided in many straight segments, and an array containing the length of each segment.
|
||||
'''
|
||||
zero = 0.000001
|
||||
i = 0
|
||||
d = 0
|
||||
lengths=[]
|
||||
|
||||
while i < len(p) - 1:
|
||||
box = inkex.bezier.pointdistance(p[i][1], p[i][2])
|
||||
box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
|
||||
box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
|
||||
chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
|
||||
|
||||
if (box - chord) > tolerance:
|
||||
b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
|
||||
p[i][2][0], p[i][2][1] = b1[1]
|
||||
p[i + 1][0][0], p[i + 1][0][1] = b2[2]
|
||||
p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
|
||||
else:
|
||||
d = (box + chord) / 2
|
||||
lengths.append(d)
|
||||
i += 1
|
||||
|
||||
new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
|
||||
new.append(p[-1][1])
|
||||
lengths = [l for l in lengths if l > zero]
|
||||
|
||||
return (new, lengths)
|
||||
|
||||
def isSkeletonClosed(sklCmp):
|
||||
|
||||
requiredPrecision = 0.005
|
||||
|
||||
sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
|
||||
sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
|
||||
|
||||
if sctest1 or sctest2:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getPolygonCentroid(polygon):
|
||||
x = 0
|
||||
y = 0
|
||||
n = len(polygon)
|
||||
|
||||
for vert in polygon:
|
||||
x += vert[0]
|
||||
y += vert[1]
|
||||
|
||||
x = x / n
|
||||
y = y / n
|
||||
|
||||
return [x, y]
|
||||
|
||||
def getPoint(p1, p2, x, y):
|
||||
x1 = p1[0]
|
||||
y1 = p1[1]
|
||||
x2 = p2[0]
|
||||
y2 = p2[1]
|
||||
|
||||
a = (y1 - y2) / (x1 - x2)
|
||||
b = y1 - a * x1
|
||||
|
||||
if x == None:
|
||||
x = (y - b) / a
|
||||
else:
|
||||
y = a * x + b
|
||||
|
||||
return [x, y]
|
||||
|
||||
def getPtOnSeg(p1, p2, segLen, l):
|
||||
if p1[0] == p2[0]:
|
||||
return [p2[0], p2[1] - l] if p2[1] < p1[1] else [p2[0], p2[1] + l]
|
||||
|
||||
if p1[1] == p2[1]:
|
||||
return [p2[0] - l, p2[1]] if p2[0] < p1[0] else [p2[0] + l, p2[1]]
|
||||
|
||||
dy = abs(p1[1] - p2[1])
|
||||
angle = asin(dy / segLen)
|
||||
dx = l * cos(angle)
|
||||
x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
|
||||
|
||||
return getPoint(p1, p2, x, None)
|
||||
|
||||
def drawfunction(nodes, width, fx):
|
||||
# x-bounds of the plane
|
||||
xstart = 0.0
|
||||
xend = 2 * pi
|
||||
# y-bounds of the plane
|
||||
ybottom = -1.0
|
||||
ytop = 1.0
|
||||
# size and location of the plane on the canvas
|
||||
height = 2
|
||||
left = 15
|
||||
bottom = 15 + height
|
||||
|
||||
# function specified by the user
|
||||
try:
|
||||
if fx != "":
|
||||
f = eval('lambda x: ' + fx.strip('"'))
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
scalex = width / (xend - xstart)
|
||||
xoff = left
|
||||
# conver x-value to coordinate
|
||||
coordx = lambda x: (x - xstart) * scalex + xoff
|
||||
|
||||
scaley = height / (ytop - ybottom)
|
||||
yoff = bottom
|
||||
# conver y-value to coordinate
|
||||
coordy = lambda y: (ybottom - y) * scaley + yoff
|
||||
|
||||
# step is the distance between nodes on x
|
||||
step = (xend - xstart) / (nodes - 1)
|
||||
third = step / 3.0
|
||||
# step used in calculating derivatives
|
||||
ds = step * 0.001
|
||||
|
||||
# initialize function and derivative for 0;
|
||||
# they are carried over from one iteration to the next, to avoid extra function calculations.
|
||||
x0 = xstart
|
||||
y0 = f(xstart)
|
||||
|
||||
# numerical derivative, using 0.001*step as the small differential
|
||||
x1 = xstart + ds # Second point AFTER first point (Good for first point)
|
||||
y1 = f(x1)
|
||||
|
||||
dx0 = (x1 - x0) / ds
|
||||
dy0 = (y1 - y0) / ds
|
||||
|
||||
# path array
|
||||
a = []
|
||||
|
||||
# Start curve
|
||||
#a.append(['M ', [coordx(x0), coordy(y0)]])
|
||||
a.append(['M', [coordx(x0), coordy(y0)]])
|
||||
|
||||
for i in range(int(nodes - 1)):
|
||||
x1 = (i + 1) * step + xstart
|
||||
x2 = x1 - ds # Second point BEFORE first point (Good for last point)
|
||||
y1 = f(x1)
|
||||
y2 = f(x2)
|
||||
|
||||
# numerical derivative
|
||||
dx1 = (x1 - x2) / ds
|
||||
dy1 = (y1 - y2) / ds
|
||||
|
||||
# create curve
|
||||
a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
|
||||
coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
|
||||
coordx(x1), coordy(y1)]])
|
||||
|
||||
# Next segment's start is this segment's end
|
||||
x0 = x1
|
||||
y0 = y1
|
||||
# Assume the function is smooth everywhere, so carry over the derivative too
|
||||
dx0 = dx1
|
||||
dy0 = dy1
|
||||
|
||||
return a
|
||||
|
||||
def offset(pathComp, dx, dy):
|
||||
for ctl in pathComp:
|
||||
for pt in ctl:
|
||||
pt[0] += dx
|
||||
pt[1] += dy
|
||||
|
||||
def stretch(pathComp, xscale, yscale, org):
|
||||
for ctl in pathComp:
|
||||
for pt in ctl:
|
||||
pt[0] = org[0] + (pt[0] - org[0]) * xscale
|
||||
pt[1] = org[1] + (pt[1] - org[1]) * yscale
|
||||
|
||||
class GuillocheContour(pathmodifier.PathModifier):
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument("--contourFunction", default="sin", help="Function defining the contour")
|
||||
pars.add_argument("--tab")
|
||||
pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
|
||||
pars.add_argument("--amplitude", type=int, default=1, help="Amplitude of the function")
|
||||
pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
|
||||
pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
|
||||
pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
|
||||
pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If Ttrue, control object will be removed")
|
||||
pars.add_argument("--strokeColor", type=inkex.Color)
|
||||
pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
|
||||
pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
|
||||
pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
|
||||
pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
|
||||
pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
|
||||
pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
|
||||
pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
|
||||
pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
|
||||
pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
|
||||
pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
|
||||
|
||||
|
||||
def prepareSelectionList(self):
|
||||
self.skeletons = self.svg.selected
|
||||
pathmodifier.PathModifier.expand_clones(self, self.skeletons, True, False)
|
||||
pathmodifier.PathModifier.objects_to_paths(self, self.skeletons, True)
|
||||
|
||||
def linearizePath(self, skelPath, offs):
|
||||
comps, lengths = linearize(skelPath)
|
||||
|
||||
self.skelCompIsClosed = isSkeletonClosed(comps)
|
||||
|
||||
if (self.skelCompIsClosed and offs != 0):
|
||||
centroid = getPolygonCentroid(comps)
|
||||
|
||||
for i in range(len(comps)):
|
||||
pt1 = comps[i]
|
||||
dist = inkex.bezier.pointdistance(centroid, pt1)
|
||||
|
||||
comps[i] = getPtOnSeg(centroid, pt1, dist, dist + offs)
|
||||
|
||||
if i > 0:
|
||||
lengths[i - 1] = inkex.bezier.pointdistance(comps[i - 1], comps[i])
|
||||
|
||||
return (comps, lengths)
|
||||
|
||||
def getFunction(self, func):
|
||||
res = ''
|
||||
|
||||
presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
|
||||
presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
|
||||
|
||||
if (func == 'sin' or func == 'cos'):
|
||||
return '(' + str(self.options.amplitude) + ') * ' + func + '(x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + '))'
|
||||
|
||||
if func == 'env1':
|
||||
presetAmp1 = presetAmp3 = 0.495
|
||||
elif func == 'env2':
|
||||
presetAmp1 = presetAmp3 = 0.65
|
||||
presetPhOf1 = presetPhOf3 = 25
|
||||
elif func == 'env3':
|
||||
presetAmp1 = 0.75
|
||||
presetPhOf1 = 25
|
||||
presetAmp3 = 0.24
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env4':
|
||||
presetAmp1 = 1.105
|
||||
presetAmp3 = 0.27625
|
||||
presetPhOf3 = 50
|
||||
elif func == 'env5':
|
||||
presetAmp1 = 0.37464375
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.5655
|
||||
presetAmp3 = 0.37464375
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env6':
|
||||
presetAmp1 = 0.413725
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.45695
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.494
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env7':
|
||||
presetAmp1 = 0.624
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.312
|
||||
presetAmp3 = 0.624
|
||||
presetPhOf3 = 25
|
||||
elif func == 'env8':
|
||||
presetAmp1 = 0.65
|
||||
presetPhOf1 = 50
|
||||
presetAmp2 = 0.585
|
||||
presetAmp3 = 0.13
|
||||
elif func == 'env9':
|
||||
presetAmp1 = 0.07605
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.33345
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.468
|
||||
presetPhOf3 = -25
|
||||
presetAmp4 = 0.32175
|
||||
elif func == 'env10':
|
||||
presetAmp1 = 0.3575
|
||||
presetPhOf1 = -25
|
||||
presetAmp2 = 0.3575
|
||||
presetAmp3 = 0.3575
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.3575
|
||||
presetPhOf4 = 50
|
||||
elif func == 'env11':
|
||||
presetAmp1 = 0.65
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.13
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.26
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.39
|
||||
elif func == 'env12':
|
||||
presetAmp1 = 0.5525
|
||||
presetPhOf1 = -25
|
||||
presetAmp2 = 0.0414375
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.15884375
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.0966875
|
||||
presetAmp5 = 0.28315625
|
||||
presetPhOf5 = -25
|
||||
|
||||
harm1 = '(' + str(self.options.amplitude * (presetAmp1 + self.options.amplitude1)) + ') * cos(1 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
|
||||
harm2 = '(' + str(self.options.amplitude * (presetAmp2 + self.options.amplitude2)) + ') * cos(2 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
|
||||
harm3 = '(' + str(self.options.amplitude * (presetAmp3 + self.options.amplitude3)) + ') * cos(3 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
|
||||
harm4 = '(' + str(self.options.amplitude * (presetAmp4 + self.options.amplitude4)) + ') * cos(4 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
|
||||
harm5 = '(' + str(self.options.amplitude * (presetAmp5 + self.options.amplitude5)) + ') * cos(5 * (x + (' + str(self.options.phaseOffset / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
|
||||
|
||||
res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
|
||||
|
||||
return res
|
||||
|
||||
def lengthToTime(self, l):
|
||||
'''
|
||||
Recieves an arc length l, and returns the index of the segment in self.skelComp
|
||||
containing the corresponding point, together with the position of the point on this segment.
|
||||
|
||||
If the deformer is closed, do computations modulo the total length.
|
||||
'''
|
||||
if self.skelCompIsClosed:
|
||||
l = l % sum(self.lengths)
|
||||
|
||||
if l <= 0:
|
||||
return 0, l / self.lengths[0]
|
||||
|
||||
i = 0
|
||||
|
||||
while (i < len(self.lengths)) and (self.lengths[i] <= l):
|
||||
l -= self.lengths[i]
|
||||
i += 1
|
||||
|
||||
t = l / self.lengths[min(i, len(self.lengths) - 1)]
|
||||
|
||||
return (i, t)
|
||||
|
||||
def applyDiffeo(self, bpt, vects=()):
|
||||
'''
|
||||
The kernel of this stuff:
|
||||
bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
|
||||
'''
|
||||
s = bpt[0] - self.skelComp[0][0]
|
||||
i, t = self.lengthToTime(s)
|
||||
|
||||
if i == len(self.skelComp) - 1:
|
||||
x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
|
||||
dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
|
||||
dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
|
||||
else:
|
||||
x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
|
||||
dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
|
||||
dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
|
||||
|
||||
vx = 0
|
||||
vy = bpt[1] - self.skelComp[0][1]
|
||||
bpt[0] = x + vx * dx - vy * dy
|
||||
bpt[1] = y + vx * dy + vy * dx
|
||||
|
||||
for v in vects:
|
||||
vx = v[0] - self.skelComp[0][0] - s
|
||||
vy = v[1] - self.skelComp[0][1]
|
||||
v[0] = x + vx * dx - vy * dy
|
||||
v[1] = y + vx * dy + vy * dx
|
||||
|
||||
def effect(self):
|
||||
|
||||
if len(self.options.ids) < 1:
|
||||
inkex.errormsg(_("This extension requires one selected path."))
|
||||
return
|
||||
|
||||
self.prepareSelectionList()
|
||||
|
||||
for skeleton in self.skeletons.__iter__():
|
||||
resPath = []
|
||||
pattern = etree.Element(inkex.addNS('path','svg'))
|
||||
|
||||
self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
|
||||
|
||||
# Copy style of skeleton with setting color and opacity
|
||||
s = skeleton.get('style')
|
||||
|
||||
# Get any path transform for the contour output
|
||||
xfm = skeleton.get('transform')
|
||||
|
||||
firstSkel = skeleton.get('d')
|
||||
|
||||
if s:
|
||||
pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
|
||||
|
||||
if xfm:
|
||||
pattern.set('transform', xfm)
|
||||
|
||||
skeletonPath = modifySkeletonPath(getSkeletonPath(skeleton.get('d'), self.options.offset))
|
||||
|
||||
self.skelComp, self.lengths = self.linearizePath(skeletonPath, self.options.offset)
|
||||
|
||||
length = sum(self.lengths)
|
||||
patternWidth = length / self.options.frequency
|
||||
selectedFunction = self.getFunction(self.options.contourFunction)
|
||||
|
||||
pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
|
||||
|
||||
# Add path into SVG structure
|
||||
skeleton.getparent().append(pattern)
|
||||
|
||||
if self.options.remove:
|
||||
skeleton.getparent().remove(skeleton)
|
||||
|
||||
# Compute bounding box
|
||||
#pattPath = inkex.paths.Path(skeleton.get('d'))
|
||||
|
||||
patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
|
||||
patternPath = inkex.paths.Path(patternCubicPath)
|
||||
|
||||
bbox = patternPath.bounding_box()
|
||||
|
||||
width = bbox.maximum[0] - bbox.minimum[0]
|
||||
dx = width
|
||||
|
||||
if dx < 0.01:
|
||||
exit(_("The total length of the pattern is too small."))
|
||||
|
||||
curPath = deepcopy(patternCubicPath)
|
||||
|
||||
xoffset = self.skelComp[0][0] - bbox.minimum[0]
|
||||
yoffset = self.skelComp[0][1] - (bbox.bottom + bbox.top) / 2
|
||||
|
||||
patternCopies = max(1, int(round(length / dx)))
|
||||
width = dx * patternCopies
|
||||
|
||||
newPath = []
|
||||
|
||||
# Repeat pattern to cover whole skeleton
|
||||
for subPath in curPath:
|
||||
for i in range(0, patternCopies, 1):
|
||||
newPath.append(deepcopy(subPath))
|
||||
offset(subPath, dx, 0)
|
||||
|
||||
curPath = newPath
|
||||
|
||||
# Offset pattern to the first node of the skeleton
|
||||
for subPath in curPath:
|
||||
offset(subPath, xoffset, yoffset)
|
||||
|
||||
# Stretch pattern to whole skeleton
|
||||
for subPath in curPath:
|
||||
stretch(subPath, length / width, 1, self.skelComp[0])
|
||||
|
||||
for subPath in curPath:
|
||||
for ctlpt in subPath:
|
||||
self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
|
||||
|
||||
# Check if there is a need to close path manually
|
||||
if self.skelCompIsClosed:
|
||||
firstPtX = round(curPath[0][0][1][0], 8)
|
||||
firstPtY = round(curPath[0][0][1][1], 8)
|
||||
finalPtX = round(curPath[-1][-1][1][0], 8)
|
||||
finalPtY = round(curPath[-1][-1][1][1], 8)
|
||||
|
||||
if (firstPtX != finalPtX or firstPtY != finalPtY):
|
||||
curPath[-1].append(curPath[0][0])
|
||||
|
||||
resPath += curPath
|
||||
|
||||
# This final step takes the newly constructed contour from a multilevel list to
|
||||
# a formal svg path
|
||||
step1rep = paths.CubicSuperPath(resPath).to_path().to_arrays()
|
||||
step2rep = str(paths.Path(step1rep))
|
||||
pattern.set('d', step2rep)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
GuillocheContour().run()
|
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Guilloche Pattern</name>
|
||||
<id>fablabchemnitz.de.guilloche_creations.guilloche_pattern</id>
|
||||
<param name="patternFunction" type="optiongroup" appearance="combo" gui-text="Function">
|
||||
<option value="line">Line</option>
|
||||
<option value="sin">Sin</option>
|
||||
<option value="cos">Cos</option>
|
||||
<option value="env1">Env1</option>
|
||||
<option value="env2">Env2</option>
|
||||
<option value="env3">Env3</option>
|
||||
<option value="env4">Env4</option>
|
||||
<option value="env5">Env5</option>
|
||||
<option value="env6">Env6</option>
|
||||
<option value="env7">Env7</option>
|
||||
<option value="env8">Env8</option>
|
||||
<option value="env9">Env9</option>
|
||||
<option value="env10">Env10</option>
|
||||
<option value="env11">Env11</option>
|
||||
<option value="env12">Env12</option>
|
||||
</param>
|
||||
<param name="tab" type="notebook">
|
||||
<page name="pattern" gui-text="Pattern">
|
||||
<param name="frequency" type="int" min="1" max="100" gui-text="Frequency:">10</param>
|
||||
<param name="amplitude" type="int" min="-300" max="300" gui-text="Amplitude:">100</param>
|
||||
<param name="phaseOffset" type="int" min="-100" max="100" gui-text="Phase offset:">0</param>
|
||||
<param name="offset" type="int" min="-100" max="100" gui-text="Offset:">0</param>
|
||||
<param name="phaseCoverage" type="int" min="-100" max="100" gui-text="Phase Coverage">100</param>
|
||||
<param name="series" type="int" min="1" max="50" gui-text="Series">1</param>
|
||||
<param name="nodes" type="int" min="2" max="1000" gui-text="Number of nodes:">20</param>
|
||||
<param name="remove" type="bool" gui-text="Remove control objects">false</param>
|
||||
<param name="strokeColor" type="color" gui-text="Stroke color"></param>
|
||||
</page>
|
||||
<page name="function" gui-text="Function">
|
||||
<param name="amplitude1" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 1:">0.0</param>
|
||||
<param name="phase1" type="int" min="-100" max="100" gui-text="Phase offset 1:">0</param>
|
||||
<param name="amplitude2" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 2:">0.0</param>
|
||||
<param name="phase2" type="int" min="-100" max="100" gui-text="Phase offset 2:">0</param>
|
||||
<param name="amplitude3" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 3:">0.0</param>
|
||||
<param name="phase3" type="int" min="-100" max="100" gui-text="Phase offset 3:">0</param>
|
||||
<param name="amplitude4" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 4:">0.0</param>
|
||||
<param name="phase4" type="int" min="-100" max="100" gui-text="Phase offset 4:">0</param>
|
||||
<param name="amplitude5" type="float" min="-10.0" max="10.0" precision="2" gui-text="Amplitude 5:">0.0</param>
|
||||
<param name="phase5" type="int" min="-100" max="100" gui-text="Phase offset 5:">0</param>
|
||||
</page>
|
||||
</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Modify existing Path(s)"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">guilloche_pattern.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,972 @@
|
||||
#! /usr/bin/env python3
|
||||
'''
|
||||
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.
|
||||
|
||||
Quick description:
|
||||
This extension is an update to the guilloche_pattern extension that works under inkscape v0.92
|
||||
|
||||
'''
|
||||
# standard library
|
||||
from math import asin, atan2, pi, sin, cos
|
||||
from copy import deepcopy
|
||||
# local library
|
||||
import inkex
|
||||
import pathmodifier
|
||||
import cubicsuperpath
|
||||
from inkex import paths
|
||||
from lxml import etree
|
||||
|
||||
def modifySkeletonPath(skelPath):
|
||||
resPath = []
|
||||
l = len(skelPath)
|
||||
resPath += skelPath[0]
|
||||
|
||||
if l > 1:
|
||||
for i in range(1, l):
|
||||
if skelPath[i][0][1] == resPath[-1][1]:
|
||||
skelPath[i][0][0] = resPath[-1][0]
|
||||
del resPath[-1]
|
||||
|
||||
resPath += skelPath[i]
|
||||
|
||||
return resPath
|
||||
|
||||
def linearize(p, tolerance=0.001):
|
||||
'''
|
||||
This function receives a component of a 'cubicsuperpath' and returns two things:
|
||||
The path subdivided in many straight segments, and an array containing the length
|
||||
of each segment.
|
||||
'''
|
||||
zero = 0.000001
|
||||
i = 0
|
||||
d = 0
|
||||
lengths=[]
|
||||
|
||||
while i < len(p) - 1:
|
||||
box = inkex.bezier.pointdistance(p[i][1], p[i][2])
|
||||
box += inkex.bezier.pointdistance(p[i][2], p[i+1][0])
|
||||
box += inkex.bezier.pointdistance(p[i+1][0], p[i+1][1])
|
||||
chord = inkex.bezier.pointdistance(p[i][1], p[i+1][1])
|
||||
|
||||
if (box - chord) > tolerance:
|
||||
b1, b2 = inkex.bezier.beziersplitatt([p[i][1], p[i][2], p[i + 1][0], p[i + 1][1]], 0.5)
|
||||
p[i][2][0], p[i][2][1] = b1[1]
|
||||
p[i + 1][0][0], p[i + 1][0][1] = b2[2]
|
||||
p.insert(i + 1, [[b1[2][0], b1[2][1]], [b1[3][0], b1[3][1]], [b2[1][0], b2[1][1]]])
|
||||
else:
|
||||
d = (box + chord) / 2
|
||||
lengths.append(d)
|
||||
i += 1
|
||||
|
||||
new = [p[i][1] for i in range(0, len(p) - 1) if lengths[i] > zero]
|
||||
new.append(p[-1][1])
|
||||
lengths = [l for l in lengths if l > zero]
|
||||
|
||||
return (new, lengths)
|
||||
|
||||
def isSkeletonClosed(sklCmp):
|
||||
|
||||
requiredPrecision = 0.005
|
||||
|
||||
sctest1 = abs(sklCmp[0][0] - sklCmp[-1][0]) > requiredPrecision
|
||||
sctest2 = abs(sklCmp[0][1] - sklCmp[-1][1]) > requiredPrecision
|
||||
|
||||
if sctest1 or sctest2:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def checkCompatibility(bbox1, bbox2, comps1, comps2):
|
||||
cl1 = isSkeletonClosed(comps1)
|
||||
cl2 = isSkeletonClosed(comps2)
|
||||
|
||||
if (cl1 and cl2):
|
||||
if ((bbox1.left >= bbox2.left) and (bbox1.right <= bbox2.right) and (bbox1.top >= bbox2.top) and (bbox1.bottom <= bbox2.bottom)):
|
||||
return (True, False)
|
||||
elif ((bbox1.left <= bbox2.left) and (bbox1.right >= bbox2.right) and (bbox1.top <= bbox2.top) and (bbox1.bottom >= bbox2.bottom)):
|
||||
return (True, True)
|
||||
|
||||
elif (not cl1 and not cl2):
|
||||
if (comps1[0][0] == comps2[0][0] and comps1[-1][0] == comps2[-1][0]):
|
||||
if ((comps1[0][0] < comps1[-1][0] and comps1[0][1] >= comps2[0][1]) or (comps1[0][0] > comps1[-1][0] and comps1[0][1] <= comps2[0][1])):
|
||||
return (True, False)
|
||||
else:
|
||||
return (True, True)
|
||||
|
||||
elif (comps1[0][1] == comps2[0][1] and comps1[-1][1] == comps2[-1][1]):
|
||||
if ((comps1[0][1] < comps1[-1][1] and comps1[0][0] <= comps2[0][0]) or (comps1[0][1] > comps1[-1][1] and comps1[0][0] >= comps2[0][0])):
|
||||
return (True, False)
|
||||
else:
|
||||
return (True, True)
|
||||
|
||||
return (False, False)
|
||||
|
||||
def linearizeEnvelopes(envs):
|
||||
|
||||
env0Path = paths.Path(envs[0].get('d'))
|
||||
env1Path = paths.Path(envs[1].get('d'))
|
||||
env0CubicPath = paths.CubicSuperPath(env0Path)
|
||||
env1CubicPath = paths.CubicSuperPath(env1Path)
|
||||
env0ReformedPath = paths.Path(env0CubicPath)
|
||||
env1ReformedPath = paths.Path(env1CubicPath)
|
||||
|
||||
bbox1 = env0ReformedPath.bounding_box()
|
||||
bbox2 = env1ReformedPath.bounding_box()
|
||||
|
||||
comps1, lengths1 = linearize(modifySkeletonPath(env0CubicPath))
|
||||
comps2, lengths2 = linearize(modifySkeletonPath(env1CubicPath))
|
||||
|
||||
correctness, shouldSwap = checkCompatibility(bbox1, bbox2, comps1, comps2)
|
||||
|
||||
if not shouldSwap:
|
||||
return (comps1, lengths1, comps2, lengths2, bbox1, bbox2, correctness)
|
||||
else:
|
||||
return (comps2, lengths2, comps1, lengths1, bbox2, bbox1, correctness)
|
||||
|
||||
def getMidPoint(p1, p2):
|
||||
return [(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2]
|
||||
|
||||
def getPoint(p1, p2, x, y):
|
||||
x1 = p1[0]
|
||||
y1 = p1[1]
|
||||
x2 = p2[0]
|
||||
y2 = p2[1]
|
||||
|
||||
a = (y1 - y2) / (x1 - x2)
|
||||
b = y1 - a * x1
|
||||
|
||||
if x == None:
|
||||
x = (y - b) / a
|
||||
else:
|
||||
y = a * x + b
|
||||
|
||||
return [x, y]
|
||||
|
||||
def getPtOnSeg(p1, p2, segLen, l):
|
||||
if p1[0] == p2[0]:
|
||||
return [p1[0], p1[1] - l] if p1[1] > p2[1] else [p1[0], p1[1] + l]
|
||||
|
||||
if p1[1] == p2[1]:
|
||||
return [p1[0] - l, p1[1]] if p1[0] > p2[0] else [p1[0] + l, p1[1]]
|
||||
|
||||
dy = abs(p1[1] - p2[1])
|
||||
angle = asin(dy / segLen)
|
||||
dx = l * cos(angle)
|
||||
x = p1[0] - dx if p1[0] > p2[0] else p1[0] + dx
|
||||
|
||||
return getPoint(p1, p2, x, None)
|
||||
|
||||
def getPtsByX(pt, comps, isClosed):
|
||||
res = []
|
||||
|
||||
for i in range(1, len(comps)):
|
||||
if ((comps[i - 1][0] <= pt[0] and pt[0] <= comps[i][0]) or (comps[i - 1][0] >= pt[0] and pt[0] >= comps[i][0])):
|
||||
if comps[i - 1][0] == comps[i][0]:
|
||||
d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
|
||||
d2 = inkex.bezier.pointdistance(pt, comps[i])
|
||||
|
||||
if d1 < d2:
|
||||
res.append(comps[i - 1])
|
||||
else:
|
||||
res.append(comps[i])
|
||||
elif comps[i - 1][1] == comps[i][1]:
|
||||
res.append([pt[0], comps[i - 1][1]])
|
||||
else:
|
||||
res.append(getPoint(comps[i - 1], comps[i], pt[0], None))
|
||||
|
||||
if not isClosed:
|
||||
return res[0]
|
||||
|
||||
return res
|
||||
|
||||
def getPtsByY(pt, comps, isClosed):
|
||||
res = []
|
||||
|
||||
for i in range(1, len(comps)):
|
||||
if ((comps[i - 1][1] <= pt[1] and pt[1] <= comps[i][1]) or (comps[i - 1][1] >= pt[1] and pt[1] >= comps[i][1])):
|
||||
if comps[i - 1][1] == comps[i][1]:
|
||||
d1 = inkex.bezier.pointdistance(pt, comps[i - 1])
|
||||
d2 = inkex.bezier.pointdistance(pt, comps[i])
|
||||
|
||||
if d1 < d2:
|
||||
res.append(comps[i - 1])
|
||||
else:
|
||||
res.append(comps[i])
|
||||
elif comps[i - 1][0] == comps[i][0]:
|
||||
res.append([comps[i - 1][0], pt[1]])
|
||||
else:
|
||||
res.append(getPoint(comps[i - 1], comps[i], None, pt[1]))
|
||||
|
||||
if not isClosed:
|
||||
return res[0]
|
||||
|
||||
return res
|
||||
|
||||
def getIntersectionPt(cntr, p, comps):
|
||||
# Find the intersection of the infinitely extended line from cntr to p with comps
|
||||
# This algorithm assumes that each comps segment is a straight line and that the comps segment subtends less than
|
||||
# pi angle at cntr
|
||||
|
||||
twopi = 2.0 * pi
|
||||
a = atan2((cntr[1] - p[1]), (cntr[0] - p[0])) # a is a four-quadrant angle of the cntr->p line, -pi <= a <= pi
|
||||
|
||||
obtPts = []
|
||||
|
||||
for i in range(1, len(comps)): # Check every interval in comps for an intersection
|
||||
a2 = atan2((cntr[1] - comps[i][1]) , (cntr[0] - comps[i][0]))
|
||||
a1 = atan2((cntr[1] - comps[i - 1][1]) , (cntr[0] - comps[i - 1][0]))
|
||||
|
||||
da1a = a - a1 # the angle from a1 to a
|
||||
da12 = a2 - a1 # the angle interval covered by comps[i-1]->comps[i]
|
||||
|
||||
#inkex.errormsg("raw da1a = " + str(da1a))
|
||||
#inkex.errormsg("raw da12 = " + str(da12))
|
||||
|
||||
if da1a > pi: # make the angle fit -pi->pi
|
||||
da1a -= twopi
|
||||
elif da1a < -pi:
|
||||
da1a += twopi
|
||||
|
||||
if da12 > pi: # make the angle fit -pi->pi
|
||||
da12 -= twopi
|
||||
elif da12 < -pi:
|
||||
da12 += twopi
|
||||
|
||||
frac = da1a / da12
|
||||
|
||||
if frac < 0.0 or frac > 1.0: # if the line does not cross the comps interval, move on
|
||||
continue
|
||||
|
||||
x = frac * comps[i][0] + (1.0 - frac) * comps[i-1][0]
|
||||
y = frac * comps[i][1] + (1.0 - frac) * comps[i-1][1]
|
||||
|
||||
obtPts.append([x,y])
|
||||
|
||||
if len(obtPts) < 1:
|
||||
inkex.errormsg("No intersection pt found")
|
||||
exit()
|
||||
else:
|
||||
return obtPts[0]
|
||||
|
||||
def getPolygonCentroid(polygon):
|
||||
x = 0
|
||||
y = 0
|
||||
n = len(polygon)
|
||||
|
||||
for vert in polygon:
|
||||
x += vert[0]
|
||||
y += vert[1]
|
||||
|
||||
x = x / n
|
||||
y = y / n
|
||||
|
||||
return [x, y]
|
||||
|
||||
def getDistBetweenFirstPts(comps1, comps2, bbox1, bbox2, nest):
|
||||
pt1 = comps1[0]
|
||||
pt2 = None
|
||||
|
||||
if (bbox1[0] == bbox2[0] and bbox1[1] == bbox2[1]):
|
||||
pt2 = getPtsByX(pt1, comps2, False)
|
||||
elif (bbox1[2] == bbox2[2] and bbox1[3] == bbox2[3]):
|
||||
pt2 = getPtsByY(pt1, comps2, False)
|
||||
elif nest:
|
||||
centroid = getPolygonCentroid(comps1)
|
||||
pt2 = getIntersectionPt(centroid, pt1, comps2)
|
||||
|
||||
dist = inkex.bezier.pointdistance(pt1, pt2)
|
||||
|
||||
return dist
|
||||
|
||||
def getCirclePath(startPt, rx):
|
||||
curX = startPt[0]
|
||||
curY = startPt[1]
|
||||
signRX = signRY = 1
|
||||
|
||||
res = 'M ' + str(curX) + ',' + str(curY) + ' A '
|
||||
|
||||
for i in range(4):
|
||||
res += str(rx) + ',' + str(rx) + ' 0 0 1 '
|
||||
|
||||
if i % 2 == 0:
|
||||
signRX = -signRX
|
||||
else:
|
||||
signRY = -signRY
|
||||
|
||||
curX += signRX * rx
|
||||
curY += signRY * rx
|
||||
|
||||
res += str(curX) + ',' + str(curY) + ' '
|
||||
|
||||
res += 'Z'
|
||||
|
||||
return res
|
||||
|
||||
def getClosedLinearizedSkeletonPath(comps1, comps2, offs, isLine):
|
||||
path = []
|
||||
lengths = []
|
||||
|
||||
centroid = getPolygonCentroid(comps1)
|
||||
|
||||
if isLine:
|
||||
for pt2 in comps2:
|
||||
pt1 = getIntersectionPt(centroid, pt2, comps1)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
if offs > 0:
|
||||
dist = inkex.bezier.pointdistance(pt1, pt2)
|
||||
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
|
||||
else:
|
||||
path.append(midPt)
|
||||
|
||||
if len(path) > 1:
|
||||
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
|
||||
else:
|
||||
pt1 = comps1[0]
|
||||
pt2 = getIntersectionPt(centroid, pt1, comps2)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
rx = inkex.bezier.pointdistance(centroid, midPt)
|
||||
|
||||
svgPath = getCirclePath(midPt, rx)
|
||||
|
||||
inkexPath = paths.Path(svgPath)
|
||||
cubicPath = paths.CubicSuperPath(inkexPath)
|
||||
path, lengths = linearize(modifySkeletonPath(cubicPath))
|
||||
|
||||
return (path, lengths)
|
||||
|
||||
def getHorizontalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
|
||||
path = []
|
||||
lengths = []
|
||||
|
||||
if isLine:
|
||||
for pt1 in comps1:
|
||||
pt2 = getPtsByX(pt1, comps2, False)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
if offs > 0:
|
||||
dist = inkex.bezier.pointdistance(pt1, pt2)
|
||||
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
|
||||
else:
|
||||
path.append(midPt)
|
||||
|
||||
if len(path) > 1:
|
||||
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
|
||||
else:
|
||||
pt1 = comps1[0]
|
||||
pt2 = comps2[0]
|
||||
|
||||
firstPt = getMidPoint(pt1, pt2)
|
||||
path.append(firstPt)
|
||||
|
||||
lastPt = [comps1[-1][0], firstPt[1]]
|
||||
path.append(lastPt)
|
||||
|
||||
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
|
||||
|
||||
return (path, lengths)
|
||||
|
||||
def getVerticalLinearizedSkeletonPath(comps1, comps2, offs, isLine):
|
||||
path = []
|
||||
lengths = []
|
||||
|
||||
if isLine:
|
||||
for pt1 in comps1:
|
||||
pt2 = getPtsByY(pt1, comps2, False)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
if offs > 0:
|
||||
dist = inkex.bezier.pointdistance(pt1, pt2)
|
||||
path.append(getPtOnSeg(pt1, pt2, dist, offs * dist))
|
||||
else:
|
||||
path.append(midPt)
|
||||
|
||||
if len(path) > 1:
|
||||
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
|
||||
else:
|
||||
pt1 = comps1[0]
|
||||
pt2 = comps2[0]
|
||||
|
||||
firstPt = getMidPoint(pt1, pt2)
|
||||
path.append(firstPt)
|
||||
|
||||
lastPt = [firstPt[0], comps1[-1][1]]
|
||||
path.append(lastPt)
|
||||
|
||||
lengths.append(inkex.bezier.pointdistance(path[-2], path[-1]))
|
||||
|
||||
return (path, lengths)
|
||||
|
||||
def getColorAndOpacity(longColor):
|
||||
'''
|
||||
Convert the long into a #rrggbb color value
|
||||
Conversion back is A + B*256^1 + G*256^2 + R*256^3
|
||||
'''
|
||||
longColor = int(longColor)
|
||||
|
||||
if longColor < 0:
|
||||
longColor = longColor & 0xFFFFFFFF
|
||||
|
||||
hexColor = hex(longColor)
|
||||
lhc = len(hexColor)
|
||||
|
||||
hexOpacity = hexColor[lhc-2 : ]
|
||||
hexColor = '#' + hexColor[2:-2].rjust(6, '0')
|
||||
|
||||
return (hexColor, hexOpacity)
|
||||
|
||||
def setColorAndOpacity(style, color, opacity):
|
||||
declarations = style.split(';')
|
||||
strokeOpacityInStyle = False
|
||||
newOpacity = round((int(opacity, 16) / 255.0), 8)
|
||||
|
||||
for i,decl in enumerate(declarations):
|
||||
parts = decl.split(':', 2)
|
||||
|
||||
if len(parts) == 2:
|
||||
(prop, val) = parts
|
||||
prop = prop.strip().lower()
|
||||
|
||||
if (prop == 'stroke' and val != color):
|
||||
declarations[i] = prop + ':' + color
|
||||
|
||||
if prop == 'stroke-opacity':
|
||||
if val != newOpacity:
|
||||
declarations[i] = prop + ':' + str(newOpacity)
|
||||
|
||||
strokeOpacityInStyle = True
|
||||
|
||||
if not strokeOpacityInStyle:
|
||||
declarations.append('stroke-opacity' + ':' + str(newOpacity))
|
||||
|
||||
return ";".join(declarations)
|
||||
|
||||
def drawfunction(nodes, width, fx):
|
||||
# x-bounds of the plane
|
||||
xstart = 0.0
|
||||
xend = 2 * pi
|
||||
# y-bounds of the plane
|
||||
ybottom = -1.0
|
||||
ytop = 1.0
|
||||
# size and location of the plane on the canvas
|
||||
height = 2
|
||||
left = 15
|
||||
bottom = 15 + height
|
||||
|
||||
# function specified by the user
|
||||
try:
|
||||
if fx != "":
|
||||
f = eval('lambda x: ' + fx.strip('"'))
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
scalex = width / (xend - xstart)
|
||||
xoff = left
|
||||
# conver x-value to coordinate
|
||||
coordx = lambda x: (x - xstart) * scalex + xoff
|
||||
|
||||
scaley = height / (ytop - ybottom)
|
||||
yoff = bottom
|
||||
# conver y-value to coordinate
|
||||
coordy = lambda y: (ybottom - y) * scaley + yoff
|
||||
|
||||
# step is the distance between nodes on x
|
||||
step = (xend - xstart) / (nodes - 1)
|
||||
third = step / 3.0
|
||||
# step used in calculating derivatives
|
||||
ds = step * 0.001
|
||||
|
||||
# initialize function and derivative for 0;
|
||||
# they are carried over from one iteration to the next, to avoid extra function calculations.
|
||||
x0 = xstart
|
||||
y0 = f(xstart)
|
||||
|
||||
# numerical derivative, using 0.001*step as the small differential
|
||||
x1 = xstart + ds # Second point AFTER first point (Good for first point)
|
||||
y1 = f(x1)
|
||||
|
||||
dx0 = (x1 - x0) / ds
|
||||
dy0 = (y1 - y0) / ds
|
||||
|
||||
# path array
|
||||
a = []
|
||||
# Start curve
|
||||
a.append(['M', [coordx(x0), coordy(y0)]])
|
||||
|
||||
for i in range(int(nodes - 1)):
|
||||
x1 = (i + 1) * step + xstart
|
||||
x2 = x1 - ds # Second point BEFORE first point (Good for last point)
|
||||
y1 = f(x1)
|
||||
y2 = f(x2)
|
||||
|
||||
# numerical derivative
|
||||
dx1 = (x1 - x2) / ds
|
||||
dy1 = (y1 - y2) / ds
|
||||
|
||||
# create curve
|
||||
a.append(['C', [coordx(x0 + (dx0 * third)), coordy(y0 + (dy0 * third)),
|
||||
coordx(x1 - (dx1 * third)), coordy(y1 - (dy1 * third)),
|
||||
coordx(x1), coordy(y1)]])
|
||||
|
||||
# Next segment's start is this segment's end
|
||||
x0 = x1
|
||||
y0 = y1
|
||||
# Assume the function is smooth everywhere, so carry over the derivative too
|
||||
dx0 = dx1
|
||||
dy0 = dy1
|
||||
|
||||
return a
|
||||
|
||||
def offset(pathComp, dx, dy):
|
||||
for ctl in pathComp:
|
||||
for pt in ctl:
|
||||
pt[0] += dx
|
||||
pt[1] += dy
|
||||
|
||||
def compsToSVGd(p):
|
||||
f = p[0]
|
||||
p = p[1:]
|
||||
svgd = 'M %.9f,%.9f ' % (f[0], f[1])
|
||||
|
||||
for x in p:
|
||||
svgd += 'L %.9f,%.9f ' % (x[0], x[1])
|
||||
|
||||
return svgd
|
||||
|
||||
scRepCounter = 0
|
||||
|
||||
def stretchComps(skelComps, patComps, comps1, comps2, bbox1, bbox2, nest, halfHeight, ampl, offs):
|
||||
res = []
|
||||
repCounter = 0
|
||||
|
||||
if nest:
|
||||
|
||||
newPt = None
|
||||
centroid = getPolygonCentroid(comps1)
|
||||
|
||||
for pt in patComps:
|
||||
skelPt = getIntersectionPt(centroid, pt, skelComps)
|
||||
pt1 = getIntersectionPt(centroid, pt, comps1)
|
||||
pt2 = getIntersectionPt(centroid, pt, comps2)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
dist1 = inkex.bezier.pointdistance(skelPt, pt)
|
||||
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
|
||||
dist3 = dist2 * dist1 / halfHeight
|
||||
|
||||
if (skelPt[0] >= centroid[0] and skelPt[1] >= centroid[1]):
|
||||
if (pt[0] >= skelPt[0] and pt[1] >= skelPt[1]):
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
elif (skelPt[0] <= centroid[0] and skelPt[1] <= centroid[1]):
|
||||
if (pt[0] <= skelPt[0] and pt[1] <= skelPt[1]):
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
elif (skelPt[0] < centroid[0] and skelPt[1] > centroid[1]):
|
||||
if (pt[0] <= skelPt[0] and pt[1] >= skelPt[1]):
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
else:
|
||||
if (pt[0] >= skelPt[0] and pt[1] <= skelPt[1]):
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
|
||||
res.append(newPt)
|
||||
|
||||
elif (bbox1.left == bbox2.left and bbox1.right == bbox2.right):
|
||||
midY = skelComps[0][1]
|
||||
newPt = None
|
||||
|
||||
if (patComps[-1][0] != comps1[-1][0] and round(patComps[-1][0], 10) == comps1[-1][0]):
|
||||
patComps[-1][0] = comps1[-1][0]
|
||||
|
||||
for pt in patComps:
|
||||
pt1 = getPtsByX(pt, comps1, False)
|
||||
pt2 = getPtsByX(pt, comps2, False)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
dist1 = abs(pt[1] - midY)
|
||||
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
|
||||
dist3 = dist2 * dist1 / halfHeight
|
||||
|
||||
if bbox1.left < bbox1.right:
|
||||
if pt[1] > midY:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
if pt[1] < midY:
|
||||
newPt = getPtOnSeg(midPt, pt1, bezmisc.pointdistance(midPt, pt1), dist3 - offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
|
||||
res.append(newPt)
|
||||
|
||||
elif (bbox1.top == bbox2.top and bbox1.bottom == bbox2.bottom):
|
||||
|
||||
midX = skelComps[0][0]
|
||||
newPt = None
|
||||
|
||||
if (patComps[-1][1] != comps1[-1][1] and round(patComps[-1][1], 10) == comps1[-1][1]):
|
||||
patComps[-1][1] = comps1[-1][1]
|
||||
|
||||
for pt in patComps:
|
||||
pt1 = getPtsByY(pt, comps1, False)
|
||||
pt2 = getPtsByY(pt, comps2, False)
|
||||
midPt = getMidPoint(pt1, pt2)
|
||||
|
||||
dist1 = abs(pt[0] - midX)
|
||||
dist2 = inkex.bezier.pointdistance(midPt, pt1) * ampl
|
||||
dist3 = dist2 * dist1 / halfHeight
|
||||
|
||||
if bbox1[2] < bbox1[3]:
|
||||
if pt[0] < midX:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
else:
|
||||
if pt[1] > midX:
|
||||
newPt = getPtOnSeg(midPt, pt1, inkex.bezier.pointdistance(midPt, pt1), dist3 - offs)
|
||||
else:
|
||||
newPt = getPtOnSeg(midPt, pt2, inkex.bezier.pointdistance(midPt, pt2), dist3 + offs)
|
||||
|
||||
res.append(newPt)
|
||||
|
||||
return res
|
||||
|
||||
def stretch(pathComp, xscale, yscale, org):
|
||||
for ctl in pathComp:
|
||||
for pt in ctl:
|
||||
pt[0] = org[0] + (pt[0] - org[0]) * xscale
|
||||
pt[1] = org[1] + (pt[1] - org[1]) * yscale
|
||||
|
||||
class GuillochePattern(pathmodifier.PathModifier):
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument("--tab")
|
||||
pars.add_argument("--patternFunction", default="sin", help="Function of the pattern")
|
||||
pars.add_argument("--frequency", type=int, default=10, help="Frequency of the function")
|
||||
pars.add_argument("--amplitude", type=int, default=100, help="Amplitude of the function")
|
||||
pars.add_argument("--phaseOffset", type=int, default=0, help="Phase offset of the function")
|
||||
pars.add_argument("--offset", type=int, default=0, help="Offset of the function")
|
||||
pars.add_argument("--phaseCoverage", type=int, default=100, help="Phase coverage of the function")
|
||||
pars.add_argument("--series", type=int, default=1, help="Series of the function")
|
||||
pars.add_argument("--nodes", type=int, default=20, help="Count of nodes")
|
||||
pars.add_argument("--remove", type=inkex.Boolean, default=False, help="If True, control objects will be removed")
|
||||
pars.add_argument("--strokeColor", type=inkex.Color, default=000, help="The line's color")
|
||||
pars.add_argument("--amplitude1", type=float, default=0.0, help="Amplitude of first harmonic")
|
||||
pars.add_argument("--phase1", type=int, default=0, help="Phase offset of first harmonic")
|
||||
pars.add_argument("--amplitude2", type=float, default=0.0, help="Amplitude of second harmonic")
|
||||
pars.add_argument("--phase2", type=int, default=0, help="Phase offset of second harmonic")
|
||||
pars.add_argument("--amplitude3", type=float, default=0.0, help="Amplitude of third harmonic")
|
||||
pars.add_argument("--phase3", type=int, default=0, help="Phase offset of third harmonic")
|
||||
pars.add_argument("--amplitude4", type=float, default=0.0, help="Amplitude of fourth harmonic")
|
||||
pars.add_argument("--phase4", type=int, default=0, help="Phase offset of fourth harmonic")
|
||||
pars.add_argument("--amplitude5", type=float, default=0.0, help="Amplitude of fifth harmonic")
|
||||
pars.add_argument("--phase5", type=int, default=0, help="Phase offset of fifth harmonic")
|
||||
|
||||
def prepareSelectionList(self):
|
||||
self.envelopes = self.svg.selected
|
||||
pathmodifier.PathModifier.expand_clones(self, self.envelopes, True, False)
|
||||
pathmodifier.PathModifier.objects_to_paths(self, self.envelopes, True)
|
||||
|
||||
def getFunction(self, func, funcOffs):
|
||||
res = ''
|
||||
|
||||
presetAmp1 = presetAmp2 = presetAmp3 = presetAmp4 = presetAmp5 = 0.0
|
||||
presetPhOf1 = presetPhOf2 = presetPhOf3 = presetPhOf4 = presetPhOf5 = presetOffs = 0
|
||||
funcOffs *= self.options.phaseCoverage / 100.0
|
||||
|
||||
if (func == 'sin' or func == 'cos'):
|
||||
return func + '(x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + '))'
|
||||
|
||||
if func == 'env1':
|
||||
presetAmp1 = presetAmp3 = 0.495
|
||||
elif func == 'env2':
|
||||
presetAmp1 = presetAmp3 = 0.65
|
||||
presetPhOf1 = presetPhOf3 = 25
|
||||
elif func == 'env3':
|
||||
presetAmp1 = 0.75
|
||||
presetPhOf1 = 25
|
||||
presetAmp3 = 0.24
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env4':
|
||||
presetAmp1 = 1.105
|
||||
presetAmp3 = 0.27625
|
||||
presetPhOf3 = 50
|
||||
elif func == 'env5':
|
||||
presetAmp1 = 0.37464375
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.5655
|
||||
presetAmp3 = 0.37464375
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env6':
|
||||
presetAmp1 = 0.413725
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.45695
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.494
|
||||
presetPhOf3 = -25
|
||||
elif func == 'env7':
|
||||
presetAmp1 = 0.624
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.312
|
||||
presetAmp3 = 0.624
|
||||
presetPhOf3 = 25
|
||||
elif func == 'env8':
|
||||
presetAmp1 = 0.65
|
||||
presetPhOf1 = 50
|
||||
presetAmp2 = 0.585
|
||||
presetAmp3 = 0.13
|
||||
elif func == 'env9':
|
||||
presetAmp1 = 0.07605
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.33345
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.468
|
||||
presetPhOf3 = -25
|
||||
presetAmp4 = 0.32175
|
||||
elif func == 'env10':
|
||||
presetAmp1 = 0.3575
|
||||
presetPhOf1 = -25
|
||||
presetAmp2 = 0.3575
|
||||
presetAmp3 = 0.3575
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.3575
|
||||
presetPhOf4 = 50
|
||||
elif func == 'env11':
|
||||
presetAmp1 = 0.65
|
||||
presetPhOf1 = 25
|
||||
presetAmp2 = 0.13
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.26
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.39
|
||||
elif func == 'env12':
|
||||
presetAmp1 = 0.5525
|
||||
presetPhOf1 = -25
|
||||
presetAmp2 = 0.0414375
|
||||
presetPhOf2 = 50
|
||||
presetAmp3 = 0.15884375
|
||||
presetPhOf3 = 25
|
||||
presetAmp4 = 0.0966875
|
||||
presetAmp5 = 0.28315625
|
||||
presetPhOf5 = -25
|
||||
|
||||
harm1 = '(' + str(presetAmp1 + self.options.amplitude1) + ') * cos(1 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf1 + self.options.phase1) / 100.0 * 2 * pi) + '))'
|
||||
harm2 = '(' + str(presetAmp2 + self.options.amplitude2) + ') * cos(2 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf2 + self.options.phase2) / 100.0 * 2 * pi) + '))'
|
||||
harm3 = '(' + str(presetAmp3 + self.options.amplitude3) + ') * cos(3 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf3 + self.options.phase3) / 100.0 * 2 * pi) + '))'
|
||||
harm4 = '(' + str(presetAmp4 + self.options.amplitude4) + ') * cos(4 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf4 + self.options.phase4) / 100.0 * 2 * pi) + '))'
|
||||
harm5 = '(' + str(presetAmp5 + self.options.amplitude5) + ') * cos(5 * (x + (' + str((self.options.phaseOffset + funcOffs) / 100.0 * 2 * pi) + ')) - (' + str((presetPhOf5 + self.options.phase5) / 100.0 * 2 * pi) + '))'
|
||||
|
||||
res = harm1 + ' + ' + harm2 + ' + ' + harm3 + ' + ' + harm4 + ' + ' + harm5
|
||||
|
||||
return res
|
||||
|
||||
def lengthToTime(self, l):
|
||||
'''
|
||||
Recieves an arc length l, and returns the index of the segment in self.skelComp
|
||||
containing the corresponding point, together with the position of the point on this segment.
|
||||
|
||||
If the deformer is closed, do computations modulo the total length.
|
||||
'''
|
||||
if self.skelCompIsClosed:
|
||||
l = l % sum(self.lengths)
|
||||
|
||||
if l <= 0:
|
||||
return 0, l / self.lengths[0]
|
||||
|
||||
i = 0
|
||||
|
||||
while (i < len(self.lengths)) and (self.lengths[i] <= l):
|
||||
l -= self.lengths[i]
|
||||
i += 1
|
||||
|
||||
t = l / self.lengths[min(i, len(self.lengths) - 1)]
|
||||
|
||||
return (i, t)
|
||||
|
||||
def applyDiffeo(self, bpt, vects=()):
|
||||
'''
|
||||
The kernel of this stuff:
|
||||
bpt is a base point and for v in vectors, v'=v-p is a tangent vector at bpt.
|
||||
'''
|
||||
s = bpt[0] - self.skelComp[0][0]
|
||||
i, t = self.lengthToTime(s)
|
||||
|
||||
if i == len(self.skelComp) - 1:
|
||||
x, y = inkex.bezier.tpoint(self.skelComp[i - 1], self.skelComp[i], t + 1)
|
||||
dx = (self.skelComp[i][0] - self.skelComp[i - 1][0]) / self.lengths[-1]
|
||||
dy = (self.skelComp[i][1] - self.skelComp[i - 1][1]) / self.lengths[-1]
|
||||
else:
|
||||
x, y = inkex.bezier.tpoint(self.skelComp[i], self.skelComp[i + 1], t)
|
||||
dx = (self.skelComp[i + 1][0] - self.skelComp[i][0]) / self.lengths[i]
|
||||
dy = (self.skelComp[i + 1][1] - self.skelComp[i][1]) / self.lengths[i]
|
||||
|
||||
vx = 0
|
||||
vy = bpt[1] - self.skelComp[0][1]
|
||||
bpt[0] = x + vx * dx - vy * dy
|
||||
bpt[1] = y + vx * dy + vy * dx
|
||||
|
||||
for v in vects:
|
||||
vx = v[0] - self.skelComp[0][0] - s
|
||||
vy = v[1] - self.skelComp[0][1]
|
||||
v[0] = x + vx * dx - vy * dy
|
||||
v[1] = y + vx * dy + vy * dx
|
||||
|
||||
def effect(self):
|
||||
if len(self.options.ids) != 2:
|
||||
inkex.errormsg(_("This extension requires two selected paths."))
|
||||
return
|
||||
|
||||
self.prepareSelectionList()
|
||||
envs = list(self.envelopes.values())
|
||||
|
||||
s = envs[0].get('style')
|
||||
parent = envs[0].getparent()
|
||||
|
||||
# Get any path transform for the contour output
|
||||
xfm = envs[0].get('transform')
|
||||
|
||||
fstEnvComps, fstEnvLengths, sndEnvComps, sndEnvLengths, fstEnvBbox, sndEnvBbox, isCorrect = linearizeEnvelopes(envs)
|
||||
|
||||
if not isCorrect:
|
||||
inkex.errormsg(_("Selected paths are not compatible."))
|
||||
return
|
||||
|
||||
areNested = isSkeletonClosed(fstEnvComps) and isSkeletonClosed(sndEnvComps)
|
||||
|
||||
self.skelComp = None
|
||||
self.lengths = None
|
||||
|
||||
countOfSkelPaths = 1
|
||||
distBetweenFirstPts = 1
|
||||
funcSeries = self.options.series
|
||||
|
||||
isLine = True if self.options.patternFunction == 'line' else False
|
||||
|
||||
if (isLine and self.options.offset > 0):
|
||||
distBetweenFirstPts = getDistBetweenFirstPts(fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested)
|
||||
countOfSkelPaths = int(distBetweenFirstPts / self.options.offset)
|
||||
funcSeries = 1
|
||||
|
||||
for cnt in range(0, countOfSkelPaths):
|
||||
curOffset = (cnt + 1) * self.options.offset / distBetweenFirstPts
|
||||
|
||||
if areNested:
|
||||
self.skelComp, self.lengths = getClosedLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
|
||||
elif (fstEnvBbox.left == sndEnvBbox.left and fstEnvBbox.right == sndEnvBbox.right):
|
||||
self.skelComp, self.lengths = getHorizontalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
|
||||
elif (fstEnvBbox.top == sndEnvBbox.top and fstEnvBbox.bottom == sndEnvBbox.bottom):
|
||||
self.skelComp, self.lengths = getVerticalLinearizedSkeletonPath(fstEnvComps, sndEnvComps, curOffset, isLine)
|
||||
|
||||
self.skelCompIsClosed = isSkeletonClosed(self.skelComp)
|
||||
length = sum(self.lengths)
|
||||
patternWidth = length / self.options.frequency
|
||||
funcOffsetStep = 100 / funcSeries
|
||||
|
||||
resPath = ''
|
||||
|
||||
|
||||
pattern = etree.Element(inkex.addNS('path','svg'))
|
||||
|
||||
self.options.strokeHexColor, self.strokeOpacity = getColorAndOpacity(self.options.strokeColor)
|
||||
|
||||
if s:
|
||||
pattern.set('style', setColorAndOpacity(s, self.options.strokeHexColor, self.strokeOpacity))
|
||||
|
||||
if xfm:
|
||||
pattern.set('transform', xfm)
|
||||
|
||||
for j in range(funcSeries):
|
||||
selectedFunction = self.getFunction(self.options.patternFunction, j * funcOffsetStep)
|
||||
|
||||
pattern.set('d', str(paths.Path(drawfunction(self.options.nodes, patternWidth, selectedFunction))))
|
||||
|
||||
# Add path into SVG structure
|
||||
parent.append(pattern)
|
||||
|
||||
patternCubicPath = paths.CubicSuperPath(paths.Path(pattern.get('d')))
|
||||
patternPath = inkex.paths.Path(patternCubicPath)
|
||||
|
||||
# Compute bounding box
|
||||
bbox = patternPath.bounding_box()
|
||||
|
||||
width = bbox.maximum[0] - bbox.minimum[0]
|
||||
height = bbox.maximum[1] - bbox.minimum[1]
|
||||
|
||||
dx = width
|
||||
|
||||
if dx < 0.01:
|
||||
exit(_("The total length of the pattern is too small."))
|
||||
|
||||
curPath = deepcopy(patternCubicPath)
|
||||
|
||||
xoffset = self.skelComp[0][0] - bbox.minimum[0]
|
||||
yoffset = self.skelComp[0][1] - (bbox.maximum[1] + bbox.minimum[1]) / 2
|
||||
|
||||
patternCopies = max(1, int(round(length / dx)))
|
||||
width = dx * patternCopies
|
||||
|
||||
newPath = []
|
||||
|
||||
# Repeat pattern to cover whole skeleton
|
||||
for subPath in curPath:
|
||||
for i in range(0, patternCopies, 1):
|
||||
newPath.append(deepcopy(subPath))
|
||||
offset(subPath, dx, 0)
|
||||
|
||||
# Offset pattern to the first node of the skeleton
|
||||
for subPath in newPath:
|
||||
offset(subPath, xoffset, yoffset)
|
||||
|
||||
curPath = deepcopy(newPath)
|
||||
|
||||
# Stretch pattern to whole skeleton
|
||||
for subPath in curPath:
|
||||
stretch(subPath, length / width, 1, self.skelComp[0])
|
||||
|
||||
for subPath in curPath:
|
||||
for ctlpt in subPath:
|
||||
self.applyDiffeo(ctlpt[1], (ctlpt[0], ctlpt[2]))
|
||||
|
||||
# Check if there is a need to close path manually
|
||||
if self.skelCompIsClosed:
|
||||
firstPtX = round(curPath[0][0][1][0], 8)
|
||||
firstPtY = round(curPath[0][0][1][1], 8)
|
||||
finalPtX = round(curPath[-1][-1][1][0], 8)
|
||||
finalPtY = round(curPath[-1][-1][1][1], 8)
|
||||
|
||||
if (firstPtX != finalPtX or firstPtY != finalPtY):
|
||||
curPath[-1].append(curPath[0][0])
|
||||
|
||||
curPathComps, curPathLengths = linearize(modifySkeletonPath(curPath))
|
||||
|
||||
if not isLine:
|
||||
curPathComps = stretchComps(self.skelComp, curPathComps, fstEnvComps, sndEnvComps, fstEnvBbox, sndEnvBbox, areNested, height / 2, self.options.amplitude / 100.0, self.options.offset)
|
||||
|
||||
resPath += compsToSVGd(curPathComps)
|
||||
|
||||
pattern.set('d', resPath)
|
||||
|
||||
if self.options.remove:
|
||||
parent.remove(envs[0])
|
||||
parent.remove(envs[1])
|
||||
|
||||
if __name__ == '__main__':
|
||||
GuillochePattern().run()
|
22
extensions/fablabchemnitz/guilloche_creations/meta.json
Normal file
22
extensions/fablabchemnitz/guilloche_creations/meta.json
Normal file
@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "Guilloche <various>",
|
||||
"id": "fablabchemnitz.de.guilloche_creations.guilloche_contour",
|
||||
"path": "guilloche_contour",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "Guilloche <various>",
|
||||
"original_id": "org.inkscape.effect.guilloche_<various>",
|
||||
"license": "GNU GPL v3",
|
||||
"license_url": "https://inkscape.org/de/~DrWiggly/%E2%98%85guillocheextensions-for-v1x",
|
||||
"comment": "fork of https://inkscape.org/de/~fluent_user/%E2%98%85guilloche-pattern-extension",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/guilloche_creations",
|
||||
"fork_url": "https://inkscape.org/de/~DrWiggly/%E2%98%85guillocheextensions-for-v1x",
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Guilloche+Pattern",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"inkscape.org/fluent_user",
|
||||
"inkscape.org/DrWiggly",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
@ -3,14 +3,20 @@
|
||||
<name>Quick Joint</name>
|
||||
<id>fablabchemnitz.de.quick_joint</id>
|
||||
<label xml:space="preserve">Adding box joint tabs or slots to selected object!</label>
|
||||
<label xml:space="preserve">Version 0.3</label>
|
||||
<label xml:space="preserve">Version 0.5</label>
|
||||
<param name="activetab" type="notebook">
|
||||
<page name="tabpage" gui-text="Tabs">
|
||||
<param name="side" type="int" min="0" max="512" gui-text="Side:">0</param>
|
||||
<param name="numtabs" type="int" min="1" max="512" gui-text="Number of tabs:">1</param>
|
||||
<param name="tSlotNutHeight" type="float" min="0.01" max="99999.00" precision="2" gui-text="t-slot nut height:">1.80</param>
|
||||
<param name="tSlotNutWidth" type="float" min="0.01" max="99999.00" precision="2" gui-text="t-slot nut width:">5.50</param>
|
||||
<param name="tSlotScrewWidth" type="float" min="0.01" max="99999.00" precision="2" gui-text="t-slot screw width:">3.10</param>
|
||||
<param name="tSlotScrewDepth" type="float" min="0.01" max="99999.00" precision="2" gui-text="t-slot screw depth:">10.00</param>
|
||||
|
||||
</page>
|
||||
<page name="slotpage" gui-text="Slots">
|
||||
<param name="numslots" type="int" min="1" max="512" gui-text="Number of slots:">1</param>
|
||||
<param name="tSlotHoleDiameter" type="float" min="0.01" max="99999.00" precision="2" gui-text="t-slot hole diameter:">3.00</param>
|
||||
</page>
|
||||
</param>
|
||||
<param name="thickness" type="float" min="0.0" max="1000.0" precision="3" gui-text="Material thickness:">3.0</param>
|
||||
@ -22,8 +28,10 @@
|
||||
<option value="in">in</option>
|
||||
<option value="cm">cm</option>
|
||||
</param>
|
||||
<param name="edgefeatures" type="bool" gui-text="Features on edges">false</param>
|
||||
<param name="featureStart" type="bool" gui-text="Feature at start">false</param>
|
||||
<param name="featureEnd" type="bool" gui-text="Feature at end">false</param>
|
||||
<param name="flipside" type="bool" gui-text="Flip side">false</param>
|
||||
<param name="tSlotEnable" type="bool" gui-text="Use t-slot definitions">false</param>
|
||||
<effect needs-live-preview="true">
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
|
@ -22,11 +22,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
'''
|
||||
import inkex, cmath
|
||||
from inkex.paths import Path, ZoneClose, Move
|
||||
import inkex, cmath, math
|
||||
from inkex import Circle
|
||||
from inkex.paths import Path, ZoneClose, Move, Line, line, Curve
|
||||
from lxml import etree
|
||||
|
||||
debugEn = False
|
||||
debugEn = False
|
||||
def debugMsg(input):
|
||||
if debugEn:
|
||||
inkex.utils.debug(input)
|
||||
@ -40,21 +41,46 @@ def linesNumber(path):
|
||||
debugMsg('Number of lines : ' + str(retval))
|
||||
return retval
|
||||
|
||||
def to_complex(point):
|
||||
c = None
|
||||
try:
|
||||
c = complex(point.x, point.y)
|
||||
except Exception as e:
|
||||
pass
|
||||
if c is not None:
|
||||
return c
|
||||
class QuickJointPath (Path):
|
||||
def Move(self, point):
|
||||
'''Append an absolute move instruction to the path, to the specified complex point'''
|
||||
debugMsg("- move: " + str(point))
|
||||
self.append(Move(point.real, point.imag))
|
||||
|
||||
def Line(self, point):
|
||||
'''Add an absolute line instruction to the path, to the specified complex point'''
|
||||
debugMsg("- line: " + str(point))
|
||||
self.append(Line(point.real, point.imag))
|
||||
|
||||
def close(self):
|
||||
'''Add a Close Path instriction to the path'''
|
||||
self.append(ZoneClose())
|
||||
|
||||
def line(self, vector):
|
||||
'''Append a relative line command to the path, using the specified vector'''
|
||||
self.append(line(vector.real, vector.imag))
|
||||
|
||||
def get_line(self, n):
|
||||
'''Return the end points of the nth line in the path as complex numbers, as well as whether that line closes the path.'''
|
||||
|
||||
if isinstance(self[n], (Move, Line, ZoneClose)):
|
||||
start = complex(self[n].x, self[n].y)
|
||||
elif isinstance(self[n], Curve):
|
||||
start = complex(self[n].x4, self[n].y4)
|
||||
# If the next point in the path closes the path, go back to the start.
|
||||
end = None
|
||||
closePath = False
|
||||
if isinstance(self[n+1], ZoneClose):
|
||||
end = complex(self[0].x, self[0].y)
|
||||
closePath = True
|
||||
else:
|
||||
inkex.utils.debug("The selection seems not be be a usable polypath. QuickJoint does not operate on curves. Try to flatten bezier curves or splitting up the path.")
|
||||
exit(1)
|
||||
|
||||
if isinstance(self[n+1], (Move, Line, ZoneClose)):
|
||||
end = complex(self[n+1].x, self[n+1].y)
|
||||
elif isinstance(self[n+1], Curve):
|
||||
end = complex(self[n+1].x4, self[n+1].y4)
|
||||
return (start, end, closePath)
|
||||
|
||||
class QuickJoint(inkex.EffectExtension):
|
||||
|
||||
def add_arguments(self, pars):
|
||||
pars.add_argument('-s', '--side', type=int, default=0, help='Object face to tabify')
|
||||
pars.add_argument('-n', '--numtabs', type=int, default=1, help='Number of tabs to add')
|
||||
@ -62,19 +88,17 @@ class QuickJoint(inkex.EffectExtension):
|
||||
pars.add_argument('-t', '--thickness', type=float, default=3.0, help='Material thickness')
|
||||
pars.add_argument('-k', '--kerf', type=float, default=0.14, help='Measured kerf of cutter')
|
||||
pars.add_argument('-u', '--units', default='mm', help='Measurement units')
|
||||
pars.add_argument('-e', '--edgefeatures', type=inkex.Boolean, default=False, help='Allow tabs to go right to edges')
|
||||
pars.add_argument('-f', '--flipside', type=inkex.Boolean, default=False, help='Flip side of lines that tabs are drawn onto')
|
||||
pars.add_argument('-a', '--activetab', default='', help='Tab or slot menus')
|
||||
pars.add_argument('-S', '--featureStart', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the start edge')
|
||||
pars.add_argument('-E', '--featureEnd', type=inkex.Boolean, default=False, help='Tab/slot instead of space on the end edge')
|
||||
pars.add_argument('-T', '--tSlotEnable', type=inkex.Boolean, default=False, help='Enable to use t-slot definitions')
|
||||
pars.add_argument('-D', '--tSlotHoleDiameter', type=float, default=3.00, help='Diameter of t slot hole')
|
||||
pars.add_argument('-H', '--tSlotNutHeight', type=float, default=1.80, help='Height of t slot nut')
|
||||
pars.add_argument('-W', '--tSlotNutWidth', type=float, default=5.50, help='Width of t slot nut')
|
||||
pars.add_argument('-N', '--tSlotScrewWidth', type=float, default=3.10, help='Scew width of t slot')
|
||||
pars.add_argument('-d', '--tSlotScrewDepth', type=float, default=10.00, help='Screw depth of t slot')
|
||||
|
||||
def to_complex(self, command, line):
|
||||
debugMsg('To complex: ' + command + ' ' + str(line))
|
||||
|
||||
return complex(line[0], line[1])
|
||||
|
||||
def get_length(self, line):
|
||||
polR, polPhi = cmath.polar(line)
|
||||
return polR
|
||||
|
||||
def draw_parallel(self, start, guideLine, stepDistance):
|
||||
polR, polPhi = cmath.polar(guideLine)
|
||||
polR = stepDistance
|
||||
@ -91,192 +115,172 @@ class QuickJoint(inkex.EffectExtension):
|
||||
debugMsg(polPhi)
|
||||
debugMsg(cmath.rect(polR, polPhi))
|
||||
return (cmath.rect(polR, polPhi) + start)
|
||||
|
||||
|
||||
def draw_box(self, start, guideLine, xDistance, yDistance, kerf):
|
||||
polR, polPhi = cmath.polar(guideLine)
|
||||
def draw_box(self, start, lengthVector, height, kerf):
|
||||
|
||||
# Kerf is a provided as a positive kerf width. Although tabs
|
||||
# need to be made larger by the width of the kerf, slots need
|
||||
# to be made narrower instead, since the cut widens them.
|
||||
|
||||
# Calculate kerfed height and length vectors
|
||||
heightEdge = self.draw_perpendicular(0, lengthVector, height - kerf, self.flipside)
|
||||
lengthEdge = self.draw_parallel(lengthVector, lengthVector, -kerf)
|
||||
|
||||
#Kerf expansion
|
||||
if self.flipside:
|
||||
start -= cmath.rect(kerf / 2, polPhi)
|
||||
start -= cmath.rect(kerf / 2, polPhi + (cmath.pi / 2))
|
||||
else:
|
||||
start -= cmath.rect(kerf / 2, polPhi)
|
||||
start -= cmath.rect(kerf / 2, polPhi - (cmath.pi / 2))
|
||||
|
||||
lines = []
|
||||
lines.append(['M', [start.real, start.imag]])
|
||||
debugMsg("draw_box; lengthEdge: " + str(lengthEdge) + ", heightEdge: " + str(heightEdge))
|
||||
|
||||
#Horizontal
|
||||
polR = xDistance
|
||||
move = cmath.rect(polR + kerf, polPhi) + start
|
||||
lines.append(['L', [move.real, move.imag]])
|
||||
start = move
|
||||
cursor = self.draw_parallel(start, lengthEdge, kerf/2)
|
||||
cursor = self.draw_parallel(cursor, heightEdge, kerf/2)
|
||||
|
||||
#Vertical
|
||||
polR = yDistance
|
||||
if self.flipside:
|
||||
polPhi += (cmath.pi / 2)
|
||||
else:
|
||||
polPhi -= (cmath.pi / 2)
|
||||
move = cmath.rect(polR + kerf, polPhi) + start
|
||||
lines.append(['L', [move.real, move.imag]])
|
||||
start = move
|
||||
path = QuickJointPath()
|
||||
path.Move(cursor)
|
||||
|
||||
#Horizontal
|
||||
polR = xDistance
|
||||
if self.flipside:
|
||||
polPhi += (cmath.pi / 2)
|
||||
else:
|
||||
polPhi -= (cmath.pi / 2)
|
||||
move = cmath.rect(polR + kerf, polPhi) + start
|
||||
lines.append(['L', [move.real, move.imag]])
|
||||
start = move
|
||||
cursor += lengthEdge
|
||||
path.Line(cursor)
|
||||
|
||||
lines.append(['Z', []])
|
||||
cursor += heightEdge
|
||||
path.Line(cursor)
|
||||
|
||||
return lines
|
||||
|
||||
cursor -= lengthEdge
|
||||
path.Line(cursor)
|
||||
|
||||
cursor -= heightEdge
|
||||
path.Line(cursor)
|
||||
|
||||
path.close()
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def draw_tabs(self, path, line):
|
||||
#Male tab creation
|
||||
start = to_complex(path[line])
|
||||
|
||||
closePath = False
|
||||
#Line is between last and first (closed) nodes
|
||||
end = None
|
||||
if isinstance(path[line+1], ZoneClose):
|
||||
end = to_complex(path[0])
|
||||
closePath = True
|
||||
else:
|
||||
end = to_complex(path[line+1])
|
||||
|
||||
debugMsg('start')
|
||||
debugMsg(start)
|
||||
debugMsg('end')
|
||||
debugMsg(end)
|
||||
|
||||
debugMsg('5-')
|
||||
|
||||
if self.edgefeatures:
|
||||
segCount = (self.numtabs * 2) - 1
|
||||
drawValley = False
|
||||
else:
|
||||
segCount = (self.numtabs * 2)
|
||||
drawValley = False
|
||||
|
||||
distance = end - start
|
||||
debugMsg('distance ' + str(distance))
|
||||
debugMsg('segCount ' + str(segCount))
|
||||
cursor, segCount, segment, closePath = self.get_segments(path, line, self.numtabs)
|
||||
|
||||
try:
|
||||
if self.edgefeatures:
|
||||
segLength = self.get_length(distance) / segCount
|
||||
else:
|
||||
segLength = self.get_length(distance) / (segCount + 1)
|
||||
except:
|
||||
debugMsg('in except')
|
||||
segLength = self.get_length(distance)
|
||||
|
||||
debugMsg('segLength - ' + str(segLength))
|
||||
newLines = []
|
||||
|
||||
# when handling firlt line need to set M back
|
||||
# Calculate kerf-compensated vectors for the parallel portion of tab and space
|
||||
tabLine = self.draw_parallel(segment, segment, self.kerf)
|
||||
spaceLine = self.draw_parallel(segment, segment, -self.kerf)
|
||||
endspaceLine = segment
|
||||
|
||||
# Calculate vectors for tabOut and tabIn: perpendicular away and towards baseline
|
||||
tabOut = self.draw_perpendicular(0, segment, self.thickness, not self.flipside)
|
||||
tabIn = self.draw_perpendicular(0, segment, self.thickness, self.flipside)
|
||||
|
||||
debugMsg("draw_tabs; tabLine=" + str(tabLine) + " spaceLine=" + str(spaceLine) + " segment=" + str(segment))
|
||||
|
||||
drawTab = self.featureStart
|
||||
newLines = QuickJointPath()
|
||||
|
||||
# First line is a move or line to our start point
|
||||
if isinstance(path[line], Move):
|
||||
newLines.append(['M', [start.real, start.imag]])
|
||||
|
||||
if self.edgefeatures == False:
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
start = self.draw_parallel(start, distance, segLength)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('Initial - ' + str(start))
|
||||
newLines.Move(cursor)
|
||||
else:
|
||||
newLines.Line(cursor)
|
||||
|
||||
|
||||
for i in range(segCount):
|
||||
if drawValley == True:
|
||||
#Vertical
|
||||
start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('ValleyV - ' + str(start))
|
||||
drawValley = False
|
||||
#Horizontal
|
||||
start = self.draw_parallel(start, distance, segLength)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('ValleyH - ' + str(start))
|
||||
debugMsg("i = " + str(i))
|
||||
if drawTab == True:
|
||||
debugMsg("- tab")
|
||||
if self.options.tSlotEnable is False:
|
||||
newLines.line(tabOut)
|
||||
newLines.line(tabLine)
|
||||
newLines.line(tabIn)
|
||||
else: #TODO
|
||||
#self.options.tSlotNutHeight
|
||||
#self.options.tSlotNutWidth
|
||||
#self.options.tSlotScrewWidth
|
||||
#self.options.tSlotScrewDepth
|
||||
newLines.line(tabOut)
|
||||
newLines.line(tabLine)
|
||||
newLines.line(tabIn)
|
||||
else:
|
||||
#Vertical
|
||||
start = self.draw_perpendicular(start, distance, self.thickness, not self.flipside)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('HillV - ' + str(start))
|
||||
drawValley = True
|
||||
#Horizontal
|
||||
start = self.draw_parallel(start, distance, segLength)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('HillH - ' + str(start))
|
||||
|
||||
if self.edgefeatures == True:
|
||||
start = self.draw_perpendicular(start, distance, self.thickness, self.flipside)
|
||||
newLines.append(['L', [start.real, start.imag]])
|
||||
debugMsg('Final - ' + str(start))
|
||||
|
||||
if i == 0 or i == segCount - 1:
|
||||
debugMsg("- endspace")
|
||||
newLines.line(endspaceLine)
|
||||
else:
|
||||
debugMsg("- space")
|
||||
newLines.line(spaceLine)
|
||||
drawTab = not drawTab
|
||||
|
||||
if closePath:
|
||||
newLines.append(['Z', []])
|
||||
newLines.close
|
||||
return newLines
|
||||
|
||||
|
||||
def add_new_path_from_lines(self, lines, line_style):
|
||||
slot_id = self.svg.get_unique_id('slot')
|
||||
g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
|
||||
|
||||
line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(lines)) }
|
||||
return etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
|
||||
|
||||
def get_segments(self, path, line, num):
|
||||
|
||||
# Calculate number of segments, including all features and spaces
|
||||
segCount = num * 2 - 1
|
||||
if not self.featureStart: segCount = segCount + 1
|
||||
if not self.featureEnd: segCount = segCount + 1
|
||||
|
||||
start, end, closePath = QuickJointPath(path).get_line(line)
|
||||
|
||||
# Calculate the length of each feature prior to kerf compensation.
|
||||
# Here we divide the specified edge into equal portions, one for each feature or space.
|
||||
|
||||
# Because the specified edge has no kerf compensation, the
|
||||
# actual length we end up with will be smaller by a kerf. We
|
||||
# need to use that distance to calculate our segment vector.
|
||||
edge = end - start
|
||||
edge = self.draw_parallel(edge, edge, -self.kerf)
|
||||
segVector = edge / segCount
|
||||
|
||||
debugMsg("get_segments; start=" + str(start) + " end=" + str(end) + " edge=" + str(edge) + " segCount=" + str(segCount) + " segVector=" + str(segVector))
|
||||
|
||||
return (start, segCount, segVector, closePath)
|
||||
|
||||
def draw_slots(self, path):
|
||||
#Female slot creation
|
||||
# Female slot creation
|
||||
|
||||
start = to_complex(path[0])
|
||||
end = to_complex(path[1])
|
||||
cursor, segCount, segVector, closePath = self.get_segments(path, 0, self.numslots)
|
||||
|
||||
if self.edgefeatures:
|
||||
segCount = (self.numslots * 2) - 1
|
||||
else:
|
||||
segCount = (self.numslots * 2)
|
||||
# I'm having a really hard time wording why this is necessary, but it is.
|
||||
# get_segments returns a vector based on a narrower edge; adjust that edge to fit within the edge we were given.
|
||||
cursor = self.draw_parallel(cursor, segVector, self.kerf/2)
|
||||
|
||||
distance = end - start
|
||||
debugMsg('distance ' + str(distance))
|
||||
debugMsg('segCount ' + str(segCount))
|
||||
|
||||
try:
|
||||
if self.edgefeatures:
|
||||
segLength = self.get_length(distance) / segCount
|
||||
else:
|
||||
segLength = self.get_length(distance) / (segCount + 1)
|
||||
except:
|
||||
segLength = self.get_length(distance)
|
||||
|
||||
debugMsg('segLength - ' + str(segLength))
|
||||
newLines = []
|
||||
|
||||
line_style = str(inkex.Style({ 'stroke': '#000000', 'fill': 'none', 'stroke-width': str(self.svg.unittouu('0.1mm')) }))
|
||||
|
||||
drawSlot = self.featureStart
|
||||
|
||||
for i in range(segCount):
|
||||
if (self.edgefeatures and (i % 2) == 0) or (not self.edgefeatures and (i % 2)):
|
||||
newLines = self.draw_box(start, distance, segLength, self.thickness, self.kerf)
|
||||
debugMsg(newLines)
|
||||
if drawSlot:
|
||||
slot = self.add_new_path_from_lines(self.draw_box(cursor, segVector, self.thickness, self.kerf), line_style)
|
||||
if self.options.tSlotEnable is True:
|
||||
cx, cy = slot.bounding_box().center
|
||||
circle = slot.getparent().add(inkex.Circle(id=self.svg.get_unique_id('tSlotHole')))
|
||||
circle.set('transform', "rotate({:0.6f} {:0.6f} {:0.6f})".format(0, cx, cy))
|
||||
circle.set('r', "{:0.6f}".format(self.tSlotHoleDiameter / 2 - self.kerf))
|
||||
circle.set('cx', "{:0.6f}".format(cx))
|
||||
circle.set('cy', "{:0.6f}".format(cy))
|
||||
circle.style = line_style
|
||||
|
||||
slot_id = self.svg.get_unique_id('slot')
|
||||
g = etree.SubElement(self.svg.get_current_layer(), 'g', {'id':slot_id})
|
||||
|
||||
line_atts = { 'style':line_style, 'id':slot_id+'-inner-close-tab', 'd':str(Path(newLines)) }
|
||||
etree.SubElement(g, inkex.addNS('path','svg'), line_atts )
|
||||
|
||||
#Find next point
|
||||
polR, polPhi = cmath.polar(distance)
|
||||
polR = segLength
|
||||
start = cmath.rect(polR, polPhi) + start
|
||||
|
||||
cursor = cursor + segVector
|
||||
drawSlot = not drawSlot
|
||||
debugMsg("i: " + str(i) + ", cursor: " + str(cursor))
|
||||
# (We don't modify the path so we don't need to close it)
|
||||
|
||||
def effect(self):
|
||||
self.side = self.options.side
|
||||
self.numtabs = self.options.numtabs
|
||||
self.numslots = self.options.numslots
|
||||
self.thickness = self.svg.unittouu(str(self.options.thickness) + self.options.units)
|
||||
self.tSlotNutHeight = self.svg.unittouu(str(self.options.tSlotNutHeight) + self.options.units)
|
||||
self.tSlotNutWidth = self.svg.unittouu(str(self.options.tSlotNutWidth) + self.options.units)
|
||||
self.tSlotScrewWidth = self.svg.unittouu(str(self.options.tSlotScrewWidth) + self.options.units)
|
||||
self.tSlotScrewDepth = self.svg.unittouu(str(self.options.tSlotScrewDepth) + self.options.units)
|
||||
self.tSlotHoleDiameter = self.svg.unittouu(str(self.options.tSlotHoleDiameter) + self.options.units)
|
||||
self.kerf = self.svg.unittouu(str(self.options.kerf) + self.options.units)
|
||||
self.units = self.options.units
|
||||
self.edgefeatures = self.options.edgefeatures
|
||||
self.featureStart = self.options.featureStart
|
||||
self.featureEnd = self.options.featureEnd
|
||||
self.flipside = self.options.flipside
|
||||
self.activetab = self.options.activetab
|
||||
|
||||
|
||||
for id, node in self.svg.selected.items():
|
||||
debugMsg(node)
|
||||
debugMsg('1')
|
||||
@ -298,13 +302,13 @@ class QuickJoint(inkex.EffectExtension):
|
||||
debugMsg(newPath)
|
||||
debugMsg('4')
|
||||
debugMsg( p[lineNum + 1:])
|
||||
finalPath = p[:lineNum] + newPath + p[lineNum + 1:]
|
||||
|
||||
finalPath = p[:lineNum + 1] + newPath + p[lineNum + 2:]
|
||||
|
||||
debugMsg(finalPath)
|
||||
|
||||
node.set('d',str(Path(finalPath)))
|
||||
elif self.activetab == 'slotpage':
|
||||
newPath = self.draw_slots(p)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
QuickJoint().run()
|
21
extensions/fablabchemnitz/visicut/meta.json
Normal file
21
extensions/fablabchemnitz/visicut/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "<various>",
|
||||
"id": "fablabchemnitz.de.open_in_visicut_<various>",
|
||||
"path": "visicut",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "<various>",
|
||||
"original_id": "visicut.<various>",
|
||||
"license": "GNU LGPL v3",
|
||||
"license_url": "https://github.com/t-oster/VisiCut/blob/master/LICENSE",
|
||||
"comment": "",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.X/src/branch/master/extensions/fablabchemnitz/visicut",
|
||||
"fork_url": "https://github.com/t-oster/VisiCut/tree/master/tools/inkscape_extension",
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/Open+in+VisiCut",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/t-oster",
|
||||
"github.com/vmario89"
|
||||
]
|
||||
}
|
||||
]
|
357
extensions/fablabchemnitz/visicut/open_in_visicut.py
Normal file
357
extensions/fablabchemnitz/visicut/open_in_visicut.py
Normal file
@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env python3
|
||||
'''
|
||||
This extension strips everything which is not selected from
|
||||
the current svg, saves it and
|
||||
calls VisiCut on it.
|
||||
|
||||
Copyright (C) 2012 Thomas Oster, thomas.oster@rwth-aachen.de
|
||||
Copyright (C) 2014-2022 Max Gaukler, development@maxgaukler.de
|
||||
|
||||
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
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from subprocess import Popen
|
||||
import traceback
|
||||
import tempfile
|
||||
import unicodedata
|
||||
import codecs
|
||||
import random
|
||||
import string
|
||||
import socket
|
||||
import inkex
|
||||
|
||||
try:
|
||||
from os import fsencode
|
||||
except ImportError:
|
||||
fsencode = lambda x: x.encode(sys.getfilesystemencoding())
|
||||
|
||||
def get_single_instance_port():
|
||||
"""
|
||||
get the single instance port used by VisiCut.
|
||||
CAUTION: This code must behave exactly the same as ApplicationInstanceManager.getSingleInstancePort() in Visicut.
|
||||
"""
|
||||
port = 6543
|
||||
if (sys.platform == "linux"):
|
||||
d = os.environ.get("DISPLAY")
|
||||
if (d != None):
|
||||
d = d.split(':')[1].split('.')[0]
|
||||
port += int(d)
|
||||
|
||||
if (sys.platform == "win32"):
|
||||
d = os.environ.get("SESSIONNAME")
|
||||
if d == None:
|
||||
# no Terminal Services installed
|
||||
pass
|
||||
else:
|
||||
# get session ID
|
||||
try:
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
id = subprocess.check_output("powershell -Command (get-process -pid $pid).sessionid",creationflags=CREATE_NO_WINDOW)
|
||||
id = int(id.strip())
|
||||
port += 2 + id
|
||||
except:
|
||||
inkex.utils.debug("Warning: Cannot determine session ID. please report this.\n")
|
||||
return port
|
||||
|
||||
# if Visicut or Inkscape cannot be found, change these lines here to VISICUTDIR="C:/Programs/Visicut" or wherever you installed it.
|
||||
# please use forward slashes (/), not backslashes (\).
|
||||
#
|
||||
# example:
|
||||
# VISICUTDIR="C:/Program Files (x86)/VisiCut/"
|
||||
# INKSCAPEDIR="C:/Program Files (x86)/Inkscape/"
|
||||
VISICUTDIR = ""
|
||||
INKSCAPEDIR = ""
|
||||
|
||||
# whether to add (true) or replace (false) current visicut's content
|
||||
IMPORT = True
|
||||
# Store the IDs of selected Elements
|
||||
elements = []
|
||||
|
||||
arguments=[]
|
||||
|
||||
for arg in sys.argv[1:]:
|
||||
if arg[0] == "-":
|
||||
if len(arg) >= 5 and arg[0:5] == "--id=":
|
||||
elements += [arg[5:]]
|
||||
elif len(arg) >= 13 and arg[0:13] == "--visicutbin=":
|
||||
# unused
|
||||
pass
|
||||
# VISICUTBIN=arg[13:]
|
||||
elif len(arg) >= 9 and arg[0:9] == "--import=":
|
||||
IMPORT = "true" in arg[9:]
|
||||
else:
|
||||
arguments += [arg]
|
||||
else:
|
||||
filename = arg
|
||||
|
||||
if IMPORT:
|
||||
# do not replace old
|
||||
arguments += ["--add"]
|
||||
|
||||
# find executable in the PATH
|
||||
def which(program, extraPaths=[]):
|
||||
pathlist = extraPaths + os.environ["PATH"].split(os.pathsep) + [""]
|
||||
if "nt" in os.name: # Windows
|
||||
if not program.lower().endswith(".exe"):
|
||||
program += ".exe"
|
||||
programfiles = os.environ.get("ProgramFiles", "C:\\Program Files\\")
|
||||
programfiles86 = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)\\")
|
||||
# also look in %ProgramFiles%/yourProgram/yourProgram.exe
|
||||
pathlist += [programfiles + "\\" + program + "\\", programfiles86 + "\\" + program + "\\"]
|
||||
|
||||
def is_exe(fpath):
|
||||
return os.path.isfile(fpath) and (os.access(fpath, os.X_OK) or fpath.endswith(".exe"))
|
||||
for path in pathlist:
|
||||
exe_file = os.path.join(path, program)
|
||||
if is_exe(exe_file):
|
||||
return exe_file
|
||||
raise Exception("Cannot find executable {0} in PATH={1}.\n\n"
|
||||
"Please report this bug on https://github.com/t-oster/VisiCut/issues\n\n"
|
||||
"For a quick fix: Set VISICUTDIR and INKSCAPEDIR in "
|
||||
"{2}"
|
||||
.format(repr(program), repr(pathlist), os.path.realpath(__file__)))
|
||||
|
||||
|
||||
def inkscape_version():
|
||||
"""Return Inkscape version number as float, e.g. version "0.92.4" --> return: float 0.92"""
|
||||
version = subprocess.check_output([INKSCAPEBIN, "--version"], stderr=subprocess.DEVNULL).decode('ASCII', 'ignore')
|
||||
assert version.startswith("Inkscape ")
|
||||
match = re.match("Inkscape ([0-9]+\.[0-9]+).*", version)
|
||||
assert match is not None
|
||||
version_float = float(match.group(1))
|
||||
return version_float
|
||||
|
||||
|
||||
|
||||
# Strip SVG to only contain selected elements, convert objects to paths, unlink clones
|
||||
# Inkscape version: takes care of special cases where the selected objects depend on non-selected ones.
|
||||
# Examples are linked clones, flowtext limited to a shape and linked flowtext boxes (overflow into the next box).
|
||||
#
|
||||
# Inkscape is called with certain "verbs" (gui actions) to do the required cleanup
|
||||
# The idea is similar to http://bazaar.launchpad.net/~nikitakit/inkscape/svg2sif/view/head:/share/extensions/synfig_prepare.py#L181 , but more primitive - there is no need for more complicated preprocessing here
|
||||
def stripSVG_inkscape(src, dest, elements):
|
||||
version = inkscape_version()
|
||||
|
||||
# create temporary file for opening with inkscape.
|
||||
# delete this file later so that it will disappear from the "recently opened" list.
|
||||
tmpfile = tempfile.NamedTemporaryFile(delete=False, prefix='temp-visicut-', suffix='.svg')
|
||||
tmpfile.close()
|
||||
tmpfile = tmpfile.name
|
||||
import shutil
|
||||
shutil.copyfile(src, tmpfile)
|
||||
|
||||
|
||||
if version < 1:
|
||||
# inkscape 0.92 long-term-support release. Will be in Linux distributions until 2025 or so
|
||||
# Selection commands: select items, invert selection, delete
|
||||
selection = []
|
||||
for el in elements:
|
||||
selection += ["--select=" + el]
|
||||
|
||||
if len(elements) > 0:
|
||||
# selection += ["--verb=FitCanvasToSelection"] # TODO add a user configuration option whether to keep the page size (and by this the position relative to the page)
|
||||
selection += ["--verb=EditInvertInAllLayers", "--verb=EditDelete"]
|
||||
|
||||
|
||||
hidegui = ["--without-gui"]
|
||||
|
||||
# currently this only works with gui because of a bug in inkscape: https://bugs.launchpad.net/inkscape/+bug/843260
|
||||
hidegui = []
|
||||
|
||||
command = [INKSCAPEBIN] + hidegui + [tmpfile, "--verb=UnlockAllInAllLayers", "--verb=UnhideAllInAllLayers"] + selection + ["--verb=EditSelectAllInAllLayers", "--verb=EditUnlinkClone", "--verb=ObjectToPath", "--verb=FileSave", "--verb=FileQuit"]
|
||||
elif version < 1.2:
|
||||
# Inkscape 1.0 (released ca 2020) or 1.1
|
||||
# inkscape --select=... --verbs=...
|
||||
# (see inkscape --help, inkscape --verb-list)
|
||||
command = [INKSCAPEBIN, tmpfile, "--batch-process"]
|
||||
verbs = ["ObjectToPath", "UnlockAllInAllLayers"]
|
||||
if elements: # something is selected
|
||||
# --select=object1,object2,object3,...
|
||||
command += ["--select=" + ",".join(elements)]
|
||||
else:
|
||||
verbs += ["EditSelectAllInAllLayers"]
|
||||
verbs += ["UnhideAllInAllLayers", "EditInvertInAllLayers", "EditDelete", "EditSelectAllInAllLayers", "EditUnlinkClone", "ObjectToPath", "FileSave"]
|
||||
# --verb=action1;action2;...
|
||||
command += ["--verb=" + ";".join(verbs)]
|
||||
|
||||
|
||||
DEBUG = False
|
||||
if DEBUG:
|
||||
# Inkscape sometimes silently ignores wrong verbs, so we need to double-check that everything's right
|
||||
for verb in verbs:
|
||||
verb_list = [line.split(":")[0] for line in subprocess.check_output([INKSCAPEBIN, "--verb-list"], stderr=subprocess.DEVNULL).split("\n")]
|
||||
if verb not in verb_list:
|
||||
inkex.utils.debug("Inkscape does not have the verb '{}'. Please report this as a VisiCut bug.".format(verb))
|
||||
else:
|
||||
# Inkscape 1.2 (released 2022)
|
||||
# inkscape --export-overwrite --actions=action1;action2...
|
||||
# (see inkscape --help, inkscape --action-list)
|
||||
# (for debugging, you can look at the intermediate state by running inkscape --with-gui --actions=... my_filename.svg)
|
||||
# Note that it is (almost?) impossible to find a sequence that works in all cases.
|
||||
# Cases to consider:
|
||||
# - selecting whole groups
|
||||
# - selecting objects within a group
|
||||
# - selecting across groups/layers (e.g., enter group, select something, then Shift-click to select things from other layers)
|
||||
# Difficulties with Inkscape:
|
||||
# - "invert selection" does not behave as expected in all these cases,
|
||||
# for example if a group is selected then inverting can select the elements within.
|
||||
# - Inkscape has a wonderful --export-id commandline switch, but it only works correctly with one ID
|
||||
|
||||
# Solution:
|
||||
actions = ["unlock-all"]
|
||||
if elements:
|
||||
# something was selected when calling the plugin.
|
||||
# -> Recreate that selection
|
||||
# - select objects
|
||||
actions += ["select-by-id:" + ",".join(elements)]
|
||||
else:
|
||||
# - select all
|
||||
actions += ["select-all:all"]
|
||||
# - convert to path
|
||||
actions += ["clone-unlink", "object-to-path"]
|
||||
if elements:
|
||||
# ensure that only the selection is exported:
|
||||
# - create group of selection
|
||||
actions += ["selection-group"]
|
||||
# - set group ID to a known value. Use a pseudo-random value to avoid collisions
|
||||
target_group_id = "TARGET-GROUP-" + "".join(random.sample(string.ascii_lowercase, 20))
|
||||
actions += ["object-set-attribute:id," + target_group_id]
|
||||
# - set export options: use only the target group ID, nothing else
|
||||
actions += ["export-id-only:true", "export-id:" + target_group_id]
|
||||
# - do export (keep position on page)
|
||||
actions += ["export-area-page"]
|
||||
|
||||
command = [INKSCAPEBIN, tmpfile, "--export-overwrite", "--actions=" + ";".join(actions)]
|
||||
|
||||
try:
|
||||
#sys.stderr.write(" ".join(command))
|
||||
# run inkscape, buffer output
|
||||
subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
inkex.utils.debug("Error: cleaning the document with inkscape failed. Something might still be shown in visicut, but it could be incorrect.\nInkscape's output was:\n" + e.output)
|
||||
|
||||
# move output to the intended destination filename
|
||||
os.rename(tmpfile, dest)
|
||||
|
||||
|
||||
"""
|
||||
Get document name (original filename) from Inkscape SVG
|
||||
|
||||
Inkscape saves the file to a random temporary name.
|
||||
However, the original filename is stored inside the SVG.
|
||||
"""
|
||||
|
||||
|
||||
def get_original_filename(filename):
|
||||
docname = None
|
||||
|
||||
# parse SVG for docname tag
|
||||
with codecs.open(filename, "r", encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if 'sodipodi:docname="' in line:
|
||||
matches = re.search('sodipodi:docname="(.*).svg"', line)
|
||||
if not matches:
|
||||
break
|
||||
try:
|
||||
docname = matches.group(1)
|
||||
except IndexError:
|
||||
# something is wrong with this line
|
||||
break
|
||||
# unescape XML string
|
||||
|
||||
docname = docname.replace('<', '<')
|
||||
docname = docname.replace('>', '>')
|
||||
docname = docname.replace('"', '"')
|
||||
docname = docname.replace('&', '&')
|
||||
|
||||
# normalize accented characters (äöü -> aou)
|
||||
docname = unicodedata.normalize('NFKD', docname).encode('ASCII', 'ignore').decode('ASCII')
|
||||
break
|
||||
|
||||
if not docname:
|
||||
# failed to read filename from SVG, return original one
|
||||
docname = os.path.basename(filename)
|
||||
if str.endswith(docname, ".svg"):
|
||||
docname = docname[:-4]
|
||||
if str.startswith(docname, "ink_ext_"):
|
||||
# inkscape temporary file, the name is useless
|
||||
docname = "new"
|
||||
|
||||
# sanitize the filename:
|
||||
# filter out special characters (@/\& ...)
|
||||
docname = "".join(x for x in docname if (x.isalnum() or x in "._- "))
|
||||
docname = docname + ".svg"
|
||||
return docname
|
||||
|
||||
# find executable paths
|
||||
import platform
|
||||
if platform.system() == 'Darwin':
|
||||
VISICUTBIN = which("VisiCut.MacOS", [VISICUTDIR])
|
||||
elif "nt" in os.name: # Windows
|
||||
VISICUTBIN = which("VisiCut.exe", [VISICUTDIR])
|
||||
else:
|
||||
VISICUTBIN = which("VisiCut.Linux", [VISICUTDIR, "/usr/share/visicut"])
|
||||
INKSCAPEBIN = which("inkscape", [INKSCAPEDIR])
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix='temp-visicut-')
|
||||
dest_filename = os.path.join(tmpdir, get_original_filename(filename))
|
||||
|
||||
# remove all non-selected elements and convert inkscape-specific elements (text-to-path etc.)
|
||||
stripSVG_inkscape(src=filename, dest=dest_filename, elements=elements)
|
||||
|
||||
# Try to connect to running VisiCut instance
|
||||
# Note: this step may be omitted, as VisiCut will do the same.
|
||||
# However, doing it here saves 2-3 seconds of waiting time on Windows.
|
||||
s = socket.socket()
|
||||
try:
|
||||
s.connect(("localhost", get_single_instance_port()))
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
if IMPORT:
|
||||
s.send(b"@" + fsencode(dest_filename) + b"\n")
|
||||
else:
|
||||
s.send(fsencode(dest_filename) + b"\n")
|
||||
sys.exit(0)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
# Try to start own VisiCut instance
|
||||
try:
|
||||
creationflags = 0
|
||||
close_fds = False
|
||||
if os.name == "nt":
|
||||
DETACHED_PROCESS = 8 # start as "daemon"
|
||||
creationflags = DETACHED_PROCESS
|
||||
close_fds = True
|
||||
else:
|
||||
try:
|
||||
import daemonize
|
||||
daemonize.createDaemon()
|
||||
except:
|
||||
inkex.utils.debug("Could not daemonize. Sorry, but Inkscape was blocked until VisiCut is closed")
|
||||
cmd = [VISICUTBIN] + arguments + [dest_filename]
|
||||
with subprocess.Popen(cmd, creationflags=creationflags, close_fds=close_fds, stderr=subprocess.DEVNULL) as p:
|
||||
p.communicate()
|
||||
except:
|
||||
inkex.utils.debug("Can not start VisiCut (" + str(sys.exc_info()[0]) + "). Please start manually or change the VISICUTDIR variable in the Inkscape-Extension script\n")
|
||||
|
||||
|
||||
|
||||
# TODO (complicated, probably WONTFIX): cleanup temporary directories -- this is really difficult because we need to make sure that visicut no longer needs the file, even for reloading!
|
17
extensions/fablabchemnitz/visicut/open_in_visicut_add.inx
Normal file
17
extensions/fablabchemnitz/visicut/open_in_visicut_add.inx
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Open In VisiCut (Add)</name>
|
||||
<id>fablabchemnitz.de.open_in_visicut_add</id>
|
||||
<param name="import" type="bool" gui-hidden="true">true</param>
|
||||
<effect needs-live-preview="false">
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Import/Export/Transfer"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">open_in_visicut.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>Open In VisiCut (Replace)</name>
|
||||
<id>fablabchemnitz.de.open_in_visicut_replace</id>
|
||||
<param name="import" type="bool" gui-hidden="true">false</param>
|
||||
<effect needs-live-preview="false">
|
||||
<object-type>path</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="FabLab Chemnitz">
|
||||
<submenu name="Import/Export/Transfer"/>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">open_in_visicut.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
Loading…
Reference in New Issue
Block a user