|
@ -0,0 +1,4 @@
|
|||
*.pyc
|
||||
.*
|
||||
/svgpathtools/nonunittests
|
||||
!/.gitignore
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andrew Allan Port
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2014 Lennart Regebro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,12 @@
|
|||
# file GENERATED by distutils, do NOT edit
|
||||
setup.py
|
||||
svgpathtools/__init__.py
|
||||
svgpathtools/bezier.py
|
||||
svgpathtools/misctools.py
|
||||
svgpathtools/parser.py
|
||||
svgpathtools/path.py
|
||||
svgpathtools/paths2svg.py
|
||||
svgpathtools/pathtools.py
|
||||
svgpathtools/polytools.py
|
||||
svgpathtools/smoothing.py
|
||||
svgpathtools/svg2paths.py
|
|
@ -0,0 +1,753 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# svgpathtools\n",
|
||||
"\n",
|
||||
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
|
||||
"\n",
|
||||
"## Features\n",
|
||||
"\n",
|
||||
"svgpathtools contains functions designed to **easily read, write and display SVG files** as well as *a large selection of geometrically\\-oriented tools* to **transform and analyze path elements**.\n",
|
||||
"\n",
|
||||
"Additionally, the submodule *bezier.py* contains tools for for working with general **nth order Bezier curves stored as n-tuples**.\n",
|
||||
"\n",
|
||||
"Some included tools:\n",
|
||||
"\n",
|
||||
"- **read**, **write**, and **display** SVG files containing Path (and other) SVG elements\n",
|
||||
"- convert Bézier path segments to **numpy.poly1d** (polynomial) objects\n",
|
||||
"- convert polynomials (in standard form) to their Bézier form\n",
|
||||
"- compute **tangent vectors** and (right-hand rule) **normal vectors**\n",
|
||||
"- compute **curvature**\n",
|
||||
"- break discontinuous paths into their **continuous subpaths**.\n",
|
||||
"- efficiently compute **intersections** between paths and/or segments\n",
|
||||
"- find a **bounding box** for a path or segment\n",
|
||||
"- **reverse** segment/path orientation\n",
|
||||
"- **crop** and **split** paths and segments\n",
|
||||
"- **smooth** paths (i.e. smooth away kinks to make paths differentiable)\n",
|
||||
"- **transition maps** from path domain to segment domain and back (T2t and t2T)\n",
|
||||
"- compute **area** enclosed by a closed path\n",
|
||||
"- compute **arc length**\n",
|
||||
"- compute **inverse arc length**\n",
|
||||
"- convert RGB color tuples to hexadecimal color strings and back\n",
|
||||
"\n",
|
||||
"## Prerequisites\n",
|
||||
"- **numpy**\n",
|
||||
"- **svgwrite**\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"If not already installed, you can **install the prerequisites** using pip.\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"$ pip install numpy\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"$ pip install svgwrite\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"Then **install svgpathtools**:\n",
|
||||
"```bash\n",
|
||||
"$ pip install svgpathtools\n",
|
||||
"``` \n",
|
||||
" \n",
|
||||
"### Alternative Setup \n",
|
||||
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"$ python setup.py install\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"## Credit where credit's due\n",
|
||||
"Much of the core of this module was taken from [the svg.path (v2.0) module](https://github.com/regebro/svg.path). Interested svg.path users should see the compatibility notes at bottom of this readme.\n",
|
||||
"\n",
|
||||
"## Basic Usage\n",
|
||||
"\n",
|
||||
"### Classes\n",
|
||||
"The svgpathtools module is primarily structured around four path segment classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. There is also a fifth class, ``Path``, whose objects are sequences of (connected or disconnected<sup id=\"a1\">[1](#f1)</sup>) path segment objects.\n",
|
||||
"\n",
|
||||
"* ``Line(start, end)``\n",
|
||||
"\n",
|
||||
"* ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See docstring for a detailed explanation of these parameters\n",
|
||||
"\n",
|
||||
"* ``QuadraticBezier(start, control, end)``\n",
|
||||
"\n",
|
||||
"* ``CubicBezier(start, control1, control2, end)``\n",
|
||||
"\n",
|
||||
"* ``Path(*segments)``\n",
|
||||
"\n",
|
||||
"See the relevant docstrings in *path.py* or the [official SVG specifications](<http://www.w3.org/TR/SVG/paths.html>) for more information on what each parameter means.\n",
|
||||
"\n",
|
||||
"<u id=\"f1\">1</u> Warning: Some of the functionality in this library has not been tested on discontinuous Path objects. A simple workaround is provided, however, by the ``Path.continuous_subpaths()`` method. [↩](#a1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from __future__ import division, print_function"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
|
||||
" Line(start=(200+300j), end=(250+350j)))\n",
|
||||
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
|
||||
" Line(start=(200+300j), end=(250+350j)))\n",
|
||||
"True\n",
|
||||
"M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0\n",
|
||||
"True\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Coordinates are given as points in the complex plane\n",
|
||||
"from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc\n",
|
||||
"seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)\n",
|
||||
"seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)\n",
|
||||
"path = Path(seg1, seg2) # A path traversing the cubic and then the line\n",
|
||||
"\n",
|
||||
"# We could alternatively created this Path object using a d-string\n",
|
||||
"from svgpathtools import parse_path\n",
|
||||
"path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')\n",
|
||||
"\n",
|
||||
"# Let's check that these two methods are equivalent\n",
|
||||
"print(path)\n",
|
||||
"print(path_alt)\n",
|
||||
"print(path == path_alt)\n",
|
||||
"\n",
|
||||
"# On a related note, the Path.d() method returns a Path object's d-string\n",
|
||||
"print(path.d())\n",
|
||||
"print(parse_path(path.d()) == path)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The ``Path`` class is a mutable sequence, so it behaves much like a list.\n",
|
||||
"So segments can **append**ed, **insert**ed, set by index, **del**eted, **enumerate**d, **slice**d out, etc."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),\n",
|
||||
" Line(start=(200+300j), end=(250+350j)),\n",
|
||||
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
|
||||
"Path(Line(start=(200+100j), end=(200+300j)),\n",
|
||||
" Line(start=(200+300j), end=(250+350j)),\n",
|
||||
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))\n",
|
||||
"path is continuous? True\n",
|
||||
"path is closed? True\n",
|
||||
"path contains non-differentiable points? True\n",
|
||||
"spath contains non-differentiable points? False\n",
|
||||
"Path(Line(start=(200+101.5j), end=(200+298.5j)),\n",
|
||||
" CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),\n",
|
||||
" Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),\n",
|
||||
" CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),\n",
|
||||
" CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),\n",
|
||||
" CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))\n",
|
||||
"Notice that path contains 3 segments and spath contains 6 segments.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Let's append another to the end of it\n",
|
||||
"path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))\n",
|
||||
"print(path)\n",
|
||||
"\n",
|
||||
"# Let's replace the first segment with a Line object\n",
|
||||
"path[0] = Line(200+100j, 200+300j)\n",
|
||||
"print(path)\n",
|
||||
"\n",
|
||||
"# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)\n",
|
||||
"print(\"path is continuous? \", path.iscontinuous())\n",
|
||||
"print(\"path is closed? \", path.isclosed())\n",
|
||||
"\n",
|
||||
"# The curve the path follows is not, however, smooth (differentiable)\n",
|
||||
"from svgpathtools import kinks, smoothed_path\n",
|
||||
"print(\"path contains non-differentiable points? \", len(kinks(path)) > 0)\n",
|
||||
"\n",
|
||||
"# If we want, we can smooth these out (Experimental and only for line/cubic paths)\n",
|
||||
"# Note: smoothing will always works (except on 180 degree turns), but you may want \n",
|
||||
"# to play with the maxjointsize and tightness parameters to get pleasing results\n",
|
||||
"# Note also: smoothing will increase the number of segments in a path\n",
|
||||
"spath = smoothed_path(path)\n",
|
||||
"print(\"spath contains non-differentiable points? \", len(kinks(spath)) > 0)\n",
|
||||
"print(spath)\n",
|
||||
"\n",
|
||||
"# Let's take a quick look at the path and its smoothed relative\n",
|
||||
"# The following commands will open two browser windows to display path and spaths\n",
|
||||
"from svgpathtools import disvg\n",
|
||||
"from time import sleep\n",
|
||||
"disvg(path) \n",
|
||||
"sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)\n",
|
||||
"disvg(spath)\n",
|
||||
"print(\"Notice that path contains {} segments and spath contains {} segments.\"\n",
|
||||
" \"\".format(len(path), len(spath)))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Reading SVGSs\n",
|
||||
"\n",
|
||||
"The **svg2paths()** function converts an svgfile to a list of Path objects and a separate list of dictionaries containing the attributes of each said path. \n",
|
||||
"Note: Line, Polyline, Polygon, and Path SVG elements can all be converted to Path objects using this function."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),\n",
|
||||
" CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))\n",
|
||||
"red\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Read SVG into a list of path objects and list of dictionaries of attributes \n",
|
||||
"from svgpathtools import svg2paths, wsvg\n",
|
||||
"paths, attributes = svg2paths('test.svg')\n",
|
||||
"\n",
|
||||
"# Let's print out the first path object and the color it was in the SVG\n",
|
||||
"# We'll see it is composed of two CubicBezier objects and, in the SVG file it \n",
|
||||
"# came from, it was red\n",
|
||||
"redpath = paths[0]\n",
|
||||
"redpath_attribs = attributes[0]\n",
|
||||
"print(redpath)\n",
|
||||
"print(redpath_attribs['stroke'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Writing SVGSs (and some geometric functions and methods)\n",
|
||||
"\n",
|
||||
"The **wsvg()** function creates an SVG file from a list of path. This function can do many things (see docstring in *paths2svg.py* for more information) and is meant to be quick and easy to use.\n",
|
||||
"Note: Use the convenience function **disvg()** (or set 'openinbrowser=True') to automatically attempt to open the created svg file in your default SVG viewer."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 25,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Let's make a new SVG that's identical to the first\n",
|
||||
"wsvg(paths, attributes=attributes, filename='output1.svg')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![output1.svg](output1.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"There will be many more examples of writing and displaying path data below.\n",
|
||||
"\n",
|
||||
"### The .point() method and transitioning between path and path segment parameterizations\n",
|
||||
"SVG Path elements and their segments have official parameterizations.\n",
|
||||
"These parameterizations can be accessed using the ``Path.point()``, ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, and ``Arc.point()`` methods.\n",
|
||||
"All these parameterizations are defined over the domain 0 <= t <= 1.\n",
|
||||
"\n",
|
||||
"**Note:** In this document and in inline documentation and doctrings, I use a capital ``T`` when referring to the parameterization of a Path object and a lower case ``t`` when referring speaking about path segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc objects). \n",
|
||||
"Given a ``T`` value, the ``Path.T2t()`` method can be used to find the corresponding segment index, ``k``, and segment parameter, ``t``, such that ``path.point(T)=path[k].point(t)``. \n",
|
||||
"There is also a ``Path.t2T()`` method to solve the inverse problem."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"True\n",
|
||||
"True\n",
|
||||
"False\n",
|
||||
"True\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example:\n",
|
||||
"\n",
|
||||
"# Let's check that the first segment of redpath starts \n",
|
||||
"# at the same point as redpath\n",
|
||||
"firstseg = redpath[0] \n",
|
||||
"print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)\n",
|
||||
"\n",
|
||||
"# Let's check that the last segment of redpath ends on the same point as redpath\n",
|
||||
"lastseg = redpath[-1] \n",
|
||||
"print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)\n",
|
||||
"\n",
|
||||
"# This next boolean should return False as redpath is composed multiple segments\n",
|
||||
"print(redpath.point(0.5) == firstseg.point(0.5))\n",
|
||||
"\n",
|
||||
"# If we want to figure out which segment of redpoint the \n",
|
||||
"# point redpath.point(0.5) lands on, we can use the path.T2t() method\n",
|
||||
"k, t = redpath.T2t(0.5)\n",
|
||||
"print(redpath[k].point(t) == redpath.point(0.5))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Tangent vectors and Bezier curves as numpy polynomial objects\n",
|
||||
"Another great way to work with the parameterizations for Line, QuadraticBezier, and CubicBezier objects is to convert them to ``numpy.poly1d`` objects. This is done easily using the ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` methods. \n",
|
||||
"There's also a ``polynomial2bezier()`` function in the pathtools.py submodule to convert polynomials back to Bezier curves. \n",
|
||||
"\n",
|
||||
"**Note:** cubic Bezier curves are parameterized as $$\\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$\n",
|
||||
"where $P_0$, $P_1$, $P_2$, and $P_3$ are the control points ``start``, ``control1``, ``control2``, and ``end``, respectively, that svgpathtools uses to define a CubicBezier object. The ``CubicBezier.poly()`` method expands this polynomial to its standard form \n",
|
||||
"$$\\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$\n",
|
||||
"where\n",
|
||||
"$$\\begin{bmatrix}c_0\\\\c_1\\\\c_2\\\\c_3\\end{bmatrix} = \n",
|
||||
"\\begin{bmatrix}\n",
|
||||
"-1 & 3 & -3 & 1\\\\\n",
|
||||
"3 & -6 & -3 & 0\\\\\n",
|
||||
"-3 & 3 & 0 & 0\\\\\n",
|
||||
"1 & 0 & 0 & 0\\\\\n",
|
||||
"\\end{bmatrix}\n",
|
||||
"\\begin{bmatrix}P_0\\\\P_1\\\\P_2\\\\P_3\\end{bmatrix}$$ \n",
|
||||
"\n",
|
||||
"QuadraticBezier.poly() and Line.poly() are defined similarly."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 27,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"True\n",
|
||||
"The CubicBezier, b.point(x) = \n",
|
||||
"\n",
|
||||
"(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3\n",
|
||||
"\n",
|
||||
"can be rewritten in standard form as \n",
|
||||
"\n",
|
||||
" 3 2\n",
|
||||
"(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Example:\n",
|
||||
"b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)\n",
|
||||
"p = b.poly()\n",
|
||||
"\n",
|
||||
"# p(t) == b.point(t)\n",
|
||||
"print(p(0.235) == b.point(0.235))\n",
|
||||
"\n",
|
||||
"# What is p(t)? It's just the cubic b written in standard form. \n",
|
||||
"bpretty = \"{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3\".format(*b.bpoints())\n",
|
||||
"print(\"The CubicBezier, b.point(x) = \\n\\n\" + \n",
|
||||
" bpretty + \"\\n\\n\" + \n",
|
||||
" \"can be rewritten in standard form as \\n\\n\" +\n",
|
||||
" str(p).replace('x','t'))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To illustrate the awesomeness of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways.\n",
|
||||
"\n",
|
||||
"### Tangent vectors (and more on polynomials)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 28,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"unit tangent check: True\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"t = 0.5\n",
|
||||
"### Method 1: the easy way\n",
|
||||
"u1 = b.unit_tangent(t)\n",
|
||||
"\n",
|
||||
"### Method 2: another easy way \n",
|
||||
"# Note: This way will fail if it encounters a removable singularity.\n",
|
||||
"u2 = b.derivative(t)/abs(b.derivative(t))\n",
|
||||
"\n",
|
||||
"### Method 2: a third easy way \n",
|
||||
"# Note: This way will also fail if it encounters a removable singularity.\n",
|
||||
"dp = p.deriv() \n",
|
||||
"u3 = dp(t)/abs(dp(t))\n",
|
||||
"\n",
|
||||
"### Method 4: the removable-singularity-proof numpy.poly1d way \n",
|
||||
"# Note: This is roughly how Method 1 works\n",
|
||||
"from svgpathtools import real, imag, rational_limit\n",
|
||||
"dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy \n",
|
||||
"p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2\n",
|
||||
"# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,\n",
|
||||
"# the limit_{t->t0}[f(t) / abs(f(t))] == \n",
|
||||
"# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])\n",
|
||||
"from cmath import sqrt\n",
|
||||
"u4 = sqrt(rational_limit(dp**2, p_mag2, t))\n",
|
||||
"\n",
|
||||
"print(\"unit tangent check:\", u1 == u2 == u3 == u4)\n",
|
||||
"\n",
|
||||
"# Let's do a visual check\n",
|
||||
"mag = b.length()/4 # so it's not hard to see the tangent line\n",
|
||||
"tangent_line = Line(b.point(t), b.point(t) + mag*u1)\n",
|
||||
"disvg([b, tangent_line], 'bg', nodes=[b.point(t)])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Translations (shifts), reversing orientation, and normal vectors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 29,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Speaking of tangents, let's add a normal vector to the picture\n",
|
||||
"n = b.normal(t)\n",
|
||||
"normal_line = Line(b.point(t), b.point(t) + mag*n)\n",
|
||||
"disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])\n",
|
||||
"\n",
|
||||
"# and let's reverse the orientation of b! \n",
|
||||
"# the tangent and normal lines should be sent to their opposites\n",
|
||||
"br = b.reversed()\n",
|
||||
"\n",
|
||||
"# Let's also shift b_r over a bit to the right so we can view it next to b\n",
|
||||
"# The simplest way to do this is br = br.translated(3*mag), but let's use \n",
|
||||
"# the .bpoints() instead, which returns a Bezier's control points\n",
|
||||
"br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # \n",
|
||||
"\n",
|
||||
"tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))\n",
|
||||
"normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))\n",
|
||||
"wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], \n",
|
||||
" 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', \n",
|
||||
" text=[\"b's tangent\", \"br's tangent\"], text_path=[tangent_line, tangent_line_r])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![vectorframes.svg](vectorframes.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Rotations and Translations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 30,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Let's take a Line and an Arc and make some pictures\n",
|
||||
"top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)\n",
|
||||
"midline = Line(-1.5, 1.5)\n",
|
||||
"\n",
|
||||
"# First let's make our ellipse whole\n",
|
||||
"bottom_half = top_half.rotated(180)\n",
|
||||
"decorated_ellipse = Path(top_half, bottom_half)\n",
|
||||
"\n",
|
||||
"# Now let's add the decorations\n",
|
||||
"for k in range(12):\n",
|
||||
" decorated_ellipse.append(midline.rotated(30*k))\n",
|
||||
" \n",
|
||||
"# Let's move it over so we can see the original Line and Arc object next\n",
|
||||
"# to the final product\n",
|
||||
"decorated_ellipse = decorated_ellipse.translated(4+0j)\n",
|
||||
"wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![decorated_ellipse.svg](decorated_ellipse.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### arc length and inverse arc length\n",
|
||||
"\n",
|
||||
"Here we'll create an SVG that shows off the parametric and geometric midpoints of the paths from ``test.svg``. We'll need to compute use the ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the related inverse arc length methods ``.ilength()`` function to do this."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 31,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# First we'll load the path data from the file test.svg\n",
|
||||
"paths, attributes = svg2paths('test.svg')\n",
|
||||
"\n",
|
||||
"# Let's mark the parametric midpoint of each segment\n",
|
||||
"# I say \"parametric\" midpoint because Bezier curves aren't \n",
|
||||
"# parameterized by arclength \n",
|
||||
"# If they're also the geometric midpoint, let's mark them\n",
|
||||
"# purple and otherwise we'll mark the geometric midpoint green\n",
|
||||
"min_depth = 5\n",
|
||||
"error = 1e-4\n",
|
||||
"dots = []\n",
|
||||
"ncols = []\n",
|
||||
"nradii = []\n",
|
||||
"for path in paths:\n",
|
||||
" for seg in path:\n",
|
||||
" parametric_mid = seg.point(0.5)\n",
|
||||
" seg_length = seg.length()\n",
|
||||
" if seg.length(0.5)/seg.length() == 1/2:\n",
|
||||
" dots += [parametric_mid]\n",
|
||||
" ncols += ['purple']\n",
|
||||
" nradii += [5]\n",
|
||||
" else:\n",
|
||||
" t_mid = seg.ilength(seg_length/2)\n",
|
||||
" geo_mid = seg.point(t_mid)\n",
|
||||
" dots += [parametric_mid, geo_mid]\n",
|
||||
" ncols += ['red', 'green']\n",
|
||||
" nradii += [5] * 2\n",
|
||||
"\n",
|
||||
"# In 'output2.svg' the paths will retain their original attributes\n",
|
||||
"wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, \n",
|
||||
" attributes=attributes, filename='output2.svg')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![output2.svg](output2.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Intersections between Bezier curves"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 32,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Let's find all intersections between redpath and the other \n",
|
||||
"redpath = paths[0]\n",
|
||||
"redpath_attribs = attributes[0]\n",
|
||||
"intersections = []\n",
|
||||
"for path in paths[1:]:\n",
|
||||
" for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):\n",
|
||||
" intersections.append(redpath.point(T1))\n",
|
||||
" \n",
|
||||
"disvg(paths, filename='output_intersections.svg', attributes=attributes,\n",
|
||||
" nodes = intersections, node_radii = [5]*len(intersections))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![output_intersections.svg](output_intersections.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### An Advanced Application: Offsetting Bezier Curves\n",
|
||||
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few Bezier cubics."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 33,
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def display_offset_curve(bezpath, offset_distance, steps=1000,\n",
|
||||
" stripe_width=3, stripe_colors='ygb',\n",
|
||||
" name='offsetcurves.svg', show_normal_line=True):\n",
|
||||
" \"\"\"Takes in a path of bezier curves, `bezpath`, and a distance,\n",
|
||||
" `offset_distance`, and displays the 'parallel' offset curve by placing a\n",
|
||||
" dot at a distance of `offset_distance` away at each step.\"\"\"\n",
|
||||
" nls = []\n",
|
||||
" nodes = []\n",
|
||||
" line_colors = ''\n",
|
||||
" node_colors = ''\n",
|
||||
"\n",
|
||||
" for bez in bezpath:\n",
|
||||
" ct = 1\n",
|
||||
" for k in range(steps):\n",
|
||||
" t = k / steps\n",
|
||||
" nl = Line(bez.point(t),\n",
|
||||
" bez.point(t) + offset_distance * bez.normal(t))\n",
|
||||
" nls.append(nl)\n",
|
||||
" line_colors += stripe_colors[ct % 3]\n",
|
||||
" if not (k % stripe_width):\n",
|
||||
" ct += 1\n",
|
||||
" nodes.append(bez.point(t))\n",
|
||||
" nodes.append(nl.end)\n",
|
||||
" node_colors += 'kr'\n",
|
||||
" # nls.reverse()\n",
|
||||
" if show_normal_line:\n",
|
||||
" wsvg([bezpath] + nls, 'k' + line_colors,\n",
|
||||
" nodes=nodes, node_colors=node_colors,\n",
|
||||
" filename=name)\n",
|
||||
" else:\n",
|
||||
" wsvg(bezpath, 'k',\n",
|
||||
" nodes=nodes, node_colors=node_colors,\n",
|
||||
" filename=name)\n",
|
||||
"\n",
|
||||
"bez1 = parse_path(\"m 288,600 c -52,-28 -42,-61 0,-97 \")[0]\n",
|
||||
"bez2 = parse_path(\"M 151,395 C 407,485 726.17662,160 634,339\")[0]\n",
|
||||
"bez3 = parse_path(\"m 117,695 c 237,-7 -103,-146 457,0\")[0]\n",
|
||||
"path = Path(bez1, bez2.translated(300), bez3.translated(500+400j))\n",
|
||||
"\n",
|
||||
"display_offset_curve(path, 500)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"![offsetcurves.svg](offsetcurves.svg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Compatibility Notes for users of svg.path (v2.0)\n",
|
||||
"\n",
|
||||
"- renamed Arc.arc attribute as Arc.large_arc\n",
|
||||
"\n",
|
||||
"- Path.d() : For behavior similar<sup id=\"a2\">[2](#f2)</sup> to svg.path (v2.0), set both useSandT and use_closed_attrib to be True.\n",
|
||||
"\n",
|
||||
"<u id=\"f2\">2</u> The behavior would be identical, but the string formatting used in this method has been changed to use default format (instead of the General format, {:G}), for inceased precision. [↩](#a2)\n",
|
||||
"\n",
|
||||
"## To Do\n",
|
||||
"\n",
|
||||
"- Implement Arc X Arc intersections.\n",
|
||||
"- Implement Arc.radialrange() method.\n",
|
||||
"\n",
|
||||
"Licence\n",
|
||||
"-------\n",
|
||||
"\n",
|
||||
"This module is under a MIT License."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 2",
|
||||
"language": "python",
|
||||
"name": "python2"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
|
@ -0,0 +1,634 @@
|
|||
|
||||
svgpathtools
|
||||
============
|
||||
|
||||
svgpathtools is a collection of tools for manipulating and analyzing SVG
|
||||
Path objects and Bézier curves.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
svgpathtools contains functions designed to **easily read, write and
|
||||
display SVG files** as well as *a large selection of
|
||||
geometrically-oriented tools* to **transform and analyze path
|
||||
elements**.
|
||||
|
||||
Additionally, the submodule *bezier.py* contains tools for for working
|
||||
with general **nth order Bezier curves stored as n-tuples**.
|
||||
|
||||
Some included tools:
|
||||
|
||||
- **read**, **write**, and **display** SVG files containing Path (and
|
||||
other) SVG elements
|
||||
- convert Bézier path segments to **numpy.poly1d** (polynomial) objects
|
||||
- convert polynomials (in standard form) to their Bézier form
|
||||
- compute **tangent vectors** and (right-hand rule) **normal vectors**
|
||||
- compute **curvature**
|
||||
- break discontinuous paths into their **continuous subpaths**.
|
||||
- efficiently compute **intersections** between paths and/or segments
|
||||
- find a **bounding box** for a path or segment
|
||||
- **reverse** segment/path orientation
|
||||
- **crop** and **split** paths and segments
|
||||
- **smooth** paths (i.e. smooth away kinks to make paths
|
||||
differentiable)
|
||||
- **transition maps** from path domain to segment domain and back (T2t
|
||||
and t2T)
|
||||
- compute **area** enclosed by a closed path
|
||||
- compute **arc length**
|
||||
- compute **inverse arc length**
|
||||
- convert RGB color tuples to hexadecimal color strings and back
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
- **numpy**
|
||||
- **svgwrite**
|
||||
|
||||
Setup
|
||||
-----
|
||||
|
||||
If not already installed, you can **install the prerequisites** using
|
||||
pip.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip install numpy
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip install svgwrite
|
||||
|
||||
Then **install svgpathtools**:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip install svgpathtools
|
||||
|
||||
Alternative Setup
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can download the source from Github and install by using the command
|
||||
(from inside the folder containing setup.py):
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ python setup.py install
|
||||
|
||||
Credit where credit's due
|
||||
-------------------------
|
||||
|
||||
Much of the core of this module was taken from `the svg.path (v2.0)
|
||||
module <https://github.com/regebro/svg.path>`__. Interested svg.path
|
||||
users should see the compatibility notes at bottom of this readme.
|
||||
|
||||
Basic Usage
|
||||
-----------
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
The svgpathtools module is primarily structured around four path segment
|
||||
classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``.
|
||||
There is also a fifth class, ``Path``, whose objects are sequences of
|
||||
(connected or disconnected\ `1 <#f1>`__\ ) path segment objects.
|
||||
|
||||
- ``Line(start, end)``
|
||||
|
||||
- ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See
|
||||
docstring for a detailed explanation of these parameters
|
||||
|
||||
- ``QuadraticBezier(start, control, end)``
|
||||
|
||||
- ``CubicBezier(start, control1, control2, end)``
|
||||
|
||||
- ``Path(*segments)``
|
||||
|
||||
See the relevant docstrings in *path.py* or the `official SVG
|
||||
specifications <http://www.w3.org/TR/SVG/paths.html>`__ for more
|
||||
information on what each parameter means.
|
||||
|
||||
1 Warning: Some of the functionality in this library has not been tested
|
||||
on discontinuous Path objects. A simple workaround is provided, however,
|
||||
by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__
|
||||
|
||||
.. code:: python
|
||||
|
||||
from __future__ import division, print_function
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Coordinates are given as points in the complex plane
|
||||
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc
|
||||
seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)
|
||||
seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)
|
||||
path = Path(seg1, seg2) # A path traversing the cubic and then the line
|
||||
|
||||
# We could alternatively created this Path object using a d-string
|
||||
from svgpathtools import parse_path
|
||||
path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')
|
||||
|
||||
# Let's check that these two methods are equivalent
|
||||
print(path)
|
||||
print(path_alt)
|
||||
print(path == path_alt)
|
||||
|
||||
# On a related note, the Path.d() method returns a Path object's d-string
|
||||
print(path.d())
|
||||
print(parse_path(path.d()) == path)
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
|
||||
Line(start=(200+300j), end=(250+350j)))
|
||||
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
|
||||
Line(start=(200+300j), end=(250+350j)))
|
||||
True
|
||||
M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0
|
||||
True
|
||||
|
||||
|
||||
The ``Path`` class is a mutable sequence, so it behaves much like a
|
||||
list. So segments can **append**\ ed, **insert**\ ed, set by index,
|
||||
**del**\ eted, **enumerate**\ d, **slice**\ d out, etc.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's append another to the end of it
|
||||
path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))
|
||||
print(path)
|
||||
|
||||
# Let's replace the first segment with a Line object
|
||||
path[0] = Line(200+100j, 200+300j)
|
||||
print(path)
|
||||
|
||||
# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)
|
||||
print("path is continuous? ", path.iscontinuous())
|
||||
print("path is closed? ", path.isclosed())
|
||||
|
||||
# The curve the path follows is not, however, smooth (differentiable)
|
||||
from svgpathtools import kinks, smoothed_path
|
||||
print("path contains non-differentiable points? ", len(kinks(path)) > 0)
|
||||
|
||||
# If we want, we can smooth these out (Experimental and only for line/cubic paths)
|
||||
# Note: smoothing will always works (except on 180 degree turns), but you may want
|
||||
# to play with the maxjointsize and tightness parameters to get pleasing results
|
||||
# Note also: smoothing will increase the number of segments in a path
|
||||
spath = smoothed_path(path)
|
||||
print("spath contains non-differentiable points? ", len(kinks(spath)) > 0)
|
||||
print(spath)
|
||||
|
||||
# Let's take a quick look at the path and its smoothed relative
|
||||
# The following commands will open two browser windows to display path and spaths
|
||||
from svgpathtools import disvg
|
||||
from time import sleep
|
||||
disvg(path)
|
||||
sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)
|
||||
disvg(spath)
|
||||
print("Notice that path contains {} segments and spath contains {} segments."
|
||||
"".format(len(path), len(spath)))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
|
||||
Line(start=(200+300j), end=(250+350j)),
|
||||
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
|
||||
Path(Line(start=(200+100j), end=(200+300j)),
|
||||
Line(start=(200+300j), end=(250+350j)),
|
||||
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
|
||||
path is continuous? True
|
||||
path is closed? True
|
||||
path contains non-differentiable points? True
|
||||
spath contains non-differentiable points? False
|
||||
Path(Line(start=(200+101.5j), end=(200+298.5j)),
|
||||
CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),
|
||||
Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),
|
||||
CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),
|
||||
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),
|
||||
CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))
|
||||
Notice that path contains 3 segments and spath contains 6 segments.
|
||||
|
||||
|
||||
Reading SVGSs
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
| The **svg2paths()** function converts an svgfile to a list of Path
|
||||
objects and a separate list of dictionaries containing the attributes
|
||||
of each said path.
|
||||
| Note: Line, Polyline, Polygon, and Path SVG elements can all be
|
||||
converted to Path objects using this function.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Read SVG into a list of path objects and list of dictionaries of attributes
|
||||
from svgpathtools import svg2paths, wsvg
|
||||
paths, attributes = svg2paths('test.svg')
|
||||
|
||||
# Let's print out the first path object and the color it was in the SVG
|
||||
# We'll see it is composed of two CubicBezier objects and, in the SVG file it
|
||||
# came from, it was red
|
||||
redpath = paths[0]
|
||||
redpath_attribs = attributes[0]
|
||||
print(redpath)
|
||||
print(redpath_attribs['stroke'])
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),
|
||||
CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))
|
||||
red
|
||||
|
||||
|
||||
Writing SVGSs (and some geometric functions and methods)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The **wsvg()** function creates an SVG file from a list of path. This
|
||||
function can do many things (see docstring in *paths2svg.py* for more
|
||||
information) and is meant to be quick and easy to use. Note: Use the
|
||||
convenience function **disvg()** (or set 'openinbrowser=True') to
|
||||
automatically attempt to open the created svg file in your default SVG
|
||||
viewer.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's make a new SVG that's identical to the first
|
||||
wsvg(paths, attributes=attributes, filename='output1.svg')
|
||||
|
||||
.. figure:: output1.svg
|
||||
:alt: output1.svg
|
||||
|
||||
output1.svg
|
||||
|
||||
There will be many more examples of writing and displaying path data
|
||||
below.
|
||||
|
||||
The .point() method and transitioning between path and path segment parameterizations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
SVG Path elements and their segments have official parameterizations.
|
||||
These parameterizations can be accessed using the ``Path.point()``,
|
||||
``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``,
|
||||
and ``Arc.point()`` methods. All these parameterizations are defined
|
||||
over the domain 0 <= t <= 1.
|
||||
|
||||
| **Note:** In this document and in inline documentation and doctrings,
|
||||
I use a capital ``T`` when referring to the parameterization of a Path
|
||||
object and a lower case ``t`` when referring speaking about path
|
||||
segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc
|
||||
objects).
|
||||
| Given a ``T`` value, the ``Path.T2t()`` method can be used to find the
|
||||
corresponding segment index, ``k``, and segment parameter, ``t``, such
|
||||
that ``path.point(T)=path[k].point(t)``.
|
||||
| There is also a ``Path.t2T()`` method to solve the inverse problem.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Example:
|
||||
|
||||
# Let's check that the first segment of redpath starts
|
||||
# at the same point as redpath
|
||||
firstseg = redpath[0]
|
||||
print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)
|
||||
|
||||
# Let's check that the last segment of redpath ends on the same point as redpath
|
||||
lastseg = redpath[-1]
|
||||
print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)
|
||||
|
||||
# This next boolean should return False as redpath is composed multiple segments
|
||||
print(redpath.point(0.5) == firstseg.point(0.5))
|
||||
|
||||
# If we want to figure out which segment of redpoint the
|
||||
# point redpath.point(0.5) lands on, we can use the path.T2t() method
|
||||
k, t = redpath.T2t(0.5)
|
||||
print(redpath[k].point(t) == redpath.point(0.5))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
True
|
||||
True
|
||||
False
|
||||
True
|
||||
|
||||
|
||||
Tangent vectors and Bezier curves as numpy polynomial objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
| Another great way to work with the parameterizations for Line,
|
||||
QuadraticBezier, and CubicBezier objects is to convert them to
|
||||
``numpy.poly1d`` objects. This is done easily using the
|
||||
``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()``
|
||||
methods.
|
||||
| There's also a ``polynomial2bezier()`` function in the pathtools.py
|
||||
submodule to convert polynomials back to Bezier curves.
|
||||
|
||||
**Note:** cubic Bezier curves are parameterized as
|
||||
|
||||
.. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3
|
||||
|
||||
where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the
|
||||
control points ``start``, ``control1``, ``control2``, and ``end``,
|
||||
respectively, that svgpathtools uses to define a CubicBezier object. The
|
||||
``CubicBezier.poly()`` method expands this polynomial to its standard
|
||||
form
|
||||
|
||||
.. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3
|
||||
|
||||
where
|
||||
|
||||
.. math::
|
||||
|
||||
\begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} =
|
||||
\begin{bmatrix}
|
||||
-1 & 3 & -3 & 1\\
|
||||
3 & -6 & -3 & 0\\
|
||||
-3 & 3 & 0 & 0\\
|
||||
1 & 0 & 0 & 0\\
|
||||
\end{bmatrix}
|
||||
\begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix}
|
||||
|
||||
QuadraticBezier.poly() and Line.poly() are defined similarly.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Example:
|
||||
b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)
|
||||
p = b.poly()
|
||||
|
||||
# p(t) == b.point(t)
|
||||
print(p(0.235) == b.point(0.235))
|
||||
|
||||
# What is p(t)? It's just the cubic b written in standard form.
|
||||
bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints())
|
||||
print("The CubicBezier, b.point(x) = \n\n" +
|
||||
bpretty + "\n\n" +
|
||||
"can be rewritten in standard form as \n\n" +
|
||||
str(p).replace('x','t'))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
True
|
||||
The CubicBezier, b.point(x) =
|
||||
|
||||
(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3
|
||||
|
||||
can be rewritten in standard form as
|
||||
|
||||
3 2
|
||||
(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)
|
||||
|
||||
|
||||
To illustrate the awesomeness of being able to convert our Bezier curve
|
||||
objects to numpy.poly1d objects and back, lets compute the unit tangent
|
||||
vector of the above CubicBezier object, b, at t=0.5 in four different
|
||||
ways.
|
||||
|
||||
Tangent vectors (and more on polynomials)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
t = 0.5
|
||||
### Method 1: the easy way
|
||||
u1 = b.unit_tangent(t)
|
||||
|
||||
### Method 2: another easy way
|
||||
# Note: This way will fail if it encounters a removable singularity.
|
||||
u2 = b.derivative(t)/abs(b.derivative(t))
|
||||
|
||||
### Method 2: a third easy way
|
||||
# Note: This way will also fail if it encounters a removable singularity.
|
||||
dp = p.deriv()
|
||||
u3 = dp(t)/abs(dp(t))
|
||||
|
||||
### Method 4: the removable-singularity-proof numpy.poly1d way
|
||||
# Note: This is roughly how Method 1 works
|
||||
from svgpathtools import real, imag, rational_limit
|
||||
dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy
|
||||
p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2
|
||||
# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,
|
||||
# the limit_{t->t0}[f(t) / abs(f(t))] ==
|
||||
# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])
|
||||
from cmath import sqrt
|
||||
u4 = sqrt(rational_limit(dp**2, p_mag2, t))
|
||||
|
||||
print("unit tangent check:", u1 == u2 == u3 == u4)
|
||||
|
||||
# Let's do a visual check
|
||||
mag = b.length()/4 # so it's not hard to see the tangent line
|
||||
tangent_line = Line(b.point(t), b.point(t) + mag*u1)
|
||||
disvg([b, tangent_line], 'bg', nodes=[b.point(t)])
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
unit tangent check: True
|
||||
|
||||
|
||||
Translations (shifts), reversing orientation, and normal vectors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Speaking of tangents, let's add a normal vector to the picture
|
||||
n = b.normal(t)
|
||||
normal_line = Line(b.point(t), b.point(t) + mag*n)
|
||||
disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])
|
||||
|
||||
# and let's reverse the orientation of b!
|
||||
# the tangent and normal lines should be sent to their opposites
|
||||
br = b.reversed()
|
||||
|
||||
# Let's also shift b_r over a bit to the right so we can view it next to b
|
||||
# The simplest way to do this is br = br.translated(3*mag), but let's use
|
||||
# the .bpoints() instead, which returns a Bezier's control points
|
||||
br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] #
|
||||
|
||||
tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))
|
||||
normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))
|
||||
wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r],
|
||||
'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg',
|
||||
text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r])
|
||||
|
||||
.. figure:: vectorframes.svg
|
||||
:alt: vectorframes.svg
|
||||
|
||||
vectorframes.svg
|
||||
|
||||
Rotations and Translations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's take a Line and an Arc and make some pictures
|
||||
top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)
|
||||
midline = Line(-1.5, 1.5)
|
||||
|
||||
# First let's make our ellipse whole
|
||||
bottom_half = top_half.rotated(180)
|
||||
decorated_ellipse = Path(top_half, bottom_half)
|
||||
|
||||
# Now let's add the decorations
|
||||
for k in range(12):
|
||||
decorated_ellipse.append(midline.rotated(30*k))
|
||||
|
||||
# Let's move it over so we can see the original Line and Arc object next
|
||||
# to the final product
|
||||
decorated_ellipse = decorated_ellipse.translated(4+0j)
|
||||
wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')
|
||||
|
||||
.. figure:: decorated_ellipse.svg
|
||||
:alt: decorated\_ellipse.svg
|
||||
|
||||
decorated\_ellipse.svg
|
||||
|
||||
arc length and inverse arc length
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Here we'll create an SVG that shows off the parametric and geometric
|
||||
midpoints of the paths from ``test.svg``. We'll need to compute use the
|
||||
``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``,
|
||||
``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the
|
||||
related inverse arc length methods ``.ilength()`` function to do this.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# First we'll load the path data from the file test.svg
|
||||
paths, attributes = svg2paths('test.svg')
|
||||
|
||||
# Let's mark the parametric midpoint of each segment
|
||||
# I say "parametric" midpoint because Bezier curves aren't
|
||||
# parameterized by arclength
|
||||
# If they're also the geometric midpoint, let's mark them
|
||||
# purple and otherwise we'll mark the geometric midpoint green
|
||||
min_depth = 5
|
||||
error = 1e-4
|
||||
dots = []
|
||||
ncols = []
|
||||
nradii = []
|
||||
for path in paths:
|
||||
for seg in path:
|
||||
parametric_mid = seg.point(0.5)
|
||||
seg_length = seg.length()
|
||||
if seg.length(0.5)/seg.length() == 1/2:
|
||||
dots += [parametric_mid]
|
||||
ncols += ['purple']
|
||||
nradii += [5]
|
||||
else:
|
||||
t_mid = seg.ilength(seg_length/2)
|
||||
geo_mid = seg.point(t_mid)
|
||||
dots += [parametric_mid, geo_mid]
|
||||
ncols += ['red', 'green']
|
||||
nradii += [5] * 2
|
||||
|
||||
# In 'output2.svg' the paths will retain their original attributes
|
||||
wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii,
|
||||
attributes=attributes, filename='output2.svg')
|
||||
|
||||
.. figure:: output2.svg
|
||||
:alt: output2.svg
|
||||
|
||||
output2.svg
|
||||
|
||||
Intersections between Bezier curves
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Let's find all intersections between redpath and the other
|
||||
redpath = paths[0]
|
||||
redpath_attribs = attributes[0]
|
||||
intersections = []
|
||||
for path in paths[1:]:
|
||||
for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):
|
||||
intersections.append(redpath.point(T1))
|
||||
|
||||
disvg(paths, filename='output_intersections.svg', attributes=attributes,
|
||||
nodes = intersections, node_radii = [5]*len(intersections))
|
||||
|
||||
.. figure:: output_intersections.svg
|
||||
:alt: output\_intersections.svg
|
||||
|
||||
output\_intersections.svg
|
||||
|
||||
An Advanced Application: Offsetting Bezier Curves
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Here we'll find the `offset
|
||||
curve <https://en.wikipedia.org/wiki/Parallel_curve>`__ for a few Bezier
|
||||
cubics.
|
||||
|
||||
.. code:: python
|
||||
|
||||
def display_offset_curve(bezpath, offset_distance, steps=1000,
|
||||
stripe_width=3, stripe_colors='ygb',
|
||||
name='offsetcurves.svg', show_normal_line=True):
|
||||
"""Takes in a path of bezier curves, `bezpath`, and a distance,
|
||||
`offset_distance`, and displays the 'parallel' offset curve by placing a
|
||||
dot at a distance of `offset_distance` away at each step."""
|
||||
nls = []
|
||||
nodes = []
|
||||
line_colors = ''
|
||||
node_colors = ''
|
||||
|
||||
for bez in bezpath:
|
||||
ct = 1
|
||||
for k in range(steps):
|
||||
t = k / steps
|
||||
nl = Line(bez.point(t),
|
||||
bez.point(t) + offset_distance * bez.normal(t))
|
||||
nls.append(nl)
|
||||
line_colors += stripe_colors[ct % 3]
|
||||
if not (k % stripe_width):
|
||||
ct += 1
|
||||
nodes.append(bez.point(t))
|
||||
nodes.append(nl.end)
|
||||
node_colors += 'kr'
|
||||
# nls.reverse()
|
||||
if show_normal_line:
|
||||
wsvg([bezpath] + nls, 'k' + line_colors,
|
||||
nodes=nodes, node_colors=node_colors,
|
||||
filename=name)
|
||||
else:
|
||||
wsvg(bezpath, 'k',
|
||||
nodes=nodes, node_colors=node_colors,
|
||||
filename=name)
|
||||
|
||||
bez1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")[0]
|
||||
bez2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339")[0]
|
||||
bez3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0")[0]
|
||||
path = Path(bez1, bez2.translated(300), bez3.translated(500+400j))
|
||||
|
||||
display_offset_curve(path, 500)
|
||||
|
||||
.. figure:: offsetcurves.svg
|
||||
:alt: offsetcurves.svg
|
||||
|
||||
offsetcurves.svg
|
||||
|
||||
Compatibility Notes for users of svg.path (v2.0)
|
||||
------------------------------------------------
|
||||
|
||||
- renamed Arc.arc attribute as Arc.large\_arc
|
||||
|
||||
- Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0),
|
||||
set both useSandT and use\_closed\_attrib to be True.
|
||||
|
||||
2 The behavior would be identical, but the string formatting used in
|
||||
this method has been changed to use default format (instead of the
|
||||
General format, {:G}), for inceased precision. `↩ <#a2>`__
|
||||
|
||||
To Do
|
||||
-----
|
||||
|
||||
- Implement Arc X Arc intersections.
|
||||
- Implement Arc.radialrange() method.
|
||||
|
||||
Licence
|
||||
-------
|
||||
|
||||
This module is under a MIT License.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="344px" version="1.1" viewBox="-2.2035 -2.4035 8.407 4.807" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M -1,0 A 1.0,2.0 0 1,1 1,0" fill="none" stroke="#000000" stroke-width="0.007"/>
|
||||
<path d="M -1.5,0.0 L 1.5,0.0" fill="none" stroke="#000000" stroke-width="0.007"/>
|
||||
<path d="M 3.0,0.0 A 1.0,2.0 0 1,1 5.0,0.0 M 5.0,-1.22464679915e-16 A 1.0,2.0 180 1,1 3.0,1.22464679915e-16 M 2.5,0.0 L 5.5,0.0 M 2.70096189432,-0.75 L 5.29903810568,0.75 M 3.25,-1.29903810568 L 4.75,1.29903810568 M 4.0,-1.5 L 4.0,1.5 M 4.75,-1.29903810568 L 3.25,1.29903810568 M 5.29903810568,-0.75 L 2.70096189432,0.75 M 5.5,-1.83697019872e-16 L 2.5,1.83697019872e-16 M 5.29903810568,0.75 L 2.70096189432,-0.75 M 4.75,1.29903810568 L 3.25,-1.29903810568 M 4.0,1.5 L 4.0,-1.5 M 3.25,1.29903810568 L 4.75,-1.29903810568 M 2.70096189432,0.75 L 5.29903810568,-0.75" fill="none" stroke="#000000" stroke-width="0.007"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" version="1.1" viewBox="161.5 79.0 152.0 242.0" width="377px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0" fill="none" stroke="blue" stroke-width="0.2"/>
|
||||
<path d="M 175.0,162.5 L 175.0,236.805280985" fill="none" stroke="green" stroke-width="0.2"/>
|
||||
<path d="M 175.0,162.5 L 249.305280985,162.5" fill="none" stroke="pink" stroke-width="0.2"/>
|
||||
<circle cx="175.0" cy="162.5" fill="#ff0000" r="1.0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 835 KiB |
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" version="1.1" viewBox="-20.6525 -20.6525 366.305 366.305" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
|
||||
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
|
||||
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
|
||||
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 776 B |
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" version="1.1" viewBox="-25.5 -25.5 376.0 376.0" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
|
||||
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
|
||||
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
|
||||
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
|
||||
<circle cx="52.5625" cy="27.5" fill="red" r="5"/>
|
||||
<circle cx="52.6653942856" cy="27.5003296408" fill="green" r="5"/>
|
||||
<circle cx="137.5" cy="132.5" fill="purple" r="5"/>
|
||||
<circle cx="104.0" cy="99.5" fill="purple" r="5"/>
|
||||
<circle cx="76.25" cy="55.0" fill="red" r="5"/>
|
||||
<circle cx="72.4661531169" cy="63.8283469353" fill="green" r="5"/>
|
||||
<circle cx="60.0" cy="265.0" fill="purple" r="5"/>
|
||||
<circle cx="120.5100001" cy="144.933334054" fill="red" r="5"/>
|
||||
<circle cx="115.010503028" cy="151.954994361" fill="green" r="5"/>
|
||||
<circle cx="167.55" cy="157.45" fill="purple" r="5"/>
|
||||
<circle cx="159.42414315" cy="96.77414315" fill="red" r="5"/>
|
||||
<circle cx="159.42414315" cy="96.77414315" fill="green" r="5"/>
|
||||
<circle cx="265.05" cy="59.95" fill="purple" r="5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" version="1.1" viewBox="-25.5 -25.5 376.0 376.0" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M 10.5,80.0 C 40.0,10.0 65.0,10.0 95.0,80.0 C 125.0,150.0 150.0,150.0 180.0,80.0" fill="none" stroke="red"/>
|
||||
<path d="M 84.0,92.0 C 94.0,102.0 114.0,102.0 124.0,92.0" fill="none" stroke="purple"/>
|
||||
<path d="M 40.0,100.0 Q 100.0,50.0 65.0,20.0" fill="none" stroke="blue"/>
|
||||
<path d="M 10.0,315.0 L 110.0,215.0 A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45 L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9 L 315.0,10.0" fill="green" fill-opacity="0.5" stroke="black" stroke-width="2"/>
|
||||
<circle cx="104.172804482" cy="99.4995576437" fill="#ff0000" r="5"/>
|
||||
<circle cx="77.8257146608" cy="46.8489740154" fill="#ff0000" r="5"/>
|
||||
<circle cx="175.113071158" cy="90.8738283981" fill="#ff0000" r="5"/>
|
||||
<circle cx="154.99853466" cy="123.15366017" fill="#ff0000" r="5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" version="1.1" viewBox="193.894237886 74.875 72.0191453624 300.25" width="144px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs/>
|
||||
<path d="M 200.0,100.0 L 200.0,300.0 L 250.0,350.0 C 275.0,350.0 250.0,225.0 200.0,100.0" fill="none" stroke="#000000" stroke-width="0.25"/>
|
||||
</svg>
|
After Width: | Height: | Size: 424 B |
|
@ -0,0 +1,25 @@
|
|||
from distutils.core import setup
|
||||
|
||||
VERSION = '1.0'
|
||||
AUTHOR_NAME = 'Andy Port'
|
||||
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
||||
|
||||
setup(name='svgpathtools',
|
||||
packages=['svgpathtools'],
|
||||
version=VERSION,
|
||||
description=('A collection of tools for manipulating and analyzing SVG '
|
||||
'Path objects and Bezier curves.'),
|
||||
long_description=open('README.rst').read(),
|
||||
author=AUTHOR_NAME,
|
||||
author_email=AUTHOR_EMAIL,
|
||||
url='https://github.com/mathandy/svgpathtools',
|
||||
download_url = 'http://github.com/mathandy/svgpathtools/tarball/1.0',
|
||||
license='MIT',
|
||||
|
||||
# install_requires=['numpy', 'svgwrite'],
|
||||
platforms="OS Independent",
|
||||
# test_suite='tests',
|
||||
requires=['numpy', 'svgwrite'],
|
||||
keywords=['svg', 'bezier', 'svg.path'],
|
||||
classifiers = [],
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
from .bezier import (bezier_point, bezier2polynomial,
|
||||
polynomial2bezier, split_bezier,
|
||||
bezier_bounding_box, bezier_intersections,
|
||||
bezier_by_line_intersections)
|
||||
from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
|
||||
bezier_segment, is_bezier_segment, is_path_segment,
|
||||
is_bezier_path, concatpaths, poly2bez, bpoints2bezier,
|
||||
closest_point_in_path, farthest_point_in_path,
|
||||
path_encloses_pt, bbox2path)
|
||||
from .parser import parse_path
|
||||
from .paths2svg import disvg, wsvg
|
||||
from .polytools import polyroots, rational_limit, real, imag
|
||||
from .misctools import hex2rgb, rgb2hex
|
||||
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
|
||||
|
||||
try:
|
||||
from .svg2paths import svg2paths
|
||||
except ImportError:
|
||||
pass
|
|
@ -0,0 +1,374 @@
|
|||
"""This submodule contains tools that deal with generic, degree n, Bezier
|
||||
curves.
|
||||
Note: Bezier curves here are always represented by the tuple of their control
|
||||
points given by their standard representation."""
|
||||
|
||||
# External dependencies:
|
||||
from __future__ import division, absolute_import, print_function
|
||||
from math import factorial as fac, ceil, log, sqrt
|
||||
from numpy import poly1d
|
||||
|
||||
# Internal dependencies
|
||||
from .polytools import real, imag, polyroots, polyroots01
|
||||
|
||||
|
||||
# Evaluation ##################################################################
|
||||
|
||||
def n_choose_k(n, k):
|
||||
return fac(n)//fac(k)//fac(n-k)
|
||||
|
||||
|
||||
def bernstein(n, t):
|
||||
"""returns a list of the Bernstein basis polynomials b_{i, n} evaluated at
|
||||
t, for i =0...n"""
|
||||
t1 = 1-t
|
||||
return [n_choose_k(n, k) * t1**(n-k) * t**k for k in range(n+1)]
|
||||
|
||||
|
||||
def bezier_point(p, t):
|
||||
"""Evaluates the Bezier curve given by it's control points, p, at t.
|
||||
Note: Uses Horner's rule for cubic and lower order Bezier curves.
|
||||
Warning: Be concerned about numerical stability when using this function
|
||||
with high order curves."""
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
p.large_arc
|
||||
return p.point(t)
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
|
||||
deg = len(p) - 1
|
||||
if deg == 3:
|
||||
return p[0] + t*(
|
||||
3*(p[1] - p[0]) + t*(
|
||||
3*(p[0] + p[2]) - 6*p[1] + t*(
|
||||
-p[0] + 3*(p[1] - p[2]) + p[3])))
|
||||
elif deg == 2:
|
||||
return p[0] + t*(
|
||||
2*(p[1] - p[0]) + t*(
|
||||
p[0] - 2*p[1] + p[2]))
|
||||
elif deg == 1:
|
||||
return p[0] + t*(p[1] - p[0])
|
||||
elif deg == 0:
|
||||
return p[0]
|
||||
else:
|
||||
bern = bernstein(deg, t)
|
||||
return sum(bern[k]*p[k] for k in range(deg+1))
|
||||
|
||||
|
||||
# Conversion ##################################################################
|
||||
|
||||
def bezier2polynomial(p, numpy_ordering=True, return_poly1d=False):
|
||||
"""Converts a tuple of Bezier control points to a tuple of coefficients
|
||||
of the expanded polynomial.
|
||||
return_poly1d : returns a numpy.poly1d object. This makes computations
|
||||
of derivatives/anti-derivatives and many other operations quite quick.
|
||||
numpy_ordering : By default (to accommodate numpy) the coefficients will
|
||||
be output in reverse standard order."""
|
||||
if len(p) == 4:
|
||||
coeffs = (-p[0] + 3*(p[1] - p[2]) + p[3],
|
||||
3*(p[0] - 2*p[1] + p[2]),
|
||||
3*(p[1]-p[0]),
|
||||
p[0])
|
||||
elif len(p) == 3:
|
||||
coeffs = (p[0] - 2*p[1] + p[2],
|
||||
2*(p[1] - p[0]),
|
||||
p[0])
|
||||
elif len(p) == 2:
|
||||
coeffs = (p[1]-p[0],
|
||||
p[0])
|
||||
elif len(p) == 1:
|
||||
coeffs = p
|
||||
else:
|
||||
# https://en.wikipedia.org/wiki/Bezier_curve#Polynomial_form
|
||||
n = len(p) + 1
|
||||
coeffs = [fac(n)//fac(n-j) * sum(
|
||||
(-1)**(i+j) * p[i] / (fac(i) * fac(j-i)) for i in xrange(j+1))
|
||||
for j in range(n+1)]
|
||||
if not numpy_ordering:
|
||||
coeffs.reverse()
|
||||
if return_poly1d:
|
||||
return poly1d(coeffs)
|
||||
return coeffs
|
||||
|
||||
|
||||
def polynomial2bezier(poly):
|
||||
"""Converts a cubic or lower order Polynomial object (or a sequence of
|
||||
coefficients) to a CubicBezier, QuadraticBezier, or Line object as
|
||||
appropriate."""
|
||||
if isinstance(poly, poly1d):
|
||||
c = poly.coeffs
|
||||
else:
|
||||
c = poly
|
||||
order = len(c)-1
|
||||
if order == 3:
|
||||
bpoints = (c[3], c[2]/3 + c[3], (c[1] + 2*c[2])/3 + c[3],
|
||||
c[0] + c[1] + c[2] + c[3])
|
||||
elif order == 2:
|
||||
bpoints = (c[2], c[1]/2 + c[2], c[0] + c[1] + c[2])
|
||||
elif order == 1:
|
||||
bpoints = (c[1], c[0] + c[1])
|
||||
else:
|
||||
raise AssertionError("This function is only implemented for linear, "
|
||||
"quadratic, and cubic polynomials.")
|
||||
return bpoints
|
||||
|
||||
|
||||
# Curve Splitting #############################################################
|
||||
|
||||
def split_bezier(bpoints, t):
|
||||
"""Uses deCasteljau's recursion to split the Bezier curve at t into two
|
||||
Bezier curves of the same order."""
|
||||
def split_bezier_recursion(bpoints_left_, bpoints_right_, bpoints_, t_):
|
||||
if len(bpoints_) == 1:
|
||||
bpoints_left_.append(bpoints_[0])
|
||||
bpoints_right_.append(bpoints_[0])
|
||||
else:
|
||||
new_points = [None]*(len(bpoints_) - 1)
|
||||
bpoints_left_.append(bpoints_[0])
|
||||
bpoints_right_.append(bpoints_[-1])
|
||||
for i in range(len(bpoints_) - 1):
|
||||
new_points[i] = (1 - t_)*bpoints_[i] + t_*bpoints_[i + 1]
|
||||
bpoints_left_, bpoints_right_ = split_bezier_recursion(
|
||||
bpoints_left_, bpoints_right_, new_points, t_)
|
||||
return bpoints_left_, bpoints_right_
|
||||
|
||||
bpoints_left = []
|
||||
bpoints_right = []
|
||||
bpoints_left, bpoints_right = \
|
||||
split_bezier_recursion(bpoints_left, bpoints_right, bpoints, t)
|
||||
bpoints_right.reverse()
|
||||
return bpoints_left, bpoints_right
|
||||
|
||||
|
||||
def halve_bezier(p):
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
p.large_arc
|
||||
return p.split(0.5)
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
|
||||
if len(p) == 4:
|
||||
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
|
||||
(p[0] + 3*p[1] + 3*p[2] + p[3])/8],
|
||||
[(p[0] + 3*p[1] + 3*p[2] + p[3])/8,
|
||||
(p[1] + 2*p[2] + p[3])/4, (p[2] + p[3])/2, p[3]])
|
||||
else:
|
||||
return split_bezier(p, 0.5)
|
||||
|
||||
|
||||
# Bounding Boxes ##############################################################
|
||||
|
||||
def bezier_real_minmax(p):
|
||||
"""returns the minimum and maximum for any real cubic bezier"""
|
||||
local_extremizers = [0, 1]
|
||||
if len(p) == 4: # cubic case
|
||||
a = [p.real for p in p]
|
||||
denom = a[0] - 3*a[1] + 3*a[2] - a[3]
|
||||
if denom != 0:
|
||||
delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3]
|
||||
if delta >= 0: # otherwise no local extrema
|
||||
sqdelta = sqrt(delta)
|
||||
tau = a[0] - 2*a[1] + a[2]
|
||||
r1 = (tau + sqdelta)/denom
|
||||
r2 = (tau - sqdelta)/denom
|
||||
if 0 < r1 < 1:
|
||||
local_extremizers.append(r1)
|
||||
if 0 < r2 < 1:
|
||||
local_extremizers.append(r2)
|
||||
local_extrema = [bezier_point(a, t) for t in local_extremizers]
|
||||
return min(local_extrema), max(local_extrema)
|
||||
|
||||
# find reverse standard coefficients of the derivative
|
||||
dcoeffs = bezier2polynomial(a, return_poly1d=True).deriv().coeffs
|
||||
|
||||
# find real roots, r, such that 0 <= r <= 1
|
||||
local_extremizers += polyroots01(dcoeffs)
|
||||
local_extrema = [bezier_point(a, t) for t in local_extremizers]
|
||||
return min(local_extrema), max(local_extrema)
|
||||
|
||||
|
||||
def bezier_bounding_box(bez):
|
||||
"""returns the bounding box for the segment in the form
|
||||
(xmin, xmax, ymin, ymax).
|
||||
Warning: For the non-cubic case this is not particularly efficient."""
|
||||
|
||||
# begin arc support block ########################
|
||||
try:
|
||||
bla = bez.large_arc
|
||||
return bez.bbox() # added to support Arc objects
|
||||
except:
|
||||
pass
|
||||
# end arc support block ##########################
|
||||
|
||||
if len(bez) == 4:
|
||||
xmin, xmax = bezier_real_minmax([p.real for p in bez])
|
||||
ymin, ymax = bezier_real_minmax([p.imag for p in bez])
|
||||
return xmin, xmax, ymin, ymax
|
||||
poly = bezier2polynomial(bez, return_poly1d=True)
|
||||
x = real(poly)
|
||||
y = imag(poly)
|
||||
dx = x.deriv()
|
||||
dy = y.deriv()
|
||||
x_extremizers = [0, 1] + polyroots(dx, realroots=True,
|
||||
condition=lambda r: 0 < r < 1)
|
||||
y_extremizers = [0, 1] + polyroots(dy, realroots=True,
|
||||
condition=lambda r: 0 < r < 1)
|
||||
x_extrema = [x(t) for t in x_extremizers]
|
||||
y_extrema = [y(t) for t in y_extremizers]
|
||||
return min(x_extrema), max(x_extrema), min(y_extrema), max(y_extrema)
|
||||
|
||||
|
||||
def box_area(xmin, xmax, ymin, ymax):
|
||||
"""
|
||||
INPUT: 2-tuple of cubics (given by control points)
|
||||
OUTPUT: boolean
|
||||
"""
|
||||
return (xmax - xmin)*(ymax - ymin)
|
||||
|
||||
|
||||
def interval_intersection_width(a, b, c, d):
|
||||
"""returns the width of the intersection of intervals [a,b] and [c,d]
|
||||
(thinking of these as intervals on the real number line)"""
|
||||
return max(0, min(b, d) - max(a, c))
|
||||
|
||||
|
||||
def boxes_intersect(box1, box2):
|
||||
"""Determines if two rectangles, each input as a tuple
|
||||
(xmin, xmax, ymin, ymax), intersect."""
|
||||
xmin1, xmax1, ymin1, ymax1 = box1
|
||||
xmin2, xmax2, ymin2, ymax2 = box2
|
||||
if interval_intersection_width(xmin1, xmax1, xmin2, xmax2) and \
|
||||
interval_intersection_width(ymin1, ymax1, ymin2, ymax2):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
# Intersections ###############################################################
|
||||
|
||||
class ApproxSolutionSet(list):
|
||||
"""A class that behaves like a set but treats two elements , x and y, as
|
||||
equivalent if abs(x-y) < self.tol"""
|
||||
def __init__(self, tol):
|
||||
self.tol = tol
|
||||
|
||||
def __contains__(self, x):
|
||||
for y in self:
|
||||
if abs(x - y) < self.tol:
|
||||
return True
|
||||
return False
|
||||
|
||||
def appadd(self, pt):
|
||||
if pt not in self:
|
||||
self.append(pt)
|
||||
|
||||
|
||||
class BPair(object):
|
||||
def __init__(self, bez1, bez2, t1, t2):
|
||||
self.bez1 = bez1
|
||||
self.bez2 = bez2
|
||||
self.t1 = t1 # t value to get the mid point of this curve from cub1
|
||||
self.t2 = t2 # t value to get the mid point of this curve from cub2
|
||||
|
||||
|
||||
def bezier_intersections(bez1, bez2, longer_length, tol=1e-8, tol_deC=1e-8):
|
||||
"""INPUT:
|
||||
bez1, bez2 = [P0,P1,P2,...PN], [Q0,Q1,Q2,...,PN] defining the two
|
||||
Bezier curves to check for intersections between.
|
||||
longer_length - the length (or an upper bound) on the longer of the two
|
||||
Bezier curves. Determines the maximum iterations needed together with tol.
|
||||
tol - is the smallest distance that two solutions can differ by and still
|
||||
be considered distinct solutions.
|
||||
OUTPUT: a list of tuples (t,s) in [0,1]x[0,1] such that
|
||||
bezier_point(cubs[0],t) - bezier_point(cubs[1],s) < tol_deC
|
||||
Note: This will return exactly one such tuple for each intersection
|
||||
(assuming tol_deC is small enough)"""
|
||||
maxits = int(ceil(1-log(tol_deC/longer_length)/log(2)))
|
||||
pair_list = [BPair(bez1, bez2, 0.5, 0.5)]
|
||||
intersection_list = []
|
||||
k = 0
|
||||
approx_point_set = ApproxSolutionSet(tol)
|
||||
while pair_list and k < maxits:
|
||||
new_pairs = []
|
||||
delta = 0.5**(k + 2)
|
||||
for pair in pair_list:
|
||||
bbox1 = bezier_bounding_box(pair.bez1)
|
||||
bbox2 = bezier_bounding_box(pair.bez2)
|
||||
if boxes_intersect(bbox1, bbox2):
|
||||
if box_area(*bbox1) < tol_deC and box_area(*bbox2) < tol_deC:
|
||||
point = bezier_point(bez1, pair.t1)
|
||||
if point not in approx_point_set:
|
||||
approx_point_set.append(point)
|
||||
# this is the point in the middle of the pair
|
||||
intersection_list.append((pair.t1, pair.t2))
|
||||
|
||||
# this prevents the output of redundant intersection points
|
||||
for otherPair in pair_list:
|
||||
if pair.bez1 == otherPair.bez1 or \
|
||||
pair.bez2 == otherPair.bez2 or \
|
||||
pair.bez1 == otherPair.bez2 or \
|
||||
pair.bez2 == otherPair.bez1:
|
||||
pair_list.remove(otherPair)
|
||||
else:
|
||||
(c11, c12) = halve_bezier(pair.bez1)
|
||||
(t11, t12) = (pair.t1 - delta, pair.t1 + delta)
|
||||
(c21, c22) = halve_bezier(pair.bez2)
|
||||
(t21, t22) = (pair.t2 - delta, pair.t2 + delta)
|
||||
new_pairs += [BPair(c11, c21, t11, t21),
|
||||
BPair(c11, c22, t11, t22),
|
||||
BPair(c12, c21, t12, t21),
|
||||
BPair(c12, c22, t12, t22)]
|
||||
pair_list = new_pairs
|
||||
k += 1
|
||||
if k >= maxits:
|
||||
raise Exception("bezier_intersections has reached maximum "
|
||||
"iterations without terminating... "
|
||||
"either there's a problem/bug or you can fix by "
|
||||
"raising the max iterations or lowering tol_deC")
|
||||
return intersection_list
|
||||
|
||||
|
||||
def bezier_by_line_intersections(bezier, line):
|
||||
"""Returns tuples (t1,t2) such that bezier.point(t1) ~= line.point(t2)."""
|
||||
# The method here is to translate (shift) then rotate the complex plane so
|
||||
# that line starts at the origin and proceeds along the positive real axis.
|
||||
# After this transformation, the intersection points are the real roots of
|
||||
# the imaginary component of the bezier for which the real component is
|
||||
# between 0 and abs(line[1]-line[0])].
|
||||
assert len(line[:]) == 2
|
||||
assert line[0] != line[1]
|
||||
if not any(p != bezier[0] for p in bezier):
|
||||
raise ValueError("bezier is nodal, use "
|
||||
"bezier_by_line_intersection(bezier[0], line) "
|
||||
"instead for a bool to be returned.")
|
||||
|
||||
# First let's shift the complex plane so that line starts at the origin
|
||||
shifted_bezier = [z - line[0] for z in bezier]
|
||||
shifted_line_end = line[1] - line[0]
|
||||
line_length = abs(shifted_line_end)
|
||||
|
||||
# Now let's rotate the complex plane so that line falls on the x-axis
|
||||
rotation_matrix = line_length/shifted_line_end
|
||||
transformed_bezier = [rotation_matrix*z for z in shifted_bezier]
|
||||
|
||||
# Now all intersections should be roots of the imaginary component of
|
||||
# the transformed bezier
|
||||
transformed_bezier_imag = [p.imag for p in transformed_bezier]
|
||||
coeffs_y = bezier2polynomial(transformed_bezier_imag)
|
||||
roots_y = list(polyroots01(coeffs_y)) # returns real roots 0 <= r <= 1
|
||||
|
||||
transformed_bezier_real = [p.real for p in transformed_bezier]
|
||||
intersection_list = []
|
||||
for bez_t in set(roots_y):
|
||||
xval = bezier_point(transformed_bezier_real, bez_t)
|
||||
if 0 <= xval <= line_length:
|
||||
line_t = xval/line_length
|
||||
intersection_list.append((bez_t, line_t))
|
||||
return intersection_list
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
"""This submodule contains miscellaneous tools that are used internally, but
|
||||
aren't specific to SVGs or related mathematical objects."""
|
||||
|
||||
# External dependencies:
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import os
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
|
||||
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
|
||||
def hex2rgb(value):
|
||||
"""Converts a hexadeximal color string to an RGB 3-tuple
|
||||
|
||||
EXAMPLE
|
||||
-------
|
||||
>>> hex2rgb('#0000FF')
|
||||
(0, 0, 255)
|
||||
"""
|
||||
value = value.lstrip('#')
|
||||
lv = len(value)
|
||||
return tuple(int(value[i:i+lv//3], 16) for i in range(0, lv, lv//3))
|
||||
|
||||
|
||||
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
|
||||
def rgb2hex(rgb):
|
||||
"""Converts an RGB 3-tuple to a hexadeximal color string.
|
||||
|
||||
EXAMPLE
|
||||
-------
|
||||
>>> rgb2hex((0,0,255))
|
||||
'#0000FF'
|
||||
"""
|
||||
return ('#%02x%02x%02x' % rgb).upper()
|
||||
|
||||
|
||||
def isclose(a, b, rtol=1e-5, atol=1e-8):
|
||||
"""This is essentially np.isclose, but slightly faster."""
|
||||
return abs(a - b) < (atol + rtol * abs(b))
|
||||
|
||||
|
||||
def open_in_browser(file_location):
|
||||
"""Attempt to open file located at file_location in the default web
|
||||
browser."""
|
||||
|
||||
# If just the name of the file was given, check if it's in the Current
|
||||
# Working Directory.
|
||||
if not os.path.isfile(file_location):
|
||||
file_location = os.path.join(os.getcwd(), file_location)
|
||||
if not os.path.isfile(file_location):
|
||||
raise IOError("\n\nFile not found.")
|
||||
|
||||
# For some reason OSX requires this adjustment (tested on 10.10.4)
|
||||
if sys.platform == "darwin":
|
||||
file_location = "file:///"+file_location
|
||||
|
||||
new = 2 # open in a new tab, if possible
|
||||
webbrowser.get().open(file_location, new=new)
|
||||
|
||||
|
||||
BugException = Exception("This code should never be reached. You've found a "
|
||||
"bug. Please submit an issue to \n"
|
||||
"https://github.com/mathandy/svgpathtools/issues"
|
||||
"\nwith an easily reproducible example.")
|
|
@ -0,0 +1,195 @@
|
|||
"""This submodule contains the path_parse() function used to convert SVG path
|
||||
element d-strings into svgpathtools Path objects.
|
||||
Note: This file was taken (nearly) as is from the svg.path module
|
||||
(v 2.0)."""
|
||||
|
||||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import re
|
||||
|
||||
# Internal dependencies
|
||||
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
|
||||
|
||||
|
||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||
UPPERCASE = set('MZLHVCSQTA')
|
||||
|
||||
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
||||
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
||||
|
||||
|
||||
def _tokenize_path(pathdef):
|
||||
for x in COMMAND_RE.split(pathdef):
|
||||
if x in COMMANDS:
|
||||
yield x
|
||||
for token in FLOAT_RE.findall(x):
|
||||
yield token
|
||||
|
||||
|
||||
def parse_path(pathdef, current_pos=0j):
|
||||
# In the SVG specs, initial movetos are absolute, even if
|
||||
# specified as 'm'. This is the default behavior here as well.
|
||||
# But if you pass in a current_pos variable, the initial moveto
|
||||
# will be relative to that current_pos. This is useful.
|
||||
elements = list(_tokenize_path(pathdef))
|
||||
# Reverse for easy use of .pop()
|
||||
elements.reverse()
|
||||
|
||||
segments = Path()
|
||||
start_pos = None
|
||||
command = None
|
||||
|
||||
while elements:
|
||||
|
||||
if elements[-1] in COMMANDS:
|
||||
# New command.
|
||||
last_command = command # Used by S and T
|
||||
command = elements.pop()
|
||||
absolute = command in UPPERCASE
|
||||
command = command.upper()
|
||||
else:
|
||||
# If this element starts with numbers, it is an implicit command
|
||||
# and we don't change the command. Check that it's allowed:
|
||||
if command is None:
|
||||
raise ValueError("Unallowed implicit command in %s, position %s" % (
|
||||
pathdef, len(pathdef.split()) - len(elements)))
|
||||
|
||||
if command == 'M':
|
||||
# Moveto command.
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if absolute:
|
||||
current_pos = pos
|
||||
else:
|
||||
current_pos += pos
|
||||
|
||||
# when M is called, reset start_pos
|
||||
# This behavior of Z is defined in svg spec:
|
||||
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
||||
start_pos = current_pos
|
||||
|
||||
# Implicit moveto commands are treated as lineto commands.
|
||||
# So we set command to lineto here, in case there are
|
||||
# further implicit commands after this moveto.
|
||||
command = 'L'
|
||||
|
||||
elif command == 'Z':
|
||||
# Close path
|
||||
segments.append(Line(current_pos, start_pos))
|
||||
segments.closed = True
|
||||
current_pos = start_pos
|
||||
start_pos = None
|
||||
command = None # You can't have implicit commands after closing.
|
||||
|
||||
elif command == 'L':
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'H':
|
||||
x = elements.pop()
|
||||
pos = float(x) + current_pos.imag * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.real
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'V':
|
||||
y = elements.pop()
|
||||
pos = current_pos.real + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.imag * 1j
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'C':
|
||||
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control1 += current_pos
|
||||
control2 += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(CubicBezier(current_pos, control1, control2, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'S':
|
||||
# Smooth curve. First control point is the "reflection" of
|
||||
# the second control point in the previous path.
|
||||
|
||||
if last_command not in 'CS':
|
||||
# If there is no previous command or if the previous command
|
||||
# was not an C, c, S or s, assume the first control point is
|
||||
# coincident with the current point.
|
||||
control1 = current_pos
|
||||
else:
|
||||
# The first control point is assumed to be the reflection of
|
||||
# the second control point on the previous command relative
|
||||
# to the current point.
|
||||
control1 = current_pos + current_pos - segments[-1].control2
|
||||
|
||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control2 += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(CubicBezier(current_pos, control1, control2, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'Q':
|
||||
control = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(QuadraticBezier(current_pos, control, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'T':
|
||||
# Smooth curve. Control point is the "reflection" of
|
||||
# the second control point in the previous path.
|
||||
|
||||
if last_command not in 'QT':
|
||||
# If there is no previous command or if the previous command
|
||||
# was not an Q, q, T or t, assume the first control point is
|
||||
# coincident with the current point.
|
||||
control = current_pos
|
||||
else:
|
||||
# The control point is assumed to be the reflection of
|
||||
# the control point on the previous command relative
|
||||
# to the current point.
|
||||
control = current_pos + current_pos - segments[-1].control
|
||||
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(QuadraticBezier(current_pos, control, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'A':
|
||||
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
rotation = float(elements.pop())
|
||||
arc = float(elements.pop())
|
||||
sweep = float(elements.pop())
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(Arc(current_pos, radius, rotation, arc, sweep, end))
|
||||
current_pos = end
|
||||
|
||||
return segments
|
|
@ -0,0 +1,379 @@
|
|||
"""This submodule contains tools for creating svg files from paths and path
|
||||
segments."""
|
||||
|
||||
# External dependencies:
|
||||
from __future__ import division, absolute_import, print_function
|
||||
from math import ceil
|
||||
from os import getcwd, path as os_path, makedirs
|
||||
from xml.dom.minidom import parse as md_xml_parse
|
||||
from svgwrite import Drawing, text as txt
|
||||
from time import time
|
||||
from warnings import warn
|
||||
|
||||
# Internal dependencies
|
||||
from .path import Path, Line, is_path_segment
|
||||
from .misctools import open_in_browser
|
||||
|
||||
# Used to convert a string colors (identified by single chars) to a list.
|
||||
color_dict = {'a': 'aqua',
|
||||
'b': 'blue',
|
||||
'c': 'cyan',
|
||||
'd': 'darkblue',
|
||||
'e': '',
|
||||
'f': '',
|
||||
'g': 'green',
|
||||
'h': '',
|
||||
'i': '',
|
||||
'j': '',
|
||||
'k': 'black',
|
||||
'l': 'lime',
|
||||
'm': 'magenta',
|
||||
'n': 'brown',
|
||||
'o': 'orange',
|
||||
'p': 'pink',
|
||||
'q': 'turquoise',
|
||||
'r': 'red',
|
||||
's': 'salmon',
|
||||
't': 'tan',
|
||||
'u': 'purple',
|
||||
'v': 'violet',
|
||||
'w': 'white',
|
||||
'x': '',
|
||||
'y': 'yellow',
|
||||
'z': 'azure'}
|
||||
|
||||
|
||||
def str2colorlist(s, default_color=None):
|
||||
color_list = [color_dict[ch] for ch in s]
|
||||
if default_color:
|
||||
for idx, c in enumerate(color_list):
|
||||
if not c:
|
||||
color_list[idx] = default_color
|
||||
return color_list
|
||||
|
||||
|
||||
def is3tuple(c):
|
||||
return isinstance(c, tuple) and len(c) == 3
|
||||
|
||||
|
||||
def big_bounding_box(paths_n_stuff):
|
||||
"""Finds a BB containing a collection of paths, Bezier path segments, and
|
||||
points (given as complex numbers)."""
|
||||
bbs = []
|
||||
for thing in paths_n_stuff:
|
||||
if is_path_segment(thing) or isinstance(thing, Path):
|
||||
bbs.append(thing.bbox())
|
||||
elif isinstance(thing, complex):
|
||||
bbs.append((thing.real, thing.real, thing.imag, thing.imag))
|
||||
else:
|
||||
try:
|
||||
complexthing = complex(thing)
|
||||
bbs.append((complexthing.real, complexthing.real,
|
||||
complexthing.imag, complexthing.imag))
|
||||
except ValueError:
|
||||
raise TypeError(
|
||||
"paths_n_stuff can only contains Path, CubicBezier, "
|
||||
"QuadraticBezier, Line, and complex objects.")
|
||||
xmins, xmaxs, ymins, ymaxs = zip(*bbs)
|
||||
xmin = min(xmins)
|
||||
xmax = max(xmaxs)
|
||||
ymin = min(ymins)
|
||||
ymax = max(ymaxs)
|
||||
return xmin, xmax, ymin, ymax
|
||||
|
||||
|
||||
def disvg(paths=None, colors=None,
|
||||
filename=os_path.join(getcwd(), 'disvg_output.svg'),
|
||||
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
|
||||
openinbrowser=True, timestamp=False,
|
||||
margin_size=0.1, mindim=600, dimensions=None,
|
||||
viewbox=None, text=None, text_path=None, font_size=None,
|
||||
attributes=None):
|
||||
"""Takes in a list of paths and creates an SVG file containing said paths.
|
||||
REQUIRED INPUTS:
|
||||
:param paths - a list of paths
|
||||
|
||||
OPTIONAL INPUT:
|
||||
:param colors - specifies the path stroke color. By default all paths
|
||||
will be black (#000000). This paramater can be input in a few ways
|
||||
1) a list of strings that will be input into the path elements stroke
|
||||
attribute (so anything that is understood by the svg viewer).
|
||||
2) a string of single character colors -- e.g. setting colors='rrr' is
|
||||
equivalent to setting colors=['red', 'red', 'red'] (see the
|
||||
'color_dict' dictionary above for a list of possibilities).
|
||||
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
|
||||
|
||||
:param filename - the desired location/filename of the SVG file
|
||||
created (by default the SVG will be stored in the current working
|
||||
directory and named 'disvg_output.svg').
|
||||
|
||||
:param stroke_widths - a list of stroke_widths to use for paths
|
||||
(default is 0.5% of the SVG's width or length)
|
||||
|
||||
:param nodes - a list of points to draw as filled-in circles
|
||||
|
||||
:param node_colors - a list of colors to use for the nodes (by default
|
||||
nodes will be red)
|
||||
|
||||
:param node_radii - a list of radii to use for the nodes (by default
|
||||
nodes will be radius will be 1 percent of the svg's width/length)
|
||||
|
||||
:param text - string or list of strings to be displayed
|
||||
|
||||
:param text_path - if text is a list, then this should be a list of
|
||||
path (or path segments of the same length. Note: the path must be
|
||||
long enough to display the text or the text will be cropped by the svg
|
||||
viewer.
|
||||
|
||||
:param font_size - a single float of list of floats.
|
||||
|
||||
:param openinbrowser - Set to True to automatically open the created
|
||||
SVG in the user's default web browser.
|
||||
|
||||
:param timestamp - if True, then the a timestamp will be appended to
|
||||
the output SVG's filename. This will fix issues with rapidly opening
|
||||
multiple SVGs in your browser.
|
||||
|
||||
:param margin_size - The min margin (empty area framing the collection
|
||||
of paths) size used for creating the canvas and background of the SVG.
|
||||
|
||||
:param mindim - The minimum dimension (height or width) of the output
|
||||
SVG (default is 600).
|
||||
|
||||
:param dimensions - The display dimensions of the output SVG. Using
|
||||
this will override the mindim parameter.
|
||||
|
||||
:param viewbox - This specifies what rectangular patch of R^2 will be
|
||||
viewable through the outputSVG. It should be input in the form
|
||||
(min_x, min_y, width, height). This is different from the display
|
||||
dimension of the svg, which can be set through mindim or dimensions.
|
||||
|
||||
:param attributes - a dictionary of attributes for the input paths.
|
||||
This will override any other conflicting settings.
|
||||
|
||||
NOTES:
|
||||
-The unit of length here is assumed to be pixels in all variables.
|
||||
|
||||
-If this function is used multiple times in quick succession to
|
||||
display multiple SVGs (all using the default filename), the
|
||||
svgviewer/browser will likely fail to load some of the SVGs in time.
|
||||
To fix this, use the timestamp attribute, or give the files unique
|
||||
names, or use a pause command (e.g. time.sleep(1)) between uses.
|
||||
"""
|
||||
|
||||
_default_relative_node_radius = 5e-3
|
||||
_default_relative_stroke_width = 1e-3
|
||||
_default_path_color = '#000000' # black
|
||||
_default_node_color = '#ff0000' # red
|
||||
_default_font_size = 12
|
||||
|
||||
# append directory to filename (if not included)
|
||||
if os_path.dirname(filename) == '':
|
||||
filename = os_path.join(getcwd(), filename)
|
||||
|
||||
# append time stamp to filename
|
||||
if timestamp:
|
||||
fbname, fext = os_path.splitext(filename)
|
||||
dirname = os_path.dirname(filename)
|
||||
tstamp = str(time()).replace('.', '')
|
||||
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
|
||||
filename = os_path.join(dirname, stfilename)
|
||||
|
||||
# check paths and colors are set
|
||||
if isinstance(paths, Path) or is_path_segment(paths):
|
||||
paths = [paths]
|
||||
if paths:
|
||||
if not colors:
|
||||
colors = [_default_path_color] * len(paths)
|
||||
else:
|
||||
assert len(colors) == len(paths)
|
||||
if isinstance(colors, str):
|
||||
colors = str2colorlist(colors,
|
||||
default_color=_default_path_color)
|
||||
elif isinstance(colors, list):
|
||||
for idx, c in enumerate(colors):
|
||||
if is3tuple(c):
|
||||
colors[idx] = "rgb" + str(c)
|
||||
|
||||
# check nodes and nodes_colors are set (node_radii are set later)
|
||||
if nodes:
|
||||
if not node_colors:
|
||||
node_colors = [_default_node_color] * len(nodes)
|
||||
else:
|
||||
assert len(node_colors) == len(nodes)
|
||||
if isinstance(node_colors, str):
|
||||
node_colors = str2colorlist(node_colors,
|
||||
default_color=_default_node_color)
|
||||
elif isinstance(node_colors, list):
|
||||
for idx, c in enumerate(node_colors):
|
||||
if is3tuple(c):
|
||||
node_colors[idx] = "rgb" + str(c)
|
||||
|
||||
# set up the viewBox and display dimensions of the output SVG
|
||||
# along the way, set stroke_widths and node_radii if not provided
|
||||
assert paths or nodes
|
||||
stuff2bound = []
|
||||
if viewbox:
|
||||
szx, szy = viewbox[2:4]
|
||||
else:
|
||||
if paths:
|
||||
stuff2bound += paths
|
||||
if nodes:
|
||||
stuff2bound += nodes
|
||||
if text_path:
|
||||
stuff2bound += text_path
|
||||
xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound)
|
||||
dx = xmax - xmin
|
||||
dy = ymax - ymin
|
||||
|
||||
if dx == 0:
|
||||
dx = 1
|
||||
if dy == 0:
|
||||
dy = 1
|
||||
|
||||
# determine stroke_widths to use (if not provided) and max_stroke_width
|
||||
if paths:
|
||||
if not stroke_widths:
|
||||
sw = max(dx, dy) * _default_relative_stroke_width
|
||||
stroke_widths = [sw]*len(paths)
|
||||
max_stroke_width = sw
|
||||
else:
|
||||
assert len(paths) == len(stroke_widths)
|
||||
max_stroke_width = max(stroke_widths)
|
||||
else:
|
||||
max_stroke_width = 0
|
||||
|
||||
# determine node_radii to use (if not provided) and max_node_diameter
|
||||
if nodes:
|
||||
if not node_radii:
|
||||
r = max(dx, dy) * _default_relative_node_radius
|
||||
node_radii = [r]*len(nodes)
|
||||
max_node_diameter = 2*r
|
||||
else:
|
||||
assert len(nodes) == len(node_radii)
|
||||
max_node_diameter = 2*max(node_radii)
|
||||
else:
|
||||
max_node_diameter = 0
|
||||
|
||||
extra_space_for_style = max(max_stroke_width, max_node_diameter)
|
||||
xmin -= margin_size*dx + extra_space_for_style/2
|
||||
ymin -= margin_size*dy + extra_space_for_style/2
|
||||
dx += 2*margin_size*dx + extra_space_for_style
|
||||
dy += 2*margin_size*dy + extra_space_for_style
|
||||
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
|
||||
if dimensions:
|
||||
szx, szy = dimensions
|
||||
else:
|
||||
if dx > dy:
|
||||
szx = str(mindim) + 'px'
|
||||
szy = str(int(ceil(mindim * dy / dx))) + 'px'
|
||||
else:
|
||||
szx = str(int(ceil(mindim * dx / dy))) + 'px'
|
||||
szy = str(mindim) + 'px'
|
||||
|
||||
# Create an SVG file
|
||||
dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox)
|
||||
|
||||
# add paths
|
||||
if paths:
|
||||
for i, p in enumerate(paths):
|
||||
if isinstance(p, Path):
|
||||
ps = p.d()
|
||||
elif is_path_segment(p):
|
||||
ps = Path(p).d()
|
||||
else: # assume this path, p, was input as a Path d-string
|
||||
ps = p
|
||||
|
||||
if attributes:
|
||||
good_attribs = {'d': ps}
|
||||
for key in attributes[i]:
|
||||
val = attributes[i][key]
|
||||
if key != 'd':
|
||||
try:
|
||||
dwg.path(ps, **{key: val})
|
||||
good_attribs.update({key: val})
|
||||
except Exception as e:
|
||||
warn(str(e))
|
||||
|
||||
dwg.add(dwg.path(**good_attribs))
|
||||
else:
|
||||
dwg.add(dwg.path(ps, stroke=colors[i],
|
||||
stroke_width=str(stroke_widths[i]),
|
||||
fill='none'))
|
||||
|
||||
# add nodes (filled in circles)
|
||||
if nodes:
|
||||
for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]):
|
||||
dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt]))
|
||||
|
||||
# add texts
|
||||
if text:
|
||||
assert isinstance(text, str) or (isinstance(text, list) and
|
||||
isinstance(text_path, list) and
|
||||
len(text_path) == len(text))
|
||||
if isinstance(text, str):
|
||||
text = [text]
|
||||
if not font_size:
|
||||
font_size = [_default_font_size]
|
||||
if not text_path:
|
||||
pos = complex(xmin + margin_size*dx, ymin + margin_size*dy)
|
||||
text_path = [Line(pos, pos + 1).d()]
|
||||
else:
|
||||
if font_size:
|
||||
if isinstance(font_size, list):
|
||||
assert len(font_size) == len(text)
|
||||
else:
|
||||
font_size = [font_size] * len(text)
|
||||
else:
|
||||
font_size = [_default_font_size] * len(text)
|
||||
for idx, s in enumerate(text):
|
||||
p = text_path[idx]
|
||||
if isinstance(p, Path):
|
||||
ps = p.d()
|
||||
elif is_path_segment(p):
|
||||
ps = Path(p).d()
|
||||
else: # assume this path, p, was input as a Path d-string
|
||||
ps = p
|
||||
|
||||
# paragraph = dwg.add(dwg.g(font_size=font_size[idx]))
|
||||
# paragraph.add(dwg.textPath(ps, s))
|
||||
pathid = 'tp' + str(idx)
|
||||
dwg.defs.add(dwg.path(d=ps, id=pathid))
|
||||
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
|
||||
txter.add(txt.TextPath('#'+pathid, s))
|
||||
|
||||
# save svg
|
||||
if not os_path.exists(os_path.dirname(filename)):
|
||||
makedirs(os_path.dirname(filename))
|
||||
dwg.save()
|
||||
|
||||
# re-open the svg, make the xml pretty, and save it again
|
||||
xmlstring = md_xml_parse(filename).toprettyxml()
|
||||
with open(filename, 'w') as f:
|
||||
f.write(xmlstring)
|
||||
|
||||
# try to open in web browser
|
||||
if openinbrowser:
|
||||
try:
|
||||
open_in_browser(filename)
|
||||
except:
|
||||
print("Failed to open output SVG in browser. SVG saved to:")
|
||||
print(filename)
|
||||
|
||||
|
||||
def wsvg(paths=None, colors=None,
|
||||
filename=os_path.join(getcwd(), 'disvg_output.svg'),
|
||||
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
|
||||
openinbrowser=False, timestamp=False,
|
||||
margin_size=0.1, mindim=600, dimensions=None,
|
||||
viewbox=None, text=None, text_path=None, font_size=None,
|
||||
attributes=None):
|
||||
"""Convenience function; identical to disvg() except that
|
||||
openinbrowser=False by default. See disvg() docstring for more info."""
|
||||
disvg(paths, colors=colors, filename=filename,
|
||||
stroke_widths=stroke_widths, nodes=nodes,
|
||||
node_colors=node_colors, node_radii=node_radii,
|
||||
openinbrowser=openinbrowser, timestamp=timestamp,
|
||||
margin_size=margin_size, mindim=mindim, dimensions=dimensions,
|
||||
viewbox=viewbox, text=text, text_path=text_path,
|
||||
font_size=font_size, attributes=attributes)
|
|
@ -0,0 +1,14 @@
|
|||
"""This submodule contains additional tools for working with paths composed of
|
||||
Line and CubicBezier objects. QuadraticBezier and Arc objects are only
|
||||
partially supported."""
|
||||
|
||||
# External dependencies:
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
# Internal dependencies
|
||||
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
|
||||
|
||||
|
||||
# Misc#########################################################################
|
||||
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
"""This submodule contains tools for working with numpy.poly1d objects."""
|
||||
|
||||
# External Dependencies
|
||||
from __future__ import division, absolute_import
|
||||
from itertools import combinations
|
||||
import numpy as np
|
||||
|
||||
# Internal Dependencies
|
||||
from .misctools import isclose
|
||||
|
||||
|
||||
def polyroots(p, realroots=False, condition=lambda r: True):
|
||||
"""
|
||||
Returns the roots of a polynomial with coefficients given in p.
|
||||
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
|
||||
INPUT:
|
||||
p - Rank-1 array-like object of polynomial coefficients.
|
||||
realroots - a boolean. If true, only real roots will be returned and the
|
||||
condition function can be written assuming all roots are real.
|
||||
condition - a boolean-valued function. Only roots satisfying this will be
|
||||
returned. If realroots==True, these conditions should assume the roots
|
||||
are real.
|
||||
OUTPUT:
|
||||
A list containing the roots of the polynomial.
|
||||
NOTE: This uses np.isclose and np.roots"""
|
||||
roots = np.roots(p)
|
||||
if realroots:
|
||||
roots = [r.real for r in roots if isclose(r.imag, 0)]
|
||||
roots = [r for r in roots if condition(r)]
|
||||
|
||||
duplicates = []
|
||||
for idx, (r1, r2) in enumerate(combinations(roots, 2)):
|
||||
if isclose(r1, r2):
|
||||
duplicates.append(idx)
|
||||
return [r for idx, r in enumerate(roots) if idx not in duplicates]
|
||||
|
||||
|
||||
def polyroots01(p):
|
||||
"""Returns the real roots between 0 and 1 of the polynomial with
|
||||
coefficients given in p,
|
||||
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
|
||||
p can also be a np.poly1d object. See polyroots for more information."""
|
||||
return polyroots(p, realroots=True, condition=lambda tval: 0 <= tval <= 1)
|
||||
|
||||
|
||||
def rational_limit(f, g, t0):
|
||||
"""Computes the limit of the rational function (f/g)(t)
|
||||
as t approaches t0."""
|
||||
assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d)
|
||||
assert g != np.poly1d([0])
|
||||
if g(t0) != 0:
|
||||
return f(t0)/g(t0)
|
||||
elif f(t0) == 0:
|
||||
return rational_limit(f.deriv(), g.deriv(), t0)
|
||||
else:
|
||||
raise ValueError("Limit does not exist.")
|
||||
|
||||
|
||||
def real(z):
|
||||
try:
|
||||
return np.poly1d(z.coeffs.real)
|
||||
except AttributeError:
|
||||
return z.real
|
||||
|
||||
|
||||
def imag(z):
|
||||
try:
|
||||
return np.poly1d(z.coeffs.imag)
|
||||
except AttributeError:
|
||||
return z.imag
|
||||
|
||||
|
||||
def poly_real_part(poly):
|
||||
"""Deprecated."""
|
||||
return np.poly1d(poly.coeffs.real)
|
||||
|
||||
|
||||
def poly_imag_part(poly):
|
||||
"""Deprecated."""
|
||||
return np.poly1d(poly.coeffs.imag)
|
|
@ -0,0 +1,201 @@
|
|||
"""This submodule contains functions related to smoothing paths of Bezier
|
||||
curves."""
|
||||
|
||||
# External Dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
# Internal Dependencies
|
||||
from .path import Path, CubicBezier, Line
|
||||
from .misctools import isclose
|
||||
from .paths2svg import disvg
|
||||
|
||||
|
||||
def is_differentiable(path, tol=1e-8):
|
||||
for idx in range(len(path)):
|
||||
u = path[(idx-1) % len(path)].unit_tangent(1)
|
||||
v = path[idx].unit_tangent(0)
|
||||
u_dot_v = u.real*v.real + u.imag*v.imag
|
||||
if abs(u_dot_v - 1) > tol:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def kinks(path, tol=1e-8):
|
||||
"""returns indices of segments that start on a non-differentiable joint."""
|
||||
kink_list = []
|
||||
for idx in xrange(len(path)):
|
||||
if idx == 0 and not path.isclosed():
|
||||
continue
|
||||
try:
|
||||
u = path[(idx - 1) % len(path)].unit_tangent(1)
|
||||
v = path[idx].unit_tangent(0)
|
||||
u_dot_v = u.real*v.real + u.imag*v.imag
|
||||
flag = False
|
||||
except ValueError:
|
||||
flag = True
|
||||
|
||||
if flag or abs(u_dot_v - 1) > tol:
|
||||
kink_list.append(idx)
|
||||
return kink_list
|
||||
|
||||
|
||||
def _report_unfixable_kinks(_path, _kink_list):
|
||||
mes = ("\n%s kinks have been detected at that cannot be smoothed.\n"
|
||||
"To ignore these kinks and fix all others, run this function "
|
||||
"again with the second argument 'ignore_unfixable_kinks=True' "
|
||||
"The locations of the unfixable kinks are at the beginnings of "
|
||||
"segments: %s" % (len(_kink_list), _kink_list))
|
||||
disvg(_path, nodes=[_path[idx].start for idx in _kink_list])
|
||||
raise Exception(mes)
|
||||
|
||||
|
||||
def smoothed_joint(seg0, seg1, maxjointsize=3, tightness=1.99):
|
||||
""" See Andy's notes on
|
||||
Smoothing Bezier Paths for an explanation of the method.
|
||||
Input: two segments seg0, seg1 such that seg0.end==seg1.start, and
|
||||
jointsize, a positive number
|
||||
|
||||
Output: seg0_trimmed, elbow, seg1_trimmed, where elbow is a cubic bezier
|
||||
object that smoothly connects seg0_trimmed and seg1_trimmed.
|
||||
|
||||
"""
|
||||
assert seg0.end == seg1.start
|
||||
assert 0 < maxjointsize
|
||||
assert 0 < tightness < 2
|
||||
# sgn = lambda x:x/abs(x)
|
||||
q = seg0.end
|
||||
|
||||
try: v = seg0.unit_tangent(1)
|
||||
except: v = seg0.unit_tangent(1 - 1e-4)
|
||||
try: w = seg1.unit_tangent(0)
|
||||
except: w = seg1.unit_tangent(1e-4)
|
||||
|
||||
max_a = maxjointsize / 2
|
||||
a = min(max_a, min(seg1.length(), seg0.length()) / 20)
|
||||
if isinstance(seg0, Line) and isinstance(seg1, Line):
|
||||
'''
|
||||
Note: Letting
|
||||
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1, w = the
|
||||
unit tangent vector of seg1 at 0,
|
||||
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
|
||||
The elbow will be the unique CubicBezier, c, such that
|
||||
c(0)= Q-av, c(1)=Q+aw, c'(0) = bv, and c'(1) = bw
|
||||
where a and b are derived above/below from tightness and
|
||||
maxjointsize.
|
||||
'''
|
||||
# det = v.imag*w.real-v.real*w.imag
|
||||
# Note:
|
||||
# If det is negative, the curvature of elbow is negative for all
|
||||
# real t if and only if b/a > 6
|
||||
# If det is positive, the curvature of elbow is negative for all
|
||||
# real t if and only if b/a < 2
|
||||
|
||||
# if det < 0:
|
||||
# b = (6+tightness)*a
|
||||
# elif det > 0:
|
||||
# b = (2-tightness)*a
|
||||
# else:
|
||||
# raise Exception("seg0 and seg1 are parallel lines.")
|
||||
b = (2 - tightness)*a
|
||||
elbow = CubicBezier(q - a*v, q - (a - b/3)*v, q + (a - b/3)*w, q + a*w)
|
||||
seg0_trimmed = Line(seg0.start, elbow.start)
|
||||
seg1_trimmed = Line(elbow.end, seg1.end)
|
||||
return seg0_trimmed, [elbow], seg1_trimmed
|
||||
elif isinstance(seg0, Line):
|
||||
'''
|
||||
Note: Letting
|
||||
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1,
|
||||
w = the unit tangent vector of seg1 at 0,
|
||||
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
|
||||
The elbow will be the unique CubicBezier, c, such that
|
||||
c(0)= Q-av, c(1)=Q, c'(0) = bv, and c'(1) = bw
|
||||
where a and b are derived above/below from tightness and
|
||||
maxjointsize.
|
||||
'''
|
||||
# det = v.imag*w.real-v.real*w.imag
|
||||
# Note: If g has the same sign as det, then the curvature of elbow is
|
||||
# negative for all real t if and only if b/a < 4
|
||||
b = (4 - tightness)*a
|
||||
# g = sgn(det)*b
|
||||
elbow = CubicBezier(q - a*v, q + (b/3 - a)*v, q - b/3*w, q)
|
||||
seg0_trimmed = Line(seg0.start, elbow.start)
|
||||
return seg0_trimmed, [elbow], seg1
|
||||
elif isinstance(seg1, Line):
|
||||
args = (seg1.reversed(), seg0.reversed(), maxjointsize, tightness)
|
||||
rseg1_trimmed, relbow, rseg0 = smoothed_joint(*args)
|
||||
elbow = relbow[0].reversed()
|
||||
return seg0, [elbow], rseg1_trimmed.reversed()
|
||||
else:
|
||||
# find a point on each seg that is about a/2 away from joint. Make
|
||||
# line between them.
|
||||
t0 = seg0.ilength(seg0.length() - a/2)
|
||||
t1 = seg1.ilength(a/2)
|
||||
seg0_trimmed = seg0.cropped(0, t0)
|
||||
seg1_trimmed = seg1.cropped(t1, 1)
|
||||
seg0_line = Line(seg0_trimmed.end, q)
|
||||
seg1_line = Line(q, seg1_trimmed.start)
|
||||
|
||||
args = (seg0_trimmed, seg0_line, maxjointsize, tightness)
|
||||
dummy, elbow0, seg0_line_trimmed = smoothed_joint(*args)
|
||||
|
||||
args = (seg1_line, seg1_trimmed, maxjointsize, tightness)
|
||||
seg1_line_trimmed, elbow1, dummy = smoothed_joint(*args)
|
||||
|
||||
args = (seg0_line_trimmed, seg1_line_trimmed, maxjointsize, tightness)
|
||||
seg0_line_trimmed, elbowq, seg1_line_trimmed = smoothed_joint(*args)
|
||||
|
||||
elbow = elbow0 + [seg0_line_trimmed] + elbowq + [seg1_line_trimmed] + elbow1
|
||||
return seg0_trimmed, elbow, seg1_trimmed
|
||||
|
||||
|
||||
def smoothed_path(path, maxjointsize=3, tightness=1.99, ignore_unfixable_kinks=False):
|
||||
"""returns a path with no non-differentiable joints."""
|
||||
if len(path) == 1:
|
||||
return path
|
||||
|
||||
assert path.iscontinuous()
|
||||
|
||||
sharp_kinks = []
|
||||
new_path = [path[0]]
|
||||
for idx in range(len(path)):
|
||||
if idx == len(path)-1:
|
||||
if not path.isclosed():
|
||||
continue
|
||||
else:
|
||||
seg1 = new_path[0]
|
||||
else:
|
||||
seg1 = path[idx + 1]
|
||||
seg0 = new_path[-1]
|
||||
|
||||
try:
|
||||
unit_tangent0 = seg0.unit_tangent(1)
|
||||
unit_tangent1 = seg1.unit_tangent(0)
|
||||
flag = False
|
||||
except ValueError:
|
||||
flag = True # unit tangent not well-defined
|
||||
|
||||
if not flag and isclose(unit_tangent0, unit_tangent1): # joint is already smooth
|
||||
if idx != len(path)-1:
|
||||
new_path.append(seg1)
|
||||
continue
|
||||
else:
|
||||
kink_idx = (idx + 1) % len(path) # kink at start of this seg
|
||||
if not flag and isclose(-unit_tangent0, unit_tangent1):
|
||||
# joint is sharp 180 deg (must be fixed manually)
|
||||
new_path.append(seg1)
|
||||
sharp_kinks.append(kink_idx)
|
||||
else: # joint is not smooth, let's smooth it.
|
||||
args = (seg0, seg1, maxjointsize, tightness)
|
||||
new_seg0, elbow_segs, new_seg1 = smoothed_joint(*args)
|
||||
new_path[-1] = new_seg0
|
||||
new_path += elbow_segs
|
||||
if idx == len(path) - 1:
|
||||
new_path[0] = new_seg1
|
||||
else:
|
||||
new_path.append(new_seg1)
|
||||
|
||||
# If unfixable kinks were found, let the user know
|
||||
if sharp_kinks and not ignore_unfixable_kinks:
|
||||
_report_unfixable_kinks(path, sharp_kinks)
|
||||
|
||||
return Path(*new_path)
|
|
@ -0,0 +1,87 @@
|
|||
"""This submodule contains tools for creating path objects from SVG files.
|
||||
The main tool being the svg2paths() function."""
|
||||
|
||||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
from xml.dom.minidom import parse
|
||||
from os import path as os_path, getcwd
|
||||
|
||||
# Internal dependencies
|
||||
from .parser import parse_path
|
||||
|
||||
|
||||
def polyline2pathd(polyline_d):
|
||||
"""converts the string from a polyline d-attribute to a string for a Path
|
||||
object d-attribute"""
|
||||
points = polyline_d.replace(', ', ',')
|
||||
points = points.replace(' ,', ',')
|
||||
points = points.split()
|
||||
|
||||
if points[0] == points[-1]:
|
||||
closed = True
|
||||
else:
|
||||
closed = False
|
||||
|
||||
d = 'M' + points.pop(0).replace(',', ' ')
|
||||
for p in points:
|
||||
d += 'L' + p.replace(',', ' ')
|
||||
if closed:
|
||||
d += 'z'
|
||||
return d
|
||||
|
||||
|
||||
def svg2paths(svg_file_location,
|
||||
convert_lines_to_paths=True,
|
||||
convert_polylines_to_paths=True,
|
||||
convert_polygons_to_paths=True):
|
||||
"""
|
||||
Converts an SVG file into a list of Path objects and a list of
|
||||
dictionaries containing their attributes. This currently supports
|
||||
SVG Path, Line, Polyline, and Polygon elements.
|
||||
:param svg_file_location: the location of the svg file
|
||||
:param convert_lines_to_paths: Set to False to disclude SVG-Line objects
|
||||
(converted to Paths)
|
||||
:param convert_polylines_to_paths: Set to False to disclude SVG-Polyline
|
||||
objects (converted to Paths)
|
||||
:param convert_polygons_to_paths: Set to False to disclude SVG-Polygon
|
||||
objects (converted to Paths)
|
||||
:return: list of Path objects
|
||||
"""
|
||||
if os_path.dirname(svg_file_location) == '':
|
||||
svg_file_location = os_path.join(getcwd(), svg_file_location)
|
||||
|
||||
doc = parse(svg_file_location) # parseString also exists
|
||||
|
||||
def dom2dict(element):
|
||||
"""Converts DOM elements to dictionaries of attributes."""
|
||||
keys = element.attributes.keys()
|
||||
values = [val.value for val in element.attributes.values()]
|
||||
return dict(zip(keys, values))
|
||||
|
||||
# Use minidom to extract path strings from input SVG
|
||||
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
|
||||
d_strings = [el['d'] for el in paths]
|
||||
attribute_dictionary_list = paths
|
||||
|
||||
# Use minidom to extract polyline strings from input SVG, convert to
|
||||
# path strings, add to list
|
||||
if convert_polylines_to_paths:
|
||||
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
|
||||
d_strings += [polyline2pathd(pl['points']) for pl in plins]
|
||||
attribute_dictionary_list += plins
|
||||
|
||||
# Use minidom to extract polygon strings from input SVG, convert to
|
||||
# path strings, add to list
|
||||
if convert_polygons_to_paths:
|
||||
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
|
||||
d_strings += [polyline2pathd(pg['points']) + 'z' for pg in pgons]
|
||||
attribute_dictionary_list += pgons
|
||||
|
||||
if convert_lines_to_paths:
|
||||
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
|
||||
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
|
||||
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
|
||||
attribute_dictionary_list += lines
|
||||
doc.unlink()
|
||||
path_list = [parse_path(d) for d in d_strings]
|
||||
return path_list, attribute_dictionary_list
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="744.09448819"
|
||||
height="1052.3622047"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="New document 1">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.49497475"
|
||||
inkscape:cx="397.2841"
|
||||
inkscape:cy="523.28933"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 165.66502,474.55493 c 77.30655,-9.33703 147.47762,-100.73205 234.35539,0 58.00169,-86.91626 49.61109,-140.63637 0,-173.74624 l 94.95434,0 c 59.26228,20.20305 59.26229,51.18106 0,92.93403 25.42828,25.65512 50.19764,62.5117 80.8122,0 22.30135,41.03559 78.56919,92.26113 0,103.03556 l -72.73098,0 c 12.66417,43.22036 75.15432,97.51317 0,121.21831 -82.83986,8.81955 -137.45198,91.73688 -258.59905,0 -16.20448,-28.83144 -149.811143,44.10743 -109.09647,10.10152 l 24.24365,-101.01525 0,-52.52793 z"
|
||||
id="path2985"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 206.07112,858.41289 206.07112,-2.02031 c -50.738,-81.14814 -20.36402,-105.87055 52.52793,-101.01525 l 103.03556,0 0,111.11678"
|
||||
id="path2987"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,21 @@
|
|||
from __future__ import division, absolute_import, print_function
|
||||
# import unittest
|
||||
# try:
|
||||
# from ..bezier import *
|
||||
# except ValueError:
|
||||
# from svgpathtools.bezier import *
|
||||
#
|
||||
#
|
||||
# class Test_bezier(unittest.TestCase):
|
||||
# def test_bernstein(self):
|
||||
# self.fail()
|
||||
#
|
||||
# def test_bezier_point(self):
|
||||
# self.fail()
|
||||
#
|
||||
# def test_split_bezier(self):
|
||||
# self.fail()
|
||||
#
|
||||
#
|
||||
# if __name__ == '__main__':
|
||||
# unittest.main()
|
|
@ -0,0 +1,56 @@
|
|||
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
||||
#------------------------------------------------------------------------------
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import unittest
|
||||
from svgpathtools import *
|
||||
|
||||
|
||||
class TestGeneration(unittest.TestCase):
|
||||
|
||||
def test_svg_examples(self):
|
||||
"""Examples from the SVG spec"""
|
||||
paths = [
|
||||
'M 100,100 L 300,100 L 200,300 Z',
|
||||
'M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z',
|
||||
'M 100,100 L 200,200',
|
||||
'M 100,200 L 200,100 L -100,-200',
|
||||
'M 100,200 C 100,100 250,100 250,200 S 400,300 400,200',
|
||||
'M 100,200 C 100,100 400,100 400,200',
|
||||
'M 100,500 C 25,400 475,400 400,500',
|
||||
'M 100,800 C 175,700 325,700 400,800',
|
||||
'M 600,200 C 675,100 975,100 900,200',
|
||||
'M 600,500 C 600,350 900,650 900,500',
|
||||
'M 600,800 C 625,700 725,700 750,800 S 875,900 900,800',
|
||||
'M 200,300 Q 400,50 600,300 T 1000,300',
|
||||
'M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38',
|
||||
'M 0,0 L 50,20 M 50,20 L 200,100 Z',
|
||||
'M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275',
|
||||
]
|
||||
pathsf = [
|
||||
'M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
|
||||
'M 0.0,0.0 L 50.0,20.0 M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
|
||||
'M 100.0,100.0 L 200.0,200.0',
|
||||
'M 100.0,200.0 L 200.0,100.0 L -100.0,-200.0',
|
||||
'M 100.0,200.0 C 100.0,100.0 250.0,100.0 250.0,200.0 C 250.0,300.0 400.0,300.0 400.0,200.0',
|
||||
'M 100.0,200.0 C 100.0,100.0 400.0,100.0 400.0,200.0',
|
||||
'M 100.0,500.0 C 25.0,400.0 475.0,400.0 400.0,500.0',
|
||||
'M 100.0,800.0 C 175.0,700.0 325.0,700.0 400.0,800.0',
|
||||
'M 600.0,200.0 C 675.0,100.0 975.0,100.0 900.0,200.0',
|
||||
'M 600.0,500.0 C 600.0,350.0 900.0,650.0 900.0,500.0',
|
||||
'M 600.0,800.0 C 625.0,700.0 725.0,700.0 750.0,800.0 C 775.0,900.0 875.0,900.0 900.0,800.0',
|
||||
'M 200.0,300.0 Q 400.0,50.0 600.0,300.0 Q 800.0,550.0 1000.0,300.0',
|
||||
'M -3.4e+38,3.4e+38 L -3.4e-38,3.4e-38',
|
||||
'M 0.0,0.0 L 50.0,20.0 L 200.0,100.0 L 50.0,20.0',
|
||||
'M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0'
|
||||
]
|
||||
|
||||
for k, path in enumerate(paths):
|
||||
self.assertTrue(parse_path(path).d() in (path, pathsf[k]))
|
||||
|
||||
def test_normalizing(self):
|
||||
# Relative paths will be made absolute, subpaths merged if they can,
|
||||
# and syntax will change.
|
||||
path = 'M0 0L3.4E2-10L100.0,100M100,100l100,-100'
|
||||
ps = 'M 0,0 L 340,-10 L 100,100 L 200,0'
|
||||
psf = 'M 0.0,0.0 L 340.0,-10.0 L 100.0,100.0 L 200.0,0.0'
|
||||
self.assertTrue(parse_path(path).d() in (ps, psf))
|
|
@ -0,0 +1,139 @@
|
|||
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
||||
#------------------------------------------------------------------------------
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import unittest
|
||||
from svgpathtools import *
|
||||
|
||||
|
||||
class TestParser(unittest.TestCase):
|
||||
|
||||
def test_svg_examples(self):
|
||||
"""Examples from the SVG spec"""
|
||||
path1 = parse_path('M 100 100 L 300 100 L 200 300 z')
|
||||
self.assertEqual(path1, Path(Line(100 + 100j, 300 + 100j),
|
||||
Line(300 + 100j, 200 + 300j),
|
||||
Line(200 + 300j, 100 + 100j)))
|
||||
self.assertTrue(path1.isclosed())
|
||||
|
||||
# for Z command behavior when there is multiple subpaths
|
||||
path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z')
|
||||
self.assertEqual(path1, Path(
|
||||
Line(0 + 0j, 50 + 20j),
|
||||
Line(100 + 100j, 300 + 100j),
|
||||
Line(300 + 100j, 200 + 300j),
|
||||
Line(200 + 300j, 100 + 100j)))
|
||||
|
||||
path1 = parse_path('M 100 100 L 200 200')
|
||||
path2 = parse_path('M100 100L200 200')
|
||||
self.assertEqual(path1, path2)
|
||||
|
||||
path1 = parse_path('M 100 200 L 200 100 L -100 -200')
|
||||
path2 = parse_path('M 100 200 L 200 100 -100 -200')
|
||||
self.assertEqual(path1, path2)
|
||||
|
||||
path1 = parse_path("""M100,200 C100,100 250,100 250,200
|
||||
S400,300 400,200""")
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j),
|
||||
CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)))
|
||||
|
||||
path1 = parse_path('M100,200 C100,100 400,100 400,200')
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)))
|
||||
|
||||
path1 = parse_path('M100,500 C25,400 475,400 400,500')
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)))
|
||||
|
||||
path1 = parse_path('M100,800 C175,700 325,700 400,800')
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)))
|
||||
|
||||
path1 = parse_path('M600,200 C675,100 975,100 900,200')
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)))
|
||||
|
||||
path1 = parse_path('M600,500 C600,350 900,650 900,500')
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)))
|
||||
|
||||
path1 = parse_path("""M600,800 C625,700 725,700 750,800
|
||||
S875,900 900,800""")
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j),
|
||||
CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j)))
|
||||
|
||||
path1 = parse_path('M200,300 Q400,50 600,300 T1000,300')
|
||||
self.assertEqual(path1,
|
||||
Path(QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j),
|
||||
QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j)))
|
||||
|
||||
path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z')
|
||||
self.assertEqual(path1,
|
||||
Path(Line(300 + 200j, 150 + 200j),
|
||||
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
|
||||
Line(300 + 50j, 300 + 200j)))
|
||||
|
||||
path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z')
|
||||
self.assertEqual(path1,
|
||||
Path(Line(275 + 175j, 275 + 25j),
|
||||
Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
|
||||
Line(125 + 175j, 275 + 175j)))
|
||||
|
||||
path1 = parse_path("""M600,350 l 50,-25
|
||||
a25,25 -30 0,1 50,-25 l 50,-25
|
||||
a25,50 -30 0,1 50,-25 l 50,-25
|
||||
a25,75 -30 0,1 50,-25 l 50,-25
|
||||
a25,100 -30 0,1 50,-25 l 50,-25""")
|
||||
self.assertEqual(path1,
|
||||
Path(Line(600 + 350j, 650 + 325j),
|
||||
Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j),
|
||||
Line(700 + 300j, 750 + 275j),
|
||||
Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j),
|
||||
Line(800 + 250j, 850 + 225j),
|
||||
Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j),
|
||||
Line(900 + 200j, 950 + 175j),
|
||||
Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j),
|
||||
Line(1000 + 150j, 1050 + 125j)))
|
||||
|
||||
def test_others(self):
|
||||
# Other paths that need testing:
|
||||
|
||||
# Relative moveto:
|
||||
path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z')
|
||||
self.assertEqual(path1, Path(
|
||||
Line(0 + 0j, 50 + 20j),
|
||||
Line(100 + 100j, 300 + 100j),
|
||||
Line(300 + 100j, 200 + 300j),
|
||||
Line(200 + 300j, 100 + 100j)))
|
||||
|
||||
# Initial smooth and relative CubicBezier
|
||||
path1 = parse_path("""M100,200 s 150,-100 150,0""")
|
||||
self.assertEqual(path1,
|
||||
Path(CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j)))
|
||||
|
||||
# Initial smooth and relative QuadraticBezier
|
||||
path1 = parse_path("""M100,200 t 150,0""")
|
||||
self.assertEqual(path1,
|
||||
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
|
||||
|
||||
# Relative QuadraticBezier
|
||||
path1 = parse_path("""M100,200 q 0,0 150,0""")
|
||||
self.assertEqual(path1,
|
||||
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
|
||||
|
||||
def test_negative(self):
|
||||
"""You don't need spaces before a minus-sign"""
|
||||
path1 = parse_path('M100,200c10-5,20-10,30-20')
|
||||
path2 = parse_path('M 100 200 c 10 -5 20 -10 30 -20')
|
||||
self.assertEqual(path1, path2)
|
||||
|
||||
def test_numbers(self):
|
||||
"""Exponents and other number format cases"""
|
||||
# It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
|
||||
path1 = parse_path('M-3.4e38 3.4E+38L-3.4E-38,3.4e-38')
|
||||
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
|
||||
self.assertEqual(path1, path2)
|
||||
|
||||
def test_errors(self):
|
||||
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')
|
|
@ -0,0 +1,906 @@
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import unittest
|
||||
from math import sqrt, pi
|
||||
|
||||
# Internal dependencies
|
||||
from svgpathtools import *
|
||||
|
||||
# A note left from the svg.path tools module:
|
||||
# -------------------------------------------
|
||||
# Most of these test points are not calculated separately, as that would
|
||||
# take too long and be too error prone. Instead the curves have been verified
|
||||
# to be correct visually with the disvg() function.
|
||||
|
||||
|
||||
class LineTest(unittest.TestCase):
|
||||
|
||||
def test_lines(self):
|
||||
# These points are calculated, and not just regression tests.
|
||||
|
||||
line1 = Line(0j, 400 + 0j)
|
||||
self.assertAlmostEqual(line1.point(0), 0j)
|
||||
self.assertAlmostEqual(line1.point(0.3), (120 + 0j))
|
||||
self.assertAlmostEqual(line1.point(0.5), (200 + 0j))
|
||||
self.assertAlmostEqual(line1.point(0.9), (360 + 0j))
|
||||
self.assertAlmostEqual(line1.point(1), (400 + 0j))
|
||||
self.assertAlmostEqual(line1.length(), 400)
|
||||
|
||||
line2 = Line(400 + 0j, 400 + 300j)
|
||||
self.assertAlmostEqual(line2.point(0), (400 + 0j))
|
||||
self.assertAlmostEqual(line2.point(0.3), (400 + 90j))
|
||||
self.assertAlmostEqual(line2.point(0.5), (400 + 150j))
|
||||
self.assertAlmostEqual(line2.point(0.9), (400 + 270j))
|
||||
self.assertAlmostEqual(line2.point(1), (400 + 300j))
|
||||
self.assertAlmostEqual(line2.length(), 300)
|
||||
|
||||
line3 = Line(400 + 300j, 0j)
|
||||
self.assertAlmostEqual(line3.point(0), (400 + 300j))
|
||||
self.assertAlmostEqual(line3.point(0.3), (280 + 210j))
|
||||
self.assertAlmostEqual(line3.point(0.5), (200 + 150j))
|
||||
self.assertAlmostEqual(line3.point(0.9), (40 + 30j))
|
||||
self.assertAlmostEqual(line3.point(1), 0j)
|
||||
self.assertAlmostEqual(line3.length(), 500)
|
||||
|
||||
def test_equality(self):
|
||||
# This is to test the __eq__ and __ne__ methods, so we can't use
|
||||
# assertEqual and assertNotEqual
|
||||
line = Line(0j, 400 + 0j)
|
||||
self.assertTrue(line == Line(0, 400))
|
||||
self.assertTrue(line != Line(100, 400))
|
||||
self.assertFalse(line == str(line))
|
||||
self.assertTrue(line != str(line))
|
||||
self.assertFalse(
|
||||
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line)
|
||||
|
||||
|
||||
class CubicBezierTest(unittest.TestCase):
|
||||
def test_approx_circle(self):
|
||||
"""This is a approximate circle drawn in Inkscape"""
|
||||
|
||||
cub1 = CubicBezier(
|
||||
complex(0, 0),
|
||||
complex(0, 109.66797),
|
||||
complex(-88.90345, 198.57142),
|
||||
complex(-198.57142, 198.57142)
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub1.point(0), 0j)
|
||||
self.assertAlmostEqual(cub1.point(0.1), (-2.59896457 + 32.20931647j))
|
||||
self.assertAlmostEqual(cub1.point(0.2), (-10.12330256 + 62.76392816j))
|
||||
self.assertAlmostEqual(cub1.point(0.3), (-22.16418039 + 91.25500149j))
|
||||
self.assertAlmostEqual(cub1.point(0.4), (-38.31276448 + 117.27370288j))
|
||||
self.assertAlmostEqual(cub1.point(0.5), (-58.16022125 + 140.41119875j))
|
||||
self.assertAlmostEqual(cub1.point(0.6), (-81.29771712 + 160.25865552j))
|
||||
self.assertAlmostEqual(cub1.point(0.7), (-107.31641851 + 176.40723961j))
|
||||
self.assertAlmostEqual(cub1.point(0.8), (-135.80749184 + 188.44811744j))
|
||||
self.assertAlmostEqual(cub1.point(0.9), (-166.36210353 + 195.97245543j))
|
||||
self.assertAlmostEqual(cub1.point(1), (-198.57142 + 198.57142j))
|
||||
|
||||
cub2 = CubicBezier(
|
||||
complex(-198.57142, 198.57142),
|
||||
complex(-109.66797 - 198.57142, 0 + 198.57142),
|
||||
complex(-198.57143 - 198.57142, -88.90345 + 198.57142),
|
||||
complex(-198.57143 - 198.57142, 0),
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub2.point(0), (-198.57142 + 198.57142j))
|
||||
self.assertAlmostEqual(cub2.point(0.1), (-230.78073675 + 195.97245543j))
|
||||
self.assertAlmostEqual(cub2.point(0.2), (-261.3353492 + 188.44811744j))
|
||||
self.assertAlmostEqual(cub2.point(0.3), (-289.82642365 + 176.40723961j))
|
||||
self.assertAlmostEqual(cub2.point(0.4), (-315.8451264 + 160.25865552j))
|
||||
self.assertAlmostEqual(cub2.point(0.5), (-338.98262375 + 140.41119875j))
|
||||
self.assertAlmostEqual(cub2.point(0.6), (-358.830082 + 117.27370288j))
|
||||
self.assertAlmostEqual(cub2.point(0.7), (-374.97866745 + 91.25500149j))
|
||||
self.assertAlmostEqual(cub2.point(0.8), (-387.0195464 + 62.76392816j))
|
||||
self.assertAlmostEqual(cub2.point(0.9), (-394.54388515 + 32.20931647j))
|
||||
self.assertAlmostEqual(cub2.point(1), (-397.14285 + 0j))
|
||||
|
||||
cub3 = CubicBezier(
|
||||
complex(-198.57143 - 198.57142, 0),
|
||||
complex(0 - 198.57143 - 198.57142, -109.66797),
|
||||
complex(88.90346 - 198.57143 - 198.57142, -198.57143),
|
||||
complex(-198.57142, -198.57143)
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub3.point(0), (-397.14285 + 0j))
|
||||
self.assertAlmostEqual(cub3.point(0.1), (-394.54388515 - 32.20931675j))
|
||||
self.assertAlmostEqual(cub3.point(0.2), (-387.0195464 - 62.7639292j))
|
||||
self.assertAlmostEqual(cub3.point(0.3), (-374.97866745 - 91.25500365j))
|
||||
self.assertAlmostEqual(cub3.point(0.4), (-358.830082 - 117.2737064j))
|
||||
self.assertAlmostEqual(cub3.point(0.5), (-338.98262375 - 140.41120375j))
|
||||
self.assertAlmostEqual(cub3.point(0.6), (-315.8451264 - 160.258662j))
|
||||
self.assertAlmostEqual(cub3.point(0.7), (-289.82642365 - 176.40724745j))
|
||||
self.assertAlmostEqual(cub3.point(0.8), (-261.3353492 - 188.4481264j))
|
||||
self.assertAlmostEqual(cub3.point(0.9), (-230.78073675 - 195.97246515j))
|
||||
self.assertAlmostEqual(cub3.point(1), (-198.57142 - 198.57143j))
|
||||
|
||||
cub4 = CubicBezier(
|
||||
complex(-198.57142, -198.57143),
|
||||
complex(109.66797 - 198.57142, 0 - 198.57143),
|
||||
complex(0, 88.90346 - 198.57143),
|
||||
complex(0, 0),
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub4.point(0), (-198.57142 - 198.57143j))
|
||||
self.assertAlmostEqual(cub4.point(0.1), (-166.36210353 - 195.97246515j))
|
||||
self.assertAlmostEqual(cub4.point(0.2), (-135.80749184 - 188.4481264j))
|
||||
self.assertAlmostEqual(cub4.point(0.3), (-107.31641851 - 176.40724745j))
|
||||
self.assertAlmostEqual(cub4.point(0.4), (-81.29771712 - 160.258662j))
|
||||
self.assertAlmostEqual(cub4.point(0.5), (-58.16022125 - 140.41120375j))
|
||||
self.assertAlmostEqual(cub4.point(0.6), (-38.31276448 - 117.2737064j))
|
||||
self.assertAlmostEqual(cub4.point(0.7), (-22.16418039 - 91.25500365j))
|
||||
self.assertAlmostEqual(cub4.point(0.8), (-10.12330256 - 62.7639292j))
|
||||
self.assertAlmostEqual(cub4.point(0.9), (-2.59896457 - 32.20931675j))
|
||||
self.assertAlmostEqual(cub4.point(1), 0j)
|
||||
|
||||
def test_svg_examples(self):
|
||||
|
||||
# M100,200 C100,100 250,100 250,200
|
||||
path1 = CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j)
|
||||
self.assertAlmostEqual(path1.point(0), (100 + 200j))
|
||||
self.assertAlmostEqual(path1.point(0.3), (132.4 + 137j))
|
||||
self.assertAlmostEqual(path1.point(0.5), (175 + 125j))
|
||||
self.assertAlmostEqual(path1.point(0.9), (245.8 + 173j))
|
||||
self.assertAlmostEqual(path1.point(1), (250 + 200j))
|
||||
|
||||
# S400,300 400,200
|
||||
path2 = CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)
|
||||
self.assertAlmostEqual(path2.point(0), (250 + 200j))
|
||||
self.assertAlmostEqual(path2.point(0.3), (282.4 + 263j))
|
||||
self.assertAlmostEqual(path2.point(0.5), (325 + 275j))
|
||||
self.assertAlmostEqual(path2.point(0.9), (395.8 + 227j))
|
||||
self.assertAlmostEqual(path2.point(1), (400 + 200j))
|
||||
|
||||
# M100,200 C100,100 400,100 400,200
|
||||
path3 = CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)
|
||||
self.assertAlmostEqual(path3.point(0), (100 + 200j))
|
||||
self.assertAlmostEqual(path3.point(0.3), (164.8 + 137j))
|
||||
self.assertAlmostEqual(path3.point(0.5), (250 + 125j))
|
||||
self.assertAlmostEqual(path3.point(0.9), (391.6 + 173j))
|
||||
self.assertAlmostEqual(path3.point(1), (400 + 200j))
|
||||
|
||||
# M100,500 C25,400 475,400 400,500
|
||||
path4 = CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)
|
||||
self.assertAlmostEqual(path4.point(0), (100 + 500j))
|
||||
self.assertAlmostEqual(path4.point(0.3), (145.9 + 437j))
|
||||
self.assertAlmostEqual(path4.point(0.5), (250 + 425j))
|
||||
self.assertAlmostEqual(path4.point(0.9), (407.8 + 473j))
|
||||
self.assertAlmostEqual(path4.point(1), (400 + 500j))
|
||||
|
||||
# M100,800 C175,700 325,700 400,800
|
||||
path5 = CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)
|
||||
self.assertAlmostEqual(path5.point(0), (100 + 800j))
|
||||
self.assertAlmostEqual(path5.point(0.3), (183.7 + 737j))
|
||||
self.assertAlmostEqual(path5.point(0.5), (250 + 725j))
|
||||
self.assertAlmostEqual(path5.point(0.9), (375.4 + 773j))
|
||||
self.assertAlmostEqual(path5.point(1), (400 + 800j))
|
||||
|
||||
# M600,200 C675,100 975,100 900,200
|
||||
path6 = CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)
|
||||
self.assertAlmostEqual(path6.point(0), (600 + 200j))
|
||||
self.assertAlmostEqual(path6.point(0.3), (712.05 + 137j))
|
||||
self.assertAlmostEqual(path6.point(0.5), (806.25 + 125j))
|
||||
self.assertAlmostEqual(path6.point(0.9), (911.85 + 173j))
|
||||
self.assertAlmostEqual(path6.point(1), (900 + 200j))
|
||||
|
||||
# M600,500 C600,350 900,650 900,500
|
||||
path7 = CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)
|
||||
self.assertAlmostEqual(path7.point(0), (600 + 500j))
|
||||
self.assertAlmostEqual(path7.point(0.3), (664.8 + 462.2j))
|
||||
self.assertAlmostEqual(path7.point(0.5), (750 + 500j))
|
||||
self.assertAlmostEqual(path7.point(0.9), (891.6 + 532.4j))
|
||||
self.assertAlmostEqual(path7.point(1), (900 + 500j))
|
||||
|
||||
# M600,800 C625,700 725,700 750,800
|
||||
path8 = CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j)
|
||||
self.assertAlmostEqual(path8.point(0), (600 + 800j))
|
||||
self.assertAlmostEqual(path8.point(0.3), (638.7 + 737j))
|
||||
self.assertAlmostEqual(path8.point(0.5), (675 + 725j))
|
||||
self.assertAlmostEqual(path8.point(0.9), (740.4 + 773j))
|
||||
self.assertAlmostEqual(path8.point(1), (750 + 800j))
|
||||
|
||||
# S875,900 900,800
|
||||
inversion = (750 + 800j) + (750 + 800j) - (725 + 700j)
|
||||
path9 = CubicBezier(750 + 800j, inversion, 875 + 900j, 900 + 800j)
|
||||
self.assertAlmostEqual(path9.point(0), (750 + 800j))
|
||||
self.assertAlmostEqual(path9.point(0.3), (788.7 + 863j))
|
||||
self.assertAlmostEqual(path9.point(0.5), (825 + 875j))
|
||||
self.assertAlmostEqual(path9.point(0.9), (890.4 + 827j))
|
||||
self.assertAlmostEqual(path9.point(1), (900 + 800j))
|
||||
|
||||
def test_length(self):
|
||||
|
||||
# A straight line:
|
||||
cub = CubicBezier(
|
||||
complex(0, 0),
|
||||
complex(0, 0),
|
||||
complex(0, 100),
|
||||
complex(0, 100)
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub.length(), 100)
|
||||
|
||||
# A diagonal line:
|
||||
cub = CubicBezier(
|
||||
complex(0, 0),
|
||||
complex(0, 0),
|
||||
complex(100, 100),
|
||||
complex(100, 100)
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(cub.length(), sqrt(2 * 100 * 100))
|
||||
|
||||
# A quarter circle large_arc with radius 100:
|
||||
kappa = 4 * (sqrt(2) - 1) / 3 # http://www.whizkidtech.redprince.net/bezier/circle/
|
||||
|
||||
cub = CubicBezier(
|
||||
complex(0, 0),
|
||||
complex(0, kappa * 100),
|
||||
complex(100 - kappa * 100, 100),
|
||||
complex(100, 100)
|
||||
)
|
||||
|
||||
# We can't compare with pi*50 here, because this is just an
|
||||
# approximation of a circle large_arc. pi*50 is 157.079632679
|
||||
# So this is just yet another "warn if this changes" test.
|
||||
# This value is not verified to be correct.
|
||||
self.assertAlmostEqual(cub.length(), 157.1016698)
|
||||
|
||||
# A recursive solution has also been suggested, but for CubicBezier
|
||||
# curves it could get a false solution on curves where the midpoint is
|
||||
# on a straight line between the start and end. For example, the
|
||||
# following curve would get solved as a straight line and get the
|
||||
# length 300.
|
||||
# Make sure this is not the case.
|
||||
cub = CubicBezier(
|
||||
complex(600, 500),
|
||||
complex(600, 350),
|
||||
complex(900, 650),
|
||||
complex(900, 500)
|
||||
)
|
||||
self.assertTrue(cub.length() > 300.0)
|
||||
|
||||
def test_equality(self):
|
||||
# This is to test the __eq__ and __ne__ methods, so we can't use
|
||||
# assertEqual and assertNotEqual
|
||||
segment = CubicBezier(complex(600, 500), complex(600, 350),
|
||||
complex(900, 650), complex(900, 500))
|
||||
|
||||
self.assertTrue(segment ==
|
||||
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment !=
|
||||
CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j))
|
||||
self.assertTrue(segment != Line(0, 400))
|
||||
|
||||
|
||||
class QuadraticBezierTest(unittest.TestCase):
|
||||
|
||||
def test_svg_examples(self):
|
||||
"""These is the path in the SVG specs"""
|
||||
# M200,300 Q400,50 600,300 T1000,300
|
||||
path1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
|
||||
self.assertAlmostEqual(path1.point(0), (200 + 300j))
|
||||
self.assertAlmostEqual(path1.point(0.3), (320 + 195j))
|
||||
self.assertAlmostEqual(path1.point(0.5), (400 + 175j))
|
||||
self.assertAlmostEqual(path1.point(0.9), (560 + 255j))
|
||||
self.assertAlmostEqual(path1.point(1), (600 + 300j))
|
||||
|
||||
# T1000, 300
|
||||
inversion = (600 + 300j) + (600 + 300j) - (400 + 50j)
|
||||
path2 = QuadraticBezier(600 + 300j, inversion, 1000 + 300j)
|
||||
self.assertAlmostEqual(path2.point(0), (600 + 300j))
|
||||
self.assertAlmostEqual(path2.point(0.3), (720 + 405j))
|
||||
self.assertAlmostEqual(path2.point(0.5), (800 + 425j))
|
||||
self.assertAlmostEqual(path2.point(0.9), (960 + 345j))
|
||||
self.assertAlmostEqual(path2.point(1), (1000 + 300j))
|
||||
|
||||
def test_length(self):
|
||||
# expected results calculated with
|
||||
# svg.path.segment_length(q, 0, 1, q.start, q.end, 1e-14, 20, 0)
|
||||
q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
|
||||
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
|
||||
closedq = QuadraticBezier(6+2j, 5-1j, 6+2j)
|
||||
linq1 = QuadraticBezier(1, 2, 3)
|
||||
linq2 = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
|
||||
nodalq = QuadraticBezier(1, 1, 1)
|
||||
tests = [(q1, 487.77109389525975),
|
||||
(q2, 379.90458193489155),
|
||||
(closedq, 3.1622776601683795),
|
||||
(linq1, 2),
|
||||
(linq2, 22.73335777124786),
|
||||
(nodalq, 0)]
|
||||
for q, exp_res in tests:
|
||||
self.assertAlmostEqual(q.length(), exp_res)
|
||||
|
||||
# partial length tests
|
||||
tests = [(q1, 212.34775387566032),
|
||||
(q2, 166.22170622052397),
|
||||
(closedq, 0.7905694150420949),
|
||||
(linq1, 1.0),
|
||||
(nodalq, 0.0)]
|
||||
t0 = 0.25
|
||||
t1 = 0.75
|
||||
for q, exp_res in tests:
|
||||
self.assertAlmostEqual(q.length(t0=t0, t1=t1), exp_res)
|
||||
|
||||
# linear partial cases
|
||||
linq2 = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
|
||||
tests = [(0, 1/24, 0.13975424859373725),
|
||||
(0, 1/12, 0.1863389981249823),
|
||||
(0, 0.5, 4.844813951249543),
|
||||
(0, 1, 22.73335777124786),
|
||||
(1/24, 1/12, 0.04658474953124506),
|
||||
(1/24, 0.5, 4.705059702655722),
|
||||
(1/24, 1, 22.59360352265412),
|
||||
(1/12, 0.5, 4.658474953124562),
|
||||
(1/12, 1, 22.54701877312288),
|
||||
(0.5, 1, 17.88854381999832)]
|
||||
for t0, t1, exp_s in tests:
|
||||
self.assertAlmostEqual(linq2.length(t0=t0, t1=t1), exp_s)
|
||||
|
||||
def test_equality(self):
|
||||
# This is to test the __eq__ and __ne__ methods, so we can't use
|
||||
# assertEqual and assertNotEqual
|
||||
segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
|
||||
self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j))
|
||||
self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j))
|
||||
self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
|
||||
self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment)
|
||||
|
||||
|
||||
class ArcTest(unittest.TestCase):
|
||||
|
||||
def test_points(self):
|
||||
arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
|
||||
self.assertAlmostEqual(arc1.center, 100 + 0j)
|
||||
self.assertAlmostEqual(arc1.theta, 180.0)
|
||||
self.assertAlmostEqual(arc1.delta, -90.0)
|
||||
|
||||
self.assertAlmostEqual(arc1.point(0.0), 0j)
|
||||
self.assertAlmostEqual(arc1.point(0.1), (1.23116594049 + 7.82172325201j))
|
||||
self.assertAlmostEqual(arc1.point(0.2), (4.89434837048 + 15.4508497187j))
|
||||
self.assertAlmostEqual(arc1.point(0.3), (10.8993475812 + 22.699524987j))
|
||||
self.assertAlmostEqual(arc1.point(0.4), (19.0983005625 + 29.3892626146j))
|
||||
self.assertAlmostEqual(arc1.point(0.5), (29.2893218813 + 35.3553390593j))
|
||||
self.assertAlmostEqual(arc1.point(0.6), (41.2214747708 + 40.4508497187j))
|
||||
self.assertAlmostEqual(arc1.point(0.7), (54.6009500260 + 44.5503262094j))
|
||||
self.assertAlmostEqual(arc1.point(0.8), (69.0983005625 + 47.5528258148j))
|
||||
self.assertAlmostEqual(arc1.point(0.9), (84.3565534960 + 49.3844170298j))
|
||||
self.assertAlmostEqual(arc1.point(1.0), (100 + 50j))
|
||||
|
||||
arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)
|
||||
self.assertAlmostEqual(arc2.center, 50j)
|
||||
self.assertAlmostEqual(arc2.theta, -90.0)
|
||||
self.assertAlmostEqual(arc2.delta, -270.0)
|
||||
|
||||
self.assertAlmostEqual(arc2.point(0.0), 0j)
|
||||
self.assertAlmostEqual(arc2.point(0.1), (-45.399049974 + 5.44967379058j))
|
||||
self.assertAlmostEqual(arc2.point(0.2), (-80.9016994375 + 20.6107373854j))
|
||||
self.assertAlmostEqual(arc2.point(0.3), (-98.7688340595 + 42.178276748j))
|
||||
self.assertAlmostEqual(arc2.point(0.4), (-95.1056516295 + 65.4508497187j))
|
||||
self.assertAlmostEqual(arc2.point(0.5), (-70.7106781187 + 85.3553390593j))
|
||||
self.assertAlmostEqual(arc2.point(0.6), (-30.9016994375 + 97.5528258148j))
|
||||
self.assertAlmostEqual(arc2.point(0.7), (15.643446504 + 99.3844170298j))
|
||||
self.assertAlmostEqual(arc2.point(0.8), (58.7785252292 + 90.4508497187j))
|
||||
self.assertAlmostEqual(arc2.point(0.9), (89.1006524188 + 72.699524987j))
|
||||
self.assertAlmostEqual(arc2.point(1.0), (100 + 50j))
|
||||
|
||||
arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j)
|
||||
self.assertAlmostEqual(arc3.center, 50j)
|
||||
self.assertAlmostEqual(arc3.theta, -90.0)
|
||||
self.assertAlmostEqual(arc3.delta, 90.0)
|
||||
|
||||
self.assertAlmostEqual(arc3.point(0.0), 0j)
|
||||
self.assertAlmostEqual(arc3.point(0.1), (15.643446504 + 0.615582970243j))
|
||||
self.assertAlmostEqual(arc3.point(0.2), (30.9016994375 + 2.44717418524j))
|
||||
self.assertAlmostEqual(arc3.point(0.3), (45.399049974 + 5.44967379058j))
|
||||
self.assertAlmostEqual(arc3.point(0.4), (58.7785252292 + 9.54915028125j))
|
||||
self.assertAlmostEqual(arc3.point(0.5), (70.7106781187 + 14.6446609407j))
|
||||
self.assertAlmostEqual(arc3.point(0.6), (80.9016994375 + 20.6107373854j))
|
||||
self.assertAlmostEqual(arc3.point(0.7), (89.1006524188 + 27.300475013j))
|
||||
self.assertAlmostEqual(arc3.point(0.8), (95.1056516295 + 34.5491502813j))
|
||||
self.assertAlmostEqual(arc3.point(0.9), (98.7688340595 + 42.178276748j))
|
||||
self.assertAlmostEqual(arc3.point(1.0), (100 + 50j))
|
||||
|
||||
arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j)
|
||||
self.assertAlmostEqual(arc4.center, 100 + 0j)
|
||||
self.assertAlmostEqual(arc4.theta, 180.0)
|
||||
self.assertAlmostEqual(arc4.delta, 270.0)
|
||||
|
||||
self.assertAlmostEqual(arc4.point(0.0), 0j)
|
||||
self.assertAlmostEqual(arc4.point(0.1), (10.8993475812 - 22.699524987j))
|
||||
self.assertAlmostEqual(arc4.point(0.2), (41.2214747708 - 40.4508497187j))
|
||||
self.assertAlmostEqual(arc4.point(0.3), (84.3565534960 - 49.3844170298j))
|
||||
self.assertAlmostEqual(arc4.point(0.4), (130.901699437 - 47.5528258148j))
|
||||
self.assertAlmostEqual(arc4.point(0.5), (170.710678119 - 35.3553390593j))
|
||||
self.assertAlmostEqual(arc4.point(0.6), (195.105651630 - 15.4508497187j))
|
||||
self.assertAlmostEqual(arc4.point(0.7), (198.768834060 + 7.82172325201j))
|
||||
self.assertAlmostEqual(arc4.point(0.8), (180.901699437 + 29.3892626146j))
|
||||
self.assertAlmostEqual(arc4.point(0.9), (145.399049974 + 44.5503262094j))
|
||||
self.assertAlmostEqual(arc4.point(1.0), (100 + 50j))
|
||||
|
||||
arc5 = Arc((725.307482225571-915.5548199281527j),
|
||||
(202.79421639137703+148.77294617167183j),
|
||||
225.6910319606926, 1, 1,
|
||||
(-624.6375539637027+896.5483089399895j))
|
||||
self.assertAlmostEqual(arc5.point(0.0), (725.307482226-915.554819928j))
|
||||
self.assertAlmostEqual(arc5.point(0.0909090909091), (1023.47397369-597.730444283j))
|
||||
self.assertAlmostEqual(arc5.point(0.181818181818), (1242.80253007-232.251400124j))
|
||||
self.assertAlmostEqual(arc5.point(0.272727272727), (1365.52445614+151.273373978j))
|
||||
self.assertAlmostEqual(arc5.point(0.363636363636), (1381.69755131+521.772981736j))
|
||||
self.assertAlmostEqual(arc5.point(0.454545454545), (1290.01156757+849.231748376j))
|
||||
self.assertAlmostEqual(arc5.point(0.545454545455), (1097.89435807+1107.12091209j))
|
||||
self.assertAlmostEqual(arc5.point(0.636363636364), (820.910116547+1274.54782658j))
|
||||
self.assertAlmostEqual(arc5.point(0.727272727273), (481.49845896+1337.94855893j))
|
||||
self.assertAlmostEqual(arc5.point(0.818181818182), (107.156499251+1292.18675889j))
|
||||
self.assertAlmostEqual(arc5.point(0.909090909091), (-271.788803303+1140.96977533j))
|
||||
|
||||
def test_length(self):
|
||||
# I'll test the length calculations by making a circle, in two parts.
|
||||
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
|
||||
arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
|
||||
self.assertAlmostEqual(arc1.length(), pi * 100)
|
||||
self.assertAlmostEqual(arc2.length(), pi * 100)
|
||||
|
||||
def test_equality(self):
|
||||
# This is to test the __eq__ and __ne__ methods, so we can't use
|
||||
# assertEqual and assertNotEqual
|
||||
segment = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
|
||||
self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
|
||||
self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j))
|
||||
|
||||
|
||||
class TestPath(unittest.TestCase):
|
||||
|
||||
def test_circle(self):
|
||||
arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
|
||||
arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
|
||||
path = Path(arc1, arc2)
|
||||
self.assertAlmostEqual(path.point(0.0), 0j)
|
||||
self.assertAlmostEqual(path.point(0.25), (100 + 100j))
|
||||
self.assertAlmostEqual(path.point(0.5), (200 + 0j))
|
||||
self.assertAlmostEqual(path.point(0.75), (100 - 100j))
|
||||
self.assertAlmostEqual(path.point(1.0), 0j)
|
||||
self.assertAlmostEqual(path.length(), pi * 200)
|
||||
|
||||
def test_svg_specs(self):
|
||||
"""The paths that are in the SVG specs"""
|
||||
|
||||
# Big pie: M300,200 h-150 a150,150 0 1,0 150,-150 z
|
||||
path = Path(Line(300 + 200j, 150 + 200j),
|
||||
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
|
||||
Line(300 + 50j, 300 + 200j))
|
||||
# The points and length for this path are calculated and not regression tests.
|
||||
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
|
||||
self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j))
|
||||
self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j))
|
||||
self.assertAlmostEqual(path.point(1 - 0.14897825542), (300 + 50j))
|
||||
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
|
||||
# The errors seem to accumulate. Still 6 decimal places is more than good enough.
|
||||
self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6)
|
||||
|
||||
# Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z
|
||||
path = Path(Line(275 + 175j, 275 + 25j),
|
||||
Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
|
||||
Line(125 + 175j, 275 + 175j))
|
||||
# The points and length for this path are calculated and not regression tests.
|
||||
self.assertAlmostEqual(path.point(0.0), (275 + 175j))
|
||||
self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j))
|
||||
self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j))
|
||||
self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j))
|
||||
self.assertAlmostEqual(path.point(1.0), (275 + 175j))
|
||||
# The errors seem to accumulate. Still 6 decimal places is more than good enough.
|
||||
self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6)
|
||||
|
||||
# Bumpy path: M600,350 l 50,-25
|
||||
# a25,25 -30 0,1 50,-25 l 50,-25
|
||||
# a25,50 -30 0,1 50,-25 l 50,-25
|
||||
# a25,75 -30 0,1 50,-25 l 50,-25
|
||||
# a25,100 -30 0,1 50,-25 l 50,-25
|
||||
|
||||
# Commented out because by Andy cause I was skeptical of path.point
|
||||
# ground truth values
|
||||
# path = Path(Line(600 + 350j, 650 + 325j),
|
||||
# Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j),
|
||||
# Line(700 + 300j, 750 + 275j),
|
||||
# Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j),
|
||||
# Line(800 + 250j, 850 + 225j),
|
||||
# Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j),
|
||||
# Line(900 + 200j, 950 + 175j),
|
||||
# Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j),
|
||||
# Line(1000 + 150j, 1050 + 125j),
|
||||
# )
|
||||
# # These are *not* calculated, but just regression tests. Be skeptical.
|
||||
# self.assertAlmostEqual(path.point(0), (600+350j))
|
||||
# self.assertAlmostEqual(path.point(0.3), (755.239799276+212.182020958j))
|
||||
# self.assertAlmostEqual(path.point(0.5), (827.730749264+147.824157418j))
|
||||
# self.assertAlmostEqual(path.point(0.9), (971.284357806+106.302352605j))
|
||||
# self.assertAlmostEqual(path.point(1), (1050+125j))
|
||||
# # The errors seem to accumulate. Still 6 decimal places is more than good enough.
|
||||
# self.assertAlmostEqual(path.length(), 928.3886394081095)
|
||||
|
||||
def test_repr(self):
|
||||
path = Path(
|
||||
Line(start=600 + 350j, end=650 + 325j),
|
||||
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
|
||||
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
|
||||
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
|
||||
self.assertEqual(eval(repr(path)), path)
|
||||
|
||||
def test_equality(self):
|
||||
# This is to test the __eq__ and __ne__ methods, so we can't use
|
||||
# assertEqual and assertNotEqual
|
||||
path1 = Path(
|
||||
Line(start=600 + 350j, end=650 + 325j),
|
||||
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
|
||||
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
|
||||
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
|
||||
path2 = Path(
|
||||
Line(start=600 + 350j, end=650 + 325j),
|
||||
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j),
|
||||
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j),
|
||||
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
|
||||
|
||||
self.assertTrue(path1 == path2)
|
||||
# Modify path2:
|
||||
path2[0].start = 601 + 350j
|
||||
self.assertTrue(path1 != path2)
|
||||
|
||||
# Modify back:
|
||||
path2[0].start = 600 + 350j
|
||||
self.assertFalse(path1 != path2)
|
||||
|
||||
# Get rid of the last segment:
|
||||
del path2[-1]
|
||||
self.assertFalse(path1 == path2)
|
||||
|
||||
# It's not equal to a list of it's segments
|
||||
self.assertTrue(path1 != path1[:])
|
||||
self.assertFalse(path1 == path1[:])
|
||||
|
||||
def test_continuous_subpaths(self):
|
||||
"""Test the Path.continuous_subpaths() method."""
|
||||
|
||||
# Continuous and open example
|
||||
q = Path(Line(1, 2))
|
||||
a = [Path(Line(1, 2))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# # Continuous and closed example
|
||||
q = Path(Line(1, 2), Line(2, 1))
|
||||
a = [Path(Line(1, 2), Line(2, 1))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = q == Path(*[seg for subpath in subpaths for seg in subpath])
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Continuous and open example
|
||||
q = Path(Line(1, 2), Line(2, 3), Line(3, 4))
|
||||
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Continuous and closed example
|
||||
q = Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1))
|
||||
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Discontinuous example
|
||||
q = Path(Line(1, 2), Line(2, 3), Line(3, 4),
|
||||
Line(10, 11))
|
||||
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4)),
|
||||
Path(Line(10, 11))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Discontinuous closed example
|
||||
q = Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1),
|
||||
Line(10, 11), Line(11, 12))
|
||||
a = [Path(Line(1, 2), Line(2, 3), Line(3, 4), Line(4, 1)),
|
||||
Path(Line(10, 11), Line(11, 12))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Discontinuous example
|
||||
q = Path(Line(1, 2),
|
||||
Line(1, 2), Line(2, 3),
|
||||
Line(10, 11), Line(11, 12), Line(12, 13),
|
||||
Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 14))
|
||||
a = [Path(Line(1, 2)),
|
||||
Path(Line(1, 2), Line(2, 3)),
|
||||
Path(Line(10, 11), Line(11, 12), Line(12, 13)),
|
||||
Path(Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 14))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
# Discontinuous example with overlapping end
|
||||
q = Path(Line(1, 2),
|
||||
Line(5, 6), Line(6, 7),
|
||||
Line(10, 11), Line(11, 12), Line(12, 13),
|
||||
Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 1))
|
||||
a = [Path(Line(1, 2)),
|
||||
Path(Line(5, 6), Line(6, 7)),
|
||||
Path(Line(10, 11), Line(11, 12), Line(12, 13)),
|
||||
Path(Line(10, 11), Line(11, 12), Line(12, 13), Line(13, 1))]
|
||||
subpaths = q.continuous_subpaths()
|
||||
chk1 = all(subpath.iscontinuous() for subpath in subpaths)
|
||||
chk2 = (q == Path(*[seg for subpath in subpaths for seg in subpath]))
|
||||
self.assertTrue(subpaths == a)
|
||||
self.assertTrue(chk1)
|
||||
self.assertTrue(chk2)
|
||||
|
||||
|
||||
class Test_ilength(unittest.TestCase):
|
||||
def test_ilength(self):
|
||||
# See svgpathtools.notes.inv_arclength.py for information on how these
|
||||
# test values were generated (using the .length() method).
|
||||
##############################################################
|
||||
# Lines
|
||||
l = Line(1, 3-1j)
|
||||
nodall = Line(1+1j, 1+1j)
|
||||
|
||||
tests = [(l, 0.01, 0.022360679774997897),
|
||||
(l, 0.1, 0.223606797749979),
|
||||
(l, 0.5, 1.118033988749895),
|
||||
(l, 0.9, 2.012461179749811),
|
||||
(l, 0.99, 2.213707297724792)]
|
||||
|
||||
for (l, t, s) in tests:
|
||||
self.assertAlmostEqual(l.ilength(s), t)
|
||||
|
||||
###############################################################
|
||||
# Quadratics
|
||||
q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
|
||||
q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j)
|
||||
closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j)
|
||||
linq = QuadraticBezier(1+3j, 2+5j, -9 - 17j)
|
||||
nodalq = QuadraticBezier(1, 1, 1)
|
||||
|
||||
tests = [(q1, 0.01, 6.364183310105577),
|
||||
(q1, 0.1, 60.23857499635088),
|
||||
(q1, 0.5, 243.8855469477619),
|
||||
(q1, 0.9, 427.53251889917294),
|
||||
(q1, 0.99, 481.40691058541813),
|
||||
(q2, 0.01, 6.365673533661836),
|
||||
(q2, 0.1, 60.31675895732397),
|
||||
(q2, 0.5, 233.24592830045907),
|
||||
(q2, 0.9, 346.42891253298706),
|
||||
(q2, 0.99, 376.32659156736844),
|
||||
(closedq, 0.01, 0.06261309767133393),
|
||||
(closedq, 0.1, 0.5692099788303084),
|
||||
(closedq, 0.5, 1.5811388300841898),
|
||||
(closedq, 0.9, 2.5930676813380713),
|
||||
(closedq, 0.99, 3.0996645624970456),
|
||||
(linq, 0.01, 0.04203807797699605),
|
||||
(linq, 0.1, 0.19379255804998186),
|
||||
(linq, 0.5, 4.844813951249544),
|
||||
(linq, 0.9, 18.0823363780483),
|
||||
(linq, 0.99, 22.24410609777091)]
|
||||
|
||||
for q, t, s in tests:
|
||||
try:
|
||||
self.assertAlmostEqual(q.ilength(s), t)
|
||||
except:
|
||||
print(q)
|
||||
print(s)
|
||||
print(t)
|
||||
raise
|
||||
|
||||
###############################################################
|
||||
# Cubics
|
||||
c1 = CubicBezier(200 + 300j, 400 + 50j, 600+100j, -200)
|
||||
symc = CubicBezier(1-2j, 10-1j, 10+1j, 1+2j)
|
||||
closedc = CubicBezier(1-2j, 10-1j, 10+1j, 1-2j)
|
||||
|
||||
tests = [(c1, 0.01, 9.53434737943073),
|
||||
(c1, 0.1, 88.89941848775852),
|
||||
(c1, 0.5, 278.5750942713189),
|
||||
(c1, 0.9, 651.4957786584646),
|
||||
(c1, 0.99, 840.2010603832538),
|
||||
(symc, 0.01, 0.2690118556702902),
|
||||
(symc, 0.1, 2.45230693868727),
|
||||
(symc, 0.5, 7.256147083644424),
|
||||
(symc, 0.9, 12.059987228602886),
|
||||
(symc, 0.99, 14.243282311619401),
|
||||
(closedc, 0.01, 0.26901140075538765),
|
||||
(closedc, 0.1, 2.451722765460998),
|
||||
(closedc, 0.5, 6.974058969750422),
|
||||
(closedc, 0.9, 11.41781741489913),
|
||||
(closedc, 0.99, 13.681324783697782)]
|
||||
|
||||
for (c, t, s) in tests:
|
||||
self.assertAlmostEqual(c.ilength(s), t)
|
||||
|
||||
###############################################################
|
||||
# Arcs
|
||||
arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
|
||||
arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)
|
||||
arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j)
|
||||
arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j)
|
||||
arc5 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j)
|
||||
arc6 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j)
|
||||
arc7 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)
|
||||
|
||||
tests = [(arc1, 0.01, 0.785495042476231),
|
||||
(arc1, 0.1, 7.949362877455911),
|
||||
(arc1, 0.5, 48.28318721111137),
|
||||
(arc1, 0.9, 105.44598206942156),
|
||||
(arc1, 0.99, 119.53485487631241),
|
||||
(arc2, 0.01, 4.71108115728524),
|
||||
(arc2, 0.1, 45.84152747676626),
|
||||
(arc2, 0.5, 169.38878996795734),
|
||||
(arc2, 0.9, 337.44707303579696),
|
||||
(arc2, 0.99, 360.95800139278765),
|
||||
(arc3, 0.01, 1.5707478805335624),
|
||||
(arc3, 0.1, 15.659620687424416),
|
||||
(arc3, 0.5, 72.82241554573457),
|
||||
(arc3, 0.9, 113.15623987939003),
|
||||
(arc3, 0.99, 120.3201077143697),
|
||||
(arc4, 0.01, 2.3588068777503897),
|
||||
(arc4, 0.1, 25.869735234740887),
|
||||
(arc4, 0.5, 193.9280183025816),
|
||||
(arc4, 0.9, 317.4752807937718),
|
||||
(arc4, 0.99, 358.6057271132536),
|
||||
(arc5, 0.01, 3.141592653589793),
|
||||
(arc5, 0.1, 31.415926535897935),
|
||||
(arc5, 0.5, 157.07963267948966),
|
||||
(arc5, 0.9, 282.7433388230814),
|
||||
(arc5, 0.99, 311.01767270538954),
|
||||
(arc6, 0.01, 3.141592653589793),
|
||||
(arc6, 0.1, 31.415926535897928),
|
||||
(arc6, 0.5, 157.07963267948966),
|
||||
(arc6, 0.9, 282.7433388230814),
|
||||
(arc6, 0.99, 311.01767270538954),
|
||||
(arc7, 0.01, 0.785495042476231),
|
||||
(arc7, 0.1, 7.949362877455911),
|
||||
(arc7, 0.5, 48.28318721111137),
|
||||
(arc7, 0.9, 105.44598206942156),
|
||||
(arc7, 0.99, 119.53485487631241)]
|
||||
|
||||
for (c, t, s) in tests:
|
||||
self.assertAlmostEqual(c.ilength(s), t)
|
||||
|
||||
###############################################################
|
||||
# Paths
|
||||
line1 = Line(600 + 350j, 650 + 325j)
|
||||
arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
|
||||
cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
|
||||
cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
|
||||
quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
|
||||
linez = Line(600 + 300j, 600 + 350j)
|
||||
|
||||
bezpath = Path(line1, cub1, cub2, quad3)
|
||||
bezpathz = Path(line1, cub1, cub2, quad3, linez)
|
||||
path = Path(line1, arc1, cub2, quad3)
|
||||
pathz = Path(line1, arc1, cub2, quad3, linez)
|
||||
lpath = Path(linez)
|
||||
qpath = Path(quad3)
|
||||
cpath = Path(cub1)
|
||||
apath = Path(arc1)
|
||||
|
||||
tests = [(bezpath, 0.0, 0.0),
|
||||
(bezpath, 0.1111111111111111, 286.2533595149515),
|
||||
(bezpath, 0.2222222222222222, 503.8620222915423),
|
||||
(bezpath, 0.3333333333333333, 592.6337135346268),
|
||||
(bezpath, 0.4444444444444444, 644.3880677233315),
|
||||
(bezpath, 0.5555555555555556, 835.0384185011363),
|
||||
(bezpath, 0.6666666666666666, 1172.8729938994575),
|
||||
(bezpath, 0.7777777777777778, 1308.6205983178952),
|
||||
(bezpath, 0.8888888888888888, 1532.8473168900994),
|
||||
(bezpath, 1.0, 1758.2427369258733),
|
||||
(bezpathz, 0.0, 0.0),
|
||||
(bezpathz, 0.1111111111111111, 294.15942308605435),
|
||||
(bezpathz, 0.2222222222222222, 512.4295461513882),
|
||||
(bezpathz, 0.3333333333333333, 594.0779370040138),
|
||||
(bezpathz, 0.4444444444444444, 658.7361976564598),
|
||||
(bezpathz, 0.5555555555555556, 874.1674336581542),
|
||||
(bezpathz, 0.6666666666666666, 1204.2371344392693),
|
||||
(bezpathz, 0.7777777777777778, 1356.773042865213),
|
||||
(bezpathz, 0.8888888888888888, 1541.808492602876),
|
||||
(bezpathz, 1.0, 1808.2427369258733),
|
||||
(path, 0.0, 0.0),
|
||||
(path, 0.1111111111111111, 81.44016397108298),
|
||||
(path, 0.2222222222222222, 164.72556816469307),
|
||||
(path, 0.3333333333333333, 206.71343564679154),
|
||||
(path, 0.4444444444444444, 265.4898349999353),
|
||||
(path, 0.5555555555555556, 367.5420981413199),
|
||||
(path, 0.6666666666666666, 487.29863861165995),
|
||||
(path, 0.7777777777777778, 511.84069655405284),
|
||||
(path, 0.8888888888888888, 579.9530841780238),
|
||||
(path, 1.0, 732.9614757397469),
|
||||
(pathz, 0.0, 0.0),
|
||||
(pathz, 0.1111111111111111, 86.99571952663854),
|
||||
(pathz, 0.2222222222222222, 174.33662608180325),
|
||||
(pathz, 0.3333333333333333, 214.42194393858466),
|
||||
(pathz, 0.4444444444444444, 289.94661033436205),
|
||||
(pathz, 0.5555555555555556, 408.38391100702125),
|
||||
(pathz, 0.6666666666666666, 504.4309373835351),
|
||||
(pathz, 0.7777777777777778, 533.774834546298),
|
||||
(pathz, 0.8888888888888888, 652.931321760894),
|
||||
(pathz, 1.0, 782.9614757397469),
|
||||
(lpath, 0.0, 0.0),
|
||||
(lpath, 0.1111111111111111, 5.555555555555555),
|
||||
(lpath, 0.2222222222222222, 11.11111111111111),
|
||||
(lpath, 0.3333333333333333, 16.666666666666664),
|
||||
(lpath, 0.4444444444444444, 22.22222222222222),
|
||||
(lpath, 0.5555555555555556, 27.77777777777778),
|
||||
(lpath, 0.6666666666666666, 33.33333333333333),
|
||||
(lpath, 0.7777777777777778, 38.88888888888889),
|
||||
(lpath, 0.8888888888888888, 44.44444444444444),
|
||||
(lpath, 1.0, 50.0),
|
||||
(qpath, 0.0, 0.0),
|
||||
(qpath, 0.1111111111111111, 17.28395061728395),
|
||||
(qpath, 0.2222222222222222, 24.69135802469136),
|
||||
(qpath, 0.3333333333333333, 27.777777777777786),
|
||||
(qpath, 0.4444444444444444, 40.12345679012344),
|
||||
(qpath, 0.5555555555555556, 62.3456790123457),
|
||||
(qpath, 0.6666666666666666, 94.44444444444446),
|
||||
(qpath, 0.7777777777777778, 136.41975308641975),
|
||||
(qpath, 0.8888888888888888, 188.27160493827154),
|
||||
(qpath, 1.0, 250.0),
|
||||
(cpath, 0.0, 0.0),
|
||||
(cpath, 0.1111111111111111, 207.35525375551356),
|
||||
(cpath, 0.2222222222222222, 366.0583590267552),
|
||||
(cpath, 0.3333333333333333, 474.34064293812787),
|
||||
(cpath, 0.4444444444444444, 530.467036317684),
|
||||
(cpath, 0.5555555555555556, 545.0444351253911),
|
||||
(cpath, 0.6666666666666666, 598.9767847757622),
|
||||
(cpath, 0.7777777777777778, 710.4080903390646),
|
||||
(cpath, 0.8888888888888888, 881.1796899225557),
|
||||
(cpath, 1.0, 1113.0914444911352),
|
||||
(apath, 0.0, 0.0),
|
||||
(apath, 0.1111111111111111, 9.756687033889872),
|
||||
(apath, 0.2222222222222222, 19.51337406777974),
|
||||
(apath, 0.3333333333333333, 29.27006110166961),
|
||||
(apath, 0.4444444444444444, 39.02674813555948),
|
||||
(apath, 0.5555555555555556, 48.783435169449355),
|
||||
(apath, 0.6666666666666666, 58.54012220333922),
|
||||
(apath, 0.7777777777777778, 68.2968092372291),
|
||||
(apath, 0.8888888888888888, 78.05349627111896),
|
||||
(apath, 1.0, 87.81018330500885)]
|
||||
|
||||
for (c, t, s) in tests:
|
||||
self.assertAlmostEqual(c.ilength(s), t)
|
||||
|
||||
###############################################################
|
||||
# Exception Cases
|
||||
nodalq = QuadraticBezier(1, 1, 1)
|
||||
with self.assertRaises(AssertionError):
|
||||
nodalq.ilength(1)
|
||||
|
||||
lin = Line(0, 0.5j)
|
||||
with self.assertRaises(ValueError):
|
||||
lin.ilength(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,244 @@
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import unittest
|
||||
from numpy import poly1d
|
||||
|
||||
# Internal dependencies
|
||||
from svgpathtools import *
|
||||
|
||||
|
||||
class TestPathTools(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.arc1 = Arc(650+325j, 25+25j, -30.0, False, True, 700+300j)
|
||||
self.line1 = Line(0, 100+100j)
|
||||
self.quadratic1 = QuadraticBezier(100+100j, 150+150j, 300+200j)
|
||||
self.cubic1 = CubicBezier(300+200j, 350+400j, 400+425j, 650+325j)
|
||||
self.path_of_all_seg_types = Path(self.line1, self.quadratic1,
|
||||
self.cubic1, self.arc1)
|
||||
self.path_of_bezier_seg_types = Path(self.line1, self.quadratic1,
|
||||
self.cubic1)
|
||||
|
||||
def test_is_bezier_segment(self):
|
||||
|
||||
# False
|
||||
self.assertFalse(is_bezier_segment(self.arc1))
|
||||
self.assertFalse(is_bezier_segment(self.path_of_bezier_seg_types))
|
||||
|
||||
# True
|
||||
self.assertTrue(is_bezier_segment(self.line1))
|
||||
self.assertTrue(is_bezier_segment(self.quadratic1))
|
||||
self.assertTrue(is_bezier_segment(self.cubic1))
|
||||
|
||||
def test_is_bezier_path(self):
|
||||
|
||||
# False
|
||||
self.assertFalse(is_bezier_path(self.path_of_all_seg_types))
|
||||
self.assertFalse(is_bezier_path(self.line1))
|
||||
self.assertFalse(is_bezier_path(self.quadratic1))
|
||||
self.assertFalse(is_bezier_path(self.cubic1))
|
||||
self.assertFalse(is_bezier_path(self.arc1))
|
||||
|
||||
# True
|
||||
self.assertTrue(is_bezier_path(self.path_of_bezier_seg_types))
|
||||
self.assertTrue(is_bezier_path(Path()))
|
||||
|
||||
def test_polynomial2bezier(self):
|
||||
|
||||
def distfcn(tup1, tup2):
|
||||
assert len(tup1) == len(tup2)
|
||||
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
|
||||
|
||||
# Case: Line
|
||||
pcoeffs = [(-1.7-2j), (6+2j)]
|
||||
p = poly1d(pcoeffs)
|
||||
correct_bpoints = [(6+2j), (4.3+0j)]
|
||||
|
||||
# Input poly1d object
|
||||
bez = poly2bez(p)
|
||||
bpoints = bez.bpoints()
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
# Input list of coefficients
|
||||
bpoints = poly2bez(pcoeffs, return_bpoints=True)
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
# Case: Quadratic
|
||||
pcoeffs = [(29.5+15.5j), (-31-19j), (7.5+5.5j)]
|
||||
p = poly1d(pcoeffs)
|
||||
correct_bpoints = [(7.5+5.5j), (-8-4j), (6+2j)]
|
||||
|
||||
# Input poly1d object
|
||||
bez = poly2bez(p)
|
||||
bpoints = bez.bpoints()
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
# Input list of coefficients
|
||||
bpoints = poly2bez(pcoeffs, return_bpoints=True)
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
# Case: Cubic
|
||||
pcoeffs = [(-18.5-12.5j), (34.5+16.5j), (-18-6j), (6+2j)]
|
||||
p = poly1d(pcoeffs)
|
||||
correct_bpoints = [(6+2j), 0j, (5.5+3.5j), (4+0j)]
|
||||
|
||||
# Input poly1d object
|
||||
bez = poly2bez(p)
|
||||
bpoints = bez.bpoints()
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
# Input list of coefficients object
|
||||
bpoints = poly2bez(pcoeffs, return_bpoints=True)
|
||||
self.assertAlmostEquals(distfcn(bpoints, correct_bpoints), 0)
|
||||
|
||||
def test_bpoints2bezier(self):
|
||||
cubic_bpoints = [(6+2j), 0, (5.5+3.5j), (4+0j)]
|
||||
quadratic_bpoints = [(6+2j), 0, (5.5+3.5j)]
|
||||
line_bpoints = [(6+2j), 0]
|
||||
self.assertTrue(isinstance(bpoints2bezier(cubic_bpoints), CubicBezier))
|
||||
self.assertTrue(isinstance(bpoints2bezier(quadratic_bpoints),
|
||||
QuadraticBezier))
|
||||
self.assertTrue(isinstance(bpoints2bezier(line_bpoints), Line))
|
||||
self.assertSequenceEqual(bpoints2bezier(cubic_bpoints).bpoints(),
|
||||
cubic_bpoints)
|
||||
self.assertSequenceEqual(bpoints2bezier(quadratic_bpoints).bpoints(),
|
||||
quadratic_bpoints)
|
||||
self.assertSequenceEqual(bpoints2bezier(line_bpoints).bpoints(),
|
||||
line_bpoints)
|
||||
|
||||
# def test_line2pathd(self):
|
||||
# bpoints = (0+1.5j, 100+10j)
|
||||
# line = Line(*bpoints)
|
||||
#
|
||||
# # from Line object
|
||||
# pathd = line2pathd(line)
|
||||
# path = parse_path(pathd)
|
||||
# self.assertTrue(path[0] == line)
|
||||
#
|
||||
# # from list of bpoints
|
||||
# pathd = line2pathd(bpoints)
|
||||
# path = parse_path(pathd)
|
||||
# self.assertTrue(path[0] == line)
|
||||
#
|
||||
# def test_cubic2pathd(self):
|
||||
# bpoints = (0+1.5j, 100+10j, 150-155.3j, 0)
|
||||
# cubic = CubicBezier(*bpoints)
|
||||
#
|
||||
# # from Line object
|
||||
# pathd = cubic2pathd(cubic)
|
||||
# path = parse_path(pathd)
|
||||
# self.assertTrue(path[0] == cubic)
|
||||
#
|
||||
# # from list of bpoints
|
||||
# pathd = cubic2pathd(bpoints)
|
||||
# path = parse_path(pathd)
|
||||
# self.assertTrue(path[0] == cubic)
|
||||
|
||||
def test_closest_point_in_path(self):
|
||||
def distfcn(tup1, tup2):
|
||||
assert len(tup1) == len(tup2)
|
||||
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
|
||||
|
||||
# Note: currently the radiialrange method is not implemented for Arc
|
||||
# objects
|
||||
# test_path = self.path_of_all_seg_types
|
||||
# origin = -123 - 123j
|
||||
# expected_result = ???
|
||||
# self.assertAlmostEqual(min_radius(origin, test_path),
|
||||
# expected_result)
|
||||
|
||||
# generic case (where is_bezier_path(test_path) == True)
|
||||
test_path = self.path_of_bezier_seg_types
|
||||
pt = 300+300j
|
||||
expected_result = (29.382522853493143, 0.17477067969145446, 2)
|
||||
result = closest_point_in_path(pt, test_path)
|
||||
err = distfcn(expected_result, result)
|
||||
self.assertAlmostEqual(err, 0)
|
||||
|
||||
# cubic test with multiple valid solutions
|
||||
test_path = Path(CubicBezier(1-2j, 10-1j, 10+1j, 1+2j))
|
||||
pt = 3
|
||||
expected_results = [(1.7191878932122302, 0.90731678233211366, 0),
|
||||
(1.7191878932122304, 0.092683217667886342, 0)]
|
||||
result = closest_point_in_path(pt, test_path)
|
||||
err = min(distfcn(e_res, result) for e_res in expected_results)
|
||||
self.assertAlmostEqual(err, 0)
|
||||
|
||||
def test_farthest_point_in_path(self):
|
||||
def distfcn(tup1, tup2):
|
||||
assert len(tup1) == len(tup2)
|
||||
return sum((tup1[i]-tup2[i])**2 for i in range(len(tup1)))**0.5
|
||||
|
||||
# Note: currently the radiialrange method is not implemented for Arc
|
||||
# objects
|
||||
# test_path = self.path_of_all_seg_types
|
||||
# origin = -123 - 123j
|
||||
# expected_result = ???
|
||||
# self.assertAlmostEqual(min_radius(origin, test_path),
|
||||
# expected_result)
|
||||
|
||||
# boundary test
|
||||
test_path = self.path_of_bezier_seg_types
|
||||
pt = 300+300j
|
||||
expected_result = (424.26406871192853, 0, 0)
|
||||
result = farthest_point_in_path(pt, test_path)
|
||||
err = distfcn(expected_result, result)
|
||||
self.assertAlmostEqual(err, 0)
|
||||
|
||||
# non-boundary test
|
||||
test_path = Path(CubicBezier(1-2j, 10-1j, 10+1j, 1+2j))
|
||||
pt = 3
|
||||
expected_result = (4.75, 0.5, 0)
|
||||
result = farthest_point_in_path(pt, test_path)
|
||||
err = distfcn(expected_result, result)
|
||||
self.assertAlmostEqual(err, 0)
|
||||
|
||||
def test_path_encloses_pt(self):
|
||||
|
||||
line1 = Line(0, 100+100j)
|
||||
quadratic1 = QuadraticBezier(100+100j, 150+150j, 300+200j)
|
||||
cubic1 = CubicBezier(300+200j, 350+400j, 400+425j, 650+325j)
|
||||
line2 = Line(650+325j, 650+10j)
|
||||
line3 = Line(650+10j, 0)
|
||||
open_bez_path = Path(line1, quadratic1, cubic1)
|
||||
closed_bez_path = Path(line1, quadratic1, cubic1, line2, line3)
|
||||
|
||||
inside_pt = 200+20j
|
||||
outside_pt1 = 1000+1000j
|
||||
outside_pt2 = 800+800j
|
||||
boundary_pt = 50+50j
|
||||
|
||||
# Note: currently the intersect() method is not implemented for Arc
|
||||
# objects
|
||||
# arc1 = Arc(650+325j, 25+25j, -30.0, False, True, 700+300j)
|
||||
# closed_path_with_arc = Path(line1, quadratic1, cubic1, arc1)
|
||||
# self.assertTrue(
|
||||
# path_encloses_pt(inside_pt, outside_pt2, closed_path_with_arc))
|
||||
|
||||
# True cases
|
||||
self.assertTrue(
|
||||
path_encloses_pt(inside_pt, outside_pt2, closed_bez_path))
|
||||
self.assertTrue(
|
||||
path_encloses_pt(boundary_pt, outside_pt2, closed_bez_path))
|
||||
|
||||
# False cases
|
||||
self.assertFalse(
|
||||
path_encloses_pt(outside_pt1, outside_pt2, closed_bez_path))
|
||||
|
||||
# Exception Cases
|
||||
with self.assertRaises(AssertionError):
|
||||
path_encloses_pt(inside_pt, outside_pt2, open_bez_path)
|
||||
|
||||
# Display test paths and points
|
||||
# ns2d = [inside_pt, outside_pt1, outside_pt2, boundary_pt]
|
||||
# ncolors = ['green', 'red', 'orange', 'purple']
|
||||
# disvg(closed_path_with_arc, nodes=ns2d, node_colors=ncolors,
|
||||
# openinbrowser=True)
|
||||
# disvg(open_bez_path, nodes=ns2d, node_colors=ncolors,
|
||||
# openinbrowser=True)
|
||||
# disvg(closed_bez_path, nodes=ns2d, node_colors=ncolors,
|
||||
# openinbrowser=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,30 @@
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
# Internal dependencies
|
||||
from svgpathtools import *
|
||||
|
||||
|
||||
class Test_polytools(unittest.TestCase):
|
||||
# def test_poly_roots(self):
|
||||
# self.fail()
|
||||
|
||||
def test_rational_limit(self):
|
||||
|
||||
# (3x^3 + x)/(4x^2 - 2x) -> -1/2 as x->0
|
||||
f = np.poly1d([3, 0, 1, 0])
|
||||
g = np.poly1d([4, -2, 0])
|
||||
lim = rational_limit(f, g, 0)
|
||||
self.assertAlmostEqual(lim, -0.5)
|
||||
|
||||
# (3x^2)/(4x^2 - 2x) -> 0 as x->0
|
||||
f = np.poly1d([3, 0, 0])
|
||||
g = np.poly1d([4, -2, 0])
|
||||
lim = rational_limit(f, g, 0)
|
||||
self.assertAlmostEqual(lim, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="600px" width="600px" version="1.1"
|
||||
viewBox="-20.6525 -20.6525 366.305 366.305"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<defs/>
|
||||
<path d="M 10.5,80.0
|
||||
C 40.0,10.0 65.0,10.0 95.0,80.0
|
||||
C 125.0,150.0 150.0,150.0 180.0,80.0"
|
||||
fill="none"
|
||||
stroke="red"/>
|
||||
<path d="m 84,92 c 10,10 30,10 40,0" fill="none" stroke="purple"/>
|
||||
<path d="M 40.0,100.0
|
||||
Q 100.0,50.0 65.0,20.0"
|
||||
fill="none"
|
||||
stroke="blue"/>
|
||||
<path d="M 10.0,315.0
|
||||
L 110.0,215.0
|
||||
A 30.6416522074,51.0694203457 0.0 0,1 162.55,162.45
|
||||
L 172.55,152.45 A 30.1,50.1 -45.0 0,1 215.1,109.9
|
||||
L 315.0,10.0"
|
||||
fill="green"
|
||||
fill-opacity="0.5"
|
||||
stroke="black"
|
||||
stroke-width="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 815 B |
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg baseProfile="full" height="368px" version="1.1" viewBox="138.46883649 65.274611702 420.978169975 257.645495611" width="600px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path d="M 175.0,162.5 L 175.0,236.805280985" id="tp0"/>
|
||||
<path d="M 397.915842954,162.5 L 397.915842954,88.1947190152" id="tp1"/>
|
||||
</defs>
|
||||
<path d="M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0" fill="none" stroke="blue" stroke-width="0.347915842954"/>
|
||||
<path d="M 175.0,162.5 L 175.0,236.805280985" fill="none" stroke="green" stroke-width="0.347915842954"/>
|
||||
<path d="M 175.0,162.5 L 249.305280985,162.5" fill="none" stroke="pink" stroke-width="0.347915842954"/>
|
||||
<path d="M 422.915842954,300.0 C 422.915842954,200.0 322.915842954,100.0 522.915842954,100.0" fill="none" stroke="black" stroke-width="0.347915842954"/>
|
||||
<path d="M 397.915842954,162.5 L 397.915842954,88.1947190152" fill="none" stroke="green" stroke-width="0.347915842954"/>
|
||||
<path d="M 397.915842954,162.5 L 323.61056197,162.5" fill="none" stroke="pink" stroke-width="0.347915842954"/>
|
||||
<circle cx="175.0" cy="162.5" fill="#ff0000" r="1.73957921477"/>
|
||||
<circle cx="397.915842954" cy="162.5" fill="#ff0000" r="1.73957921477"/>
|
||||
<text font-size="12">
|
||||
<textPath xlink:href="#tp0">b's tangent</textPath>
|
||||
</text>
|
||||
<text font-size="12">
|
||||
<textPath xlink:href="#tp1">br's tangent</textPath>
|
||||
</text>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |