2022-02-02 02:33:38 +01:00

612 lines
17 KiB
Dart

/// 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.
import 'dart:collection';
import 'dart:math' as math;
import 'dart:math' show sqrt, sin, cos, acos, log, pi;
import 'package:bisection/extension.dart';
import '../common/point.dart';
num radians(num n) => n * pi / 180;
num degrees(num n) => n * 180 / pi;
const defaultMinDepth = 5;
const defaultError = 1e-12;
extension _RemovePointIfInt on num {
num get removePointIfInt => truncate() == this ? truncate() : this;
}
/// Recursively approximates the length by straight lines
num segmentLength({
required SvgPath curve,
required num start,
required num end,
required Point startPoint,
required Point endPoint,
required num error,
required int minDepth,
required num depth,
}) {
num mid = (start + end) / 2;
Point midPoint = curve.point(mid);
num length = (endPoint - startPoint).abs();
num firstHalf = (midPoint - startPoint).abs();
num secondHalf = (endPoint - midPoint).abs();
num 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,
});
@override
bool operator ==(Object other) =>
other is SvgPath && start == other.start && end == other.end;
@override
int get hashCode => start.hashCode ^ end.hashCode;
/// 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
num size({num error = defaultError, int minDepth = defaultMinDepth});
}
abstract class Bezier extends SvgPath {
const Bezier({
required Point start,
required Point end,
}) : super(start: start, end: end);
@override
bool operator ==(Object other) => other is Bezier && super == other;
@override
int get hashCode => super.hashCode + 0;
/// 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);
@override
bool operator ==(Object other) => other is Linear && super == other;
@override
int get hashCode => super.hashCode + 0;
@override
Point point(num pos) => start + (end - start).times(pos);
@override
num size({num 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
bool operator ==(Object other) => other is Line && super == other;
@override
int get hashCode => super.hashCode + 0;
@override
String toString() {
return "Line(start=$start, end=$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
bool operator ==(Object other) =>
other is CubicBezier &&
control1 == other.control1 &&
control2 == other.control2 &&
super == other;
@override
int get hashCode => super.hashCode ^ control1.hashCode ^ control2.hashCode;
@override
String toString() => "CubicBezier(start=$start, control1=$control1, "
"control2=$control2, end=$end)";
@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
num size({num 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
bool operator ==(Object other) =>
other is QuadraticBezier && control == other.control && super == other;
@override
int get hashCode => super.hashCode ^ control.hashCode;
@override
String toString() =>
"QuadraticBezier(start=$start, control=$control, end=$end)";
@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
num size({num 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 num 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 num sabc = 2 * sqrt(A + B + C);
final num a2 = sqrt(A);
final num a32 = 2 * A * a2;
final num c2 = 2 * sqrt(C);
final num 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 num rotation;
final bool arc;
final bool sweep;
// late final num radiusScale;
// late final Point center;
// late final num theta;
// late num delta;
const Arc({
required Point start,
required Point end,
required this.radius,
required this.rotation,
required this.arc,
required this.sweep,
}) : super(start: start, end: end);
@override
bool operator ==(Object other) =>
other is Arc &&
radius == other.radius &&
rotation == other.rotation &&
arc == other.arc &&
sweep == other.sweep &&
super == other;
@override
int get hashCode =>
super.hashCode ^
radius.hashCode ^
rotation.hashCode ^
arc.hashCode ^
sweep.hashCode;
@override
String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, '
'arc=$arc, sweep=$sweep, end=$end)';
// Conversion from endpoint to center parameterization
// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
num get _cosr => cos(radians(rotation));
num get _sinr => sin(radians(rotation));
num get _dx => (start.x - end.x) / 2;
num get _dy => (start.y - end.y) / 2;
num get _x1prim => _cosr * _dx + _sinr * _dy;
num get _x1primSq => _x1prim * _x1prim;
num get _y1prim => -_sinr * _dx + _cosr * _dy;
num get _y1primSq => _y1prim * _y1prim;
num get _rx => (radiusScale > 1 ? radiusScale : 1) * radius.x;
num get _ry => (radiusScale > 1 ? radiusScale : 1) * radius.y;
num get _rxSq => _rx * _rx;
num get _rySq => _ry * _ry;
num get _ux => (_x1prim - _cxprim) / _rx;
num get _uy => (_y1prim - _cyprim) / _ry;
num get _vx => (-_x1prim - _cxprim) / _rx;
num get _vy => (-_y1prim - _cyprim) / _ry;
num get t1 => _rxSq * _y1primSq;
num get t2 => _rySq * _x1primSq;
num get c =>
(arc == sweep ? 1 : -1) *
sqrt(((_rxSq * _rySq - t1 - t2) / (t1 + t2)).abs());
num get _cxprim => c * _rx * _y1prim / _ry;
num get _cyprim => -c * _ry * _x1prim / _rx;
num get radiusScale {
final rs = (_x1primSq / (radius.x * radius.x)) +
(_y1primSq / (radius.y * radius.y));
return rs > 1 ? sqrt(rs) : 1;
}
Point get center => Point(
(_cosr * _cxprim - _sinr * _cyprim) + ((start.x + end.x) / 2),
(_sinr * _cxprim + _cosr * _cyprim) + ((start.y + end.y) / 2),
);
num get theta {
final num n = sqrt(_ux * _ux + _uy * _uy);
final num p = _ux;
return (((_uy < 0) ? -1 : 1) * degrees(acos(p / n))) % 360;
}
num get delta {
final num n = sqrt((_ux * _ux + _uy * _uy) * (_vx * _vx + _vy * _vy));
final num 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;
}
return ((((_ux * _vy - _uy * _vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360 - (!sweep ? 360 : 0);
}
@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
num size({num 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
bool operator ==(Object other) => other is Move && super == other;
@override
int get hashCode => super.hashCode + 0;
@override
String toString() => "Move(to=$start)";
@override
Point point(num pos) => start;
@override
num size({num 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);
@override
bool operator ==(Object other) => other is Close && super == other;
@override
int get hashCode => super.hashCode + 0;
@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 = [];
}
Path.fromSegments(this.segments);
@override
bool operator ==(Object other) => other is Path && segments == other.segments;
@override
int get hashCode => segments.hashCode;
@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(
{num 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, num 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.bisectRight(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.removePointIfInt.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(segment.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},${segment.sweep ? 1 : 0} ${coord(segment.end)}",
);
}
currentPos = segment.end;
previousSegment = segment;
}
return parts.join(" ").toUpperCase();
}
}