Initial commit

This commit is contained in:
2022-01-31 18:10:19 +01:00
commit 1ab1f067b5
16 changed files with 1979 additions and 0 deletions

410
lib/svg/parser.dart Normal file
View File

@@ -0,0 +1,410 @@
/// SVG Path specification parser
///
import 'path.dart'
show Arc, Close, CubicBezier, Line, Move, Path, Point, QuadraticBezier;
const COMMANDS = {
'M',
'm',
'Z',
'z',
'L',
'l',
'H',
'h',
'V',
'v',
'C',
'c',
'S',
's',
'Q',
'q',
'T',
't',
'A',
'a'
};
const UPPERCASE = {'M', 'Z', 'L', 'H', 'V', 'C', 'S', 'Q', 'T', 'A'};
final COMMAND_RE = RegExp("(?=[${COMMANDS.join('')}])");
final FLOAT_RE = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?");
class ParserResult<T> {
final T value;
final String remaining;
const ParserResult({required this.value, required this.remaining});
}
class InvalidPathError implements Exception {
final String msg;
const InvalidPathError(this.msg);
@override
String toString() => 'InvalidPathError: $msg';
}
// The argument sequences from the grammar, made sane.
// u: Non-negative number
// s: Signed number or coordinate
// c: coordinate-pair, which is two coordinates/numbers, separated by whitespace
// f: A one character flag, doesn't need whitespace, 1 or 0
const ARGUMENT_SEQUENCE = {
"M": "c",
"Z": "",
"L": "c",
"H": "s",
"V": "s",
"C": "ccc",
"S": "cc",
"Q": "cc",
"T": "c",
"A": "uusffc",
};
/// Strips whitespace and commas
String strip_array(String arg_array) {
// EBNF wsp:(#x20 | #x9 | #xD | #xA) + comma: 0x2C
while (arg_array.isNotEmpty && ' \t\n\r,'.contains(arg_array[0])) {
arg_array = arg_array.substring(1);
}
return arg_array;
}
ParserResult<double> pop_number(String arg_array) {
final res = FLOAT_RE.firstMatch(arg_array);
if (res == null) {
throw InvalidPathError("Expected a number, got '$arg_array'.");
}
final number = double.parse(res.group(0)!);
final start = res.start;
final end = res.end;
arg_array = arg_array.substring(0, start) + arg_array.substring(end);
arg_array = strip_array(arg_array);
return ParserResult(value: number, remaining: arg_array);
}
ParserResult<double> pop_unsigned_number(arg_array) {
final number = pop_number(arg_array);
if (number.value < 0) {
throw InvalidPathError("Expected a non-negative number, got '$number'.");
}
return number;
}
ParserResult<Point> pop_coordinate_pair(arg_array) {
final x = pop_number(arg_array);
final y = pop_number(x.remaining);
return ParserResult(value: Point(x.value, y.value), remaining: y.remaining);
}
ParserResult<bool> pop_flag(String arg_array) {
final flag = arg_array[0];
arg_array = arg_array.substring(1);
arg_array = strip_array(arg_array);
if (flag == '0') return ParserResult(value: false, remaining: arg_array);
if (flag == '1') return ParserResult(value: true, remaining: arg_array);
throw InvalidPathError("Expected either 1 or 0, got '$flag'");
}
const FIELD_POPPERS = {
"u": pop_unsigned_number,
"s": pop_number,
"c": pop_coordinate_pair,
"f": pop_flag,
};
class Command {
final String command;
final String args;
const Command({required this.command, required this.args});
@override
String toString() => 'Command: $command $args';
}
// Splits path into commands and arguments
List<Command> _commandify_path(String pathdef) {
List<Command> tokens = [];
List<String> token = [];
for (String c in pathdef.split(COMMAND_RE)) {
String x = c[0];
String? y = (c.length > 1) ? c.substring(1).trim() : null;
if (!COMMANDS.contains(x)) {
throw InvalidPathError("Path does not start with a command: $pathdef");
}
if (token.isNotEmpty) {
tokens.add(Command(command: token[0], args: token[1]));
// yield token;
}
if (x == "z" || x == "Z") {
// The end command takes no arguments, so add a blank one
token.addAll([x, ""]);
} else {
// token = [x, x.substring(1).trim()];
token = [x];
}
if (y != null) {
token.add(y);
}
}
tokens.add(Command(command: token[0], args: token[1]));
// yield token;
return tokens;
}
class Token {
final String command;
final List<Object> args;
const Token({required this.command, required this.args});
@override
String toString() => 'Token: $command ($args)';
}
List<Token> _tokenize_path(String pathdef) {
List<Token> tokens = [];
for (final token in _commandify_path(pathdef)) {
// _commandify_path(pathdef).forEach((List<String> token) {
String command = token.command;
String args = token.args;
// Shortcut this for the close command, that doesn't have arguments:
if (command == "z" || command == "Z") {
tokens.add(Token(command: command, args: []));
continue;
}
// For the rest of the commands, we parse the arguments and
// yield one command per full set of arguments
final String arg_sequence = ARGUMENT_SEQUENCE[command.toUpperCase()]!;
String arguments = args;
while (arguments.isNotEmpty) {
final List<Object> command_arguments = [];
for (final arg in arg_sequence.split('')) {
try {
final result = FIELD_POPPERS[arg]!.call(arguments);
arguments = result.remaining;
command_arguments.add(result.value);
} on InvalidPathError {
throw InvalidPathError("Invalid path element $command $args");
}
}
tokens.add(Token(command: command, args: command_arguments));
// yield (command,) + tuple(command_arguments)
// Implicit Moveto commands should be treated as Lineto commands.
if (command == "m") {
command = "l";
} else if (command == "M") {
command = "L";
}
}
}
return tokens;
}
Path parse_path(String pathdef) {
final segments = Path();
Point? start_pos;
String? last_command;
Point current_pos = Point.zero;
for (final token in _tokenize_path(pathdef)) {
final command = token.command.toUpperCase();
final absolute = token.command.toUpperCase() == token.command;
if (command == "M") {
final pos = token.args[0] as Point;
if (absolute) {
current_pos = pos;
} else {
current_pos += pos;
}
segments.add(Move(to: current_pos));
start_pos = current_pos;
} else if (command == "Z") {
// TODO Throw error if not available:
segments.add(Close(start: current_pos, end: start_pos!));
current_pos = start_pos;
} else if (command == "L") {
Point pos = token.args[0] as Point;
if (!absolute) {
pos += current_pos;
}
segments.add(Line(start: current_pos, end: pos));
current_pos = pos;
} else if (command == "H") {
double hpos = token.args[0] as double;
if (!absolute) {
hpos += current_pos.x;
}
final pos = Point(hpos, current_pos.y);
segments.add(Line(start: current_pos, end: pos));
current_pos = pos;
} else if (command == "V") {
double vpos = token.args[0] as double;
if (!absolute) {
vpos += current_pos.y;
}
final pos = Point(current_pos.x, vpos);
segments.add(Line(start: current_pos, end: pos));
current_pos = pos;
} else if (command == "C") {
Point control1 = token.args[0] as Point;
Point control2 = token.args[1] as Point;
Point end = token.args[2] as Point;
if (!absolute) {
control1 += current_pos;
control2 += current_pos;
end += current_pos;
}
segments.add(
CubicBezier(
start: current_pos,
control1: control1,
control2: control2,
end: end,
),
);
current_pos = end;
} else if (command == "S") {
// Smooth curve. First control point is the "reflection" of
// the second control point in the previous path.
Point control2 = token.args[0] as Point;
Point end = token.args[1] as Point;
if (!absolute) {
control2 += current_pos;
end += current_pos;
}
late final Point control1;
if (last_command == 'C' || last_command == 'S') {
// The first control point is assumed to be the reflection of
// the second control point on the previous command relative
// to the current point.
control1 =
current_pos + current_pos - (segments.last as CubicBezier).control2;
} else {
// If there is no previous command or if the previous command
// was not an C, c, S or s, assume the first control point is
// coincident with the current point.
control1 = current_pos;
}
segments.add(
CubicBezier(
start: current_pos,
control1: control1,
control2: control2,
end: end),
);
current_pos = end;
} else if (command == "Q") {
Point control = token.args[0] as Point;
Point end = token.args[1] as Point;
if (!absolute) {
control += current_pos;
end += current_pos;
}
segments.add(
QuadraticBezier(start: current_pos, control: control, end: end),
);
current_pos = end;
} else if (command == "T") {
// Smooth curve. Control point is the "reflection" of
// the second control point in the previous path.
Point end = token.args[0] as Point;
if (!absolute) {
end += current_pos;
}
late final Point control;
if (last_command == "Q" || last_command == 'T') {
// The control point is assumed to be the reflection of
// the control point on the previous command relative
// to the current point.
control = current_pos +
current_pos -
(segments.last as QuadraticBezier).control;
} else {
// If there is no previous command or if the previous command
// was not an Q, q, T or t, assume the first control point is
// coincident with the current point.
control = current_pos;
}
segments.add(
QuadraticBezier(start: current_pos, control: control, end: end),
);
current_pos = end;
} else if (command == "A") {
// For some reason I implemented the Arc with a complex radius.
// That doesn't really make much sense, but... *shrugs*
final radius = Point(token.args[0] as double, token.args[1] as double);
final rotation = token.args[2] as double;
final arc = token.args[3] as bool;
final sweep = token.args[4] as bool;
Point end = token.args[5] as Point;
if (!absolute) {
end += current_pos;
}
segments.add(
Arc(
start: current_pos,
radius: radius,
rotation: rotation,
arc: arc,
sweep: sweep,
end: end,
),
);
current_pos = end;
}
// Finish up the loop in preparation for next command
last_command = command;
}
return segments;
}
void main(List<String> args) {
// print(_commandify_path('M 10 10 C 20 20, 40 20, 50 10'));
// print(_tokenize_path('M 10 10 C 20 20, 40 20, 50 10'));
// print(_tokenize_path('M 10 80 Q 52.5 10, 95 80 T 180 80'));
// print(_tokenize_path("""
// M 10 315
// L 110 215
// A 30 50 0 0 1 162.55 162.45
// L 172.55 152.45
// A 30 50 -45 0 1 215.1 109.9
// L 315 10
// """));
print(parse_path('M 10 10 C 20 20, 40 20, 50 10'));
print(parse_path('M 10 80 Q 52.5 10, 95 80 T 180 80'));
print(parse_path("""
M 10 315
L 110 215
A 30 50 0 0 1 162.55 162.45
L 172.55 152.45
A 30 50 -45 0 1 215.1 109.9
L 315 10
"""));
}

682
lib/svg/path.dart Normal file
View File

@@ -0,0 +1,682 @@
import 'dart:collection';
import 'dart:math' as math;
import 'dart:math' show sqrt, sin, cos, acos, log, pi;
import 'package:bisect/bisect.dart';
// try:
// from collections.abc import MutableSequence
// except ImportError:
// from collections import MutableSequence
// This file contains classes for the different types of SVG path segments as
// well as a Path object that contains a sequence of path segments.
double radians(num n) => n * pi / 180;
double degrees(num n) => n * 180 / pi;
class Point {
final num x;
final num y;
const Point(this.x, this.y);
const Point.from({this.x = 0, this.y = 0});
static const zero = Point(0, 0);
operator +(covariant Point p) => Point(x + p.x, y + p.y);
operator -(covariant Point p) => Point(x - p.x, y - p.y);
operator *(covariant Point p) => Point(x * p.x, y * p.y);
operator /(covariant Point p) => Point(x / p.x, y / p.y);
Point addX(num n) => Point(x + n, y);
Point addY(num n) => Point(x, y + n);
Point add(num n) => Point(x + n, y + n);
Point subtractX(num n) => Point(x - n, y);
Point subtractY(num n) => Point(x, y - n);
Point subtractXY(num n) => Point(x - n, y - n);
Point xSubtract(num n) => Point(n - x, y);
Point ySubtract(num n) => Point(x, n - y);
Point xySubtract(num n) => Point(n - x, n - y);
Point timesX(num n) => Point(x * n, y);
Point timesY(num n) => Point(x, y * n);
Point times(num n) => Point(x * n, y * n);
Point dividesX(num n) => Point(x / n, y);
Point dividesY(num n) => Point(x, y / n);
Point divides(num n) => Point(x / n, y / n);
Point pow(int n) => Point(math.pow(x, n), math.pow(y, n));
double abs() => math.sqrt(x * x + y * y);
@override
String toString() => '($x,$y)';
}
const defaultMinDepth = 5;
const defaultError = 1e-12;
/// Recursively approximates the length by straight lines
double segmentLength({
required SvgPath curve,
required num start,
required num end,
required Point startPoint,
required Point endPoint,
required double error,
required int minDepth,
required double depth,
}) {
num mid = (start + end) / 2;
Point midPoint = curve.point(mid);
double length = (endPoint - startPoint).abs();
double firstHalf = (midPoint - startPoint).abs();
double secondHalf = (endPoint - midPoint).abs();
double length2 = firstHalf + secondHalf;
if ((length2 - length > error) || (depth < minDepth)) {
// Calculate the length of each segment:
depth += 1;
return segmentLength(
curve: curve,
start: start,
end: mid,
startPoint: startPoint,
endPoint: midPoint,
error: error,
minDepth: minDepth,
depth: depth,
) +
segmentLength(
curve: curve,
start: mid,
end: end,
startPoint: midPoint,
endPoint: endPoint,
error: error,
minDepth: minDepth,
depth: depth,
);
}
// This is accurate enough.
return length2;
}
abstract class SvgPath {
final Point start;
final Point end;
const SvgPath({
required this.start,
required this.end,
});
/// Calculate the x,y position at a certain position of the path
Point point(num pos);
/// Calculate the length of the path up to a certain position
double size({double error = defaultError, int minDepth = defaultMinDepth});
}
abstract class Bezier extends SvgPath {
const Bezier({
required Point start,
required Point end,
}) : super(start: start, end: end);
/// Checks if this segment would be a smooth segment following the previous
bool isSmoothFrom(Object? previous);
}
/// A straight line
/// The base for Line() and Close().
class Linear extends SvgPath {
const Linear({
required Point start,
required Point end,
}) : super(start: start, end: end);
// def __ne__(self, other):
// if not isinstance(other, Line):
// return NotImplemented
// return not self == other
@override
Point point(num pos) => start + (end - start).times(pos);
@override
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
final distance = end - start;
return sqrt(distance.x * distance.x + distance.y * distance.y);
}
}
class Line extends Linear {
const Line({
required Point start,
required Point end,
}) : super(start: start, end: end);
@override
String toString() {
return "Line(start=$start, end=$end)";
}
// @override
// operator ==(covariant Line other) => start == other.start && end == other.end;
}
class CubicBezier extends Bezier {
final Point control1;
final Point control2;
const CubicBezier({
required Point start,
required this.control1,
required this.control2,
required Point end,
}) : super(start: start, end: end);
@override
String toString() => "CubicBezier(start=$start, control1=$control1, "
"control2=$control2, end=$end)";
// @override
// operator ==(covariant CubicBezier other) =>
// start == other.start &&
// and end == other.end &&
// and control1 == other.control1 &&
// and control2 == other.control2;
// def __ne__(self, other):
// if not isinstance(other, CubicBezier):
// return NotImplemented
// return not self == other
@override
bool isSmoothFrom(Object? previous) => previous is CubicBezier
? start == previous.end &&
control1 - start == previous.end - previous.control2
: control1 == start;
@override
Point point(num pos) =>
start.times(math.pow(1 - pos, 3)) +
control1.times(math.pow(1 - pos, 2) * 3 * pos) +
control2.times(math.pow(pos, 2) * 3 * (1 - pos)) +
end.times(math.pow(pos, 3));
@override
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
final startPoint = point(0);
final endPoint = point(1);
return segmentLength(
curve: this,
start: 0,
end: 1,
startPoint: startPoint,
endPoint: endPoint,
error: error,
minDepth: minDepth,
depth: 0);
}
}
class QuadraticBezier extends Bezier {
final Point control;
const QuadraticBezier({
required Point start,
required Point end,
required this.control,
}) : super(
start: start,
end: end,
);
@override
String toString() =>
"QuadraticBezier(start=$start, control=$control, end=$end)";
// def __eq__(self, other):
// if not isinstance(other, QuadraticBezier):
// return NotImplemented
// return (
// self.start == other.start
// and self.end == other.end
// and self.control == other.control
// )
// def __ne__(self, other):
// if not isinstance(other, QuadraticBezier):
// return NotImplemented
// return not self == other
@override
bool isSmoothFrom(Object? previous) => previous is QuadraticBezier
? start == previous.end &&
(control - start) == (previous.end - previous.control)
: control == start;
@override
Point point(num pos) =>
start.times(math.pow(1 - pos, 2)) +
control.times(pos * (1 - pos) * 2) +
end.times(math.pow(pos, 2));
@override
double size({double error = defaultError, int minDepth = defaultMinDepth}) {
final Point a = start - control.times(2) + end;
final Point b = (control - start).times(2);
final num aDotB = a.x * b.x + a.y * b.y;
late final double s;
if (a.abs() < 1e-12) {
s = b.abs();
} else if ((aDotB + a.abs() * b.abs()).abs() < 1e-12) {
final k = b.abs() / a.abs();
s = (k >= 2) ? b.abs() - a.abs() : a.abs() * ((k * k) / 2 - k + 1);
} else {
// For an explanation of this case, see
// http://www.malczak.info/blog/quadratic-bezier-curve-length/
final num A = 4 * (a.x * a.x + a.y * a.y);
final num B = 4 * (a.x * b.x + a.y * b.y);
final num C = b.x * b.x + b.y * b.y;
final double sabc = 2 * sqrt(A + B + C);
final double a2 = sqrt(A);
final double a32 = 2 * A * a2;
final double c2 = 2 * sqrt(C);
final double bA = B / a2;
s = (a32 * sabc +
a2 * B * (sabc - c2) +
(4 * C * A - (B * B)) * log((2 * a2 + bA + sabc) / (bA + c2))) /
(4 * a32);
}
return s;
}
}
/// radius is complex, rotation is in degrees,
/// large and sweep are 1 or 0 (True/False also work)
class Arc extends SvgPath {
final Point radius;
final double rotation;
final bool arc;
final bool sweep;
late final num radiusScale;
late final Point center;
late final num theta;
late final num delta;
Arc({
required Point start,
required Point end,
required this.radius,
required this.rotation,
required this.arc,
required this.sweep,
}) : super(start: start, end: end) {
_parameterize();
}
@override
String toString() => "Arc(start=$start, radius=$radius, rotation=$rotation, "
"arc=$arc, sweep=$sweep, end=$end)";
// def __eq__(self, other):
// if not isinstance(other, Arc):
// return NotImplemented
// return (
// self.start == other.start
// and self.end == other.end
// and self.radius == other.radius
// and self.rotation == other.rotation
// and self.arc == other.arc
// and self.sweep == other.sweep
// )
// def __ne__(self, other):
// if not isinstance(other, Arc):
// return NotImplemented
// return not self == other
void _parameterize() {
// Conversion from endpoint to center parameterization
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
// This is equivalent of omitting the segment, so do nothing
if (start == end) return;
// This should be treated as a straight line
if (radius.x == 0 || radius.y == 0) return;
final cosr = cos(radians(rotation));
final sinr = sin(radians(rotation));
final dx = (start.x - end.x) / 2;
final dy = (start.y - end.y) / 2;
final x1prim = cosr * dx + sinr * dy;
final x1primSq = x1prim * x1prim;
final y1prim = -sinr * dx + cosr * dy;
final y1primSq = y1prim * y1prim;
num rx = radius.x;
num rxSq = rx * rx;
num ry = radius.y;
num rySq = ry * ry;
// Correct out of range radii
num radiusScale = (x1primSq / rxSq) + (y1primSq / rySq);
if (radiusScale > 1) {
radiusScale = sqrt(radiusScale);
rx *= radiusScale;
ry *= radiusScale;
rxSq = rx * rx;
rySq = ry * ry;
this.radiusScale = radiusScale;
} else {
// SVG spec only scales UP
this.radiusScale = 1;
}
final t1 = rxSq * y1primSq;
final t2 = rySq * x1primSq;
double c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs());
if (arc == sweep) {
c = -c;
}
final cxprim = c * rx * y1prim / ry;
final cyprim = -c * ry * x1prim / rx;
center = Point(
(cosr * cxprim - sinr * cyprim) + ((start.x + end.x) / 2),
(sinr * cxprim + cosr * cyprim) + ((start.y + end.y) / 2),
);
final ux = (x1prim - cxprim) / rx;
final uy = (y1prim - cyprim) / ry;
final vx = (-x1prim - cxprim) / rx;
final vy = (-y1prim - cyprim) / ry;
num n = sqrt(ux * ux + uy * uy);
num p = ux;
theta = (((uy < 0) ? -1 : 1) * degrees(acos(p / n))) % 360;
n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
num d = p / n;
// In certain cases the above calculation can through inaccuracies
// become just slightly out of range, f ex -1.0000000000000002.
if (d > 1.0) {
d = 1.0;
} else if (d < -1.0) {
d = -1.0;
}
delta = ((((ux * vy - uy * vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360;
if (!sweep) delta -= 360;
}
@override
Point point(num pos) {
// This is equivalent of omitting the segment
if (start == end) return start;
// This should be treated as a straight line
if (this.radius.x == 0 || this.radius.y == 0) {
return start + (end - start) * pos;
}
final angle = radians(theta + pos * delta);
final cosr = cos(radians(rotation));
final sinr = sin(radians(rotation));
final radius = this.radius.times(radiusScale);
final x =
cosr * cos(angle) * radius.x - sinr * sin(angle) * radius.y + center.x;
final y =
sinr * cos(angle) * radius.x + cosr * sin(angle) * radius.y + center.y;
return Point(x, y);
}
/// The length of an elliptical arc segment requires numerical
/// integration, and in that case it's simpler to just do a geometric
/// approximation, as for cubic bezier curves.
@override
double size({double error = defaultError, minDepth = defaultMinDepth}) {
// This is equivalent of omitting the segment
if (start == end) return 0;
// This should be treated as a straight line
if (radius.x == 0 || radius.y == 0) {
final distance = end - start;
return sqrt(distance.x * distance.x + distance.y * distance.y);
}
if (radius.x == radius.y) {
// It's a circle, which simplifies this a LOT.
final radius = this.radius.x * radiusScale;
return radians(radius * delta).abs();
}
final startPoint = point(0);
final endPoint = point(1);
return segmentLength(
curve: this,
start: 0,
end: 1,
startPoint: startPoint,
endPoint: endPoint,
error: error,
minDepth: minDepth,
depth: 0);
}
}
// Represents move commands. Does nothing, but is there to handle
// paths that consist of only move commands, which is valid, but pointless.
class Move extends SvgPath {
const Move({required Point to}) : super(start: to, end: to);
@override
String toString() => "Move(to=$start)";
// def __eq__(self, other):
// if not isinstance(other, Move):
// return NotImplemented
// return self.start == other.start
// def __ne__(self, other):
// if not isinstance(other, Move):
// return NotImplemented
// return not self == other
@override
Point point(num pos) => start;
@override
double size({double error = defaultError, int minDepth = defaultMinDepth}) => 0;
}
// Represents the closepath command
class Close extends Linear {
const Close({
required Point start,
required Point end,
}) : super(start: start, end: end);
// def __eq__(self, other):
// if not isinstance(other, Close):
// return NotImplemented
// return self.start == other.start and self.end == other.end
@override
String toString() => "Close(start=$start, end=$end)";
}
/// A Path is a sequence of path segments
class Path extends ListBase<SvgPath> {
late final List<SvgPath> segments;
List<num>? _memoizedLengths;
num? _memoizedLength;
final List<num> _fractions = [];
Path() {
segments = [];
}
@override
SvgPath operator [](int index) => segments[index];
@override
void operator []=(int index, SvgPath value) {
segments[index] = value;
_memoizedLength = null;
}
@override
int get length => segments.length;
@override
set length(int newLength) => segments.length = newLength;
@override
String toString() =>
'Path(${[for (final s in segments) s.toString()].join(", ")})';
void _calcLengths({double error = defaultError, int minDepth = defaultMinDepth}) {
if (_memoizedLength != null) return;
final lengths = [
for (final s in segments) s.size(error: error, minDepth: minDepth)
];
_memoizedLength = lengths.reduce((a, b) => a + b);
if (_memoizedLength == 0) {
_memoizedLengths = lengths;
} else {
_memoizedLengths = [for (final l in lengths) l / _memoizedLength!];
}
// Calculate the fractional distance for each segment to use in point()
num fraction = 0;
for (final l in _memoizedLengths!) {
fraction += l;
_fractions.add(fraction);
}
}
Point point({required num pos, double error = defaultError}) {
// Shortcuts
if (pos == 0.0) {
return segments[0].point(pos);
}
if (pos == 1.0) {
return segments.last.point(pos);
}
_calcLengths(error: error);
// Fix for paths of length 0 (i.e. points)
if (length == 0) {
return segments[0].point(0.0);
}
// Find which segment the point we search for is located on:
late final num segmentPos;
int i = _fractions.bisect(pos);
if (i == 0) {
segmentPos = pos / _fractions[0];
} else {
segmentPos =
(pos - _fractions[i - 1]) / (_fractions[i] - _fractions[i - 1]);
}
return segments[i].point(segmentPos);
}
num size({error = defaultError, minDepth = defaultMinDepth}) {
_calcLengths(error: error, minDepth: minDepth);
return _memoizedLength!;
}
String d() {
Point? currentPos;
final parts = [];
SvgPath? previousSegment;
final end = last.end;
String formatNumber(num n) => n.toString();
String coord(Point p) => '${formatNumber(p.x)},${formatNumber(p.y)}';
for (final segment in this) {
final start = segment.start;
// If the start of this segment does not coincide with the end of
// the last segment or if this segment is actually the close point
// of a closed path, then we should start a new subpath here.
if (segment is Close) {
parts.add("Z");
} else if (segment is Move ||
(currentPos != start) ||
(start == end && previousSegment is! Move)) {
parts.add("M ${coord(start)}");
}
if (segment is Line) {
parts.add("L ${coord(segment.end)}");
} else if (segment is CubicBezier) {
if (segment.isSmoothFrom(previousSegment)) {
parts.add("S ${coord(segment.control2)} ${coord(segment.end)}");
} else {
parts.add(
"C ${coord(segment.control1)} ${coord(segment.control2)} ${coord(segment.end)}",
);
}
} else if (segment is QuadraticBezier) {
if (segment.isSmoothFrom(previousSegment)) {
parts.add("T ${coord(segment.end)}");
} else {
parts.add("Q ${coord(segment.control)} ${coord(segment.end)}");
}
} else if (segment is Arc) {
parts.add(
"A ${coord(segment.radius)} ${formatNumber(segment.rotation)} "
"${(segment.arc ? 1 : 0).toDouble},${(segment.sweep ? 1 : 0).toDouble} ${coord(end)}",
);
}
currentPos = segment.end;
previousSegment = segment;
}
return parts.join(" ");
}
// def __delitem__(self, index):
// del self._segments[index]
// self._length = None
// def reverse(self):
// # Reversing the order of a path would require reversing each element
// # as well. That's not implemented.
// raise NotImplementedError
// def __len__(self):
// return len(self._segments)
// def __eq__(self, other):
// if not isinstance(other, Path):
// return NotImplemented
// if len(self) != len(other):
// return False
// for s, o in zip(self._segments, other._segments):
// if not s == o:
// return False
// return True
// def __ne__(self, other):
// if not isinstance(other, Path):
// return NotImplemented
// return not self == other
}