blob: fde50c5e4156f2b8aecbf01a215b400ef2558905 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:ui/ui.dart' as ui;
import 'path_ref.dart';
/// Mask used to keep track of types of verbs used in a path segment.
class SPathSegmentMask {
static const int kLine_SkPathSegmentMask = 1 << 0;
static const int kQuad_SkPathSegmentMask = 1 << 1;
static const int kConic_SkPathSegmentMask = 1 << 2;
static const int kCubic_SkPathSegmentMask = 1 << 3;
}
/// Types of path operations.
class SPathVerb {
static const int kMove = 0; // 1 point
static const int kLine = 1; // 2 points
static const int kQuad = 2; // 3 points
static const int kConic = 3; // 3 points + 1 weight
static const int kCubic = 4; // 4 points
static const int kClose = 5; // 0 points
}
abstract class SPath {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
SPath._();
static const int kMoveVerb = SPathVerb.kMove;
static const int kLineVerb = SPathVerb.kLine;
static const int kQuadVerb = SPathVerb.kQuad;
static const int kConicVerb = SPathVerb.kConic;
static const int kCubicVerb = SPathVerb.kCubic;
static const int kCloseVerb = SPathVerb.kClose;
static const int kDoneVerb = SPathVerb.kClose + 1;
static const int kLineSegmentMask = SPathSegmentMask.kLine_SkPathSegmentMask;
static const int kQuadSegmentMask = SPathSegmentMask.kQuad_SkPathSegmentMask;
static const int kConicSegmentMask =
SPathSegmentMask.kConic_SkPathSegmentMask;
static const int kCubicSegmentMask =
SPathSegmentMask.kCubic_SkPathSegmentMask;
static const double scalarNearlyZero = 1.0 / (1 << 12);
/// Square root of 2 divided by 2. Useful for sin45 = cos45 = 1/sqrt(2).
static const double scalarRoot2Over2 = 0.707106781;
/// True if (a <= b <= c) || (a >= b >= c)
static bool between(double a, double b, double c) {
return (a - b) * (c - b) <= 0;
}
/// Returns -1 || 0 || 1 depending on the sign of value:
/// -1 if x < 0
/// 0 if x == 0
/// 1 if x > 0
static int scalarSignedAsInt(double x) {
return x < 0 ? -1 : ((x > 0) ? 1 : 0);
}
static bool nearlyEqual(double value1, double value2) =>
(value1 - value2).abs() < SPath.scalarNearlyZero;
// Snaps a value to zero if almost zero (within tolerance).
static double snapToZero(double value) => SPath.nearlyEqual(value, 0.0) ? 0.0 : value;
static bool isInteger(double value) => value.floor() == value;
}
class SPathAddPathMode {
// Append to destination unaltered.
static const int kAppend = 0;
// Add line if prior contour is not closed.
static const int kExtend = 1;
}
class SPathDirection {
/// Uninitialized value for empty paths.
static const int kUnknown = -1;
/// clockwise direction for adding closed contours.
static const int kCW = 0;
/// counter-clockwise direction for adding closed contours.
static const int kCCW = 1;
}
class SPathConvexityType {
static const int kUnknown = -1;
static const int kConvex = 0;
static const int kConcave = 1;
}
class SPathSegmentState {
/// The current contour is empty. Starting processing or have just closed
/// a contour.
static const int kEmptyContour = 0;
/// Have seen a move, but nothing else.
static const int kAfterMove = 1;
/// Have seen a primitive but not yet closed the path. Also the initial state.
static const int kAfterPrimitive = 2;
}
/// Quadratic roots. See Numerical Recipes in C.
///
/// Q = -1/2 (B + sign(B) sqrt[B*B - 4*A*C])
/// x1 = Q / A
/// x2 = C / Q
class QuadRoots {
double? root0;
double? root1;
QuadRoots();
/// Returns roots as list.
List<double> get roots => (root0 == null)
? <double>[]
: (root1 == null ? <double>[root0!] : <double>[root0!, root1!]);
int findRoots(double a, double b, double c) {
int rootCount = 0;
if (a == 0) {
root0 = validUnitDivide(-c, b);
return root0 == null ? 0 : 1;
}
double dr = b * b - 4 * a * c;
if (dr < 0) {
return 0;
}
dr = math.sqrt(dr);
if (!dr.isFinite) {
return 0;
}
final double q = (b < 0) ? -(b - dr) / 2 : -(b + dr) / 2;
double? res = validUnitDivide(q, a);
if (res != null) {
root0 = res;
++rootCount;
}
res = validUnitDivide(c, q);
if (res != null) {
if (rootCount == 0) {
root0 = res;
++rootCount;
} else {
root1 = res;
++rootCount;
}
}
if (rootCount == 2) {
if (root0! > root1!) {
final double swap = root0!;
root0 = root1;
root1 = swap;
} else if (root0 == root1) {
return 1; // skip the double root
}
}
return rootCount;
}
}
double? validUnitDivide(double numer, double denom) {
if (numer < 0) {
numer = -numer;
denom = -denom;
}
if (denom == 0 || numer == 0 || numer >= denom) {
return null;
}
final double r = numer / denom;
if (r.isNaN) {
return null;
}
if (r == 0) {
// catch underflow if numer <<<< denom
return null;
}
return r;
}
bool isRRectOval(ui.RRect rrect) {
if ((rrect.tlRadiusX + rrect.trRadiusX) != rrect.width) {
return false;
}
if ((rrect.tlRadiusY + rrect.trRadiusY) != rrect.height) {
return false;
}
if (rrect.tlRadiusX != rrect.blRadiusX ||
rrect.trRadiusX != rrect.brRadiusX ||
rrect.tlRadiusY != rrect.blRadiusY ||
rrect.trRadiusY != rrect.brRadiusY) {
return false;
}
return true;
}
/// Evaluates degree 2 polynomial (quadratic).
double polyEval(double A, double B, double C, double t) => (A * t + B) * t + C;
/// Evaluates degree 3 polynomial (cubic).
double polyEval4(double A, double B, double C, double D, double t) =>
((A * t + B) * t + C) * t + D;
// Interpolate between two doubles (Not using lerpDouble here since it null
// checks and treats values as 0).
double interpolate(double startValue, double endValue, double t) =>
(startValue * (1 - t)) + endValue * t;
double dotProduct(double x0, double y0, double x1, double y1) {
return x0 * x1 + y0 * y1;
}
// Helper class for computing convexity for a single contour.
//
// Iteratively looks at angle (using cross product) between consecutive vectors
// formed by path.
class Convexicator {
static const int kValueNeverReturnedBySign = 2;
// Second point of contour start that forms a vector.
// Used to handle close operator to compute angle between last vector and
// first.
double? firstVectorEndPointX;
double? firstVectorEndPointY;
double? priorX;
double? priorY;
double? lastX;
double? lastY;
double? currX;
double? currY;
// Last vector to use to compute angle.
double? lastVecX;
double? lastVecY;
bool _isFinite = true;
int _firstDirection = SPathDirection.kUnknown;
int _reversals = 0;
/// SPathDirection of contour.
int get firstDirection => _firstDirection;
DirChange _expectedDirection = DirChange.kInvalid;
void setMovePt(double x, double y) {
currX = priorX = lastX = x;
currY = priorY = lastY = y;
}
bool addPoint(double x, double y) {
if (x == currX && y == currY) {
// Skip zero length vector.
return true;
}
currX = x;
currY = y;
final double vecX = currX! - lastX!;
final double vecY = currY! - lastY!;
if (priorX == lastX && priorY == lastY) {
// First non-zero vector.
lastVecX = vecX;
lastVecY = vecY;
firstVectorEndPointX = x;
firstVectorEndPointY = y;
} else if (!_addVector(vecX, vecY)) {
return false;
}
priorX = lastX;
priorY = lastY;
lastX = x;
lastY = y;
return true;
}
bool close() {
// Add another point from path closing point to end of first vector.
return addPoint(firstVectorEndPointX!, firstVectorEndPointY!);
}
bool get isFinite => _isFinite;
int get reversals => _reversals;
DirChange _directionChange(double curVecX, double curVecY) {
// Cross product = ||lastVec|| * ||curVec|| * sin(theta) * N
// sin(theta) angle between two vectors is positive for angles 0..180 and
// negative for greater, providing left or right direction.
final double lastX = lastVecX!;
final double lastY = lastVecY!;
final double cross = lastX * curVecY - lastY * curVecX;
if (!cross.isFinite) {
return DirChange.kUnknown;
}
// Detect straight and backwards direction change.
// Instead of comparing absolute crossproduct size, compare
// largest component double+crossproduct.
final double smallest =
math.min(curVecX, math.min(curVecY, math.min(lastX, lastY)));
final double largest = math.max(
math.max(curVecX, math.max(curVecY, math.max(lastX, lastY))),
-smallest);
if (SPath.nearlyEqual(largest, largest + cross)) {
const double nearlyZeroSquared =
SPath.scalarNearlyZero * SPath.scalarNearlyZero;
if (SPath.nearlyEqual(lengthSquared(lastX, lastY), nearlyZeroSquared) ||
SPath.nearlyEqual(lengthSquared(curVecX, curVecY), nearlyZeroSquared)) {
// Length of either vector is smaller than tolerance to be able
// to compute direction.
return DirChange.kUnknown;
}
// The vectors are parallel, sign of dot product gives us direction.
// cosine is positive for straight -90 < Theta < 90
return dotProduct(lastX, lastY, curVecX, curVecY) < 0
? DirChange.kBackwards
: DirChange.kStraight;
}
return cross > 0 ? DirChange.kRight : DirChange.kLeft;
}
bool _addVector(double curVecX, double curVecY) {
final DirChange dir = _directionChange(curVecX, curVecY);
final bool isDirectionRight = dir == DirChange.kRight;
if (dir == DirChange.kLeft || isDirectionRight) {
if (_expectedDirection == DirChange.kInvalid) {
// First valid direction. From this point on expect always left.
_expectedDirection = dir;
_firstDirection =
isDirectionRight ? SPathDirection.kCW : SPathDirection.kCCW;
} else if (dir != _expectedDirection) {
_firstDirection = SPathDirection.kUnknown;
return false;
}
lastVecX = curVecX;
lastVecY = curVecY;
} else {
switch (dir) {
case DirChange.kBackwards:
// Allow path to reverse direction twice.
// Given path.moveTo(0,0) lineTo(1,1)
// - First reversal: direction change formed by line (0,0 1,1),
// line (1,1 0,0)
// - Second reversal: direction change formed by line (1,1 0,0),
// line (0,0 1,1)
lastVecX = curVecX;
lastVecY = curVecY;
return ++_reversals < 3;
case DirChange.kUnknown:
return _isFinite = false;
default:
break;
}
}
return true;
}
// Quick test to detect concave by looking at number of changes in direction
// of vectors formed by path points (excluding control points).
static int bySign(PathRef pathRef, int pointIndex, int numPoints) {
final int lastPointIndex = pointIndex + numPoints;
int currentPoint = pointIndex++;
final int firstPointIndex = currentPoint;
int signChangeCountX = 0;
int signChangeCountY = 0;
int lastSx = kValueNeverReturnedBySign;
int lastSy = kValueNeverReturnedBySign;
for (int outerLoop = 0; outerLoop < 2; ++outerLoop) {
while (pointIndex != lastPointIndex) {
final double vecX = pathRef.pointXAt(pointIndex) -
pathRef.pointXAt(currentPoint);
final double vecY = pathRef.pointYAt(pointIndex) -
pathRef.pointYAt(currentPoint);
if (!(vecX == 0 && vecY == 0)) {
// Give up if vector construction failed.
// give up if vector construction failed
if (!(vecX.isFinite && vecY.isFinite)) {
return SPathConvexityType.kUnknown;
}
final int sx = vecX < 0 ? 1 : 0;
final int sy = vecY < 0 ? 1 : 0;
signChangeCountX += (sx != lastSx) ? 1 : 0;
signChangeCountY += (sy != lastSy) ? 1 : 0;
if (signChangeCountX > 3 || signChangeCountY > 3) {
return SPathConvexityType.kConcave;
}
lastSx = sx;
lastSy = sy;
}
currentPoint = pointIndex++;
if (outerLoop != 0) {
break;
}
}
pointIndex = firstPointIndex;
}
return SPathConvexityType.kConvex;
}
}
enum DirChange {
kUnknown,
kLeft,
kRight,
kStraight,
kBackwards, // if double back, allow simple lines to be convex
kInvalid
}
double lengthSquaredOffset(ui.Offset offset) {
final double dx = offset.dx;
final double dy = offset.dy;
return dx * dx + dy * dy;
}
double lengthSquared(double dx, double dy) => dx * dx + dy * dy;
/// Evaluates A * t^2 + B * t + C = 0 for quadratic curve.
class SkQuadCoefficients {
SkQuadCoefficients(
double x0, double y0, double x1, double y1, double x2, double y2)
: cx = x0,
cy = y0,
bx = 2 * (x1 - x0),
by = 2 * (y1 - y0),
ax = x2 - (2 * x1) + x0,
ay = y2 - (2 * y1) + y0;
final double ax, ay, bx, by, cx, cy;
double evalX(double t) => (ax * t + bx) * t + cx;
double evalY(double t) => (ay * t + by) * t + cy;
}