451 lines
16 KiB
JavaScript
451 lines
16 KiB
JavaScript
import { Point } from '../../../../maths/point/Point.mjs';
|
|
import { uid } from '../../../../utils/data/uid.mjs';
|
|
import { warn } from '../../../../utils/logging/warn.mjs';
|
|
import { SVGToGraphicsPath } from '../svg/SVGToGraphicsPath.mjs';
|
|
import { ShapePath } from './ShapePath.mjs';
|
|
|
|
"use strict";
|
|
class GraphicsPath {
|
|
/**
|
|
* Creates a `GraphicsPath` instance optionally from an SVG path string or an array of `PathInstruction`.
|
|
* @param instructions - An SVG path string or an array of `PathInstruction` objects.
|
|
*/
|
|
constructor(instructions) {
|
|
this.instructions = [];
|
|
/** unique id for this graphics path */
|
|
this.uid = uid("graphicsPath");
|
|
this._dirty = true;
|
|
if (typeof instructions === "string") {
|
|
SVGToGraphicsPath(instructions, this);
|
|
} else {
|
|
this.instructions = instructions?.slice() ?? [];
|
|
}
|
|
}
|
|
/**
|
|
* Provides access to the internal shape path, ensuring it is up-to-date with the current instructions.
|
|
* @returns The `ShapePath` instance associated with this `GraphicsPath`.
|
|
*/
|
|
get shapePath() {
|
|
if (!this._shapePath) {
|
|
this._shapePath = new ShapePath(this);
|
|
}
|
|
if (this._dirty) {
|
|
this._dirty = false;
|
|
this._shapePath.buildPath();
|
|
}
|
|
return this._shapePath;
|
|
}
|
|
/**
|
|
* Adds another `GraphicsPath` to this path, optionally applying a transformation.
|
|
* @param path - The `GraphicsPath` to add.
|
|
* @param transform - An optional transformation to apply to the added path.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
addPath(path, transform) {
|
|
path = path.clone();
|
|
this.instructions.push({ action: "addPath", data: [path, transform] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
arc(...args) {
|
|
this.instructions.push({ action: "arc", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
arcTo(...args) {
|
|
this.instructions.push({ action: "arcTo", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
arcToSvg(...args) {
|
|
this.instructions.push({ action: "arcToSvg", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
bezierCurveTo(...args) {
|
|
this.instructions.push({ action: "bezierCurveTo", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Adds a cubic Bezier curve to the path.
|
|
* It requires two points: the second control point and the end point. The first control point is assumed to be
|
|
* The starting point is the last point in the current path.
|
|
* @param cp2x - The x-coordinate of the second control point.
|
|
* @param cp2y - The y-coordinate of the second control point.
|
|
* @param x - The x-coordinate of the end point.
|
|
* @param y - The y-coordinate of the end point.
|
|
* @param smoothness - Optional parameter to adjust the smoothness of the curve.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
bezierCurveToShort(cp2x, cp2y, x, y, smoothness) {
|
|
const last = this.instructions[this.instructions.length - 1];
|
|
const lastPoint = this.getLastPoint(Point.shared);
|
|
let cp1x = 0;
|
|
let cp1y = 0;
|
|
if (!last || last.action !== "bezierCurveTo") {
|
|
cp1x = lastPoint.x;
|
|
cp1y = lastPoint.y;
|
|
} else {
|
|
cp1x = last.data[2];
|
|
cp1y = last.data[3];
|
|
const currentX = lastPoint.x;
|
|
const currentY = lastPoint.y;
|
|
cp1x = currentX + (currentX - cp1x);
|
|
cp1y = currentY + (currentY - cp1y);
|
|
}
|
|
this.instructions.push({ action: "bezierCurveTo", data: [cp1x, cp1y, cp2x, cp2y, x, y, smoothness] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Closes the current path by drawing a straight line back to the start.
|
|
* If the shape is already closed or there are no points in the path, this method does nothing.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
closePath() {
|
|
this.instructions.push({ action: "closePath", data: [] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
ellipse(...args) {
|
|
this.instructions.push({ action: "ellipse", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
lineTo(...args) {
|
|
this.instructions.push({ action: "lineTo", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
moveTo(...args) {
|
|
this.instructions.push({ action: "moveTo", data: args });
|
|
return this;
|
|
}
|
|
quadraticCurveTo(...args) {
|
|
this.instructions.push({ action: "quadraticCurveTo", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Adds a quadratic curve to the path. It uses the previous point as the control point.
|
|
* @param x - The x-coordinate of the end point.
|
|
* @param y - The y-coordinate of the end point.
|
|
* @param smoothness - Optional parameter to adjust the smoothness of the curve.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
quadraticCurveToShort(x, y, smoothness) {
|
|
const last = this.instructions[this.instructions.length - 1];
|
|
const lastPoint = this.getLastPoint(Point.shared);
|
|
let cpx1 = 0;
|
|
let cpy1 = 0;
|
|
if (!last || last.action !== "quadraticCurveTo") {
|
|
cpx1 = lastPoint.x;
|
|
cpy1 = lastPoint.y;
|
|
} else {
|
|
cpx1 = last.data[0];
|
|
cpy1 = last.data[1];
|
|
const currentX = lastPoint.x;
|
|
const currentY = lastPoint.y;
|
|
cpx1 = currentX + (currentX - cpx1);
|
|
cpy1 = currentY + (currentY - cpy1);
|
|
}
|
|
this.instructions.push({ action: "quadraticCurveTo", data: [cpx1, cpy1, x, y, smoothness] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Draws a rectangle shape. This method adds a new rectangle path to the current drawing.
|
|
* @param x - The x-coordinate of the top-left corner of the rectangle.
|
|
* @param y - The y-coordinate of the top-left corner of the rectangle.
|
|
* @param w - The width of the rectangle.
|
|
* @param h - The height of the rectangle.
|
|
* @param transform - An optional `Matrix` object to apply a transformation to the rectangle.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
rect(x, y, w, h, transform) {
|
|
this.instructions.push({ action: "rect", data: [x, y, w, h, transform] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Draws a circle shape. This method adds a new circle path to the current drawing.
|
|
* @param x - The x-coordinate of the center of the circle.
|
|
* @param y - The y-coordinate of the center of the circle.
|
|
* @param radius - The radius of the circle.
|
|
* @param transform - An optional `Matrix` object to apply a transformation to the circle.
|
|
* @returns The instance of the current object for chaining.
|
|
*/
|
|
circle(x, y, radius, transform) {
|
|
this.instructions.push({ action: "circle", data: [x, y, radius, transform] });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
roundRect(...args) {
|
|
this.instructions.push({ action: "roundRect", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
poly(...args) {
|
|
this.instructions.push({ action: "poly", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
regularPoly(...args) {
|
|
this.instructions.push({ action: "regularPoly", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
roundPoly(...args) {
|
|
this.instructions.push({ action: "roundPoly", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
roundShape(...args) {
|
|
this.instructions.push({ action: "roundShape", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
filletRect(...args) {
|
|
this.instructions.push({ action: "filletRect", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
chamferRect(...args) {
|
|
this.instructions.push({ action: "chamferRect", data: args });
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Draws a star shape centered at a specified location. This method allows for the creation
|
|
* of stars with a variable number of points, outer radius, optional inner radius, and rotation.
|
|
* The star is drawn as a closed polygon with alternating outer and inner vertices to create the star's points.
|
|
* An optional transformation can be applied to scale, rotate, or translate the star as needed.
|
|
* @param x - The x-coordinate of the center of the star.
|
|
* @param y - The y-coordinate of the center of the star.
|
|
* @param points - The number of points of the star.
|
|
* @param radius - The outer radius of the star (distance from the center to the outer points).
|
|
* @param innerRadius - Optional. The inner radius of the star
|
|
* (distance from the center to the inner points between the outer points).
|
|
* If not provided, defaults to half of the `radius`.
|
|
* @param rotation - Optional. The rotation of the star in radians, where 0 is aligned with the y-axis.
|
|
* Defaults to 0, meaning one point is directly upward.
|
|
* @param transform - An optional `Matrix` object to apply a transformation to the star.
|
|
* This can include rotations, scaling, and translations.
|
|
* @returns The instance of the current object for chaining further drawing commands.
|
|
*/
|
|
// eslint-disable-next-line max-len
|
|
star(x, y, points, radius, innerRadius, rotation, transform) {
|
|
innerRadius = innerRadius || radius / 2;
|
|
const startAngle = -1 * Math.PI / 2 + rotation;
|
|
const len = points * 2;
|
|
const delta = Math.PI * 2 / len;
|
|
const polygon = [];
|
|
for (let i = 0; i < len; i++) {
|
|
const r = i % 2 ? innerRadius : radius;
|
|
const angle = i * delta + startAngle;
|
|
polygon.push(
|
|
x + r * Math.cos(angle),
|
|
y + r * Math.sin(angle)
|
|
);
|
|
}
|
|
this.poly(polygon, true, transform);
|
|
return this;
|
|
}
|
|
/**
|
|
* Creates a copy of the current `GraphicsPath` instance. This method supports both shallow and deep cloning.
|
|
* A shallow clone copies the reference of the instructions array, while a deep clone creates a new array and
|
|
* copies each instruction individually, ensuring that modifications to the instructions of the cloned `GraphicsPath`
|
|
* do not affect the original `GraphicsPath` and vice versa.
|
|
* @param deep - A boolean flag indicating whether the clone should be deep.
|
|
* @returns A new `GraphicsPath` instance that is a clone of the current instance.
|
|
*/
|
|
clone(deep = false) {
|
|
const newGraphicsPath2D = new GraphicsPath();
|
|
if (!deep) {
|
|
newGraphicsPath2D.instructions = this.instructions.slice();
|
|
} else {
|
|
for (let i = 0; i < this.instructions.length; i++) {
|
|
const instruction = this.instructions[i];
|
|
newGraphicsPath2D.instructions.push({ action: instruction.action, data: instruction.data.slice() });
|
|
}
|
|
}
|
|
return newGraphicsPath2D;
|
|
}
|
|
clear() {
|
|
this.instructions.length = 0;
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
/**
|
|
* Applies a transformation matrix to all drawing instructions within the `GraphicsPath`.
|
|
* This method enables the modification of the path's geometry according to the provided
|
|
* transformation matrix, which can include translations, rotations, scaling, and skewing.
|
|
*
|
|
* Each drawing instruction in the path is updated to reflect the transformation,
|
|
* ensuring the visual representation of the path is consistent with the applied matrix.
|
|
*
|
|
* Note: The transformation is applied directly to the coordinates and control points of the drawing instructions,
|
|
* not to the path as a whole. This means the transformation's effects are baked into the individual instructions,
|
|
* allowing for fine-grained control over the path's appearance.
|
|
* @param matrix - A `Matrix` object representing the transformation to apply.
|
|
* @returns The instance of the current object for chaining further operations.
|
|
*/
|
|
transform(matrix) {
|
|
if (matrix.isIdentity())
|
|
return this;
|
|
const a = matrix.a;
|
|
const b = matrix.b;
|
|
const c = matrix.c;
|
|
const d = matrix.d;
|
|
const tx = matrix.tx;
|
|
const ty = matrix.ty;
|
|
let x = 0;
|
|
let y = 0;
|
|
let cpx1 = 0;
|
|
let cpy1 = 0;
|
|
let cpx2 = 0;
|
|
let cpy2 = 0;
|
|
let rx = 0;
|
|
let ry = 0;
|
|
for (let i = 0; i < this.instructions.length; i++) {
|
|
const instruction = this.instructions[i];
|
|
const data = instruction.data;
|
|
switch (instruction.action) {
|
|
case "moveTo":
|
|
case "lineTo":
|
|
x = data[0];
|
|
y = data[1];
|
|
data[0] = a * x + c * y + tx;
|
|
data[1] = b * x + d * y + ty;
|
|
break;
|
|
case "bezierCurveTo":
|
|
cpx1 = data[0];
|
|
cpy1 = data[1];
|
|
cpx2 = data[2];
|
|
cpy2 = data[3];
|
|
x = data[4];
|
|
y = data[5];
|
|
data[0] = a * cpx1 + c * cpy1 + tx;
|
|
data[1] = b * cpx1 + d * cpy1 + ty;
|
|
data[2] = a * cpx2 + c * cpy2 + tx;
|
|
data[3] = b * cpx2 + d * cpy2 + ty;
|
|
data[4] = a * x + c * y + tx;
|
|
data[5] = b * x + d * y + ty;
|
|
break;
|
|
case "quadraticCurveTo":
|
|
cpx1 = data[0];
|
|
cpy1 = data[1];
|
|
x = data[2];
|
|
y = data[3];
|
|
data[0] = a * cpx1 + c * cpy1 + tx;
|
|
data[1] = b * cpx1 + d * cpy1 + ty;
|
|
data[2] = a * x + c * y + tx;
|
|
data[3] = b * x + d * y + ty;
|
|
break;
|
|
case "arcToSvg":
|
|
x = data[5];
|
|
y = data[6];
|
|
rx = data[0];
|
|
ry = data[1];
|
|
data[0] = a * rx + c * ry;
|
|
data[1] = b * rx + d * ry;
|
|
data[5] = a * x + c * y + tx;
|
|
data[6] = b * x + d * y + ty;
|
|
break;
|
|
case "circle":
|
|
data[4] = adjustTransform(data[3], matrix);
|
|
break;
|
|
case "rect":
|
|
data[4] = adjustTransform(data[4], matrix);
|
|
break;
|
|
case "ellipse":
|
|
data[8] = adjustTransform(data[8], matrix);
|
|
break;
|
|
case "roundRect":
|
|
data[5] = adjustTransform(data[5], matrix);
|
|
break;
|
|
case "addPath":
|
|
data[0].transform(matrix);
|
|
break;
|
|
case "poly":
|
|
data[2] = adjustTransform(data[2], matrix);
|
|
break;
|
|
default:
|
|
warn("unknown transform action", instruction.action);
|
|
break;
|
|
}
|
|
}
|
|
this._dirty = true;
|
|
return this;
|
|
}
|
|
get bounds() {
|
|
return this.shapePath.bounds;
|
|
}
|
|
/**
|
|
* Retrieves the last point from the current drawing instructions in the `GraphicsPath`.
|
|
* This method is useful for operations that depend on the path's current endpoint,
|
|
* such as connecting subsequent shapes or paths. It supports various drawing instructions,
|
|
* ensuring the last point's position is accurately determined regardless of the path's complexity.
|
|
*
|
|
* If the last instruction is a `closePath`, the method iterates backward through the instructions
|
|
* until it finds an actionable instruction that defines a point (e.g., `moveTo`, `lineTo`,
|
|
* `quadraticCurveTo`, etc.). For compound paths added via `addPath`, it recursively retrieves
|
|
* the last point from the nested path.
|
|
* @param out - A `Point` object where the last point's coordinates will be stored.
|
|
* This object is modified directly to contain the result.
|
|
* @returns The `Point` object containing the last point's coordinates.
|
|
*/
|
|
getLastPoint(out) {
|
|
let index = this.instructions.length - 1;
|
|
let lastInstruction = this.instructions[index];
|
|
if (!lastInstruction) {
|
|
out.x = 0;
|
|
out.y = 0;
|
|
return out;
|
|
}
|
|
while (lastInstruction.action === "closePath") {
|
|
index--;
|
|
if (index < 0) {
|
|
out.x = 0;
|
|
out.y = 0;
|
|
return out;
|
|
}
|
|
lastInstruction = this.instructions[index];
|
|
}
|
|
switch (lastInstruction.action) {
|
|
case "moveTo":
|
|
case "lineTo":
|
|
out.x = lastInstruction.data[0];
|
|
out.y = lastInstruction.data[1];
|
|
break;
|
|
case "quadraticCurveTo":
|
|
out.x = lastInstruction.data[2];
|
|
out.y = lastInstruction.data[3];
|
|
break;
|
|
case "bezierCurveTo":
|
|
out.x = lastInstruction.data[4];
|
|
out.y = lastInstruction.data[5];
|
|
break;
|
|
case "arc":
|
|
case "arcToSvg":
|
|
out.x = lastInstruction.data[5];
|
|
out.y = lastInstruction.data[6];
|
|
break;
|
|
case "addPath":
|
|
lastInstruction.data[0].getLastPoint(out);
|
|
break;
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
function adjustTransform(currentMatrix, transform) {
|
|
if (currentMatrix) {
|
|
return currentMatrix.prepend(transform);
|
|
}
|
|
return transform.clone();
|
|
}
|
|
|
|
export { GraphicsPath };
|
|
//# sourceMappingURL=GraphicsPath.mjs.map
|