remove venv
This commit is contained in:
parent
056387013d
commit
0680c7594e
13999 changed files with 0 additions and 2895688 deletions
|
|
@ -1,15 +0,0 @@
|
|||
# Copyright 2016 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .qu2cu import *
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import sys
|
||||
|
||||
from .cli import _main as main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,56 +0,0 @@
|
|||
"""Benchmark the qu2cu algorithm performance."""
|
||||
|
||||
from .qu2cu import *
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
import random
|
||||
import timeit
|
||||
|
||||
MAX_ERR = 0.5
|
||||
NUM_CURVES = 5
|
||||
|
||||
|
||||
def generate_curves(n):
|
||||
points = [
|
||||
tuple(float(random.randint(0, 2048)) for coord in range(2))
|
||||
for point in range(1 + 3 * n)
|
||||
]
|
||||
curves = []
|
||||
for i in range(n):
|
||||
curves.append(tuple(points[i * 3 : i * 3 + 4]))
|
||||
return curves
|
||||
|
||||
|
||||
def setup_quadratic_to_curves():
|
||||
curves = generate_curves(NUM_CURVES)
|
||||
quadratics = [curve_to_quadratic(curve, MAX_ERR) for curve in curves]
|
||||
return quadratics, MAX_ERR
|
||||
|
||||
|
||||
def run_benchmark(module, function, setup_suffix="", repeat=25, number=1):
|
||||
setup_func = "setup_" + function
|
||||
if setup_suffix:
|
||||
print("%s with %s:" % (function, setup_suffix), end="")
|
||||
setup_func += "_" + setup_suffix
|
||||
else:
|
||||
print("%s:" % function, end="")
|
||||
|
||||
def wrapper(function, setup_func):
|
||||
function = globals()[function]
|
||||
setup_func = globals()[setup_func]
|
||||
|
||||
def wrapped():
|
||||
return function(*setup_func())
|
||||
|
||||
return wrapped
|
||||
|
||||
results = timeit.repeat(wrapper(function, setup_func), repeat=repeat, number=number)
|
||||
print("\t%5.1fus" % (min(results) * 1000000.0 / number))
|
||||
|
||||
|
||||
def main():
|
||||
run_benchmark("qu2cu", "quadratic_to_curves")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
random.seed(1)
|
||||
main()
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import os
|
||||
import argparse
|
||||
import logging
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.pens.qu2cuPen import Qu2CuPen
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
import fontTools
|
||||
|
||||
|
||||
logger = logging.getLogger("fontTools.qu2cu")
|
||||
|
||||
|
||||
def _font_to_cubic(input_path, output_path=None, **kwargs):
|
||||
font = TTFont(input_path)
|
||||
logger.info("Converting curves for %s", input_path)
|
||||
|
||||
stats = {} if kwargs["dump_stats"] else None
|
||||
qu2cu_kwargs = {
|
||||
"stats": stats,
|
||||
"max_err": kwargs["max_err_em"] * font["head"].unitsPerEm,
|
||||
"all_cubic": kwargs["all_cubic"],
|
||||
}
|
||||
|
||||
assert "gvar" not in font, "Cannot convert variable font"
|
||||
glyphSet = font.getGlyphSet()
|
||||
glyphOrder = font.getGlyphOrder()
|
||||
glyf = font["glyf"]
|
||||
for glyphName in glyphOrder:
|
||||
glyph = glyphSet[glyphName]
|
||||
ttpen = TTGlyphPen(glyphSet)
|
||||
pen = Qu2CuPen(ttpen, **qu2cu_kwargs)
|
||||
glyph.draw(pen)
|
||||
glyf[glyphName] = ttpen.glyph(dropImpliedOnCurves=True)
|
||||
|
||||
font["head"].glyphDataFormat = 1
|
||||
|
||||
if kwargs["dump_stats"]:
|
||||
logger.info("Stats: %s", stats)
|
||||
|
||||
logger.info("Saving %s", output_path)
|
||||
font.save(output_path)
|
||||
|
||||
|
||||
def _main(args=None):
|
||||
"""Convert an OpenType font from quadratic to cubic curves"""
|
||||
parser = argparse.ArgumentParser(prog="qu2cu")
|
||||
parser.add_argument("--version", action="version", version=fontTools.__version__)
|
||||
parser.add_argument(
|
||||
"infiles",
|
||||
nargs="+",
|
||||
metavar="INPUT",
|
||||
help="one or more input TTF source file(s).",
|
||||
)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0)
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--conversion-error",
|
||||
type=float,
|
||||
metavar="ERROR",
|
||||
default=0.001,
|
||||
help="maxiumum approximation error measured in EM (default: 0.001)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--all-cubic",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="whether to only use cubic curves",
|
||||
)
|
||||
|
||||
output_parser = parser.add_mutually_exclusive_group()
|
||||
output_parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
default=None,
|
||||
metavar="OUTPUT",
|
||||
help=("output filename for the converted TTF."),
|
||||
)
|
||||
output_parser.add_argument(
|
||||
"-d",
|
||||
"--output-dir",
|
||||
default=None,
|
||||
metavar="DIRECTORY",
|
||||
help="output directory where to save converted TTFs",
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
if not options.verbose:
|
||||
level = "WARNING"
|
||||
elif options.verbose == 1:
|
||||
level = "INFO"
|
||||
else:
|
||||
level = "DEBUG"
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
if len(options.infiles) > 1 and options.output_file:
|
||||
parser.error("-o/--output-file can't be used with multile inputs")
|
||||
|
||||
if options.output_dir:
|
||||
output_dir = options.output_dir
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
elif not os.path.isdir(output_dir):
|
||||
parser.error("'%s' is not a directory" % output_dir)
|
||||
output_paths = [
|
||||
os.path.join(output_dir, os.path.basename(p)) for p in options.infiles
|
||||
]
|
||||
elif options.output_file:
|
||||
output_paths = [options.output_file]
|
||||
else:
|
||||
output_paths = [
|
||||
makeOutputFileName(p, overWrite=True, suffix=".cubic")
|
||||
for p in options.infiles
|
||||
]
|
||||
|
||||
kwargs = dict(
|
||||
dump_stats=options.verbose > 0,
|
||||
max_err_em=options.conversion_error,
|
||||
all_cubic=options.all_cubic,
|
||||
)
|
||||
|
||||
for input_path, output_path in zip(options.infiles, output_paths):
|
||||
_font_to_cubic(input_path, output_path, **kwargs)
|
||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -1,405 +0,0 @@
|
|||
# cython: language_level=3
|
||||
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
|
||||
|
||||
# Copyright 2023 Google Inc. All Rights Reserved.
|
||||
# Copyright 2023 Behdad Esfahbod. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
try:
|
||||
import cython
|
||||
except (AttributeError, ImportError):
|
||||
# if cython not installed, use mock module with no-op decorators and types
|
||||
from fontTools.misc import cython
|
||||
COMPILED = cython.compiled
|
||||
|
||||
from fontTools.misc.bezierTools import splitCubicAtTC
|
||||
from collections import namedtuple
|
||||
import math
|
||||
from typing import (
|
||||
List,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["quadratic_to_curves"]
|
||||
|
||||
|
||||
# Copied from cu2qu
|
||||
@cython.cfunc
|
||||
@cython.returns(cython.int)
|
||||
@cython.locals(
|
||||
tolerance=cython.double,
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
)
|
||||
@cython.locals(mid=cython.complex, deriv3=cython.complex)
|
||||
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
||||
"""Check if a cubic Bezier lies within a given distance of the origin.
|
||||
|
||||
"Origin" means *the* origin (0,0), not the start of the curve. Note that no
|
||||
checks are made on the start and end positions of the curve; this function
|
||||
only checks the inside of the curve.
|
||||
|
||||
Args:
|
||||
p0 (complex): Start point of curve.
|
||||
p1 (complex): First handle of curve.
|
||||
p2 (complex): Second handle of curve.
|
||||
p3 (complex): End point of curve.
|
||||
tolerance (double): Distance from origin.
|
||||
|
||||
Returns:
|
||||
bool: True if the cubic Bezier ``p`` entirely lies within a distance
|
||||
``tolerance`` of the origin, False otherwise.
|
||||
"""
|
||||
# First check p2 then p1, as p2 has higher error early on.
|
||||
if abs(p2) <= tolerance and abs(p1) <= tolerance:
|
||||
return True
|
||||
|
||||
# Split.
|
||||
mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
|
||||
if abs(mid) > tolerance:
|
||||
return False
|
||||
deriv3 = (p3 + p2 - p1 - p0) * 0.125
|
||||
return cubic_farthest_fit_inside(
|
||||
p0, (p0 + p1) * 0.5, mid - deriv3, mid, tolerance
|
||||
) and cubic_farthest_fit_inside(mid, mid + deriv3, (p2 + p3) * 0.5, p3, tolerance)
|
||||
|
||||
|
||||
@cython.locals(
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p1_2_3=cython.complex,
|
||||
)
|
||||
def elevate_quadratic(p0, p1, p2):
|
||||
"""Given a quadratic bezier curve, return its degree-elevated cubic."""
|
||||
|
||||
# https://pomax.github.io/bezierinfo/#reordering
|
||||
p1_2_3 = p1 * (2 / 3)
|
||||
return (
|
||||
p0,
|
||||
(p0 * (1 / 3) + p1_2_3),
|
||||
(p2 * (1 / 3) + p1_2_3),
|
||||
p2,
|
||||
)
|
||||
|
||||
|
||||
@cython.cfunc
|
||||
@cython.locals(
|
||||
start=cython.int,
|
||||
n=cython.int,
|
||||
k=cython.int,
|
||||
prod_ratio=cython.double,
|
||||
sum_ratio=cython.double,
|
||||
ratio=cython.double,
|
||||
t=cython.double,
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
)
|
||||
def merge_curves(curves, start, n):
|
||||
"""Give a cubic-Bezier spline, reconstruct one cubic-Bezier
|
||||
that has the same endpoints and tangents and approxmates
|
||||
the spline."""
|
||||
|
||||
# Reconstruct the t values of the cut segments
|
||||
prod_ratio = 1.0
|
||||
sum_ratio = 1.0
|
||||
ts = [1]
|
||||
for k in range(1, n):
|
||||
ck = curves[start + k]
|
||||
c_before = curves[start + k - 1]
|
||||
|
||||
# |t_(k+1) - t_k| / |t_k - t_(k - 1)| = ratio
|
||||
assert ck[0] == c_before[3]
|
||||
ratio = abs(ck[1] - ck[0]) / abs(c_before[3] - c_before[2])
|
||||
|
||||
prod_ratio *= ratio
|
||||
sum_ratio += prod_ratio
|
||||
ts.append(sum_ratio)
|
||||
|
||||
# (t(n) - t(n - 1)) / (t_(1) - t(0)) = prod_ratio
|
||||
|
||||
ts = [t / sum_ratio for t in ts[:-1]]
|
||||
|
||||
p0 = curves[start][0]
|
||||
p1 = curves[start][1]
|
||||
p2 = curves[start + n - 1][2]
|
||||
p3 = curves[start + n - 1][3]
|
||||
|
||||
# Build the curve by scaling the control-points.
|
||||
p1 = p0 + (p1 - p0) / (ts[0] if ts else 1)
|
||||
p2 = p3 + (p2 - p3) / ((1 - ts[-1]) if ts else 1)
|
||||
|
||||
curve = (p0, p1, p2, p3)
|
||||
|
||||
return curve, ts
|
||||
|
||||
|
||||
@cython.locals(
|
||||
count=cython.int,
|
||||
num_offcurves=cython.int,
|
||||
i=cython.int,
|
||||
off1=cython.complex,
|
||||
off2=cython.complex,
|
||||
on=cython.complex,
|
||||
)
|
||||
def add_implicit_on_curves(p):
|
||||
q = list(p)
|
||||
count = 0
|
||||
num_offcurves = len(p) - 2
|
||||
for i in range(1, num_offcurves):
|
||||
off1 = p[i]
|
||||
off2 = p[i + 1]
|
||||
on = off1 + (off2 - off1) * 0.5
|
||||
q.insert(i + 1 + count, on)
|
||||
count += 1
|
||||
return q
|
||||
|
||||
|
||||
Point = Union[Tuple[float, float], complex]
|
||||
|
||||
|
||||
@cython.locals(
|
||||
cost=cython.int,
|
||||
is_complex=cython.int,
|
||||
)
|
||||
def quadratic_to_curves(
|
||||
quads: List[List[Point]],
|
||||
max_err: float = 0.5,
|
||||
all_cubic: bool = False,
|
||||
) -> List[Tuple[Point, ...]]:
|
||||
"""Converts a connecting list of quadratic splines to a list of quadratic
|
||||
and cubic curves.
|
||||
|
||||
A quadratic spline is specified as a list of points. Either each point is
|
||||
a 2-tuple of X,Y coordinates, or each point is a complex number with
|
||||
real/imaginary components representing X,Y coordinates.
|
||||
|
||||
The first and last points are on-curve points and the rest are off-curve
|
||||
points, with an implied on-curve point in the middle between every two
|
||||
consequtive off-curve points.
|
||||
|
||||
Returns:
|
||||
The output is a list of tuples of points. Points are represented
|
||||
in the same format as the input, either as 2-tuples or complex numbers.
|
||||
|
||||
Each tuple is either of length three, for a quadratic curve, or four,
|
||||
for a cubic curve. Each curve's last point is the same as the next
|
||||
curve's first point.
|
||||
|
||||
Args:
|
||||
quads: quadratic splines
|
||||
|
||||
max_err: absolute error tolerance; defaults to 0.5
|
||||
|
||||
all_cubic: if True, only cubic curves are generated; defaults to False
|
||||
"""
|
||||
is_complex = type(quads[0][0]) is complex
|
||||
if not is_complex:
|
||||
quads = [[complex(x, y) for (x, y) in p] for p in quads]
|
||||
|
||||
q = [quads[0][0]]
|
||||
costs = [1]
|
||||
cost = 1
|
||||
for p in quads:
|
||||
assert q[-1] == p[0]
|
||||
for i in range(len(p) - 2):
|
||||
cost += 1
|
||||
costs.append(cost)
|
||||
costs.append(cost)
|
||||
qq = add_implicit_on_curves(p)[1:]
|
||||
costs.pop()
|
||||
q.extend(qq)
|
||||
cost += 1
|
||||
costs.append(cost)
|
||||
|
||||
curves = spline_to_curves(q, costs, max_err, all_cubic)
|
||||
|
||||
if not is_complex:
|
||||
curves = [tuple((c.real, c.imag) for c in curve) for curve in curves]
|
||||
return curves
|
||||
|
||||
|
||||
Solution = namedtuple("Solution", ["num_points", "error", "start_index", "is_cubic"])
|
||||
|
||||
|
||||
@cython.locals(
|
||||
i=cython.int,
|
||||
j=cython.int,
|
||||
k=cython.int,
|
||||
start=cython.int,
|
||||
i_sol_count=cython.int,
|
||||
j_sol_count=cython.int,
|
||||
this_sol_count=cython.int,
|
||||
tolerance=cython.double,
|
||||
err=cython.double,
|
||||
error=cython.double,
|
||||
i_sol_error=cython.double,
|
||||
j_sol_error=cython.double,
|
||||
all_cubic=cython.int,
|
||||
is_cubic=cython.int,
|
||||
count=cython.int,
|
||||
p0=cython.complex,
|
||||
p1=cython.complex,
|
||||
p2=cython.complex,
|
||||
p3=cython.complex,
|
||||
v=cython.complex,
|
||||
u=cython.complex,
|
||||
)
|
||||
def spline_to_curves(q, costs, tolerance=0.5, all_cubic=False):
|
||||
"""
|
||||
q: quadratic spline with alternating on-curve / off-curve points.
|
||||
|
||||
costs: cumulative list of encoding cost of q in terms of number of
|
||||
points that need to be encoded. Implied on-curve points do not
|
||||
contribute to the cost. If all points need to be encoded, then
|
||||
costs will be range(1, len(q)+1).
|
||||
"""
|
||||
|
||||
assert len(q) >= 3, "quadratic spline requires at least 3 points"
|
||||
|
||||
# Elevate quadratic segments to cubic
|
||||
elevated_quadratics = [
|
||||
elevate_quadratic(*q[i : i + 3]) for i in range(0, len(q) - 2, 2)
|
||||
]
|
||||
|
||||
# Find sharp corners; they have to be oncurves for sure.
|
||||
forced = set()
|
||||
for i in range(1, len(elevated_quadratics)):
|
||||
p0 = elevated_quadratics[i - 1][2]
|
||||
p1 = elevated_quadratics[i][0]
|
||||
p2 = elevated_quadratics[i][1]
|
||||
if abs(p1 - p0) + abs(p2 - p1) > tolerance + abs(p2 - p0):
|
||||
forced.add(i)
|
||||
|
||||
# Dynamic-Programming to find the solution with fewest number of
|
||||
# cubic curves, and within those the one with smallest error.
|
||||
sols = [Solution(0, 0, 0, False)]
|
||||
impossible = Solution(len(elevated_quadratics) * 3 + 1, 0, 1, False)
|
||||
start = 0
|
||||
for i in range(1, len(elevated_quadratics) + 1):
|
||||
best_sol = impossible
|
||||
for j in range(start, i):
|
||||
j_sol_count, j_sol_error = sols[j].num_points, sols[j].error
|
||||
|
||||
if not all_cubic:
|
||||
# Solution with quadratics between j:i
|
||||
this_count = costs[2 * i - 1] - costs[2 * j] + 1
|
||||
i_sol_count = j_sol_count + this_count
|
||||
i_sol_error = j_sol_error
|
||||
i_sol = Solution(i_sol_count, i_sol_error, i - j, False)
|
||||
if i_sol < best_sol:
|
||||
best_sol = i_sol
|
||||
|
||||
if this_count <= 3:
|
||||
# Can't get any better than this in the path below
|
||||
continue
|
||||
|
||||
# Fit elevated_quadratics[j:i] into one cubic
|
||||
try:
|
||||
curve, ts = merge_curves(elevated_quadratics, j, i - j)
|
||||
except ZeroDivisionError:
|
||||
continue
|
||||
|
||||
# Now reconstruct the segments from the fitted curve
|
||||
reconstructed_iter = splitCubicAtTC(*curve, *ts)
|
||||
reconstructed = []
|
||||
|
||||
# Knot errors
|
||||
error = 0
|
||||
for k, reconst in enumerate(reconstructed_iter):
|
||||
orig = elevated_quadratics[j + k]
|
||||
err = abs(reconst[3] - orig[3])
|
||||
error = max(error, err)
|
||||
if error > tolerance:
|
||||
break
|
||||
reconstructed.append(reconst)
|
||||
if error > tolerance:
|
||||
# Not feasible
|
||||
continue
|
||||
|
||||
# Interior errors
|
||||
for k, reconst in enumerate(reconstructed):
|
||||
orig = elevated_quadratics[j + k]
|
||||
p0, p1, p2, p3 = tuple(v - u for v, u in zip(reconst, orig))
|
||||
|
||||
if not cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
|
||||
error = tolerance + 1
|
||||
break
|
||||
if error > tolerance:
|
||||
# Not feasible
|
||||
continue
|
||||
|
||||
# Save best solution
|
||||
i_sol_count = j_sol_count + 3
|
||||
i_sol_error = max(j_sol_error, error)
|
||||
i_sol = Solution(i_sol_count, i_sol_error, i - j, True)
|
||||
if i_sol < best_sol:
|
||||
best_sol = i_sol
|
||||
|
||||
if i_sol_count == 3:
|
||||
# Can't get any better than this
|
||||
break
|
||||
|
||||
sols.append(best_sol)
|
||||
if i in forced:
|
||||
start = i
|
||||
|
||||
# Reconstruct solution
|
||||
splits = []
|
||||
cubic = []
|
||||
i = len(sols) - 1
|
||||
while i:
|
||||
count, is_cubic = sols[i].start_index, sols[i].is_cubic
|
||||
splits.append(i)
|
||||
cubic.append(is_cubic)
|
||||
i -= count
|
||||
curves = []
|
||||
j = 0
|
||||
for i, is_cubic in reversed(list(zip(splits, cubic))):
|
||||
if is_cubic:
|
||||
curves.append(merge_curves(elevated_quadratics, j, i - j)[0])
|
||||
else:
|
||||
for k in range(j, i):
|
||||
curves.append(q[k * 2 : k * 2 + 3])
|
||||
j = i
|
||||
|
||||
return curves
|
||||
|
||||
|
||||
def main():
|
||||
from fontTools.cu2qu.benchmark import generate_curve
|
||||
from fontTools.cu2qu import curve_to_quadratic
|
||||
|
||||
tolerance = 0.05
|
||||
reconstruct_tolerance = tolerance * 1
|
||||
curve = generate_curve()
|
||||
quadratics = curve_to_quadratic(curve, tolerance)
|
||||
print(
|
||||
"cu2qu tolerance %g. qu2cu tolerance %g." % (tolerance, reconstruct_tolerance)
|
||||
)
|
||||
print("One random cubic turned into %d quadratics." % len(quadratics))
|
||||
curves = quadratic_to_curves([quadratics], reconstruct_tolerance)
|
||||
print("Those quadratics turned back into %d cubics. " % len(curves))
|
||||
print("Original curve:", curve)
|
||||
print("Reconstructed curve(s):", curves)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue