diff --git a/cypress/e2e/unit/math.cy.js b/cypress/e2e/unit/math.cy.js index a3a4ebc3c..665695cb5 100644 --- a/cypress/e2e/unit/math.cy.js +++ b/cypress/e2e/unit/math.cy.js @@ -4,103 +4,317 @@ import * as math from '../../../packages/svgcanvas/core/math.js' describe('math', function () { const svg = document.createElementNS(NS.SVG, 'svg') - it('Test svgedit.math package', function () { - assert.ok(math) - assert.ok(math.transformPoint) - assert.ok(math.isIdentity) - assert.ok(math.matrixMultiply) - assert.equal(typeof math.transformPoint, typeof function () { /* empty fn */ }) - assert.equal(typeof math.isIdentity, typeof function () { /* empty fn */ }) - assert.equal(typeof math.matrixMultiply, typeof function () { /* empty fn */ }) + before(() => { + // Ensure the SVG element is attached to the document for transform list tests + document.body.appendChild(svg) + }) + + after(() => { + // Cleanup + document.body.removeChild(svg) + }) + + it('Test svgedit.math package exports', function () { + assert.ok(math, 'math module should exist') + const expectedFunctions = [ + 'transformPoint', + 'getTransformList', + 'isIdentity', + 'matrixMultiply', + 'hasMatrixTransform', + 'transformBox', + 'transformListToTransform', + 'getMatrix', + 'snapToAngle', + 'rectsIntersect' + ] + expectedFunctions.forEach(fn => { + assert.ok( + typeof math[fn] === 'function', + `Expected "${fn}" to be a function` + ) + }) }) it('Test svgedit.math.transformPoint() function', function () { const { transformPoint } = math const m = svg.createSVGMatrix() - m.a = 1; m.b = 0 - m.c = 0; m.d = 1 - m.e = 0; m.f = 0 + m.a = 1 + m.b = 0 + m.c = 0 + m.d = 1 + m.e = 0 + m.f = 0 let pt = transformPoint(100, 200, m) - assert.equal(pt.x, 100) - assert.equal(pt.y, 200) + assert.equal(pt.x, 100, 'X should be unchanged by identity matrix') + assert.equal(pt.y, 200, 'Y should be unchanged by identity matrix') - m.e = 300; m.f = 400 + m.e = 300 + m.f = 400 pt = transformPoint(100, 200, m) - assert.equal(pt.x, 400) - assert.equal(pt.y, 600) + assert.equal(pt.x, 400, 'X should be translated by 300') + assert.equal(pt.y, 600, 'Y should be translated by 400') - m.a = 0.5; m.b = 0.75 - m.c = 1.25; m.d = 2 + m.a = 0.5 + m.b = 0.75 + m.c = 1.25 + m.d = 2 pt = transformPoint(100, 200, m) - assert.equal(pt.x, 100 * m.a + 200 * m.c + m.e) - assert.equal(pt.y, 100 * m.b + 200 * m.d + m.f) + assert.equal( + pt.x, + 100 * m.a + 200 * m.c + m.e, + 'X should match matrix multiplication' + ) + assert.equal( + pt.y, + 100 * m.b + 200 * m.d + m.f, + 'Y should match matrix multiplication' + ) }) it('Test svgedit.math.isIdentity() function', function () { - assert.ok(math.isIdentity(svg.createSVGMatrix())) + const { isIdentity } = math + + assert.ok( + isIdentity(svg.createSVGMatrix()), + 'Default matrix should be identity' + ) const m = svg.createSVGMatrix() - m.a = 1; m.b = 0 - m.c = 0; m.d = 1 - m.e = 0; m.f = 0 - assert.ok(math.isIdentity(m)) + m.a = 1 + m.b = 0 + m.c = 0 + m.d = 1 + m.e = 0 + m.f = 0 + assert.ok( + isIdentity(m), + 'Modified matrix matching identity values should be identity' + ) + + m.e = 10 + assert.notOk(isIdentity(m), 'Matrix with translation is not identity') }) it('Test svgedit.math.matrixMultiply() function', function () { - const mult = math.matrixMultiply - const { isIdentity } = math + const { matrixMultiply, isIdentity } = math + + // Test empty arguments + const iDefault = matrixMultiply() + assert.ok( + isIdentity(iDefault), + 'No arguments should return identity matrix' + ) - // translate there and back + // Translate there and back const tr1 = svg.createSVGMatrix().translate(100, 50) const tr2 = svg.createSVGMatrix().translate(-90, 0) const tr3 = svg.createSVGMatrix().translate(-10, -50) - let I = mult(tr1, tr2, tr3) - assert.ok(isIdentity(I), 'Expected identity matrix when translating there and back') + let I = matrixMultiply(tr1, tr2, tr3) + assert.ok(isIdentity(I), 'Translating there and back should yield identity') - // rotate there and back - // TODO: currently Mozilla fails this when rotating back at -50 and then -40 degrees - // (b and c are *almost* zero, but not zero) + // Rotate there and back const rotThere = svg.createSVGMatrix().rotate(90) - const rotBack = svg.createSVGMatrix().rotate(-90) // TODO: set this to -50 - const rotBackMore = svg.createSVGMatrix().rotate(0) // TODO: set this to -40 - I = mult(rotThere, rotBack, rotBackMore) - assert.ok(isIdentity(I), 'Expected identity matrix when rotating there and back') + const rotBack = svg.createSVGMatrix().rotate(-90) + I = matrixMultiply(rotThere, rotBack) + assert.ok(isIdentity(I), 'Rotating and rotating back should yield identity') - // scale up and down + // Scale up and down const scaleUp = svg.createSVGMatrix().scale(4) - const scaleDown = svg.createSVGMatrix().scaleNonUniform(0.25, 1) - const scaleDownMore = svg.createSVGMatrix().scaleNonUniform(1, 0.25) - I = mult(scaleUp, scaleDown, scaleDownMore) - assert.ok(isIdentity(I), 'Expected identity matrix when scaling up and down') - - // test multiplication with its inverse - I = mult(rotThere, rotThere.inverse()) - assert.ok(isIdentity(I), 'Expected identity matrix when multiplying a matrix by its inverse') - I = mult(rotThere.inverse(), rotThere) - assert.ok(isIdentity(I), 'Expected identity matrix when multiplying a matrix by its inverse') + const scaleDownX = svg.createSVGMatrix().scaleNonUniform(0.25, 1) + const scaleDownY = svg.createSVGMatrix().scaleNonUniform(1, 0.25) + I = matrixMultiply(scaleUp, scaleDownX, scaleDownY) + assert.ok( + isIdentity(I), + 'Scaling up and then scaling down back to original should yield identity' + ) + + // Multiplying a matrix by its inverse + const someMatrix = svg + .createSVGMatrix() + .rotate(33) + .translate(100, 200) + .scale(2) + I = matrixMultiply(someMatrix, someMatrix.inverse()) + cy.log(I) + cy.log('-----------------------------------------') + assert.ok( + isIdentity(I), + 'Matrix multiplied by its inverse should be identity' + ) }) it('Test svgedit.math.transformBox() function', function () { const { transformBox } = math const m = svg.createSVGMatrix() - m.a = 1; m.b = 0 - m.c = 0; m.d = 1 - m.e = 0; m.f = 0 - + // Identity const r = transformBox(10, 10, 200, 300, m) - assert.equal(r.tl.x, 10) - assert.equal(r.tl.y, 10) - assert.equal(r.tr.x, 210) - assert.equal(r.tr.y, 10) - assert.equal(r.bl.x, 10) - assert.equal(r.bl.y, 310) - assert.equal(r.br.x, 210) - assert.equal(r.br.y, 310) - assert.equal(r.aabox.x, 10) - assert.equal(r.aabox.y, 10) - assert.equal(r.aabox.width, 200) - assert.equal(r.aabox.height, 300) + assert.equal(r.tl.x, 10, 'Top-left X should be 10') + assert.equal(r.tl.y, 10, 'Top-left Y should be 10') + assert.equal(r.tr.x, 210, 'Top-right X should be 210') + assert.equal(r.tr.y, 10, 'Top-right Y should be 10') + assert.equal(r.bl.x, 10, 'Bottom-left X should be 10') + assert.equal(r.bl.y, 310, 'Bottom-left Y should be 310') + assert.equal(r.br.x, 210, 'Bottom-right X should be 210') + assert.equal(r.br.y, 310, 'Bottom-right Y should be 310') + assert.equal(r.aabox.x, 10, 'AABBox X should be 10') + assert.equal(r.aabox.y, 10, 'AABBox Y should be 10') + assert.equal(r.aabox.width, 200, 'AABBox width should be 200') + assert.equal(r.aabox.height, 300, 'AABBox height should be 300') + + // Transformed box + m.e = 50 + m.f = 50 + const r2 = transformBox(0, 0, 100, 100, m) + assert.equal(r2.aabox.x, 50, 'AABBox x should be translated by 50') + assert.equal(r2.aabox.y, 50, 'AABBox y should be translated by 50') + }) + + it('Test svgedit.math.getTransformList() and hasMatrixTransform() functions', function () { + const { getTransformList, hasMatrixTransform } = math + + // An element with no transform + const rect = document.createElementNS(NS.SVG, 'rect') + svg.appendChild(rect) + const tlist = getTransformList(rect) + assert.ok(tlist, 'Should get a transform list (empty)') + assert.equal(tlist.numberOfItems, 0, 'Transform list should be empty') + assert.notOk( + hasMatrixTransform(tlist), + 'No matrix transform in an empty transform list' + ) + + // Add a non-identity matrix transform + const nonIdentityMatrix = svg.createSVGMatrix().translate(10, 20).scale(2) + const tf = svg.createSVGTransformFromMatrix(nonIdentityMatrix) + tlist.appendItem(tf) + assert.equal(tlist.numberOfItems, 1, 'Transform list should have one item') + assert.ok( + hasMatrixTransform(tlist), + 'Non-identity matrix transform should be detected' + ) + + // Add an identity transform + const tfIdentity = svg.createSVGTransformFromMatrix(svg.createSVGMatrix()) // identity matrix + tlist.appendItem(tfIdentity) + assert.equal( + tlist.numberOfItems, + 2, + 'Transform list should have two items now' + ) + // Still should have a non-identity matrix transform present + assert.ok( + hasMatrixTransform(tlist), + 'Still have a non-identity matrix transform after adding an identity transform' + ) + + // Cleanup + svg.removeChild(rect) + }) + + it('Test svgedit.math.transformListToTransform() and getMatrix() functions', function () { + const { transformListToTransform, getMatrix } = math + + const g = document.createElementNS(NS.SVG, 'g') + svg.appendChild(g) + + const tlist = g.transform.baseVal + const m1 = svg.createSVGTransformFromMatrix( + svg.createSVGMatrix().translate(10, 20) + ) + const m2 = svg.createSVGTransformFromMatrix( + svg.createSVGMatrix().rotate(45) + ) + tlist.appendItem(m1) + tlist.appendItem(m2) + + const consolidated = transformListToTransform(tlist) + const expected = m1.matrix.multiply(m2.matrix) + assert.equal( + consolidated.matrix.a, + expected.a, + 'Consolidated matrix a should match expected' + ) + assert.equal( + consolidated.matrix.d, + expected.d, + 'Consolidated matrix d should match expected' + ) + + const elemMatrix = getMatrix(g) + assert.equal( + elemMatrix.a, + expected.a, + 'Element matrix a should match expected' + ) + assert.equal( + elemMatrix.d, + expected.d, + 'Element matrix d should match expected' + ) + + svg.removeChild(g) + }) + + it('Test svgedit.math.snapToAngle() function', function () { + const { snapToAngle } = math + + const result = snapToAngle(0, 0, 10, 0) // Expect snap to 0 degrees + assert.equal( + result.x, + 10, + 'Snapped x should remain 10 when angle is already at 0°' + ) + assert.equal( + result.y, + 0, + 'Snapped y should remain 0 when angle is already at 0°' + ) + + // 45-degree snap from an angle close to 45° (e.g., 50°) + const angleDegrees = 50 + const angleRadians = angleDegrees * (Math.PI / 180) + const dx = Math.cos(angleRadians) * 100 + const dy = Math.sin(angleRadians) * 100 + const snapped = snapToAngle(0, 0, dx, dy) + // Should snap to exactly 45° + const expectedAngle = Math.PI / 4 + const dist = Math.hypot(dx, dy) + assert.closeTo( + snapped.x, + dist * Math.cos(expectedAngle), + 0.00001, + 'X should be close to 45° projection' + ) + assert.closeTo( + snapped.y, + dist * Math.sin(expectedAngle), + 0.00001, + 'Y should be close to 45° projection' + ) + }) + + it('Test svgedit.math.rectsIntersect() function', function () { + const { rectsIntersect } = math + const r1 = { x: 0, y: 0, width: 50, height: 50 } + const r2 = { x: 25, y: 25, width: 50, height: 50 } + const r3 = { x: 100, y: 100, width: 10, height: 10 } + + assert.ok(rectsIntersect(r1, r2), 'Rectangles overlapping should intersect') + assert.notOk( + rectsIntersect(r1, r3), + 'Non-overlapping rectangles should not intersect' + ) + + // Edge case: touching edges + const r4 = { x: 50, y: 0, width: 50, height: 50 } + // Note: Depending on interpretation, touching at the border might be considered intersecting or not. + // The given function checks strict overlapping (not just touching), so this should return false. + assert.notOk( + rectsIntersect(r1, r4), + 'Rectangles touching at the edge should not be considered intersecting' + ) }) }) diff --git a/packages/svgcanvas/core/math.js b/packages/svgcanvas/core/math.js index 7d4bc5c64..b954778eb 100644 --- a/packages/svgcanvas/core/math.js +++ b/packages/svgcanvas/core/math.js @@ -3,117 +3,111 @@ * @module math * @license MIT * - * @copyright 2010 Alexis Deveria, 2010 Jeff Schiller + * ©2010 Alexis Deveria, ©2010 Jeff Schiller */ /** - * @typedef {PlainObject} module:math.AngleCoord45 - * @property {Float} x - The angle-snapped x value - * @property {Float} y - The angle-snapped y value - * @property {Integer} a - The angle at which to snap + * @typedef {Object} AngleCoord45 + * @property {number} x - The angle-snapped x value + * @property {number} y - The angle-snapped y value + * @property {number} a - The angle (in radians) at which to snap */ /** - * @typedef {PlainObject} module:math.XYObject - * @property {Float} x - * @property {Float} y + * @typedef {Object} XYObject + * @property {number} x + * @property {number} y */ import { NS } from './namespaces.js' // Constants -const NEAR_ZERO = 1e-14 +const NEAR_ZERO = 1e-10 -// Throw away SVGSVGElement used for creating matrices/transforms. +// Create a throwaway SVG element for matrix operations const svg = document.createElementNS(NS.SVG, 'svg') /** - * A (hopefully) quicker function to transform a point by a matrix - * (this function avoids any DOM calls and just does the math). - * @function module:math.transformPoint - * @param {Float} x - Float representing the x coordinate - * @param {Float} y - Float representing the y coordinate - * @param {SVGMatrix} m - Matrix object to transform the point with - * @returns {module:math.XYObject} An x, y object representing the transformed point + * Transforms a point by a given matrix without DOM calls. + * @function transformPoint + * @param {number} x - The x coordinate + * @param {number} y - The y coordinate + * @param {SVGMatrix} m - The transformation matrix + * @returns {XYObject} The transformed point */ -export const transformPoint = function (x, y, m) { - return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f } -} +export const transformPoint = (x, y, m) => ({ + x: m.a * x + m.c * y + m.e, + y: m.b * x + m.d * y + m.f +}) -export const getTransformList = (elem) => { - if (elem.transform) { - return elem.transform?.baseVal +/** + * Gets the transform list (baseVal) from an element if it exists. + * @function getTransformList + * @param {Element} elem - An SVG element or element with a transform list + * @returns {SVGTransformList|undefined} The transform list, if any + */ +export const getTransformList = elem => { + if (elem.transform?.baseVal) { + return elem.transform.baseVal } - if (elem.gradientTransform) { - return elem.gradientTransform?.baseVal + if (elem.gradientTransform?.baseVal) { + return elem.gradientTransform.baseVal } - if (elem.patternTransform) { - return elem.patternTransform?.baseVal + if (elem.patternTransform?.baseVal) { + return elem.patternTransform.baseVal } - console.warn('no transform list found - check browser version', elem) + console.warn('No transform list found. Check browser compatibility.', elem) } /** - * Helper function to check if the matrix performs no actual transform - * (i.e. exists for identity purposes). - * @function module:math.isIdentity - * @param {SVGMatrix} m - The matrix object to check - * @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0 + * Checks if a matrix is the identity matrix. + * @function isIdentity + * @param {SVGMatrix} m - The matrix to check + * @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0) */ -export const isIdentity = function (m) { - return ( - m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0 - ) -} +export const isIdentity = m => + m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0 /** - * This function tries to return a `SVGMatrix` that is the multiplication `m1 * m2`. - * We also round to zero when it's near zero. - * @function module:math.matrixMultiply - * @param {...SVGMatrix} args - Matrix objects to multiply - * @returns {SVGMatrix} The matrix object resulting from the calculation + * Multiplies multiple matrices together (m1 * m2 * ...). + * Near-zero values are rounded to zero. + * @function matrixMultiply + * @param {...SVGMatrix} args - The matrices to multiply + * @returns {SVGMatrix} The resulting matrix */ -export const matrixMultiply = function (...args) { - const m = args.reduceRight((prev, m1) => { - return m1.multiply(prev) - }) - - if (Math.abs(m.a) < NEAR_ZERO) { - m.a = 0 - } - if (Math.abs(m.b) < NEAR_ZERO) { - m.b = 0 - } - if (Math.abs(m.c) < NEAR_ZERO) { - m.c = 0 - } - if (Math.abs(m.d) < NEAR_ZERO) { - m.d = 0 - } - if (Math.abs(m.e) < NEAR_ZERO) { - m.e = 0 - } - if (Math.abs(m.f) < NEAR_ZERO) { - m.f = 0 +export const matrixMultiply = (...args) => { + // If no matrices are given, return an identity matrix + if (args.length === 0) { + return svg.createSVGMatrix() } + const m = args.reduceRight((prev, curr) => curr.multiply(prev)) + + // Round near-zero values to zero + if (Math.abs(m.a) < NEAR_ZERO) m.a = 0 + if (Math.abs(m.b) < NEAR_ZERO) m.b = 0 + if (Math.abs(m.c) < NEAR_ZERO) m.c = 0 + if (Math.abs(m.d) < NEAR_ZERO) m.d = 0 + if (Math.abs(m.e) < NEAR_ZERO) m.e = 0 + if (Math.abs(m.f) < NEAR_ZERO) m.f = 0 + return m } /** - * See if the given transformlist includes a non-indentity matrix transform. - * @function module:math.hasMatrixTransform - * @param {SVGTransformList} [tlist] - The transformlist to check - * @returns {boolean} Whether or not a matrix transform was found + * Checks if a transform list includes a non-identity matrix transform. + * @function hasMatrixTransform + * @param {SVGTransformList} [tlist] - The transform list to check + * @returns {boolean} True if a matrix transform is found */ -export const hasMatrixTransform = function (tlist) { - if (!tlist) { - return false - } - let num = tlist.numberOfItems - while (num--) { - const xform = tlist.getItem(num) - if (xform.type === 1 && !isIdentity(xform.matrix)) { +export const hasMatrixTransform = tlist => { + if (!tlist) return false + for (let i = 0; i < tlist.numberOfItems; i++) { + const xform = tlist.getItem(i) + if ( + xform.type === SVGTransform.SVG_TRANSFORM_MATRIX && + !isIdentity(xform.matrix) + ) { return true } } @@ -121,29 +115,29 @@ export const hasMatrixTransform = function (tlist) { } /** - * @typedef {PlainObject} module:math.TransformedBox An object with the following values - * @property {module:math.XYObject} tl - The top left coordinate - * @property {module:math.XYObject} tr - The top right coordinate - * @property {module:math.XYObject} bl - The bottom left coordinate - * @property {module:math.XYObject} br - The bottom right coordinate - * @property {PlainObject} aabox - Object with the following values: - * @property {Float} aabox.x - Float with the axis-aligned x coordinate - * @property {Float} aabox.y - Float with the axis-aligned y coordinate - * @property {Float} aabox.width - Float with the axis-aligned width coordinate - * @property {Float} aabox.height - Float with the axis-aligned height coordinate + * @typedef {Object} TransformedBox + * @property {XYObject} tl - Top-left coordinate + * @property {XYObject} tr - Top-right coordinate + * @property {XYObject} bl - Bottom-left coordinate + * @property {XYObject} br - Bottom-right coordinate + * @property {Object} aabox + * @property {number} aabox.x - Axis-aligned x + * @property {number} aabox.y - Axis-aligned y + * @property {number} aabox.width - Axis-aligned width + * @property {number} aabox.height - Axis-aligned height */ /** - * Transforms a rectangle based on the given matrix. - * @function module:math.transformBox - * @param {Float} l - Float with the box's left coordinate - * @param {Float} t - Float with the box's top coordinate - * @param {Float} w - Float with the box width - * @param {Float} h - Float with the box height - * @param {SVGMatrix} m - Matrix object to transform the box by - * @returns {module:math.TransformedBox} + * Transforms a rectangular box using a given matrix. + * @function transformBox + * @param {number} l - Left coordinate + * @param {number} t - Top coordinate + * @param {number} w - Width + * @param {number} h - Height + * @param {SVGMatrix} m - Transformation matrix + * @returns {TransformedBox} The transformed box information */ -export const transformBox = function (l, t, w, h, m) { +export const transformBox = (l, t, w, h, m) => { const tl = transformPoint(l, t, m) const tr = transformPoint(l + w, t, m) const bl = transformPoint(l, t + h, m) @@ -169,91 +163,81 @@ export const transformBox = function (l, t, w, h, m) { } /** - * This returns a single matrix Transform for a given Transform List - * (this is the equivalent of `SVGTransformList.consolidate()` but unlike - * that method, this one does not modify the actual `SVGTransformList`). - * This function is very liberal with its `min`, `max` arguments. - * @function module:math.transformListToTransform - * @param {SVGTransformList} tlist - The transformlist object - * @param {Integer} [min=0] - Optional integer indicating start transform position - * @param {Integer} [max] - Optional integer indicating end transform position; - * defaults to one less than the tlist's `numberOfItems` - * @returns {SVGTransform} A single matrix transform object + * Consolidates a transform list into a single matrix transform without modifying the original list. + * @function transformListToTransform + * @param {SVGTransformList} tlist - The transform list + * @param {number} [min=0] - Optional start index + * @param {number} [max] - Optional end index, defaults to tlist length-1 + * @returns {SVGTransform} A single transform from the combined matrices */ -export const transformListToTransform = function (tlist, min, max) { +export const transformListToTransform = (tlist, min = 0, max = null) => { if (!tlist) { - // Or should tlist = null have been prevented before this? return svg.createSVGTransformFromMatrix(svg.createSVGMatrix()) } - min = min || 0 - max = max || tlist.numberOfItems - 1 - min = Number.parseInt(min) - max = Number.parseInt(max) - if (min > max) { - const temp = max - max = min - min = temp - } - let m = svg.createSVGMatrix() - for (let i = min; i <= max; ++i) { - // if our indices are out of range, just use a harmless identity matrix - const mtom = + + const start = Number.parseInt(min, 10) + const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10) + const low = Math.min(start, end) + const high = Math.max(start, end) + + let combinedMatrix = svg.createSVGMatrix() + for (let i = low; i <= high; i++) { + // If out of range, use identity + const currentMatrix = i >= 0 && i < tlist.numberOfItems ? tlist.getItem(i).matrix : svg.createSVGMatrix() - m = matrixMultiply(m, mtom) + combinedMatrix = matrixMultiply(combinedMatrix, currentMatrix) } - return svg.createSVGTransformFromMatrix(m) + + return svg.createSVGTransformFromMatrix(combinedMatrix) } /** - * Get the matrix object for a given element. - * @function module:math.getMatrix - * @param {Element} elem - The DOM element to check - * @returns {SVGMatrix} The matrix object associated with the element's transformlist + * Gets the matrix of a given element's transform list. + * @function getMatrix + * @param {Element} elem - The element to check + * @returns {SVGMatrix} The transformation matrix */ -export const getMatrix = (elem) => { +export const getMatrix = elem => { const tlist = getTransformList(elem) return transformListToTransform(tlist).matrix } /** - * Returns a 45 degree angle coordinate associated with the two given - * coordinates. - * @function module:math.snapToAngle - * @param {Integer} x1 - First coordinate's x value - * @param {Integer} y1 - First coordinate's y value - * @param {Integer} x2 - Second coordinate's x value - * @param {Integer} y2 - Second coordinate's y value - * @returns {module:math.AngleCoord45} + * Returns a coordinate snapped to the nearest 45-degree angle. + * @function snapToAngle + * @param {number} x1 - First point's x + * @param {number} y1 - First point's y + * @param {number} x2 - Second point's x + * @param {number} y2 - Second point's y + * @returns {AngleCoord45} The angle-snapped coordinates and angle */ export const snapToAngle = (x1, y1, x2, y2) => { const snap = Math.PI / 4 // 45 degrees const dx = x2 - x1 const dy = y2 - y1 const angle = Math.atan2(dy, dx) - const dist = Math.sqrt(dx * dx + dy * dy) - const snapangle = Math.round(angle / snap) * snap + const dist = Math.hypot(dx, dy) + const snapAngle = Math.round(angle / snap) * snap return { - x: x1 + dist * Math.cos(snapangle), - y: y1 + dist * Math.sin(snapangle), - a: snapangle + x: x1 + dist * Math.cos(snapAngle), + y: y1 + dist * Math.sin(snapAngle), + a: snapAngle } } /** - * Check if two rectangles (BBoxes objects) intersect each other. - * @function module:math.rectsIntersect - * @param {SVGRect} r1 - The first BBox-like object - * @param {SVGRect} r2 - The second BBox-like object - * @returns {boolean} True if rectangles intersect + * Checks if two rectangles intersect. + * Both r1 and r2 are expected to have {x, y, width, height}. + * @function rectsIntersect + * @param {{x:number,y:number,width:number,height:number}} r1 - First rectangle + * @param {{x:number,y:number,width:number,height:number}} r2 - Second rectangle + * @returns {boolean} True if the rectangles intersect */ -export const rectsIntersect = (r1, r2) => { - return ( - r2.x < r1.x + r1.width && - r2.x + r2.width > r1.x && - r2.y < r1.y + r1.height && - r2.y + r2.height > r1.y - ) -} +export const rectsIntersect = (r1, r2) => + r2.x < r1.x + r1.width && + r2.x + r2.width > r1.x && + r2.y < r1.y + r1.height && + r2.y + r2.height > r1.y