Compare commits
3 Commits
master
...
vectorize-
Author | SHA1 | Date |
---|---|---|
Andrew Port | 026b1dc6e6 | |
Andrew Port | 2824a26a6c | |
Andrew Port | 8fd4fd73b8 |
|
@ -1,14 +0,0 @@
|
||||||
name: Codacy
|
|
||||||
|
|
||||||
on: ["push"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codacy-analysis-cli:
|
|
||||||
name: Codacy Analysis CLI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@main
|
|
||||||
|
|
||||||
- name: Run Codacy Analysis CLI
|
|
||||||
uses: codacy/codacy-analysis-cli-action@master
|
|
|
@ -1,65 +0,0 @@
|
||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 2 * * 3'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'python' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
|
@ -1,34 +0,0 @@
|
||||||
name: Github CI Unit Testing for Legacy Environments
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
continue-on-error: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-18.04, macos-10.15, windows-2019]
|
|
||||||
python-version: [2.7, 3.5, 3.6]
|
|
||||||
steps:
|
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# configure python
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
# install deps
|
|
||||||
- name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }}
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install scipy
|
|
||||||
|
|
||||||
# find and run all unit tests
|
|
||||||
- name: Run unit tests
|
|
||||||
run: python -m unittest discover test
|
|
|
@ -1,34 +0,0 @@
|
||||||
name: Github CI Unit Testing
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
continue-on-error: true
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
||||||
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
|
|
||||||
steps:
|
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# configure python
|
|
||||||
- uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
# install deps
|
|
||||||
- name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }}
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
pip install scipy
|
|
||||||
|
|
||||||
# find and run all unit tests
|
|
||||||
- name: Run unit tests
|
|
||||||
run: python -m unittest discover test
|
|
|
@ -1,37 +0,0 @@
|
||||||
name: Publish to TestPyPI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-n-publish:
|
|
||||||
name: Build and publish to TestPyPI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
- name: Set up Python 3
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3
|
|
||||||
- name: Install pypa/build
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
pip install
|
|
||||||
build
|
|
||||||
--user
|
|
||||||
- name: Build a binary wheel and a source tarball
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
build
|
|
||||||
--sdist
|
|
||||||
--wheel
|
|
||||||
--outdir dist/
|
|
||||||
.
|
|
||||||
- name: Publish to Test PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
with:
|
|
||||||
skip_existing: true
|
|
||||||
password: ${{ secrets.TESTPYPI_API_TOKEN }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
|
|
@ -1,42 +0,0 @@
|
||||||
name: Publish to PyPI if new version
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-n-publish:
|
|
||||||
name: Build and publish to TestPyPI and PyPI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
- name: Set up Python 3
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
|
||||||
python-version: 3
|
|
||||||
- name: Install pypa/build
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
pip install
|
|
||||||
build
|
|
||||||
--user
|
|
||||||
- name: Build a binary wheel and a source tarball
|
|
||||||
run: >-
|
|
||||||
python -m
|
|
||||||
build
|
|
||||||
--sdist
|
|
||||||
--wheel
|
|
||||||
--outdir dist/
|
|
||||||
.
|
|
||||||
- name: Publish to Test PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
with:
|
|
||||||
skip_existing: true
|
|
||||||
password: ${{ secrets.TESTPYPI_API_TOKEN }}
|
|
||||||
repository_url: https://test.pypi.org/legacy/
|
|
||||||
- name: Publish to PyPI
|
|
||||||
if: startsWith(github.ref, 'refs/tags')
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
with:
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
|
@ -5,4 +5,3 @@ build
|
||||||
svgpathtools.egg-info
|
svgpathtools.egg-info
|
||||||
!.travis.yml
|
!.travis.yml
|
||||||
!/.gitignore
|
!/.gitignore
|
||||||
!/.github
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- '2.7'
|
||||||
|
- '3.6'
|
||||||
|
install:
|
||||||
|
- pip install numpy svgwrite scipy
|
||||||
|
script:
|
||||||
|
- python -m unittest discover test
|
||||||
|
deploy:
|
||||||
|
provider: pypi
|
||||||
|
username: __token__
|
||||||
|
password:
|
||||||
|
secure: DaP6S0yj9KlXICtYCWkd54f+yulPYMqr01giOFs2xv5kvMHF1GV160XvYdeLDfwLwAsjKr7438vhgeik99T9iWVb9SYQQsOj8zx+uKcNvNxY/+cHxN9joVOMbhowiqELtKbyNSf1fzax8CGv5+L6k7HfiFV6Zxy4r/4a7JJnavqlsyFn2fixD5FmY+gXjMlnWAer3Q9/1GCWhoUEkON9TRVUcZkYvvBGG16A3m/5o9brCiilaNH5lMiHjOQfbEjxYRgIy6wLGOT9u7EwYAhbnzKhqiaR2jQ5ZxFn52CVoy3r8+T9zvVtToDcjdn9bI+CEFiU37FR1sdJhANFozsoVT3LZid97e7BuMa5+UfFZWZIU7SOcPkzOYxEGwrwqrBWfSeQ3/R76fCcVyy4mNh7YJ5q/89y+NbIpWl6LxVJIwkfix9SUv0z1w4jFzOk5anzrCAJKtJxVFZSUC70ERPaDRK3t9jrZZNS9LIh7wcWGHuGyD4h/8gZr9y3STmNImDq1KdJnldgnOefIS2OXwM5IRkxKvtGeWgn123mp/1XoKDGX8ZhoWp0W0y1A9nZtOkWFjTUjmmj8SeD2R98KBQACsTUqTg3jtZR5dh3VHIPvUc2GeMXQVTZJspi9nyDhJbTLluMk+694S1nRb6Qv0UgIcz651gnv4s8qa6lGXKxEEg=
|
||||||
|
on:
|
||||||
|
tags: true
|
16
README.ipynb
16
README.ipynb
|
@ -4,10 +4,6 @@
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)\n",
|
|
||||||
"![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)\n",
|
|
||||||
"[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)\n",
|
|
||||||
"[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)\n",
|
|
||||||
"# svgpathtools\n",
|
"# svgpathtools\n",
|
||||||
"\n",
|
"\n",
|
||||||
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
|
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
|
||||||
|
@ -40,10 +36,20 @@
|
||||||
"## Prerequisites\n",
|
"## Prerequisites\n",
|
||||||
"- **numpy**\n",
|
"- **numpy**\n",
|
||||||
"- **svgwrite**\n",
|
"- **svgwrite**\n",
|
||||||
"- **scipy** (optional but recommended for performance)\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
"## Setup\n",
|
"## Setup\n",
|
||||||
"\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",
|
"```bash\n",
|
||||||
"$ pip install svgpathtools\n",
|
"$ pip install svgpathtools\n",
|
||||||
"``` \n",
|
"``` \n",
|
||||||
|
|
17
README.md
17
README.md
|
@ -1,10 +1,5 @@
|
||||||
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD)
|
|
||||||
![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
|
|
||||||
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
|
|
||||||
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
|
|
||||||
# svgpathtools
|
# svgpathtools
|
||||||
|
|
||||||
|
|
||||||
svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.
|
svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -35,10 +30,20 @@ Some included tools:
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
- **numpy**
|
- **numpy**
|
||||||
- **svgwrite**
|
- **svgwrite**
|
||||||
- **scipy** (optional, but recommended for performance)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
If not already installed, you can **install the prerequisites** using pip.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pip install numpy
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pip install svgwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **install svgpathtools**:
|
||||||
```bash
|
```bash
|
||||||
$ pip install svgpathtools
|
$ pip install svgpathtools
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
To report any security vulnerability, email andyaport@gmail.com
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,16 +0,0 @@
|
||||||
<?xml version="1.0" ?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="50px" version="1.1" viewBox="-15.075 -5.075 180.15 60.15" width="150px">
|
|
||||||
<defs>
|
|
||||||
<path d="M 10.0,24.0 L 140.0,24.0" id="tp0"/>
|
|
||||||
<path d="M 10.0,40.0 L 140.0,40.0" id="tp1"/>
|
|
||||||
</defs>
|
|
||||||
<a href="https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&currency_code=USD">
|
|
||||||
<path d="M 0.0,25.0 C 0.0,0.0 0.0,0.0 75.0,0.0 C 150.0,0.0 150.0,0.0 150.0,25.0 C 150.0,50.0 150.0,50.0 75.0,50.0 C 0.0,50.0 0.0,50.0 0.0,25.0" fill="#34eb86" stroke="#000000" stroke-width="0.15"/>
|
|
||||||
<text font-size="15" font-weight="bold">
|
|
||||||
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp0">Donate to the creator</textPath>
|
|
||||||
</text>
|
|
||||||
<text font-size="16">
|
|
||||||
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp1">(He's a student.)</textPath>
|
|
||||||
</text>
|
|
||||||
</a>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.0 KiB |
|
@ -8,7 +8,7 @@ Note: The relevant matrix transformation for quadratics can be found in the
|
||||||
svgpathtools.bezier module."""
|
svgpathtools.bezier module."""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
|
from svgpathtools import *
|
||||||
|
|
||||||
|
|
||||||
class HigherOrderBezier:
|
class HigherOrderBezier:
|
||||||
|
|
|
@ -7,8 +7,7 @@ Path.continuous_subpaths() method to split a paths into a list of its
|
||||||
continuous subpaths.
|
continuous subpaths.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from svgpathtools import Path, Line
|
from svgpathtools import *
|
||||||
|
|
||||||
|
|
||||||
def path1_is_contained_in_path2(path1, path2):
|
def path1_is_contained_in_path2(path1, path2):
|
||||||
assert path2.isclosed() # This question isn't well-defined otherwise
|
assert path2.isclosed() # This question isn't well-defined otherwise
|
||||||
|
@ -17,11 +16,11 @@ def path1_is_contained_in_path2(path1, path2):
|
||||||
|
|
||||||
# find a point that's definitely outside path2
|
# find a point that's definitely outside path2
|
||||||
xmin, xmax, ymin, ymax = path2.bbox()
|
xmin, xmax, ymin, ymax = path2.bbox()
|
||||||
b = (xmin + 1) + 1j*(ymax + 1)
|
B = (xmin + 1) + 1j*(ymax + 1)
|
||||||
|
|
||||||
a = path1.start # pick an arbitrary point in path1
|
A = path1.start # pick an arbitrary point in path1
|
||||||
ab_line = Path(Line(a, b))
|
AB_line = Path(Line(A, B))
|
||||||
number_of_intersections = len(ab_line.intersect(path2))
|
number_of_intersections = len(AB_line.intersect(path2))
|
||||||
if number_of_intersections % 2: # if number of intersections is odd
|
if number_of_intersections % 2: # if number of intersections is odd
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
from svgpathtools import disvg, Line, CubicBezier
|
from svgpathtools import *
|
||||||
from scipy.optimize import fminbound
|
|
||||||
|
|
||||||
# create some example paths
|
# create some example paths
|
||||||
path1 = CubicBezier(1,2+3j,3-5j,4+1j)
|
path1 = CubicBezier(1,2+3j,3-5j,4+1j)
|
||||||
path2 = path1.rotated(60).translated(3)
|
path2 = path1.rotated(60).translated(3)
|
||||||
|
|
||||||
|
# find minimizer
|
||||||
|
from scipy.optimize import fminbound
|
||||||
def dist(t):
|
def dist(t):
|
||||||
return path1.radialrange(path2.point(t))[0][0]
|
return path1.radialrange(path2.point(t))[0][0]
|
||||||
|
|
||||||
|
|
||||||
# find minimizer
|
|
||||||
T2 = fminbound(dist, 0, 1)
|
T2 = fminbound(dist, 0, 1)
|
||||||
|
|
||||||
# Let's do a visual check
|
# Let's do a visual check
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script>
|
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>svgpathtools in JS!</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<button id="go_button" onclick="tick()" hidden>Click Me!</button>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div>Output:</div>
|
|
||||||
<label for="output"></label>
|
|
||||||
<textarea id="output" style="width: 100%;" rows="6" disabled></textarea>
|
|
||||||
|
|
||||||
<svg height="100" width="100">
|
|
||||||
<circle cx="50" cy="50" r="40" stroke-width="2" stroke="black" fill="blue"/>
|
|
||||||
<path id="ticker" d="M 50 50 L 50 15" stroke-width="2" stroke="black"/>
|
|
||||||
<circle cx="50" cy="50" r="3" stroke-width="2" stroke="black" fill="green"/>
|
|
||||||
Sorry, your browser does not support inline SVG.
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// init Pyodide environment and install svgpathtools
|
|
||||||
async function main() {
|
|
||||||
let pyodide = await loadPyodide({
|
|
||||||
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
|
|
||||||
});
|
|
||||||
await pyodide.loadPackage("micropip");
|
|
||||||
pyodide.runPythonAsync(`
|
|
||||||
import micropip
|
|
||||||
await micropip.install('svgpathtools')
|
|
||||||
`);
|
|
||||||
output.value += "svgpathtools is ready!\n";
|
|
||||||
return pyodide;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tick() {
|
|
||||||
let clock_hand = document.getElementById("ticker");
|
|
||||||
let pyodide = await pyodideReadyPromise;
|
|
||||||
try {
|
|
||||||
let result = pyodide.runPython(`
|
|
||||||
from svgpathtools import parse_path
|
|
||||||
parse_path('${clock_hand.getAttribute('d')}').rotated(45, origin=50+50j).d()
|
|
||||||
`);
|
|
||||||
clock_hand.setAttribute('d', result);
|
|
||||||
} catch (err) {
|
|
||||||
output.value += err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pyodideReadyPromise = main();
|
|
||||||
$(document).ready(function(){
|
|
||||||
const output = document.getElementById("output");
|
|
||||||
output.value = "Initializing...\n";
|
|
||||||
document.getElementById("go_button").removeAttribute('hidden');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,3 +1,2 @@
|
||||||
numpy
|
numpy
|
||||||
svgwrite
|
svgwrite
|
||||||
scipy
|
|
||||||
|
|
31
setup.py
31
setup.py
|
@ -3,17 +3,18 @@ import codecs
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.6.1'
|
VERSION = '1.4.1'
|
||||||
AUTHOR_NAME = 'Andy Port'
|
AUTHOR_NAME = 'Andy Port'
|
||||||
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
||||||
GITHUB = 'https://github.com/mathandy/svgpathtools'
|
|
||||||
|
|
||||||
_here = os.path.abspath(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def read(relative_path):
|
def read(*parts):
|
||||||
"""Reads file at relative path, returning contents as string."""
|
"""
|
||||||
with codecs.open(os.path.join(_here, relative_path), "rb", "utf-8") as f:
|
Build an absolute path from *parts* and and return the contents of the
|
||||||
|
resulting file. Assume UTF-8 encoding.
|
||||||
|
"""
|
||||||
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,12 +27,12 @@ setup(name='svgpathtools',
|
||||||
long_description_content_type='text/markdown',
|
long_description_content_type='text/markdown',
|
||||||
author=AUTHOR_NAME,
|
author=AUTHOR_NAME,
|
||||||
author_email=AUTHOR_EMAIL,
|
author_email=AUTHOR_EMAIL,
|
||||||
url=GITHUB,
|
url='https://github.com/mathandy/svgpathtools',
|
||||||
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
|
# download_url = 'http://github.com/mathandy/svgpathtools/tarball/'+VERSION,
|
||||||
''.format(GITHUB, VERSION, VERSION),
|
|
||||||
license='MIT',
|
license='MIT',
|
||||||
install_requires=['numpy', 'svgwrite', 'scipy'],
|
install_requires=['numpy', 'svgwrite'],
|
||||||
platforms="OS Independent",
|
platforms="OS Independent",
|
||||||
|
requires=['numpy', 'svgwrite'],
|
||||||
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
|
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
|
@ -40,14 +41,6 @@ setup(name='svgpathtools',
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 2.7",
|
|
||||||
"Programming Language :: Python :: 3.5",
|
|
||||||
"Programming Language :: Python :: 3.6",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
|
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
|
||||||
"Topic :: Scientific/Engineering",
|
"Topic :: Scientific/Engineering",
|
||||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||||
|
|
|
@ -17,6 +17,6 @@ from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS,
|
||||||
from .svg_io_sax import SaxDocument
|
from .svg_io_sax import SaxDocument
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
|
from .svg_to_paths import svg2paths, svg2paths2
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -13,7 +13,7 @@ An Historic Note:
|
||||||
Example:
|
Example:
|
||||||
Typical usage looks something like the following.
|
Typical usage looks something like the following.
|
||||||
|
|
||||||
>> from svgpathtools import Document
|
>> from svgpathtools import *
|
||||||
>> doc = Document('my_file.html')
|
>> doc = Document('my_file.html')
|
||||||
>> for path in doc.paths():
|
>> for path in doc.paths():
|
||||||
>> # Do something with the transformed Path object.
|
>> # Do something with the transformed Path object.
|
||||||
|
@ -41,10 +41,8 @@ import xml.etree.ElementTree as etree
|
||||||
from xml.etree.ElementTree import Element, SubElement, register_namespace
|
from xml.etree.ElementTree import Element, SubElement, register_namespace
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
import warnings
|
import warnings
|
||||||
from io import StringIO
|
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
from time import time
|
from time import time
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
|
@ -52,17 +50,13 @@ from .parser import parse_transform
|
||||||
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
|
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
|
||||||
polyline2pathd, polygon2pathd, rect2pathd)
|
polyline2pathd, polygon2pathd, rect2pathd)
|
||||||
from .misctools import open_in_browser
|
from .misctools import open_in_browser
|
||||||
from .path import transform, Path, is_path_segment
|
from .path import *
|
||||||
|
|
||||||
# To maintain forward/backward compatibility
|
# To maintain forward/backward compatibility
|
||||||
try:
|
try:
|
||||||
string = basestring
|
str = basestring
|
||||||
except NameError:
|
except NameError:
|
||||||
string = str
|
pass
|
||||||
try:
|
|
||||||
from os import PathLike
|
|
||||||
except ImportError:
|
|
||||||
PathLike = string
|
|
||||||
|
|
||||||
# Let xml.etree.ElementTree know about the SVG namespace
|
# Let xml.etree.ElementTree know about the SVG namespace
|
||||||
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
|
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
|
||||||
|
@ -241,14 +235,13 @@ class Document:
|
||||||
The output Path objects will be transformed based on their parent groups.
|
The output Path objects will be transformed based on their parent groups.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath (str or file-like): The filepath of the
|
filepath (str): The filepath of the DOM-style object.
|
||||||
DOM-style object or a file-like object containing it.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# strings are interpreted as file location everything else is treated as
|
# remember location of original svg file
|
||||||
# file-like object and passed to the xml parser directly
|
self.original_filepath = filepath
|
||||||
from_filepath = isinstance(filepath, string) or isinstance(filepath, PathLike)
|
if filepath is not None and os.path.dirname(filepath) == '':
|
||||||
self.original_filepath = os.path.abspath(filepath) if from_filepath else None
|
self.original_filepath = os.path.join(os.getcwd(), filepath)
|
||||||
|
|
||||||
if filepath is None:
|
if filepath is None:
|
||||||
self.tree = etree.ElementTree(Element('svg'))
|
self.tree = etree.ElementTree(Element('svg'))
|
||||||
|
@ -258,14 +251,6 @@ class Document:
|
||||||
|
|
||||||
self.root = self.tree.getroot()
|
self.root = self.tree.getroot()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_svg_string(cls, svg_string):
|
|
||||||
"""Constructor for creating a Document object from a string."""
|
|
||||||
# wrap string into StringIO object
|
|
||||||
svg_file_obj = StringIO(svg_string)
|
|
||||||
# create document from file object
|
|
||||||
return Document(svg_file_obj)
|
|
||||||
|
|
||||||
def paths(self, group_filter=lambda x: True,
|
def paths(self, group_filter=lambda x: True,
|
||||||
path_filter=lambda x: True, path_conversions=CONVERSIONS):
|
path_filter=lambda x: True, path_conversions=CONVERSIONS):
|
||||||
"""Returns a list of all paths in the document.
|
"""Returns a list of all paths in the document.
|
||||||
|
@ -278,7 +263,7 @@ class Document:
|
||||||
|
|
||||||
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
|
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
|
||||||
path_filter=lambda x: True, path_conversions=CONVERSIONS):
|
path_filter=lambda x: True, path_conversions=CONVERSIONS):
|
||||||
if all(isinstance(s, string) for s in group):
|
if all(isinstance(s, str) for s in group):
|
||||||
# If we're given a list of strings, assume it represents a
|
# If we're given a list of strings, assume it represents a
|
||||||
# nested sequence
|
# nested sequence
|
||||||
group = self.get_group(group)
|
group = self.get_group(group)
|
||||||
|
@ -304,7 +289,7 @@ class Document:
|
||||||
|
|
||||||
# If given a list of strings (one or more), assume it represents
|
# If given a list of strings (one or more), assume it represents
|
||||||
# a sequence of nested group names
|
# a sequence of nested group names
|
||||||
elif len(group) > 0 and all(isinstance(elem, str) for elem in group):
|
elif all(isinstance(elem, str) for elem in group):
|
||||||
group = self.get_or_add_group(group)
|
group = self.get_or_add_group(group)
|
||||||
|
|
||||||
elif not isinstance(group, Element):
|
elif not isinstance(group, Element):
|
||||||
|
@ -323,7 +308,7 @@ class Document:
|
||||||
path_svg = path.d()
|
path_svg = path.d()
|
||||||
elif is_path_segment(path):
|
elif is_path_segment(path):
|
||||||
path_svg = Path(path).d()
|
path_svg = Path(path).d()
|
||||||
elif isinstance(path, string):
|
elif isinstance(path, str):
|
||||||
# Assume this is a valid d-string.
|
# Assume this is a valid d-string.
|
||||||
# TODO: Should we sanity check the input string?
|
# TODO: Should we sanity check the input string?
|
||||||
path_svg = path
|
path_svg = path
|
||||||
|
|
|
@ -12,14 +12,12 @@ except ImportError:
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from itertools import tee
|
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
# these imports were originally from math and cmath, now are from numpy
|
# these imports were originally from math and cmath, now are from numpy
|
||||||
# in order to encourage code that generalizes to vector inputs
|
# in order to encourage code that generalizes to vector inputs
|
||||||
from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \
|
from numpy import sqrt, cos, sin, tan, arccos as acos, arcsin as asin, \
|
||||||
degrees, radians, log, pi, ceil
|
degrees, radians, log, pi, ceil
|
||||||
from numpy import exp, sqrt as csqrt, angle as phase, isnan
|
from numpy import exp, sqrt as csqrt, angle as phase
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from scipy.integrate import quad
|
from scipy.integrate import quad
|
||||||
|
@ -43,8 +41,8 @@ except NameError:
|
||||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||||
UPPERCASE = set('MZLHVCSQTA')
|
UPPERCASE = set('MZLHVCSQTA')
|
||||||
|
|
||||||
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
|
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
||||||
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
||||||
|
|
||||||
# Default Parameters ##########################################################
|
# Default Parameters ##########################################################
|
||||||
|
|
||||||
|
@ -80,14 +78,11 @@ _is_smooth_from_warning = \
|
||||||
|
|
||||||
def bezier_segment(*bpoints):
|
def bezier_segment(*bpoints):
|
||||||
if len(bpoints) == 2:
|
if len(bpoints) == 2:
|
||||||
start, end = bpoints
|
return Line(*bpoints)
|
||||||
return Line(start, end)
|
|
||||||
elif len(bpoints) == 4:
|
elif len(bpoints) == 4:
|
||||||
start, control1, control2, end = bpoints
|
return CubicBezier(*bpoints)
|
||||||
return CubicBezier(start, control1, control2, end)
|
|
||||||
elif len(bpoints) == 3:
|
elif len(bpoints) == 3:
|
||||||
start, control, end = bpoints
|
return QuadraticBezier(*bpoints)
|
||||||
return QuadraticBezier(start, control, end)
|
|
||||||
else:
|
else:
|
||||||
assert len(bpoints) in (2, 3, 4)
|
assert len(bpoints) in (2, 3, 4)
|
||||||
|
|
||||||
|
@ -137,7 +132,6 @@ def polygon(*points):
|
||||||
return Path(*[Line(points[i], points[(i + 1) % len(points)])
|
return Path(*[Line(points[i], points[(i + 1) % len(points)])
|
||||||
for i in range(len(points))])
|
for i in range(len(points))])
|
||||||
|
|
||||||
|
|
||||||
# Conversion###################################################################
|
# Conversion###################################################################
|
||||||
|
|
||||||
def bpoints2bezier(bpoints):
|
def bpoints2bezier(bpoints):
|
||||||
|
@ -186,22 +180,13 @@ def bez2poly(bez, numpy_ordering=True, return_poly1d=False):
|
||||||
|
|
||||||
|
|
||||||
# Geometric####################################################################
|
# Geometric####################################################################
|
||||||
def transform_segments_together(path, transformation):
|
|
||||||
"""Makes sure that, if joints were continuous, they're kept that way."""
|
|
||||||
transformed_segs = [transformation(seg) for seg in path]
|
|
||||||
|
|
||||||
for i, (sa, sb) in enumerate(path.joints()):
|
|
||||||
if sa.end == sb.start:
|
|
||||||
transformed_segs[i].end = transformed_segs[(i + 1) % len(path)].start
|
|
||||||
return Path(*transformed_segs)
|
|
||||||
|
|
||||||
|
|
||||||
def rotate(curve, degs, origin=None):
|
def rotate(curve, degs, origin=None):
|
||||||
"""Returns curve rotated by `degs` degrees (CCW) around the point `origin`
|
"""Returns curve rotated by `degs` degrees (CCW) around the point `origin`
|
||||||
(a complex number). By default origin is either `curve.point(0.5)`, or in
|
(a complex number). By default origin is either `curve.point(0.5)`, or in
|
||||||
the case that curve is an Arc object, `origin` defaults to `curve.center`.
|
the case that curve is an Arc object, `origin` defaults to `curve.center`.
|
||||||
"""
|
"""
|
||||||
def rotate_point(z):
|
def transform(z):
|
||||||
return exp(1j*radians(degs))*(z - origin) + origin
|
return exp(1j*radians(degs))*(z - origin) + origin
|
||||||
|
|
||||||
if origin is None:
|
if origin is None:
|
||||||
|
@ -211,13 +196,12 @@ def rotate(curve, degs, origin=None):
|
||||||
origin = curve.point(0.5)
|
origin = curve.point(0.5)
|
||||||
|
|
||||||
if isinstance(curve, Path):
|
if isinstance(curve, Path):
|
||||||
transformation = lambda seg: rotate(seg, degs, origin=origin)
|
return Path(*[rotate(seg, degs, origin=origin) for seg in curve])
|
||||||
return transform_segments_together(curve, transformation)
|
|
||||||
elif is_bezier_segment(curve):
|
elif is_bezier_segment(curve):
|
||||||
return bpoints2bezier([rotate_point(bpt) for bpt in curve.bpoints()])
|
return bpoints2bezier([transform(bpt) for bpt in curve.bpoints()])
|
||||||
elif isinstance(curve, Arc):
|
elif isinstance(curve, Arc):
|
||||||
new_start = rotate_point(curve.start)
|
new_start = transform(curve.start)
|
||||||
new_end = rotate_point(curve.end)
|
new_end = transform(curve.end)
|
||||||
new_rotation = curve.rotation + degs
|
new_rotation = curve.rotation + degs
|
||||||
return Arc(new_start, radius=curve.radius, rotation=new_rotation,
|
return Arc(new_start, radius=curve.radius, rotation=new_rotation,
|
||||||
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
|
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
|
||||||
|
@ -230,8 +214,7 @@ def translate(curve, z0):
|
||||||
"""Shifts the curve by the complex quantity z such that
|
"""Shifts the curve by the complex quantity z such that
|
||||||
translate(curve, z0).point(t) = curve.point(t) + z0"""
|
translate(curve, z0).point(t) = curve.point(t) + z0"""
|
||||||
if isinstance(curve, Path):
|
if isinstance(curve, Path):
|
||||||
transformation = lambda seg: translate(seg, z0)
|
return Path(*[translate(seg, z0) for seg in curve])
|
||||||
return transform_segments_together(curve, transformation)
|
|
||||||
elif is_bezier_segment(curve):
|
elif is_bezier_segment(curve):
|
||||||
return bpoints2bezier([bpt + z0 for bpt in curve.bpoints()])
|
return bpoints2bezier([bpt + z0 for bpt in curve.bpoints()])
|
||||||
elif isinstance(curve, Arc):
|
elif isinstance(curve, Arc):
|
||||||
|
@ -272,8 +255,7 @@ def scale(curve, sx, sy=None, origin=0j):
|
||||||
return poly2bez(p)
|
return poly2bez(p)
|
||||||
|
|
||||||
if isinstance(curve, Path):
|
if isinstance(curve, Path):
|
||||||
transformation = lambda seg: scale(seg, sx, sy, origin)
|
return Path(*[scale(seg, sx, sy, origin) for seg in curve])
|
||||||
return transform_segments_together(curve, transformation)
|
|
||||||
elif is_bezier_segment(curve):
|
elif is_bezier_segment(curve):
|
||||||
return scale_bezier(curve)
|
return scale_bezier(curve)
|
||||||
elif isinstance(curve, Arc):
|
elif isinstance(curve, Arc):
|
||||||
|
@ -294,10 +276,6 @@ def scale(curve, sx, sy=None, origin=0j):
|
||||||
|
|
||||||
def transform(curve, tf):
|
def transform(curve, tf):
|
||||||
"""Transforms the curve by the homogeneous transformation matrix tf"""
|
"""Transforms the curve by the homogeneous transformation matrix tf"""
|
||||||
|
|
||||||
if all((tf == np.eye(3)).ravel()):
|
|
||||||
return curve # tf is identity, return curve as is
|
|
||||||
|
|
||||||
def to_point(p):
|
def to_point(p):
|
||||||
return np.array([[p.real], [p.imag], [1.0]])
|
return np.array([[p.real], [p.imag], [1.0]])
|
||||||
|
|
||||||
|
@ -308,45 +286,20 @@ def transform(curve, tf):
|
||||||
return v.item(0) + 1j * v.item(1)
|
return v.item(0) + 1j * v.item(1)
|
||||||
|
|
||||||
if isinstance(curve, Path):
|
if isinstance(curve, Path):
|
||||||
transformation = lambda seg: transform(seg, tf)
|
return Path(*[transform(segment, tf) for segment in curve])
|
||||||
return transform_segments_together(curve, transformation)
|
|
||||||
|
|
||||||
elif is_bezier_segment(curve):
|
elif is_bezier_segment(curve):
|
||||||
return bpoints2bezier([to_complex(tf.dot(to_point(p)))
|
return bpoints2bezier([to_complex(tf.dot(to_point(p)))
|
||||||
for p in curve.bpoints()])
|
for p in curve.bpoints()])
|
||||||
elif isinstance(curve, Arc):
|
elif isinstance(curve, Arc):
|
||||||
new_start = to_complex(tf.dot(to_point(curve.start)))
|
new_start = to_complex(tf.dot(to_point(curve.start)))
|
||||||
new_end = to_complex(tf.dot(to_point(curve.end)))
|
new_end = to_complex(tf.dot(to_point(curve.end)))
|
||||||
|
new_radius = to_complex(tf.dot(to_vector(curve.radius)))
|
||||||
# Based on https://math.stackexchange.com/questions/2349726/
|
|
||||||
rx2 = curve.radius.real ** 2
|
|
||||||
ry2 = curve.radius.imag ** 2
|
|
||||||
|
|
||||||
Q = np.array([[1/rx2, 0], [0, 1/ry2]])
|
|
||||||
invT = np.linalg.inv(tf[:2,:2])
|
|
||||||
D = reduce(np.matmul, [invT.T, Q, invT])
|
|
||||||
|
|
||||||
eigvals, eigvecs = np.linalg.eig(D)
|
|
||||||
|
|
||||||
rx = 1 / np.sqrt(eigvals[0])
|
|
||||||
ry = 1 / np.sqrt(eigvals[1])
|
|
||||||
|
|
||||||
new_radius = complex(rx, ry)
|
|
||||||
|
|
||||||
xeigvec = eigvecs[:, 0]
|
|
||||||
rot = np.degrees(np.arccos(xeigvec[0]))
|
|
||||||
|
|
||||||
if new_radius.real == 0 or new_radius.imag == 0 :
|
|
||||||
return Line(new_start, new_end)
|
|
||||||
else:
|
|
||||||
if tf[0][0] * tf[1][1] >= 0.0:
|
if tf[0][0] * tf[1][1] >= 0.0:
|
||||||
new_sweep = curve.sweep
|
new_sweep = curve.sweep
|
||||||
else:
|
else:
|
||||||
new_sweep = not curve.sweep
|
new_sweep = not curve.sweep
|
||||||
return Arc(new_start, radius=new_radius, rotation=curve.rotation + rot,
|
return Arc(new_start, radius=new_radius, rotation=curve.rotation,
|
||||||
large_arc=curve.large_arc, sweep=new_sweep, end=new_end,
|
large_arc=curve.large_arc, sweep=new_sweep, end=new_end)
|
||||||
autoscale_radius=True)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("Input `curve` should be a Path, Line, "
|
raise TypeError("Input `curve` should be a Path, Line, "
|
||||||
"QuadraticBezier, CubicBezier, or Arc object.")
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
||||||
|
@ -575,7 +528,8 @@ def inv_arclength(curve, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
||||||
|
|
||||||
|
|
||||||
def crop_bezier(seg, t0, t1):
|
def crop_bezier(seg, t0, t1):
|
||||||
"""Crop a copy of this `self` from `self.point(t0)` to `self.point(t1)`."""
|
"""returns a cropped copy of this segment which starts at self.point(t0)
|
||||||
|
and ends at self.point(t1)."""
|
||||||
assert t0 < t1
|
assert t0 < t1
|
||||||
if t0 == 0:
|
if t0 == 0:
|
||||||
cropped_seg = seg.split(t1)[0]
|
cropped_seg = seg.split(t1)[0]
|
||||||
|
@ -602,9 +556,6 @@ class Line(object):
|
||||||
self.start = start
|
self.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.start, self.end))
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Line(start=%s, end=%s)' % (self.start, self.end)
|
return 'Line(start=%s, end=%s)' % (self.start, self.end)
|
||||||
|
|
||||||
|
@ -644,7 +595,7 @@ class Line(object):
|
||||||
|
|
||||||
def points(self, ts):
|
def points(self, ts):
|
||||||
"""Faster than running Path.point many times."""
|
"""Faster than running Path.point many times."""
|
||||||
return self.poly()(ts)
|
return self.poly(ts)
|
||||||
|
|
||||||
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
||||||
"""returns the length of the line segment between t0 and t1."""
|
"""returns the length of the line segment between t0 and t1."""
|
||||||
|
@ -715,19 +666,6 @@ class Line(object):
|
||||||
Note: This will fail if the two segments coincide for more than a
|
Note: This will fail if the two segments coincide for more than a
|
||||||
finite collection of points.
|
finite collection of points.
|
||||||
tol is not used."""
|
tol is not used."""
|
||||||
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
|
|
||||||
ob = [e.real for e in other_seg.bpoints()]
|
|
||||||
sb = [e.real for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
ob = [e.imag for e in other_seg.bpoints()]
|
|
||||||
sb = [e.imag for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
if isinstance(other_seg, Line):
|
if isinstance(other_seg, Line):
|
||||||
assert other_seg.end != other_seg.start and self.end != self.start
|
assert other_seg.end != other_seg.start and self.end != self.start
|
||||||
assert self != other_seg
|
assert self != other_seg
|
||||||
|
@ -874,9 +812,6 @@ class QuadraticBezier(object):
|
||||||
# used to know if self._length needs to be updated
|
# used to know if self._length needs to be updated
|
||||||
self._length_info = {'length': None, 'bpoints': None}
|
self._length_info = {'length': None, 'bpoints': None}
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.start, self.control, self.end))
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
|
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
|
||||||
self.start, self.control, self.end)
|
self.start, self.control, self.end)
|
||||||
|
@ -934,7 +869,7 @@ class QuadraticBezier(object):
|
||||||
|
|
||||||
def points(self, ts):
|
def points(self, ts):
|
||||||
"""Faster than running Path.point many times."""
|
"""Faster than running Path.point many times."""
|
||||||
return self.poly()(ts)
|
return self.poly(ts)
|
||||||
|
|
||||||
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
def length(self, t0=0, t1=1, error=None, min_depth=None):
|
||||||
if t0 == 1 and t1 == 0:
|
if t0 == 1 and t1 == 0:
|
||||||
|
@ -946,6 +881,15 @@ class QuadraticBezier(object):
|
||||||
|
|
||||||
if abs(a) < 1e-12:
|
if abs(a) < 1e-12:
|
||||||
s = abs(b)*(t1 - t0)
|
s = abs(b)*(t1 - t0)
|
||||||
|
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
|
||||||
|
tstar = abs(b)/(2*abs(a))
|
||||||
|
if t1 < tstar:
|
||||||
|
return abs(a)*(t0**2 - t1**2) - abs(b)*(t0 - t1)
|
||||||
|
elif tstar < t0:
|
||||||
|
return abs(a)*(t1**2 - t0**2) - abs(b)*(t1 - t0)
|
||||||
|
else:
|
||||||
|
return abs(a)*(t1**2 + t0**2) - abs(b)*(t1 + t0) + \
|
||||||
|
abs(b)**2/(2*abs(a))
|
||||||
else:
|
else:
|
||||||
c2 = 4*(a.real**2 + a.imag**2)
|
c2 = 4*(a.real**2 + a.imag**2)
|
||||||
c1 = 4*a_dot_b
|
c1 = 4*a_dot_b
|
||||||
|
@ -958,18 +902,10 @@ class QuadraticBezier(object):
|
||||||
dq0_mag = sqrt(c2*t0**2 + c1*t0 + c0)
|
dq0_mag = sqrt(c2*t0**2 + c1*t0 + c0)
|
||||||
logarand = (sqrt(c2)*(t1 + beta) + dq1_mag) / \
|
logarand = (sqrt(c2)*(t1 + beta) + dq1_mag) / \
|
||||||
(sqrt(c2)*(t0 + beta) + dq0_mag)
|
(sqrt(c2)*(t0 + beta) + dq0_mag)
|
||||||
|
|
||||||
s = (t1 + beta)*dq1_mag - (t0 + beta)*dq0_mag + \
|
s = (t1 + beta)*dq1_mag - (t0 + beta)*dq0_mag + \
|
||||||
gamma*sqrt(c2)*log(logarand)
|
gamma*sqrt(c2)*log(logarand)
|
||||||
s /= 2
|
s /= 2
|
||||||
if isnan(s):
|
|
||||||
tstar = abs(b) / (2 * abs(a))
|
|
||||||
if t1 < tstar:
|
|
||||||
return abs(a) * (t0 ** 2 - t1 ** 2) - abs(b) * (t0 - t1)
|
|
||||||
elif tstar < t0:
|
|
||||||
return abs(a) * (t1 ** 2 - t0 ** 2) - abs(b) * (t1 - t0)
|
|
||||||
else:
|
|
||||||
return abs(a) * (t1 ** 2 + t0 ** 2) - abs(b) * (t1 + t0) + \
|
|
||||||
abs(b) ** 2 / (2 * abs(a))
|
|
||||||
|
|
||||||
if t0 == 1 and t1 == 0:
|
if t0 == 1 and t1 == 0:
|
||||||
self._length_info['length'] = s
|
self._length_info['length'] = s
|
||||||
|
@ -1055,19 +991,6 @@ class QuadraticBezier(object):
|
||||||
self.point(t1) == other_seg.point(t2).
|
self.point(t1) == other_seg.point(t2).
|
||||||
Note: This will fail if the two segments coincide for more than a
|
Note: This will fail if the two segments coincide for more than a
|
||||||
finite collection of points."""
|
finite collection of points."""
|
||||||
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
|
|
||||||
ob = [e.real for e in other_seg.bpoints()]
|
|
||||||
sb = [e.real for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
ob = [e.imag for e in other_seg.bpoints()]
|
|
||||||
sb = [e.imag for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
if isinstance(other_seg, Line):
|
if isinstance(other_seg, Line):
|
||||||
return bezier_by_line_intersections(self, other_seg)
|
return bezier_by_line_intersections(self, other_seg)
|
||||||
elif isinstance(other_seg, QuadraticBezier):
|
elif isinstance(other_seg, QuadraticBezier):
|
||||||
|
@ -1145,9 +1068,6 @@ class CubicBezier(object):
|
||||||
self._length_info = {'length': None, 'bpoints': None, 'error': None,
|
self._length_info = {'length': None, 'bpoints': None, 'error': None,
|
||||||
'min_depth': None}
|
'min_depth': None}
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.start, self.control1, self.control2, self.end))
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
|
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
|
||||||
self.start, self.control1, self.control2, self.end)
|
self.start, self.control1, self.control2, self.end)
|
||||||
|
@ -1211,7 +1131,7 @@ class CubicBezier(object):
|
||||||
|
|
||||||
def points(self, ts):
|
def points(self, ts):
|
||||||
"""Faster than running Path.point many times."""
|
"""Faster than running Path.point many times."""
|
||||||
return self.poly()(ts)
|
return self.poly(ts)
|
||||||
|
|
||||||
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
||||||
"""Calculate the length of the path up to a certain position"""
|
"""Calculate the length of the path up to a certain position"""
|
||||||
|
@ -1319,51 +1239,37 @@ class CubicBezier(object):
|
||||||
|
|
||||||
def intersect(self, other_seg, tol=1e-12):
|
def intersect(self, other_seg, tol=1e-12):
|
||||||
"""Finds the intersections of two segments.
|
"""Finds the intersections of two segments.
|
||||||
|
returns a list of tuples (t1, t2) such that
|
||||||
Returns:
|
|
||||||
(list[tuple[float]]) a list of tuples (t1, t2) such that
|
|
||||||
self.point(t1) == other_seg.point(t2).
|
self.point(t1) == other_seg.point(t2).
|
||||||
|
Note: This will fail if the two segments coincide for more than a
|
||||||
Scope:
|
finite collection of points."""
|
||||||
This will fail if the two segments coincide for more than a
|
|
||||||
finite collection of points.
|
|
||||||
"""
|
|
||||||
if isinstance(other_seg, (Line, QuadraticBezier, CubicBezier)):
|
|
||||||
ob = [e.real for e in other_seg.bpoints()]
|
|
||||||
sb = [e.real for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
ob = [e.imag for e in other_seg.bpoints()]
|
|
||||||
sb = [e.imag for e in self.bpoints()]
|
|
||||||
if min(ob) > max(sb):
|
|
||||||
return []
|
|
||||||
if max(ob) < min(sb):
|
|
||||||
return []
|
|
||||||
if isinstance(other_seg, Line):
|
if isinstance(other_seg, Line):
|
||||||
return bezier_by_line_intersections(self, other_seg)
|
return bezier_by_line_intersections(self, other_seg)
|
||||||
elif (isinstance(other_seg, QuadraticBezier) or
|
elif (isinstance(other_seg, QuadraticBezier) or
|
||||||
isinstance(other_seg, CubicBezier)):
|
isinstance(other_seg, CubicBezier)):
|
||||||
assert self != other_seg
|
assert self != other_seg
|
||||||
longer_length = max(self.length(), other_seg.length())
|
longer_length = max(self.length(), other_seg.length())
|
||||||
return bezier_intersections(
|
return bezier_intersections(self, other_seg,
|
||||||
self, other_seg, longer_length=longer_length, tol=tol, tol_deC=tol
|
longer_length=longer_length,
|
||||||
)
|
tol=tol, tol_deC=tol)
|
||||||
elif isinstance(other_seg, Arc):
|
elif isinstance(other_seg, Arc):
|
||||||
return [(t1, t2) for t2, t1 in other_seg.intersect(self)]
|
t2t1s = other_seg.intersect(self)
|
||||||
|
return [(t1, t2) for t2, t1 in t2t1s]
|
||||||
elif isinstance(other_seg, Path):
|
elif isinstance(other_seg, Path):
|
||||||
raise TypeError("`other_seg` must be a path segment, not a "
|
raise TypeError(
|
||||||
"`Path` object, use `Path.intersect()`.")
|
"other_seg must be a path segment, not a Path object, use "
|
||||||
|
"Path.intersect().")
|
||||||
else:
|
else:
|
||||||
raise TypeError("`other_seg` must be a path segment.")
|
raise TypeError("other_seg must be a path segment.")
|
||||||
|
|
||||||
def bbox(self):
|
def bbox(self):
|
||||||
"""returns bounding box in format (xmin, xmax, ymin, ymax)."""
|
"""returns the bounding box for the segment in the form
|
||||||
|
(xmin, xmax, ymin, ymax)."""
|
||||||
return bezier_bounding_box(self)
|
return bezier_bounding_box(self)
|
||||||
|
|
||||||
def split(self, t):
|
def split(self, t):
|
||||||
"""Splits a copy of `self` at t and returns the two subsegments."""
|
"""returns two segments, whose union is this segment and which join at
|
||||||
|
self.point(t)."""
|
||||||
bpoints1, bpoints2 = split_bezier(self.bpoints(), t)
|
bpoints1, bpoints2 = split_bezier(self.bpoints(), t)
|
||||||
return CubicBezier(*bpoints1), CubicBezier(*bpoints2)
|
return CubicBezier(*bpoints1), CubicBezier(*bpoints2)
|
||||||
|
|
||||||
|
@ -1375,8 +1281,8 @@ class CubicBezier(object):
|
||||||
def radialrange(self, origin, return_all_global_extrema=False):
|
def radialrange(self, origin, return_all_global_extrema=False):
|
||||||
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
|
"""returns the tuples (d_min, t_min) and (d_max, t_max) which minimize
|
||||||
and maximize, respectively, the distance d = |self.point(t)-origin|."""
|
and maximize, respectively, the distance d = |self.point(t)-origin|."""
|
||||||
return bezier_radialrange(
|
return bezier_radialrange(self, origin,
|
||||||
self, origin, return_all_global_extrema=return_all_global_extrema)
|
return_all_global_extrema=return_all_global_extrema)
|
||||||
|
|
||||||
def rotated(self, degs, origin=None):
|
def rotated(self, degs, origin=None):
|
||||||
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
"""Returns a copy of self rotated by `degs` degrees (CCW) around the
|
||||||
|
@ -1398,7 +1304,7 @@ class CubicBezier(object):
|
||||||
class Arc(object):
|
class Arc(object):
|
||||||
def __init__(self, start, radius, rotation, large_arc, sweep, end,
|
def __init__(self, start, radius, rotation, large_arc, sweep, end,
|
||||||
autoscale_radius=True):
|
autoscale_radius=True):
|
||||||
r"""
|
"""
|
||||||
This should be thought of as a part of an ellipse connecting two
|
This should be thought of as a part of an ellipse connecting two
|
||||||
points on that ellipse, start and end.
|
points on that ellipse, start and end.
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -1754,48 +1660,33 @@ class Arc(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def centeriso(self, z):
|
def centeriso(self, z):
|
||||||
"""Isometry to a centered aligned ellipse.
|
"""This is an isometry that translates and rotates self so that it
|
||||||
|
is centered on the origin and has its axes aligned with the xy axes."""
|
||||||
This is an isometry that shifts and rotates `self`'s underlying
|
|
||||||
ellipse so that it's centered on the origin and has its axes
|
|
||||||
aligned with the xy-axes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
z (:obj:`complex` or :obj:`numpy.ndarray[complex]`): a point
|
|
||||||
to send through the above-described isometry.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(:obj:`complex` or :obj:`numpy.ndarray[complex]`) The point(s) f(z),
|
|
||||||
where f is the above described isometry of the xy-plane (i.e.
|
|
||||||
the one-dimensional complex plane).
|
|
||||||
"""
|
|
||||||
return (1/self.rot_matrix)*(z - self.center)
|
return (1/self.rot_matrix)*(z - self.center)
|
||||||
|
|
||||||
def icenteriso(self, zeta):
|
def icenteriso(self, zeta):
|
||||||
"""The inverse of the `centeriso()` method."""
|
"""This is an isometry, the inverse of standardiso()."""
|
||||||
return self.rot_matrix*zeta + self.center
|
return self.rot_matrix*zeta + self.center
|
||||||
|
|
||||||
def u1transform(self, z):
|
def u1transform(self, z):
|
||||||
"""Similar to the `centeriso()` method, but maps to the unit circle."""
|
"""This is an affine transformation (same as used in
|
||||||
zeta = self.centeriso(z)
|
self._parameterize()) that sends self to the unit circle."""
|
||||||
|
zeta = (1/self.rot_matrix)*(z - self.center) # same as centeriso(z)
|
||||||
x, y = real(zeta), imag(zeta)
|
x, y = real(zeta), imag(zeta)
|
||||||
return x/self.radius.real + 1j*y/self.radius.imag
|
return x/self.radius.real + 1j*y/self.radius.imag
|
||||||
|
|
||||||
def iu1transform(self, zeta):
|
def iu1transform(self, zeta):
|
||||||
"""The inverse of the `u1transform()` method."""
|
"""This is an affine transformation, the inverse of
|
||||||
|
self.u1transform()."""
|
||||||
x = real(zeta)
|
x = real(zeta)
|
||||||
y = imag(zeta)
|
y = imag(zeta)
|
||||||
z = x*self.radius.real + y*self.radius.imag
|
z = x*self.radius.real + y*self.radius.imag
|
||||||
return self.rot_matrix*z + self.center
|
return self.rot_matrix*z + self.center
|
||||||
|
|
||||||
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
def length(self, t0=0, t1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
||||||
"""Computes the length of the Arc segment, `self`, from t0 to t1.
|
"""The length of an elliptical large_arc segment requires numerical
|
||||||
|
|
||||||
Notes:
|
|
||||||
* The length of an elliptical large_arc segment requires numerical
|
|
||||||
integration, and in that case it's simpler to just do a geometric
|
integration, and in that case it's simpler to just do a geometric
|
||||||
approximation, as for cubic bezier curves.
|
approximation, as for cubic bezier curves."""
|
||||||
"""
|
|
||||||
assert 0 <= t0 <= 1 and 0 <= t1 <= 1
|
assert 0 <= t0 <= 1 and 0 <= t1 <= 1
|
||||||
|
|
||||||
if t0 == 0 and t1 == 1:
|
if t0 == 0 and t1 == 1:
|
||||||
|
@ -1819,17 +1710,8 @@ class Arc(object):
|
||||||
|
|
||||||
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
def ilength(self, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS,
|
||||||
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
error=ILENGTH_ERROR, min_depth=ILENGTH_MIN_DEPTH):
|
||||||
"""Approximates the unique `t` such that self.length(0, t) = s.
|
"""Returns a float, t, such that self.length(0, t) is approximately s.
|
||||||
|
See the inv_arclength() docstring for more details."""
|
||||||
Args:
|
|
||||||
s (float): A length between 0 and `self.length()`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(float) The t, such that self.length(0, t) is approximately s.
|
|
||||||
|
|
||||||
For more info:
|
|
||||||
See the inv_arclength() docstring.
|
|
||||||
"""
|
|
||||||
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
return inv_arclength(self, s, s_tol=s_tol, maxits=maxits, error=error,
|
||||||
min_depth=min_depth)
|
min_depth=min_depth)
|
||||||
|
|
||||||
|
@ -1927,18 +1809,9 @@ class Arc(object):
|
||||||
not self.sweep, self.start)
|
not self.sweep, self.start)
|
||||||
|
|
||||||
def phase2t(self, psi):
|
def phase2t(self, psi):
|
||||||
"""Converts phase to t-value.
|
"""Given phase -pi < psi <= pi,
|
||||||
|
returns the t value such that
|
||||||
I.e. given phase, psi, such that -np.pi < psi <= np.pi, approximates
|
exp(1j*psi) = self.u1transform(self.point(t)).
|
||||||
the unique t-value such that `self.u1transform(self.point(t))` equals
|
|
||||||
`np.exp(1j*psi)`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
psi (float): The phase in radians.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(float): the corresponding t-value.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _deg(rads, domain_lower_limit):
|
def _deg(rads, domain_lower_limit):
|
||||||
# Convert rads to degrees in [0, 360) domain
|
# Convert rads to degrees in [0, 360) domain
|
||||||
|
@ -1957,6 +1830,7 @@ class Arc(object):
|
||||||
degs = _deg(psi, domain_lower_limit=self.theta)
|
degs = _deg(psi, domain_lower_limit=self.theta)
|
||||||
return (degs - self.theta)/self.delta
|
return (degs - self.theta)/self.delta
|
||||||
|
|
||||||
|
|
||||||
def intersect(self, other_seg, tol=1e-12):
|
def intersect(self, other_seg, tol=1e-12):
|
||||||
"""NOT FULLY IMPLEMENTED. Finds the intersections of two segments.
|
"""NOT FULLY IMPLEMENTED. Finds the intersections of two segments.
|
||||||
returns a list of tuples (t1, t2) such that
|
returns a list of tuples (t1, t2) such that
|
||||||
|
@ -2086,19 +1960,11 @@ class Arc(object):
|
||||||
return intersections
|
return intersections
|
||||||
|
|
||||||
elif is_bezier_segment(other_seg):
|
elif is_bezier_segment(other_seg):
|
||||||
# if self and other_seg intersect, they will itersect at the
|
u1poly = self.u1transform(other_seg.poly())
|
||||||
# same points after being passed through the `u1transform`
|
|
||||||
# isometry. Since this isometry maps self to the unit circle,
|
|
||||||
# the intersections will be easy to find (just look for any
|
|
||||||
# points where other_seg is a distance of one from the origin.
|
|
||||||
# Moreoever, the t-values that the intersection happen at will
|
|
||||||
# be unchanged by the isometry.
|
|
||||||
u1poly = np.poly1d(self.u1transform(other_seg.poly()))
|
|
||||||
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
|
u1poly_mag2 = real(u1poly)**2 + imag(u1poly)**2
|
||||||
t2s = [t for t in polyroots01(u1poly_mag2 - 1) if 0 <= t <= 1]
|
t2s = polyroots01(u1poly_mag2 - 1)
|
||||||
t1s = [self.phase2t(phase(u1poly(t2))) for t2 in t2s]
|
t1s = [self.phase2t(phase(u1poly(t2))) for t2 in t2s]
|
||||||
|
return list(zip(t1s, t2s))
|
||||||
return [(t1, t2) for t1, t2 in zip(t1s, t2s) if 0 <= t1 <= 1]
|
|
||||||
|
|
||||||
elif isinstance(other_seg, Arc):
|
elif isinstance(other_seg, Arc):
|
||||||
assert other_seg != self
|
assert other_seg != self
|
||||||
|
@ -2135,23 +2001,19 @@ class Arc(object):
|
||||||
|
|
||||||
def point_in_seg_interior(point, seg):
|
def point_in_seg_interior(point, seg):
|
||||||
t = seg.point_to_t(point)
|
t = seg.point_to_t(point)
|
||||||
if (not t or
|
if t is None: return False
|
||||||
np.isclose(t, 0.0, rtol=0.0, atol=1e-6) or
|
if np.isclose(t, 0.0, rtol=0.0, atol=1e-6): return False
|
||||||
np.isclose(t, 1.0, rtol=0.0, atol=1e-6)):
|
if np.isclose(t, 1.0, rtol=0.0, atol=1e-6): return False
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# If either end of either segment is in the interior
|
# If either end of either segment is in the interior
|
||||||
# of the other segment, then the Arcs overlap
|
# of the other segment, then the Arcs overlap
|
||||||
# in an infinite number of points, and we return
|
# in an infinite number of points, and we return
|
||||||
# "no intersections".
|
# "no intersections".
|
||||||
if (
|
if point_in_seg_interior(self.start, other_seg): return []
|
||||||
point_in_seg_interior(self.start, other_seg) or
|
if point_in_seg_interior(self.end, other_seg): return []
|
||||||
point_in_seg_interior(self.end, other_seg) or
|
if point_in_seg_interior(other_seg.start, self): return []
|
||||||
point_in_seg_interior(other_seg.start, self) or
|
if point_in_seg_interior(other_seg.end, self): return []
|
||||||
point_in_seg_interior(other_seg.end, self)
|
|
||||||
):
|
|
||||||
return []
|
|
||||||
|
|
||||||
# If they touch at their endpoint(s) and don't go
|
# If they touch at their endpoint(s) and don't go
|
||||||
# in "overlapping directions", then we accept that
|
# in "overlapping directions", then we accept that
|
||||||
|
@ -2454,6 +2316,16 @@ class Arc(object):
|
||||||
current_t = next_t
|
current_t = next_t
|
||||||
|
|
||||||
|
|
||||||
|
def is_bezier_segment(x):
|
||||||
|
return (isinstance(x, Line) or
|
||||||
|
isinstance(x, QuadraticBezier) or
|
||||||
|
isinstance(x, CubicBezier))
|
||||||
|
|
||||||
|
|
||||||
|
def is_path_segment(x):
|
||||||
|
return is_bezier_segment(x) or isinstance(x, Arc)
|
||||||
|
|
||||||
|
|
||||||
class Path(MutableSequence):
|
class Path(MutableSequence):
|
||||||
"""A Path is a sequence of path segments"""
|
"""A Path is a sequence of path segments"""
|
||||||
|
|
||||||
|
@ -2494,9 +2366,6 @@ class Path(MutableSequence):
|
||||||
if 'tree_element' in kw:
|
if 'tree_element' in kw:
|
||||||
self._tree_element = kw['tree_element']
|
self._tree_element = kw['tree_element']
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((tuple(self._segments), self._closed))
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def __getitem__(self, index):
|
||||||
return self._segments[index]
|
return self._segments[index]
|
||||||
|
|
||||||
|
@ -2509,12 +2378,8 @@ class Path(MutableSequence):
|
||||||
def __delitem__(self, index):
|
def __delitem__(self, index):
|
||||||
del self._segments[index]
|
del self._segments[index]
|
||||||
self._length = None
|
self._length = None
|
||||||
if len(self._segments) > 0:
|
|
||||||
self._start = self._segments[0].start
|
self._start = self._segments[0].start
|
||||||
self._end = self._segments[-1].end
|
self._end = self._segments[-1].end
|
||||||
else:
|
|
||||||
self._start = None
|
|
||||||
self._end = None
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return self._segments.__iter__()
|
return self._segments.__iter__()
|
||||||
|
@ -2563,33 +2428,33 @@ class Path(MutableSequence):
|
||||||
lengths = [each.length(error=error, min_depth=min_depth) for each in
|
lengths = [each.length(error=error, min_depth=min_depth) for each in
|
||||||
self._segments]
|
self._segments]
|
||||||
self._length = sum(lengths)
|
self._length = sum(lengths)
|
||||||
if self._length == 0:
|
|
||||||
self._lengths = lengths # all lengths are 0.
|
|
||||||
else:
|
|
||||||
self._lengths = [each/self._length for each in lengths]
|
self._lengths = [each/self._length for each in lengths]
|
||||||
|
|
||||||
def point(self, pos):
|
def point(self, T):
|
||||||
|
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
if len(self._segments) == 0:
|
if T == 0.0:
|
||||||
raise ValueError("This path contains no segments!")
|
return self._segments[0].point(T)
|
||||||
if pos == 0.0:
|
if T == 1.0:
|
||||||
return self._segments[0].point(pos)
|
return self._segments[-1].point(T)
|
||||||
if pos == 1.0:
|
|
||||||
return self._segments[-1].point(pos)
|
|
||||||
|
|
||||||
self._calc_lengths()
|
self._calc_lengths()
|
||||||
# Find which segment the point we search for is located on:
|
# Find which segment the point we search for is located on:
|
||||||
segment_start = 0
|
cumulative_relative_lengths = np.cumsum(self._lengths)
|
||||||
for index, segment in enumerate(self._segments):
|
|
||||||
segment_end = segment_start + self._lengths[index]
|
if hasattr(T, '__iter__'):
|
||||||
if segment_end >= pos:
|
T = np.array(T).reshape(1, len(T))
|
||||||
# This is the segment! How far in on the segment is the point?
|
relevant_seg_indices = np.argmax(cumulative_relative_lengths[:, None] >= T, axis=0)
|
||||||
segment_pos = (pos - segment_start)/(
|
T0, T1 = cumulative_relative_lengths[relevant_seg_indices - 1],\
|
||||||
segment_end - segment_start)
|
cumulative_relative_lengths[relevant_seg_indices]
|
||||||
return segment.point(segment_pos)
|
t = (T - T0) / (T1 - T0)
|
||||||
segment_start = segment_end
|
return [self[i].point(tval) for i, tval in zip(relevant_seg_indices, t)]
|
||||||
raise RuntimeError("Something has gone wrong. Could not compute Path.point({}) for path {}".format(pos, self))
|
else: # assume T is a scalar
|
||||||
|
relevant_seg_index = np.argmax(cumulative_relative_lengths >= T)
|
||||||
|
T0, T1 = cumulative_relative_lengths[relevant_seg_index - 1],\
|
||||||
|
cumulative_relative_lengths[relevant_seg_index]
|
||||||
|
t = (T - T0) / (T1 - T0)
|
||||||
|
return self[relevant_seg_index].point(t)
|
||||||
|
|
||||||
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
def length(self, T0=0, T1=1, error=LENGTH_ERROR, min_depth=LENGTH_MIN_DEPTH):
|
||||||
self._calc_lengths(error=error, min_depth=min_depth)
|
self._calc_lengths(error=error, min_depth=min_depth)
|
||||||
|
@ -2646,10 +2511,7 @@ class Path(MutableSequence):
|
||||||
return self.start == self.end
|
return self.start == self.end
|
||||||
|
|
||||||
def _is_closable(self):
|
def _is_closable(self):
|
||||||
try:
|
|
||||||
end = self[-1].end
|
end = self[-1].end
|
||||||
except IndexError:
|
|
||||||
return True
|
|
||||||
for segment in self:
|
for segment in self:
|
||||||
if segment.start == end:
|
if segment.start == end:
|
||||||
return True
|
return True
|
||||||
|
@ -2677,34 +2539,31 @@ class Path(MutableSequence):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def start(self):
|
def start(self):
|
||||||
if not self._start and len(self._segments)>0:
|
if not self._start:
|
||||||
self._start = self._segments[0].start
|
self._start = self._segments[0].start
|
||||||
return self._start
|
return self._start
|
||||||
|
|
||||||
@start.setter
|
@start.setter
|
||||||
def start(self, pt):
|
def start(self, pt):
|
||||||
self._start = pt
|
self._start = pt
|
||||||
if len(self._segments)>0:
|
|
||||||
self._segments[0].start = pt
|
self._segments[0].start = pt
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end(self):
|
def end(self):
|
||||||
if not self._end and len(self._segments)>0:
|
if not self._end:
|
||||||
self._end = self._segments[-1].end
|
self._end = self._segments[-1].end
|
||||||
return self._end
|
return self._end
|
||||||
|
|
||||||
@end.setter
|
@end.setter
|
||||||
def end(self, pt):
|
def end(self, pt):
|
||||||
self._end = pt
|
self._end = pt
|
||||||
if len(self._segments)>0:
|
|
||||||
self._segments[-1].end = pt
|
self._segments[-1].end = pt
|
||||||
|
|
||||||
def d(self, useSandT=False, use_closed_attrib=False, rel=False):
|
def d(self, useSandT=False, use_closed_attrib=False, rel=False):
|
||||||
"""Returns a path d-string for the path object.
|
"""Returns a path d-string for the path object.
|
||||||
For an explanation of useSandT and use_closed_attrib, see the
|
For an explanation of useSandT and use_closed_attrib, see the
|
||||||
compatibility notes in the README."""
|
compatibility notes in the README."""
|
||||||
if len(self) == 0:
|
|
||||||
return ''
|
|
||||||
if use_closed_attrib:
|
if use_closed_attrib:
|
||||||
self_closed = self.iscontinuous() and self.isclosed()
|
self_closed = self.iscontinuous() and self.isclosed()
|
||||||
if self_closed:
|
if self_closed:
|
||||||
|
@ -2948,10 +2807,10 @@ class Path(MutableSequence):
|
||||||
area_enclosed += integral(1) - integral(0)
|
area_enclosed += integral(1) - integral(0)
|
||||||
return area_enclosed
|
return area_enclosed
|
||||||
|
|
||||||
def seg2lines(seg_):
|
def seg2lines(seg):
|
||||||
"""Find piecewise-linear approximation of `seg`."""
|
"""Find piecewise-linear approximation of `seg`."""
|
||||||
num_lines = int(ceil(seg_.length() / chord_length))
|
num_lines = int(ceil(seg.length() / chord_length))
|
||||||
pts = [seg_.point(t) for t in np.linspace(0, 1, num_lines+1)]
|
pts = [seg.point(t) for t in np.linspace(0, 1, num_lines+1)]
|
||||||
return [Line(pts[i], pts[i+1]) for i in range(num_lines)]
|
return [Line(pts[i], pts[i+1]) for i in range(num_lines)]
|
||||||
|
|
||||||
assert self.isclosed()
|
assert self.isclosed()
|
||||||
|
@ -2965,29 +2824,20 @@ class Path(MutableSequence):
|
||||||
return area_without_arcs(Path(*bezier_path_approximation))
|
return area_without_arcs(Path(*bezier_path_approximation))
|
||||||
|
|
||||||
def intersect(self, other_curve, justonemode=False, tol=1e-12):
|
def intersect(self, other_curve, justonemode=False, tol=1e-12):
|
||||||
"""Finds intersections of `self` with `other_curve`
|
"""returns list of pairs of pairs ((T1, seg1, t1), (T2, seg2, t2))
|
||||||
|
giving the intersection points.
|
||||||
Args:
|
If justonemode==True, then returns just the first
|
||||||
other_curve: the path or path segment to check for intersections
|
|
||||||
with `self`
|
|
||||||
justonemode (bool): if true, returns only the first
|
|
||||||
intersection found.
|
intersection found.
|
||||||
tol (float): A tolerance used to check for redundant intersections
|
tol is used to check for redundant intersections (see comment above
|
||||||
(see comment above the code block where tol is used).
|
the code block where tol is used).
|
||||||
|
Note: If the two path objects coincide for more than a finite set of
|
||||||
Returns:
|
points, this code will fail."""
|
||||||
(list[tuple[float, Curve, float]]): list of intersections, each
|
|
||||||
in the format ((T1, seg1, t1), (T2, seg2, t2)), where
|
|
||||||
self.point(T1) == seg1.point(t1) == seg2.point(t2) == other_curve.point(T2)
|
|
||||||
|
|
||||||
Scope:
|
|
||||||
If the two path objects coincide for more than a finite set of
|
|
||||||
points, this code will iterate to max depth and/or raise an error.
|
|
||||||
"""
|
|
||||||
path1 = self
|
path1 = self
|
||||||
path2 = other_curve if isinstance(other_curve, Path) else Path(other_curve)
|
if isinstance(other_curve, Path):
|
||||||
|
path2 = other_curve
|
||||||
|
else:
|
||||||
|
path2 = Path(other_curve)
|
||||||
assert path1 != path2
|
assert path1 != path2
|
||||||
|
|
||||||
intersection_list = []
|
intersection_list = []
|
||||||
for seg1 in path1:
|
for seg1 in path1:
|
||||||
for seg2 in path2:
|
for seg2 in path2:
|
||||||
|
@ -2997,7 +2847,6 @@ class Path(MutableSequence):
|
||||||
T1 = path1.t2T(seg1, t1)
|
T1 = path1.t2T(seg1, t1)
|
||||||
T2 = path2.t2T(seg2, t2)
|
T2 = path2.t2T(seg2, t2)
|
||||||
intersection_list.append(((T1, seg1, t1), (T2, seg2, t2)))
|
intersection_list.append(((T1, seg1, t1), (T2, seg2, t2)))
|
||||||
|
|
||||||
if justonemode and intersection_list:
|
if justonemode and intersection_list:
|
||||||
return intersection_list[0]
|
return intersection_list[0]
|
||||||
|
|
||||||
|
@ -3006,7 +2855,8 @@ class Path(MutableSequence):
|
||||||
# redundant intersection. This code block checks for and removes said
|
# redundant intersection. This code block checks for and removes said
|
||||||
# redundancies.
|
# redundancies.
|
||||||
if intersection_list:
|
if intersection_list:
|
||||||
pts = [_seg1.point(_t1) for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
|
pts = [seg1.point(_t1)
|
||||||
|
for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]]
|
||||||
indices2remove = []
|
indices2remove = []
|
||||||
for ind1 in range(len(pts)):
|
for ind1 in range(len(pts)):
|
||||||
for ind2 in range(ind1 + 1, len(pts)):
|
for ind2 in range(ind1 + 1, len(pts)):
|
||||||
|
@ -3019,7 +2869,8 @@ class Path(MutableSequence):
|
||||||
return intersection_list
|
return intersection_list
|
||||||
|
|
||||||
def bbox(self):
|
def bbox(self):
|
||||||
"""returns bounding box in the form (xmin, xmax, ymin, ymax)."""
|
"""returns a bounding box for the input Path object in the form
|
||||||
|
(xmin, xmax, ymin, ymax)."""
|
||||||
bbs = [seg.bbox() for seg in self._segments]
|
bbs = [seg.bbox() for seg in self._segments]
|
||||||
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
|
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
|
||||||
xmin = min(xmins)
|
xmin = min(xmins)
|
||||||
|
@ -3167,18 +3018,6 @@ class Path(MutableSequence):
|
||||||
arc_required = int(ceil(abs(segment.delta) / sweep_limit))
|
arc_required = int(ceil(abs(segment.delta) / sweep_limit))
|
||||||
self[s:s+1] = list(segment.as_quad_curves(arc_required))
|
self[s:s+1] = list(segment.as_quad_curves(arc_required))
|
||||||
|
|
||||||
def joints(self):
|
|
||||||
"""returns generator of segment joints
|
|
||||||
|
|
||||||
I.e. Path(s0, s1, s2, ..., sn).joints() returns generator
|
|
||||||
(s0, s1), (s1, s2), ..., (sn, s0)
|
|
||||||
|
|
||||||
credit: https://docs.python.org/3/library/itertools.html#recipes
|
|
||||||
"""
|
|
||||||
a, b = tee(self)
|
|
||||||
next(b, None)
|
|
||||||
return zip(a, b)
|
|
||||||
|
|
||||||
def _tokenize_path(self, pathdef):
|
def _tokenize_path(self, pathdef):
|
||||||
for x in COMMAND_RE.split(pathdef):
|
for x in COMMAND_RE.split(pathdef):
|
||||||
if x in COMMANDS:
|
if x in COMMANDS:
|
||||||
|
|
|
@ -100,7 +100,7 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
mindim=600, dimensions=None, viewbox=None, text=None,
|
mindim=600, dimensions=None, viewbox=None, text=None,
|
||||||
text_path=None, font_size=None, attributes=None,
|
text_path=None, font_size=None, attributes=None,
|
||||||
svg_attributes=None, svgwrite_debug=False,
|
svg_attributes=None, svgwrite_debug=False,
|
||||||
paths2Drawing=False, baseunit='px'):
|
paths2Drawing=False):
|
||||||
"""Creates (and optionally displays) an SVG file.
|
"""Creates (and optionally displays) an SVG file.
|
||||||
|
|
||||||
REQUIRED INPUTS:
|
REQUIRED INPUTS:
|
||||||
|
@ -214,13 +214,10 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
timestamp = True if timestamp is None else timestamp
|
timestamp = True if timestamp is None else timestamp
|
||||||
filename = os_path.join(gettempdir(), 'disvg_output.svg')
|
filename = os_path.join(gettempdir(), 'disvg_output.svg')
|
||||||
|
|
||||||
dirname = os_path.abspath(os_path.dirname(filename))
|
|
||||||
if not os_path.exists(dirname):
|
|
||||||
makedirs(dirname)
|
|
||||||
|
|
||||||
# append time stamp to filename
|
# append time stamp to filename
|
||||||
if timestamp:
|
if timestamp:
|
||||||
fbname, fext = os_path.splitext(filename)
|
fbname, fext = os_path.splitext(filename)
|
||||||
|
dirname = os_path.dirname(filename)
|
||||||
tstamp = str(time()).replace('.', '')
|
tstamp = str(time()).replace('.', '')
|
||||||
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
|
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
|
||||||
filename = os_path.join(dirname, stfilename)
|
filename = os_path.join(dirname, stfilename)
|
||||||
|
@ -316,16 +313,12 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
dy += 2*margin_size*dy + extra_space_for_style
|
dy += 2*margin_size*dy + extra_space_for_style
|
||||||
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
|
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
|
||||||
|
|
||||||
if mindim is None:
|
|
||||||
szx = "{}{}".format(dx, baseunit)
|
|
||||||
szy = "{}{}".format(dy, baseunit)
|
|
||||||
else:
|
|
||||||
if dx > dy:
|
if dx > dy:
|
||||||
szx = str(mindim) + baseunit
|
szx = str(mindim) + 'px'
|
||||||
szy = str(int(ceil(mindim * dy / dx))) + baseunit
|
szy = str(int(ceil(mindim * dy / dx))) + 'px'
|
||||||
else:
|
else:
|
||||||
szx = str(int(ceil(mindim * dx / dy))) + baseunit
|
szx = str(int(ceil(mindim * dx / dy))) + 'px'
|
||||||
szy = str(mindim) + baseunit
|
szy = str(mindim) + 'px'
|
||||||
dimensions = szx, szy
|
dimensions = szx, szy
|
||||||
|
|
||||||
# Create an SVG file
|
# Create an SVG file
|
||||||
|
@ -410,6 +403,9 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
if paths2Drawing:
|
if paths2Drawing:
|
||||||
return dwg
|
return dwg
|
||||||
|
|
||||||
|
# save svg
|
||||||
|
if not os_path.exists(os_path.dirname(filename)):
|
||||||
|
makedirs(os_path.dirname(filename))
|
||||||
dwg.save()
|
dwg.save()
|
||||||
|
|
||||||
# re-open the svg, make the xml pretty, and save it again
|
# re-open the svg, make the xml pretty, and save it again
|
||||||
|
@ -432,7 +428,7 @@ def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
mindim=600, dimensions=None, viewbox=None, text=None,
|
mindim=600, dimensions=None, viewbox=None, text=None,
|
||||||
text_path=None, font_size=None, attributes=None,
|
text_path=None, font_size=None, attributes=None,
|
||||||
svg_attributes=None, svgwrite_debug=False,
|
svg_attributes=None, svgwrite_debug=False,
|
||||||
paths2Drawing=False, baseunit='px'):
|
paths2Drawing=False):
|
||||||
"""Create SVG and write to disk.
|
"""Create SVG and write to disk.
|
||||||
|
|
||||||
Note: This is identical to `disvg()` except that `openinbrowser`
|
Note: This is identical to `disvg()` except that `openinbrowser`
|
||||||
|
@ -451,7 +447,7 @@ def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
text_path=text_path, font_size=font_size,
|
text_path=text_path, font_size=font_size,
|
||||||
attributes=attributes, svg_attributes=svg_attributes,
|
attributes=attributes, svg_attributes=svg_attributes,
|
||||||
svgwrite_debug=svgwrite_debug,
|
svgwrite_debug=svgwrite_debug,
|
||||||
paths2Drawing=paths2Drawing, baseunit=baseunit)
|
paths2Drawing=paths2Drawing)
|
||||||
|
|
||||||
|
|
||||||
def paths2Drawing(paths=None, colors=None, filename=None,
|
def paths2Drawing(paths=None, colors=None, filename=None,
|
||||||
|
@ -460,7 +456,7 @@ def paths2Drawing(paths=None, colors=None, filename=None,
|
||||||
margin_size=0.1, mindim=600, dimensions=None,
|
margin_size=0.1, mindim=600, dimensions=None,
|
||||||
viewbox=None, text=None, text_path=None,
|
viewbox=None, text=None, text_path=None,
|
||||||
font_size=None, attributes=None, svg_attributes=None,
|
font_size=None, attributes=None, svg_attributes=None,
|
||||||
svgwrite_debug=False, paths2Drawing=True, baseunit='px'):
|
svgwrite_debug=False, paths2Drawing=True):
|
||||||
"""Create and return `svg.Drawing` object.
|
"""Create and return `svg.Drawing` object.
|
||||||
|
|
||||||
Note: This is identical to `disvg()` except that `paths2Drawing`
|
Note: This is identical to `disvg()` except that `paths2Drawing`
|
||||||
|
@ -478,4 +474,4 @@ def paths2Drawing(paths=None, colors=None, filename=None,
|
||||||
text_path=text_path, font_size=font_size,
|
text_path=text_path, font_size=font_size,
|
||||||
attributes=attributes, svg_attributes=svg_attributes,
|
attributes=attributes, svg_attributes=svg_attributes,
|
||||||
svgwrite_debug=svgwrite_debug,
|
svgwrite_debug=svgwrite_debug,
|
||||||
paths2Drawing=paths2Drawing, baseunit=baseunit)
|
paths2Drawing=paths2Drawing)
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import os
|
import os
|
||||||
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement
|
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
|
@ -14,13 +13,13 @@ from .parser import parse_transform
|
||||||
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
|
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
|
||||||
polyline2pathd, polygon2pathd, rect2pathd)
|
polyline2pathd, polygon2pathd, rect2pathd)
|
||||||
from .misctools import open_in_browser
|
from .misctools import open_in_browser
|
||||||
from .path import transform
|
from .path import *
|
||||||
|
|
||||||
# To maintain forward/backward compatibility
|
# To maintain forward/backward compatibility
|
||||||
try:
|
try:
|
||||||
string = basestring
|
str = basestring
|
||||||
except NameError:
|
except NameError:
|
||||||
string = str
|
pass
|
||||||
|
|
||||||
NAME_SVG = "svg"
|
NAME_SVG = "svg"
|
||||||
ATTR_VERSION = "version"
|
ATTR_VERSION = "version"
|
||||||
|
@ -165,17 +164,17 @@ class SaxDocument:
|
||||||
if matrix is not None and not np.all(np.equal(matrix, identity)):
|
if matrix is not None and not np.all(np.equal(matrix, identity)):
|
||||||
matrix_string = "matrix("
|
matrix_string = "matrix("
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[0][0])
|
matrix_string += str(matrix[0][0])
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[1][0])
|
matrix_string += str(matrix[1][0])
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[0][1])
|
matrix_string += str(matrix[0][1])
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[1][1])
|
matrix_string += str(matrix[1][1])
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[0][2])
|
matrix_string += str(matrix[0][2])
|
||||||
matrix_string += " "
|
matrix_string += " "
|
||||||
matrix_string += string(matrix[1][2])
|
matrix_string += str(matrix[1][2])
|
||||||
matrix_string += ")"
|
matrix_string += ")"
|
||||||
path.set(ATTR_TRANSFORM, matrix_string)
|
path.set(ATTR_TRANSFORM, matrix_string)
|
||||||
if ATTR_DATA in values:
|
if ATTR_DATA in values:
|
||||||
|
|
|
@ -4,13 +4,8 @@ The main tool being the svg2paths() function."""
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
from xml.dom.minidom import parse
|
from xml.dom.minidom import parse
|
||||||
import os
|
from os import path as os_path, getcwd
|
||||||
from io import StringIO
|
|
||||||
import re
|
import re
|
||||||
try:
|
|
||||||
from os import PathLike as FilePathLike
|
|
||||||
except ImportError:
|
|
||||||
FilePathLike = str
|
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
|
@ -22,11 +17,9 @@ COORD_PAIR_TMPLT = re.compile(
|
||||||
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
|
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def path2pathd(path):
|
def path2pathd(path):
|
||||||
return path.get('d', '')
|
return path.get('d', '')
|
||||||
|
|
||||||
|
|
||||||
def ellipse2pathd(ellipse):
|
def ellipse2pathd(ellipse):
|
||||||
"""converts the parameters from an ellipse or a circle to a string for a
|
"""converts the parameters from an ellipse or a circle to a string for a
|
||||||
Path object d-attribute"""
|
Path object d-attribute"""
|
||||||
|
@ -51,7 +44,7 @@ def ellipse2pathd(ellipse):
|
||||||
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
|
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
|
||||||
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
|
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
|
||||||
|
|
||||||
return d + 'z'
|
return d
|
||||||
|
|
||||||
|
|
||||||
def polyline2pathd(polyline, is_polygon=False):
|
def polyline2pathd(polyline, is_polygon=False):
|
||||||
|
@ -91,39 +84,14 @@ def rect2pathd(rect):
|
||||||
|
|
||||||
The rectangle will start at the (x,y) coordinate specified by the
|
The rectangle will start at the (x,y) coordinate specified by the
|
||||||
rectangle object and proceed counter-clockwise."""
|
rectangle object and proceed counter-clockwise."""
|
||||||
x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
|
x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0))
|
||||||
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
|
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
|
||||||
if 'rx' in rect or 'ry' in rect:
|
x1, y1 = x0 + w, y0
|
||||||
|
x2, y2 = x0 + w, y0 + h
|
||||||
# if only one, rx or ry, is present, use that value for both
|
x3, y3 = x0, y0 + h
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
|
|
||||||
rx = rect.get('rx', None)
|
|
||||||
ry = rect.get('ry', None)
|
|
||||||
if rx is None:
|
|
||||||
rx = ry or 0.
|
|
||||||
if ry is None:
|
|
||||||
ry = rx or 0.
|
|
||||||
rx, ry = float(rx), float(ry)
|
|
||||||
|
|
||||||
d = "M {} {} ".format(x + rx, y) # right of p0
|
|
||||||
d += "L {} {} ".format(x + w - rx, y) # go to p1
|
|
||||||
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w, y+ry) # arc for p1
|
|
||||||
d += "L {} {} ".format(x+w, y+h-ry) # above p2
|
|
||||||
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w-rx, y+h) # arc for p2
|
|
||||||
d += "L {} {} ".format(x+rx, y+h) # right of p3
|
|
||||||
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x, y+h-ry) # arc for p3
|
|
||||||
d += "L {} {} ".format(x, y+ry) # below p0
|
|
||||||
d += "A {} {} 0 0 1 {} {} z".format(rx, ry, x+rx, y) # arc for p0
|
|
||||||
return d
|
|
||||||
|
|
||||||
x0, y0 = x, y
|
|
||||||
x1, y1 = x + w, y
|
|
||||||
x2, y2 = x + w, y + h
|
|
||||||
x3, y3 = x, y + h
|
|
||||||
|
|
||||||
d = ("M{} {} L {} {} L {} {} L {} {} z"
|
d = ("M{} {} L {} {} L {} {} L {} {} z"
|
||||||
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,9 +117,7 @@ def svg2paths(svg_file_location,
|
||||||
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
|
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
svg_file_location (string or file-like object): the location of the
|
svg_file_location (string): the location of the svg file
|
||||||
svg file on disk or a file-like object containing the content of a
|
|
||||||
svg file
|
|
||||||
return_svg_attributes (bool): Set to True and a dictionary of
|
return_svg_attributes (bool): Set to True and a dictionary of
|
||||||
svg-attributes will be extracted and returned. See also the
|
svg-attributes will be extracted and returned. See also the
|
||||||
`svg2paths2()` function.
|
`svg2paths2()` function.
|
||||||
|
@ -175,10 +141,8 @@ def svg2paths(svg_file_location,
|
||||||
list: The list of corresponding path attribute dictionaries.
|
list: The list of corresponding path attribute dictionaries.
|
||||||
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
|
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
|
||||||
"""
|
"""
|
||||||
# strings are interpreted as file location everything else is treated as
|
if os_path.dirname(svg_file_location) == '':
|
||||||
# file-like object and passed to the xml parser directly
|
svg_file_location = os_path.join(getcwd(), svg_file_location)
|
||||||
from_filepath = isinstance(svg_file_location, str) or isinstance(svg_file_location, FilePathLike)
|
|
||||||
svg_file_location = os.path.abspath(svg_file_location) if from_filepath else svg_file_location
|
|
||||||
|
|
||||||
doc = parse(svg_file_location)
|
doc = parse(svg_file_location)
|
||||||
|
|
||||||
|
@ -258,26 +222,3 @@ def svg2paths2(svg_file_location,
|
||||||
convert_polylines_to_paths=convert_polylines_to_paths,
|
convert_polylines_to_paths=convert_polylines_to_paths,
|
||||||
convert_polygons_to_paths=convert_polygons_to_paths,
|
convert_polygons_to_paths=convert_polygons_to_paths,
|
||||||
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
||||||
|
|
||||||
|
|
||||||
def svgstr2paths(svg_string,
|
|
||||||
return_svg_attributes=False,
|
|
||||||
convert_circles_to_paths=True,
|
|
||||||
convert_ellipses_to_paths=True,
|
|
||||||
convert_lines_to_paths=True,
|
|
||||||
convert_polylines_to_paths=True,
|
|
||||||
convert_polygons_to_paths=True,
|
|
||||||
convert_rectangles_to_paths=True):
|
|
||||||
"""Convenience function; identical to svg2paths() except that it takes the
|
|
||||||
svg object as string. See svg2paths() docstring for more
|
|
||||||
info."""
|
|
||||||
# wrap string into StringIO object
|
|
||||||
svg_file_obj = StringIO(svg_string)
|
|
||||||
return svg2paths(svg_file_location=svg_file_obj,
|
|
||||||
return_svg_attributes=return_svg_attributes,
|
|
||||||
convert_circles_to_paths=convert_circles_to_paths,
|
|
||||||
convert_ellipses_to_paths=convert_ellipses_to_paths,
|
|
||||||
convert_lines_to_paths=convert_lines_to_paths,
|
|
||||||
convert_polylines_to_paths=convert_polylines_to_paths,
|
|
||||||
convert_polygons_to_paths=convert_polygons_to_paths,
|
|
||||||
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100mm" height="100mm" viewBox="-100 -200 500 500" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
|
||||||
<g id="Sketch" transform="scale(1,-1)">
|
|
||||||
<path id="slot" d="
|
|
||||||
M 0 10
|
|
||||||
L 0 80
|
|
||||||
A 30 30 0 1 0 0 140
|
|
||||||
A 10 10 0 0 1 0 100
|
|
||||||
L 100 100
|
|
||||||
A 10 10 0 1 1 100 140
|
|
||||||
A 30 30 0 0 0 100 80
|
|
||||||
L 100 10
|
|
||||||
A 10 10 0 0 0 90 0
|
|
||||||
L 10 0
|
|
||||||
A 10 10 0 0 0 0 10
|
|
||||||
" stroke="#ff0000" stroke-width="0.35 px"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 665 B |
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier
|
from svgpathtools.bezier import *
|
||||||
from svgpathtools.path import bpoints2bezier
|
from svgpathtools.path import bpoints2bezier
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
|
||||||
import unittest
|
|
||||||
from svgpathtools import Document
|
|
||||||
from io import StringIO
|
|
||||||
from io import open # overrides build-in open for compatibility with python2
|
|
||||||
from os.path import join, dirname
|
|
||||||
from sys import version_info
|
|
||||||
|
|
||||||
|
|
||||||
class TestDocument(unittest.TestCase):
|
|
||||||
def test_from_file_path_string(self):
|
|
||||||
"""Test reading svg from file provided as path"""
|
|
||||||
doc = Document(join(dirname(__file__), 'polygons.svg'))
|
|
||||||
|
|
||||||
self.assertEqual(len(doc.paths()), 2)
|
|
||||||
|
|
||||||
def test_from_file_path(self):
|
|
||||||
"""Test reading svg from file provided as path"""
|
|
||||||
if version_info >= (3, 6):
|
|
||||||
import pathlib
|
|
||||||
doc = Document(pathlib.Path(__file__).parent / 'polygons.svg')
|
|
||||||
|
|
||||||
self.assertEqual(len(doc.paths()), 2)
|
|
||||||
|
|
||||||
def test_from_file_object(self):
|
|
||||||
"""Test reading svg from file object that has already been opened"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'), 'r') as file:
|
|
||||||
doc = Document(file)
|
|
||||||
|
|
||||||
self.assertEqual(len(doc.paths()), 2)
|
|
||||||
|
|
||||||
def test_from_stringio(self):
|
|
||||||
"""Test reading svg object contained in a StringIO object"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'),
|
|
||||||
'r', encoding='utf-8') as file:
|
|
||||||
# read entire file into string
|
|
||||||
file_content = file.read()
|
|
||||||
# prepare stringio object
|
|
||||||
file_as_stringio = StringIO(file_content)
|
|
||||||
|
|
||||||
doc = Document(file_as_stringio)
|
|
||||||
|
|
||||||
self.assertEqual(len(doc.paths()), 2)
|
|
||||||
|
|
||||||
def test_from_string(self):
|
|
||||||
"""Test reading svg object contained in a string"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'),
|
|
||||||
'r', encoding='utf-8') as file:
|
|
||||||
# read entire file into string
|
|
||||||
file_content = file.read()
|
|
||||||
|
|
||||||
doc = Document.from_svg_string(file_content)
|
|
||||||
|
|
||||||
self.assertEqual(len(doc.paths()), 2)
|
|
|
@ -2,7 +2,7 @@
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import parse_path
|
from svgpathtools import *
|
||||||
|
|
||||||
|
|
||||||
class TestGeneration(unittest.TestCase):
|
class TestGeneration(unittest.TestCase):
|
||||||
|
|
|
@ -5,15 +5,11 @@ $ python -m unittest test.test_groups.TestGroups.test_group_flatten
|
||||||
"""
|
"""
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import Document, SVG_NAMESPACE, parse_path, Line, Arc
|
from svgpathtools import *
|
||||||
from os.path import join, dirname
|
from os.path import join, dirname
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
# When an assert fails, show the full error message, don't truncate it.
|
|
||||||
unittest.util._MAX_LENGTH = 999999999
|
|
||||||
|
|
||||||
|
|
||||||
def get_desired_path(name, paths):
|
def get_desired_path(name, paths):
|
||||||
return next(p for p in paths
|
return next(p for p in paths
|
||||||
if p.element.get('{some://testuri}name') == name)
|
if p.element.get('{some://testuri}name') == name)
|
||||||
|
@ -46,22 +42,6 @@ class TestGroups(unittest.TestCase):
|
||||||
self.check_values(tf.dot(v_s), actual.start)
|
self.check_values(tf.dot(v_s), actual.start)
|
||||||
self.check_values(tf.dot(v_e), actual.end)
|
self.check_values(tf.dot(v_e), actual.end)
|
||||||
|
|
||||||
def test_group_transform(self):
|
|
||||||
# The input svg has a group transform of "scale(1,-1)", which
|
|
||||||
# can mess with Arc sweeps.
|
|
||||||
doc = Document(join(dirname(__file__), 'negative-scale.svg'))
|
|
||||||
path = doc.paths()[0]
|
|
||||||
self.assertEqual(path[0], Line(start=-10j, end=-80j))
|
|
||||||
self.assertEqual(path[1], Arc(start=-80j, radius=(30+30j), rotation=0.0, large_arc=True, sweep=True, end=-140j))
|
|
||||||
self.assertEqual(path[2], Arc(start=-140j, radius=(20+20j), rotation=0.0, large_arc=False, sweep=False, end=-100j))
|
|
||||||
self.assertEqual(path[3], Line(start=-100j, end=(100-100j)))
|
|
||||||
self.assertEqual(path[4], Arc(start=(100-100j), radius=(20+20j), rotation=0.0, large_arc=True, sweep=False, end=(100-140j)))
|
|
||||||
self.assertEqual(path[5], Arc(start=(100-140j), radius=(30+30j), rotation=0.0, large_arc=False, sweep=True, end=(100-80j)))
|
|
||||||
self.assertEqual(path[6], Line(start=(100-80j), end=(100-10j)))
|
|
||||||
self.assertEqual(path[7], Arc(start=(100-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(90+0j)))
|
|
||||||
self.assertEqual(path[8], Line(start=(90+0j), end=(10+0j)))
|
|
||||||
self.assertEqual(path[9], Arc(start=(10+0j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=-10j))
|
|
||||||
|
|
||||||
def test_group_flatten(self):
|
def test_group_flatten(self):
|
||||||
# Test the Document.paths() function against the
|
# Test the Document.paths() function against the
|
||||||
# groups.svg test file.
|
# groups.svg test file.
|
||||||
|
@ -256,10 +236,3 @@ class TestGroups(unittest.TestCase):
|
||||||
path = parse_path(path_d)
|
path = parse_path(path_d)
|
||||||
svg_path = doc.add_path(path, group=new_leaf)
|
svg_path = doc.add_path(path, group=new_leaf)
|
||||||
self.assertEqual(path_d, svg_path.get('d'))
|
self.assertEqual(path_d, svg_path.get('d'))
|
||||||
|
|
||||||
# Test that paths are added to the correct group
|
|
||||||
new_sibling = doc.get_or_add_group(
|
|
||||||
['base_group', 'new_parent', 'new_sibling'])
|
|
||||||
doc.add_path(path, group=new_sibling)
|
|
||||||
self.assertEqual(len(new_sibling), 1)
|
|
||||||
self.assertEqual(path_d, new_sibling[0].get('d'))
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path
|
from svgpathtools import *
|
||||||
import svgpathtools
|
import svgpathtools
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@ import unittest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from svgpathtools import rational_limit
|
from svgpathtools import *
|
||||||
|
|
||||||
|
|
||||||
class Test_polytools(unittest.TestCase):
|
class Test_polytools(unittest.TestCase):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import SaxDocument
|
from svgpathtools import *
|
||||||
from os.path import join, dirname
|
from os.path import join, dirname
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,7 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import Path, Line, Arc, svg2paths, svgstr2paths
|
from svgpathtools import *
|
||||||
from io import StringIO
|
|
||||||
from io import open # overrides build-in open for compatibility with python2
|
|
||||||
import os
|
|
||||||
from os.path import join, dirname
|
from os.path import join, dirname
|
||||||
from sys import version_info
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
from svgpathtools.svg_to_paths import rect2pathd
|
|
||||||
|
|
||||||
|
|
||||||
class TestSVG2Paths(unittest.TestCase):
|
class TestSVG2Paths(unittest.TestCase):
|
||||||
def test_svg2paths_polygons(self):
|
def test_svg2paths_polygons(self):
|
||||||
|
@ -59,78 +50,3 @@ class TestSVG2Paths(unittest.TestCase):
|
||||||
self.assertTrue(len(path_circle)==2)
|
self.assertTrue(len(path_circle)==2)
|
||||||
self.assertTrue(path_circle==path_circle_correct)
|
self.assertTrue(path_circle==path_circle_correct)
|
||||||
self.assertTrue(path_circle.isclosed())
|
self.assertTrue(path_circle.isclosed())
|
||||||
|
|
||||||
# test for issue #198 (circles not being closed)
|
|
||||||
svg = u"""<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="40mm" height="40mm"
|
|
||||||
viewBox="0 0 40 40" version="1.1">
|
|
||||||
|
|
||||||
<g id="layer">
|
|
||||||
<circle id="c1" cx="20.000" cy="20.000" r="11.000" />
|
|
||||||
<circle id="c2" cx="20.000" cy="20.000" r="5.15" />
|
|
||||||
</g>
|
|
||||||
</svg>"""
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
|
||||||
svgfile = os.path.join(tmpdir, 'test.svg')
|
|
||||||
with open(svgfile, 'w') as f:
|
|
||||||
f.write(svg)
|
|
||||||
paths, _ = svg2paths(svgfile)
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
self.assertTrue(paths[0].isclosed())
|
|
||||||
self.assertTrue(paths[1].isclosed())
|
|
||||||
shutil.rmtree(tmpdir)
|
|
||||||
|
|
||||||
def test_rect2pathd(self):
|
|
||||||
non_rounded = {"x":"10", "y":"10", "width":"100","height":"100"}
|
|
||||||
self.assertEqual(rect2pathd(non_rounded), 'M10.0 10.0 L 110.0 10.0 L 110.0 110.0 L 10.0 110.0 z')
|
|
||||||
rounded = {"x":"10", "y":"10", "width":"100","height":"100", "rx":"15", "ry": "12"}
|
|
||||||
self.assertEqual(rect2pathd(rounded), "M 25.0 10.0 L 95.0 10.0 A 15.0 12.0 0 0 1 110.0 22.0 L 110.0 98.0 A 15.0 12.0 0 0 1 95.0 110.0 L 25.0 110.0 A 15.0 12.0 0 0 1 10.0 98.0 L 10.0 22.0 A 15.0 12.0 0 0 1 25.0 10.0 z")
|
|
||||||
|
|
||||||
def test_from_file_path_string(self):
|
|
||||||
"""Test reading svg from file provided as path"""
|
|
||||||
paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg'))
|
|
||||||
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
|
|
||||||
def test_from_file_path(self):
|
|
||||||
"""Test reading svg from file provided as pathlib POSIXPath"""
|
|
||||||
if version_info >= (3, 6):
|
|
||||||
import pathlib
|
|
||||||
paths, _ = svg2paths(pathlib.Path(__file__).parent / 'polygons.svg')
|
|
||||||
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
|
|
||||||
def test_from_file_object(self):
|
|
||||||
"""Test reading svg from file object that has already been opened"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'), 'r') as file:
|
|
||||||
paths, _ = svg2paths(file)
|
|
||||||
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
|
|
||||||
def test_from_stringio(self):
|
|
||||||
"""Test reading svg object contained in a StringIO object"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'),
|
|
||||||
'r', encoding='utf-8') as file:
|
|
||||||
# read entire file into string
|
|
||||||
file_content = file.read()
|
|
||||||
# prepare stringio object
|
|
||||||
file_as_stringio = StringIO(file_content)
|
|
||||||
|
|
||||||
paths, _ = svg2paths(file_as_stringio)
|
|
||||||
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
|
|
||||||
def test_from_string(self):
|
|
||||||
"""Test reading svg object contained in a string"""
|
|
||||||
with open(join(dirname(__file__), 'polygons.svg'),
|
|
||||||
'r', encoding='utf-8') as file:
|
|
||||||
# read entire file into string
|
|
||||||
file_content = file.read()
|
|
||||||
|
|
||||||
paths, _ = svgstr2paths(file_content)
|
|
||||||
|
|
||||||
self.assertEqual(len(paths), 2)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
|
|
Loading…
Reference in New Issue