'use strict'; var Point = require('../../../../maths/point/Point.js'); var uid = require('../../../../utils/data/uid.js'); var warn = require('../../../../utils/logging/warn.js'); var SVGToGraphicsPath = require('../svg/SVGToGraphicsPath.js'); var ShapePath = require('./ShapePath.js'); "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.uid("graphicsPath"); this._dirty = true; if (typeof instructions === "string") { SVGToGraphicsPath.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.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.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.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.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(); } exports.GraphicsPath = GraphicsPath; //# sourceMappingURL=GraphicsPath.js.map