2020-11-04 20:06:19 +01:00
|
|
|
import { Bezier } from 'bezier-js'
|
2022-08-28 02:14:39 +02:00
|
|
|
import { Path } from './path.mjs'
|
|
|
|
import { Point } from './point.mjs'
|
2018-12-09 14:17:46 +01:00
|
|
|
|
2023-09-20 18:56:00 +02:00
|
|
|
export const goldenRatio = 1.618034
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// PUBLIC METHODS //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the intersections between an endless line (beam) and a circle
|
|
|
|
*
|
|
|
|
* @param {Point} c - The center Point of the circle
|
|
|
|
* @param {float} r - The radius of the circle
|
|
|
|
* @param {Point} p1 - First Point on the line
|
|
|
|
* @param {Point} p2 - Second Point on the line
|
|
|
|
* @param {string} sort - Controls the sort of the resulting intersections
|
|
|
|
* @return {Array} intersections - An array with Point objects for the intersections
|
|
|
|
*/
|
|
|
|
export function beamIntersectsCircle(c, r, p1, p2, sort = 'x') {
|
|
|
|
let dx = p2.x - p1.x
|
|
|
|
let dy = p2.y - p1.y
|
|
|
|
let A = Math.pow(dx, 2) + Math.pow(dy, 2)
|
|
|
|
let B = 2 * (dx * (p1.x - c.x) + dy * (p1.y - c.y))
|
|
|
|
let C = Math.pow(p1.x - c.x, 2) + Math.pow(p1.y - c.y, 2) - Math.pow(r, 2)
|
|
|
|
|
|
|
|
let det = Math.pow(B, 2) - 4 * A * C
|
|
|
|
|
|
|
|
if (A <= 0.0000001 || det < 0) return false
|
|
|
|
// No real solutions
|
|
|
|
else if (det === 0) {
|
|
|
|
// One solution
|
|
|
|
let t = (-1 * B) / (2 * A)
|
|
|
|
let i1 = new Point(p1.x + t * dx, p1.y + t * dy)
|
|
|
|
return [i1]
|
|
|
|
} else {
|
|
|
|
// Two solutions
|
|
|
|
let t = (-1 * B + Math.sqrt(det)) / (2 * A)
|
|
|
|
let i1 = new Point(p1.x + t * dx, p1.y + t * dy)
|
|
|
|
t = (-1 * B - Math.sqrt(det)) / (2 * A)
|
|
|
|
let i2 = new Point(p1.x + t * dx, p1.y + t * dy)
|
|
|
|
if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2]
|
|
|
|
else return [i2, i1]
|
|
|
|
}
|
2018-12-16 18:06:01 +01:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
2022-12-07 18:23:02 -08:00
|
|
|
* Finds where an endless line intersects with a given X-value
|
2022-09-18 15:11:10 +02:00
|
|
|
*
|
|
|
|
* @param {Point} from - First Point on the line
|
|
|
|
* @param {Point} to - Second Point on the line
|
|
|
|
* @param {float} x - X-value to check
|
|
|
|
* @return {Point} intersection - The Point at the intersection
|
|
|
|
*/
|
|
|
|
export function beamIntersectsX(from, to, x) {
|
|
|
|
if (from.x === to.x) return false // Vertical line
|
|
|
|
let top = new Point(x, -10)
|
|
|
|
let bottom = new Point(x, 10)
|
|
|
|
|
|
|
|
return beamsIntersect(from, to, top, bottom)
|
2020-07-18 16:48:29 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
2022-12-07 18:23:02 -08:00
|
|
|
* Finds where an endless line intersects with a given Y-value
|
2022-09-18 15:11:10 +02:00
|
|
|
*
|
|
|
|
* @param {Point} from - First Point 1 on the line
|
|
|
|
* @param {Point} to - Second Point on the line
|
|
|
|
* @param {float} y - Y-value to check
|
|
|
|
* @return {Point} intersection - The Point at the intersection
|
|
|
|
*/
|
|
|
|
export function beamIntersectsY(from, to, y) {
|
|
|
|
if (from.y === to.y) return false // Horizontal line
|
|
|
|
let left = new Point(-10, y)
|
|
|
|
let right = new Point(10, y)
|
|
|
|
|
|
|
|
return beamsIntersect(from, to, left, right)
|
2018-07-12 12:53:49 +00:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Finds the intersection of two endless lines (beams)
|
|
|
|
*
|
|
|
|
* @param {Point} a1 - Point 1 of line A
|
|
|
|
* @param {Point} a2 - Point 2 of line A
|
|
|
|
* @param {Point} b1 - Point 1 of line B
|
|
|
|
* @param {Point} b2 - Point 2 of line B
|
|
|
|
* @return {Point} intersections - The Point at the intersection
|
|
|
|
*/
|
2018-08-23 15:57:23 +02:00
|
|
|
export function beamsIntersect(a1, a2, b1, b2) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let slopeA = a1.slope(a2)
|
|
|
|
let slopeB = b1.slope(b2)
|
|
|
|
if (slopeA === slopeB) return false // Parallel lines
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2021-07-14 18:01:04 +02:00
|
|
|
// Check for vertical line A
|
2021-08-30 11:40:16 +02:00
|
|
|
if (Math.round(a1.x * 10000) === Math.round(a2.x * 10000))
|
2021-07-14 18:01:04 +02:00
|
|
|
return new Point(a1.x, slopeB * a1.x + (b1.y - slopeB * b1.x))
|
|
|
|
// Check for vertical line B
|
2021-08-30 11:40:16 +02:00
|
|
|
else if (Math.round(b1.x * 10000) === Math.round(b2.x * 10000))
|
2021-07-14 18:01:04 +02:00
|
|
|
return new Point(b1.x, slopeA * b1.x + (a1.y - slopeA * a1.x))
|
2018-07-14 16:04:39 +00:00
|
|
|
else {
|
|
|
|
// Swap points if line A or B goes from right to left
|
2019-08-03 15:03:33 +02:00
|
|
|
if (a1.x > a2.x) a1 = a2.copy()
|
|
|
|
if (b1.x > b2.x) b1 = b2.copy()
|
2018-07-14 16:04:39 +00:00
|
|
|
// Find y intercept
|
2019-08-03 15:03:33 +02:00
|
|
|
let iA = a1.y - slopeA * a1.x
|
|
|
|
let iB = b1.y - slopeB * b1.x
|
2018-07-14 16:04:39 +00:00
|
|
|
|
|
|
|
// Find intersection
|
2019-08-03 15:03:33 +02:00
|
|
|
let x = (iB - iA) / (slopeA - slopeB)
|
|
|
|
let y = slopeA * x + iA
|
2018-07-14 16:04:39 +00:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return new Point(x, y)
|
2018-07-14 16:04:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-28 16:42:35 +00:00
|
|
|
/**
|
|
|
|
* Find the intersections between an endless line (beam) and a curve
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {Point} start - Start Point of the line
|
|
|
|
* @param {Point} end - End Point of the line
|
|
|
|
* @param {Point} from - Start Point of the curve
|
|
|
|
* @param {Point} cp1 - Control Point at the start of the curve
|
|
|
|
* @param {Point} cp2 - Control Point at the end of the curve
|
|
|
|
* @param {Point} to - End Point of the curve
|
|
|
|
* @return {Array} intersections - An array of Points at the intersections
|
|
|
|
*/
|
|
|
|
export function beamIntersectsCurve(start, end, from, cp1, cp2, to) {
|
|
|
|
let _start = new Point(start.x + (start.x - end.x) * 1000, start.y + (start.y - end.y) * 1000)
|
|
|
|
let _end = new Point(end.x + (end.x - start.x) * 1000, end.y + (end.y - start.y) * 1000)
|
|
|
|
return lineIntersectsCurve(_start, _end, from, cp1, cp2, to)
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Returns the string you pass with with the first character converted to uppercase
|
|
|
|
*
|
|
|
|
* @param {string} string - The string to capitalize
|
|
|
|
* @return {string} capitalized - The capitalized string
|
|
|
|
*/
|
|
|
|
export function capitalize(string) {
|
|
|
|
return string.charAt(0).toUpperCase() + string.slice(1)
|
2018-08-21 13:49:12 +02:00
|
|
|
}
|
2018-08-23 15:57:23 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Find the intersections between two circles
|
|
|
|
*
|
|
|
|
* @param {Point} c1 - The center Point of the first circle
|
|
|
|
* @param {float} r1 - The radius of the first circle
|
|
|
|
* @param {Point} c2 - The center Point of the second circle
|
|
|
|
* @param {float} r2 - The radius of the second circle
|
|
|
|
* @param {string} sort - Controls the sort of the resulting intersections
|
|
|
|
* @return {Array} intersections - An array with Point objects for the intersections
|
|
|
|
*/
|
2019-08-03 15:03:33 +02:00
|
|
|
export function circlesIntersect(c1, r1, c2, r2, sort = 'x') {
|
|
|
|
let dx = c1.dx(c2)
|
|
|
|
let dy = c1.dy(c2)
|
|
|
|
let dist = c1.dist(c2)
|
2018-08-23 15:57:23 +02:00
|
|
|
// Check for edge cases
|
2019-08-03 15:03:33 +02:00
|
|
|
if (dist > parseFloat(r1) + parseFloat(r2)) return false // Circles do not intersect
|
|
|
|
if (dist < parseFloat(r2) - parseFloat(r1)) return false // One circle is contained in the other
|
|
|
|
if (dist === 0 && r1 === r2) return false // Two circles are identical
|
|
|
|
let chorddistance = (Math.pow(r1, 2) - Math.pow(r2, 2) + Math.pow(dist, 2)) / (2 * dist)
|
|
|
|
let halfchordlength = Math.sqrt(Math.pow(r1, 2) - Math.pow(chorddistance, 2))
|
|
|
|
let chordmidpointx = c1.x + (chorddistance * dx) / dist
|
|
|
|
let chordmidpointy = c1.y + (chorddistance * dy) / dist
|
2018-08-23 15:57:23 +02:00
|
|
|
let i1 = new Point(
|
|
|
|
chordmidpointx + (halfchordlength * dy) / dist,
|
|
|
|
chordmidpointy - (halfchordlength * dx) / dist
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2018-08-23 15:57:23 +02:00
|
|
|
let i2 = new Point(
|
|
|
|
chordmidpointx - (halfchordlength * dy) / dist,
|
|
|
|
chordmidpointy + (halfchordlength * dx) / dist
|
2019-08-03 15:03:33 +02:00
|
|
|
)
|
2018-08-23 15:57:23 +02:00
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2]
|
|
|
|
else return [i2, i1]
|
2018-08-23 15:57:23 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Finds the edge of a cubic Bezier curve
|
|
|
|
*
|
|
|
|
* @param {BezierJs} curve - A BezierJs curve instance
|
|
|
|
* @param {string} edge - The edge to find: top, bottom, right, or left
|
|
|
|
* @param {int} steps - The number of steps to divide the curve in while walking it
|
2022-12-07 18:23:02 -08:00
|
|
|
* @return {Point} edgepoint - A Point object located on the edge of the curve. Returns the first point found, if more than one lies on the edge.
|
2022-09-18 15:11:10 +02:00
|
|
|
*/
|
2018-09-03 12:07:02 +02:00
|
|
|
export function curveEdge(curve, edge, steps = 500) {
|
2019-08-03 15:03:33 +02:00
|
|
|
let x = Infinity
|
|
|
|
let y = Infinity
|
|
|
|
let p
|
|
|
|
if (edge === 'bottom') y = -Infinity
|
|
|
|
if (edge === 'right') x = -Infinity
|
2018-09-03 12:07:02 +02:00
|
|
|
for (let i = 0; i < steps; i++) {
|
2019-08-03 15:03:33 +02:00
|
|
|
p = curve.get(i / steps)
|
2018-09-03 12:07:02 +02:00
|
|
|
if (
|
2019-08-03 15:03:33 +02:00
|
|
|
(edge === 'top' && p.y < y) ||
|
|
|
|
(edge === 'bottom' && p.y > y) ||
|
|
|
|
(edge === 'right' && p.x > x) ||
|
|
|
|
(edge === 'left' && p.x < x)
|
2018-09-03 12:07:02 +02:00
|
|
|
) {
|
2019-08-03 15:03:33 +02:00
|
|
|
x = p.x
|
|
|
|
y = p.y
|
2018-09-03 12:07:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:03:33 +02:00
|
|
|
return new Point(x, y)
|
2018-09-03 12:07:02 +02:00
|
|
|
}
|
|
|
|
|
2018-08-31 09:44:12 +02:00
|
|
|
/**
|
2022-09-18 15:11:10 +02:00
|
|
|
* Find where a curve intersections with a given X-value
|
2018-08-31 09:44:12 +02:00
|
|
|
*
|
2022-09-18 15:11:10 +02:00
|
|
|
* @param {Point} from - Start Point of the curve
|
|
|
|
* @param {Point} cp1 - Control Point at the start of the curve
|
|
|
|
* @param {Point} cp2 - Control Point at the end of the curve
|
|
|
|
* @param {Point} to - End Point of the curve
|
|
|
|
* @param {float} x - X-value to check for intersections
|
2022-12-07 18:23:02 -08:00
|
|
|
* @return {Array} intersections - An Array of Point objects of all intersections
|
2018-08-31 09:44:12 +02:00
|
|
|
*/
|
2022-09-18 15:11:10 +02:00
|
|
|
export function curveIntersectsX(from, cp1, cp2, to, x) {
|
|
|
|
let start = new Point(x, -10000)
|
|
|
|
let end = new Point(x, 10000)
|
|
|
|
return lineIntersectsCurve(start, end, from, cp1, cp2, to)
|
2018-09-22 10:41:51 +02:00
|
|
|
}
|
2019-02-17 21:40:26 +01:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Find where a curve intersections with a given Y-value
|
|
|
|
*
|
|
|
|
* @param {Point} from - Start Point of the curve
|
|
|
|
* @param {Point} cp1 - Control Point at the start of the curve
|
|
|
|
* @param {Point} cp2 - Control Point at the end of the curve
|
|
|
|
* @param {Point} to - End Point of the curve
|
|
|
|
* @param {float} y - Y-value to check for intersections
|
2022-12-07 18:23:02 -08:00
|
|
|
* @return {Array} intersections - An Array of Point objects of all intersections
|
2022-09-18 15:11:10 +02:00
|
|
|
*/
|
|
|
|
export function curveIntersectsY(from, cp1, cp2, to, y) {
|
|
|
|
let start = new Point(-10000, y)
|
|
|
|
let end = new Point(10000, y)
|
|
|
|
return lineIntersectsCurve(start, end, from, cp1, cp2, to)
|
2019-02-17 21:40:26 +01:00
|
|
|
}
|
2019-07-11 16:49:02 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Find where a curve intersections with another curve
|
|
|
|
*
|
|
|
|
* @param {Point} fromA - Start Point of the first curve
|
|
|
|
* @param {Point} cp1A - Control Point at the start of the first curve
|
|
|
|
* @param {Point} cp2A - Control Point at the end of the first curve
|
|
|
|
* @param {Point} toA - End Point of the first curve
|
|
|
|
* @param {Point} fromB - Start Point of the second curve
|
|
|
|
* @param {Point} cp1B - Control Point at the start of the second curve
|
|
|
|
* @param {Point} cp2B - Control Point at the end of the second curve
|
|
|
|
* @param {Point} toB - End Point of the fsecond curve
|
2023-01-04 16:32:49 -08:00
|
|
|
* @return {Array} intersections - An Array of Point objects of all intersections between the curves, when there are more than 1 intersection
|
|
|
|
* @return {Point} intersection - A Point object of the intersection when there is exactly 1 intersection
|
|
|
|
* @return {Boolean} - false when there are no intersections
|
2022-09-18 15:11:10 +02:00
|
|
|
*/
|
|
|
|
export function curvesIntersect(fromA, cp1A, cp2A, toA, fromB, cp1B, cp2B, toB) {
|
|
|
|
let precision = 0.005 // See https://github.com/Pomax/bezierjs/issues/99
|
|
|
|
let intersections = []
|
|
|
|
let curveA = new Bezier(
|
|
|
|
{ x: fromA.x, y: fromA.y },
|
|
|
|
{ x: cp1A.x, y: cp1A.y },
|
|
|
|
{ x: cp2A.x, y: cp2A.y },
|
|
|
|
{ x: toA.x, y: toA.y }
|
|
|
|
)
|
|
|
|
let curveB = new Bezier(
|
|
|
|
{ x: fromB.x, y: fromB.y },
|
|
|
|
{ x: cp1B.x, y: cp1B.y },
|
|
|
|
{ x: cp2B.x, y: cp2B.y },
|
|
|
|
{ x: toB.x, y: toB.y }
|
|
|
|
)
|
2020-04-18 11:38:08 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
for (let tvalues of curveA.intersects(curveB, precision)) {
|
|
|
|
let intersection = curveA.get(tvalues.substr(0, tvalues.indexOf('/')))
|
|
|
|
intersections.push(new Point(intersection.x, intersection.y))
|
|
|
|
}
|
2021-09-15 20:20:59 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
if (intersections.length === 0) return false
|
|
|
|
else if (intersections.length === 1) return intersections.shift()
|
|
|
|
else {
|
|
|
|
let unique = []
|
|
|
|
for (let i of intersections) {
|
|
|
|
let dupe = false
|
|
|
|
for (let u of unique) {
|
|
|
|
if (i.sitsRoughlyOn(u)) dupe = true
|
|
|
|
}
|
|
|
|
if (!dupe) unique.push(i)
|
|
|
|
}
|
2023-01-06 11:14:45 -08:00
|
|
|
return unique.length === 1 ? unique.shift() : unique
|
2021-09-15 20:20:59 +02:00
|
|
|
}
|
|
|
|
}
|
2022-07-02 20:05:31 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Converts degrees to radians
|
|
|
|
*
|
|
|
|
* @param {float} degrees - The degrees to convert
|
|
|
|
* @return {float} radians - The provided degrees in radians
|
|
|
|
*/
|
|
|
|
export function deg2rad(degrees) {
|
|
|
|
return degrees * (Math.PI / 180)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates the transform attributes needed for a given stack
|
|
|
|
*
|
|
|
|
* @param {float} x - The translate value along the X-axis
|
|
|
|
* @param {float} y - The translate value along the Y-axis
|
|
|
|
* @param {float} rotate - The rotation
|
|
|
|
* @param {bool} flipX - Whether or not to flip/mirror along the X-axis
|
|
|
|
* @param {bool} flipY - Whether or not to flip/mirror along the Y-axis
|
|
|
|
* @param {Stack} stack - The Stack instance
|
2023-04-28 15:46:33 -04:00
|
|
|
* @return {String[]} transform - An array of SVG transform values
|
2022-09-18 15:11:10 +02:00
|
|
|
*/
|
2023-04-28 15:46:33 -04:00
|
|
|
export function generateStackTransform(
|
2022-10-07 21:41:45 +02:00
|
|
|
x = 0,
|
|
|
|
y = 0,
|
|
|
|
rotate = 0,
|
|
|
|
flipX = false,
|
|
|
|
flipY = false,
|
|
|
|
stack
|
2023-04-28 15:46:33 -04:00
|
|
|
) {
|
2022-08-09 16:16:06 -05:00
|
|
|
const transforms = []
|
2022-09-09 20:20:38 +02:00
|
|
|
let xTotal = x || 0
|
|
|
|
let yTotal = y || 0
|
2022-08-09 16:16:06 -05:00
|
|
|
let scaleX = 1
|
|
|
|
let scaleY = 1
|
|
|
|
|
2022-08-14 16:50:16 -05:00
|
|
|
// move the part an additional offset so it ends up in the correct spot after flipping.
|
|
|
|
// it will scale around the part's 0, 0, which isn't always the top left, so we need to move it over so that 0,0 lines up with topRight + topLeft
|
2022-08-09 16:16:06 -05:00
|
|
|
if (flipX) {
|
2022-09-18 15:11:10 +02:00
|
|
|
xTotal += stack.topLeft.x
|
|
|
|
xTotal += stack.bottomRight.x
|
2022-08-14 16:50:16 -05:00
|
|
|
// reverse the x scale
|
2022-08-09 16:16:06 -05:00
|
|
|
scaleX = -1
|
|
|
|
}
|
|
|
|
if (flipY) {
|
2022-09-18 15:11:10 +02:00
|
|
|
yTotal += stack.topLeft.y
|
|
|
|
yTotal += stack.bottomRight.y
|
2022-08-09 16:16:06 -05:00
|
|
|
scaleY = -1
|
2022-07-02 20:05:31 +02:00
|
|
|
}
|
|
|
|
|
2022-08-14 16:50:16 -05:00
|
|
|
// add the scaling to the transforms
|
2022-08-09 16:16:06 -05:00
|
|
|
if (scaleX + scaleY < 2) {
|
2023-04-18 18:47:49 -04:00
|
|
|
transforms.push(`scale(${scaleX}, ${scaleY})`)
|
2022-08-09 16:16:06 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (rotate) {
|
2022-08-14 16:50:16 -05:00
|
|
|
// we can put the center as the rotation origin, so get the center
|
2022-08-09 16:16:06 -05:00
|
|
|
const center = {
|
2022-09-18 15:11:10 +02:00
|
|
|
x: stack.topLeft.x + stack.width / 2,
|
|
|
|
y: stack.topLeft.y + stack.height / 2,
|
2022-08-09 16:16:06 -05:00
|
|
|
}
|
|
|
|
|
2022-08-14 16:50:16 -05:00
|
|
|
// add the rotation around the center to the transforms
|
2023-04-18 18:47:49 -04:00
|
|
|
transforms.push(`rotate(${rotate}, ${center.x}, ${center.y})`)
|
2022-08-09 16:16:06 -05:00
|
|
|
}
|
|
|
|
|
2022-08-14 16:50:16 -05:00
|
|
|
// put the translation before any other transforms to avoid having to make complex calculations once the matrix has been rotated or scaled
|
2023-04-18 18:47:49 -04:00
|
|
|
if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal}, ${yTotal})`)
|
2022-07-02 20:05:31 +02:00
|
|
|
|
2023-04-28 15:46:33 -04:00
|
|
|
return transforms
|
2022-07-02 20:05:31 +02:00
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Find the intersections between a line segment and a circle
|
|
|
|
*
|
|
|
|
* @param {Point} c - The center Point of the circle
|
|
|
|
* @param {float} r - The radius of the circle
|
|
|
|
* @param {Point} p1 - Start Point of the line segment
|
|
|
|
* @param {Point} p2 - End Point of the line segment
|
|
|
|
* @param {string} sort - Controls the sort of the resulting intersections
|
|
|
|
* @return {Array} intersections - An array with Point objects for the intersections
|
|
|
|
*/
|
|
|
|
export function lineIntersectsCircle(c, r, p1, p2, sort = 'x') {
|
|
|
|
let intersections = beamIntersectsCircle(c, r, p1, p2, sort)
|
|
|
|
if (intersections === false) return false
|
|
|
|
else {
|
|
|
|
if (intersections.length === 1) {
|
|
|
|
if (pointOnLine(p1, p2, intersections[0])) return intersections
|
|
|
|
else return false
|
|
|
|
} else {
|
|
|
|
let i1 = intersections[0]
|
|
|
|
let i2 = intersections[1]
|
|
|
|
if (!pointOnLine(p1, p2, i1, 5) && !pointOnLine(p1, p2, i2, 5)) return false
|
|
|
|
else if (pointOnLine(p1, p2, i1, 5) && pointOnLine(p1, p2, i2, 5)) {
|
|
|
|
if ((sort === 'x' && i1.x <= i2.x) || (sort === 'y' && i1.y <= i2.y)) return [i1, i2]
|
|
|
|
else return [i2, i1]
|
|
|
|
} else if (pointOnLine(p1, p2, i1, 5)) return [i1]
|
|
|
|
else if (pointOnLine(p1, p2, i2, 5)) return [i2]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the intersection of two line segments
|
|
|
|
*
|
|
|
|
* @param {Point} a1 - Point 1 of line A
|
|
|
|
* @param {Point} a2 - Point 2 of line A
|
|
|
|
* @param {Point} b1 - Point 1 of line B
|
|
|
|
* @param {Point} b2 - Point 2 of line B
|
|
|
|
* @return {Point} intersection - The Point at the intersection
|
|
|
|
*/
|
|
|
|
export function linesIntersect(a1, a2, b1, b2) {
|
|
|
|
let p = beamsIntersect(a1, a2, b1, b2)
|
|
|
|
if (!p) return false
|
|
|
|
let lenA = a1.dist(a2)
|
|
|
|
let lenB = b1.dist(b2)
|
|
|
|
let lenC = a1.dist(p) + p.dist(a2)
|
|
|
|
let lenD = b1.dist(p) + p.dist(b2)
|
|
|
|
if (Math.round(lenA) == Math.round(lenC) && Math.round(lenB) == Math.round(lenD)) return p
|
|
|
|
else return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds the intersections of a line and a curve
|
|
|
|
*
|
|
|
|
* @param {Point} start - Start Point of the line
|
|
|
|
* @param {Point} end - End Point of the line
|
|
|
|
* @param {Point} from - Start Point of the curve
|
|
|
|
* @param {Point} cp1 - Control Point at the start of the curve
|
|
|
|
* @param {Point} cp2 - Control Point at the end of the curve
|
|
|
|
* @param {Point} to - End Point of the curve
|
|
|
|
* @return {Array} intersections - An array of Points at the intersections
|
|
|
|
*/
|
|
|
|
export function lineIntersectsCurve(start, end, from, cp1, cp2, to) {
|
|
|
|
let intersections = []
|
|
|
|
let bz = new Bezier(
|
|
|
|
{ x: from.x, y: from.y },
|
|
|
|
{ x: cp1.x, y: cp1.y },
|
|
|
|
{ x: cp2.x, y: cp2.y },
|
|
|
|
{ x: to.x, y: to.y }
|
|
|
|
)
|
|
|
|
let line = {
|
|
|
|
p1: { x: start.x, y: start.y },
|
|
|
|
p2: { x: end.x, y: end.y },
|
|
|
|
}
|
|
|
|
for (let t of bz.intersects(line)) {
|
|
|
|
let isect = bz.get(t)
|
|
|
|
intersections.push(new Point(isect.x, isect.y))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (intersections.length === 0) return false
|
|
|
|
else if (intersections.length === 1) return intersections[0]
|
|
|
|
else return intersections
|
|
|
|
}
|
|
|
|
|
2023-06-09 20:44:19 +02:00
|
|
|
/**
|
|
|
|
* Helper method to merge translation files from different designs
|
|
|
|
*
|
2023-07-02 13:50:46 +02:00
|
|
|
* @param {array} designs - One or more translation objects for designs
|
|
|
|
* @param {object} options - Configuration object for how to merge these designs
|
2023-06-09 20:44:19 +02:00
|
|
|
* @return {object} result - A merged object of translations
|
|
|
|
*/
|
|
|
|
export function mergeI18n(designs, options) {
|
|
|
|
const i18n = {}
|
|
|
|
for (const design of designs) {
|
|
|
|
for (const lang in design) {
|
|
|
|
const obj = design[lang]
|
|
|
|
if (typeof i18n[lang] === 'undefined') i18n[lang] = {}
|
|
|
|
if (obj.t) i18n[lang].t = obj.t
|
|
|
|
if (obj.d) i18n[lang].d = obj.d
|
|
|
|
for (const section of 'spo') {
|
|
|
|
if (obj[section]) {
|
|
|
|
if (typeof i18n[lang][section] === 'undefined') i18n[lang][section] = {}
|
|
|
|
for (const [key, val] of Object.entries(obj[section])) {
|
2023-06-17 20:15:33 +02:00
|
|
|
if (__keepTranslation(key, options?.[section])) i18n[lang][section][key] = val
|
2023-06-09 20:44:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return i18n
|
|
|
|
}
|
|
|
|
|
2023-06-11 14:10:17 +02:00
|
|
|
/**
|
|
|
|
* Helper method to merge passed in options with default options from the pattern config
|
|
|
|
*
|
|
|
|
* @param {object} settings - The settings passed to the pattern
|
|
|
|
* @param {object} optionsConfig - The pattern's options config
|
|
|
|
* @return {object} result - An object with the merged options and their values
|
|
|
|
*/
|
|
|
|
export function mergeOptions(settings, optionsConfig) {
|
|
|
|
const merged = typeof settings.options === 'undefined' ? {} : { ...settings.option }
|
|
|
|
for (const [key, option] of Object.entries(optionsConfig)) {
|
|
|
|
if (typeof option === 'object') {
|
|
|
|
if (typeof option.pct !== 'undefined') merged[key] = option.pct / 100
|
|
|
|
else if (typeof option.mm !== 'undefined') merged[key] = option.mm
|
|
|
|
else if (typeof option.deg !== 'undefined') merged[key] = option.deg
|
|
|
|
else if (typeof option.count !== 'undefined') merged[key] = option.count
|
|
|
|
else if (typeof option.bool !== 'undefined') merged[key] = option.bool
|
|
|
|
else if (typeof option.dflt !== 'undefined') merged[key] = option.dflt
|
|
|
|
} else merged[key] = option
|
|
|
|
}
|
|
|
|
|
|
|
|
return merged
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Helper method to calculate abolute option value based on a measurement
|
|
|
|
*
|
|
|
|
* @param {string} measurement - The measurement to base the calculation on
|
|
|
|
* @return {object} result - An object with the toAbs() and fromAbs() methods
|
|
|
|
*/
|
|
|
|
export function pctBasedOn(measurement) {
|
|
|
|
return {
|
|
|
|
toAbs: (val, { measurements }) => measurements[measurement] * val,
|
|
|
|
fromAbs: (val, { measurements }) =>
|
|
|
|
Math.round((10000 * val) / measurements[measurement]) / 10000,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds out whether a Point lies on an endless line (beam)
|
|
|
|
*
|
|
|
|
* @param {Point} from - First Point on the line
|
|
|
|
* @param {Point} to - Second Point on the line
|
|
|
|
* @param {Point} check - Point to check
|
|
|
|
* @param {float} preciesion - How precise we should check
|
|
|
|
* @return {bool} result - True of the Point is on the line, false when not
|
|
|
|
*/
|
|
|
|
export function pointOnBeam(from, to, check, precision = 1e6) {
|
|
|
|
if (from.sitsOn(check)) return true
|
|
|
|
if (to.sitsOn(check)) return true
|
|
|
|
let cross = check.dx(from) * to.dy(from) - check.dy(from) * to.dx(from)
|
2022-08-15 16:01:42 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
if (Math.abs(Math.round(cross * precision) / precision) === 0) return true
|
|
|
|
else return false
|
|
|
|
}
|
2022-08-15 16:01:42 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Finds out whether a Point lies on a (cubic) Bezier curve
|
|
|
|
*
|
|
|
|
* @param {Point} from - Start of the curve
|
|
|
|
* @param {Point} cp1 - Control point at the start of the curve
|
|
|
|
* @param {Point} cp1 - Control point at the end of the curve
|
|
|
|
* @param {Point} end - End of the curve
|
|
|
|
* @param {Point} check - Point to check
|
|
|
|
* @return {bool} result - True of the Point is on the curve, false when not
|
|
|
|
*/
|
|
|
|
export function pointOnCurve(start, cp1, cp2, end, check) {
|
|
|
|
if (start.sitsOn(check)) return true
|
|
|
|
if (end.sitsOn(check)) return true
|
|
|
|
let curve = new Bezier(
|
|
|
|
{ x: start.x, y: start.y },
|
|
|
|
{ x: cp1.x, y: cp1.y },
|
|
|
|
{ x: cp2.x, y: cp2.y },
|
|
|
|
{ x: end.x, y: end.y }
|
|
|
|
)
|
|
|
|
let intersections = curve.intersects({
|
|
|
|
p1: { x: check.x - 1, y: check.y },
|
|
|
|
p2: { x: check.x + 1, y: check.y },
|
|
|
|
})
|
|
|
|
if (intersections.length === 0) {
|
|
|
|
// Handle edge case of a curve that's a perfect horizontal line
|
|
|
|
intersections = curve.intersects({
|
|
|
|
p1: { x: check.x, y: check.y - 1 },
|
|
|
|
p2: { x: check.x, y: check.y + 1 },
|
|
|
|
})
|
2022-09-09 20:20:38 +02:00
|
|
|
}
|
2022-08-15 16:01:42 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
if (intersections.length > 0) return intersections.shift()
|
|
|
|
else return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds out whether a Point lies on a line segment
|
|
|
|
*
|
|
|
|
* @param {Point} from - Start of the line segment
|
|
|
|
* @param {Point} to - End of the line segment
|
|
|
|
* @param {Point} check - Point to check
|
|
|
|
* @param {float} preciesion - How precise we should check
|
|
|
|
* @return {bool} result - True of the Point is on the line segment, false when not
|
|
|
|
*/
|
|
|
|
export function pointOnLine(from, to, check, precision = 1e6) {
|
|
|
|
if (!pointOnBeam(from, to, check, precision)) return false
|
|
|
|
let lenA = from.dist(to)
|
|
|
|
let lenB = from.dist(check) + check.dist(to)
|
|
|
|
if (Math.round(lenA) == Math.round(lenB)) return true
|
|
|
|
else return false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts radians to degrees
|
|
|
|
*
|
|
|
|
* @param {float} radians - The radiand to convert
|
|
|
|
* @return {float} degrees - The provided radians in degrees
|
|
|
|
*/
|
|
|
|
export function rad2deg(radians) {
|
|
|
|
return (radians / Math.PI) * 180
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rounds a value to 2 digits
|
|
|
|
*
|
|
|
|
* @param {float} value - The value to round
|
|
|
|
* @return {float} rounded - The rounded value
|
|
|
|
*/
|
|
|
|
export function round(value) {
|
|
|
|
return Math.round(value * 1e2) / 1e2
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Splits curve on a Point
|
|
|
|
*
|
|
|
|
* @param {Point} from - Start of the curve
|
|
|
|
* @param {Point} cp1 - Control point at the start of the curve
|
|
|
|
* @param {Point} cp1 - Control point at the end of the curve
|
|
|
|
* @param {Point} end - End of the curve
|
|
|
|
* @param {Point} split - Point to split the curve on
|
|
|
|
* @return {Array} halves - An array with the two halves of the Path
|
|
|
|
*/
|
|
|
|
export function splitCurve(start, cp1, cp2, end, split) {
|
|
|
|
let [c1, c2] = new Path().move(start).curve(cp1, cp2, end).split(split)
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
|
|
|
start: c1.ops[0].to,
|
|
|
|
cp1: c1.ops[1].cp1,
|
|
|
|
cp2: c1.ops[1].cp2,
|
|
|
|
end: c1.ops[1].to,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
start: c2.ops[0].to,
|
|
|
|
cp1: c2.ops[1].cp1,
|
|
|
|
cp2: c2.ops[1].cp2,
|
|
|
|
end: c2.ops[1].to,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates scale factor based on stretch factor
|
|
|
|
*
|
|
|
|
* The way people measure stretch intuitively is
|
|
|
|
* different from the way we handle stretch in code.
|
|
|
|
* When people say '25% stretch' they mean that
|
|
|
|
* 10cm fabric should get stretched to 12.5cm fabric.
|
|
|
|
* In our code, that means we need to scale things by 80%.
|
|
|
|
* This method does that calculation.
|
|
|
|
*
|
|
|
|
* @param {float} stretch - Strech factor
|
|
|
|
* @return {float} scale - The scale for the provided stretch factor
|
|
|
|
*/
|
|
|
|
export function stretchToScale(stretch) {
|
|
|
|
return 1 / (1 + parseFloat(stretch))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert value in mm to cm or imperial units
|
|
|
|
*
|
|
|
|
* @param {float} value - Value in millimeter
|
|
|
|
* @param {astring} to - Either 'metric' or 'imperial'
|
|
|
|
* @return {string} formatted - The value formatted according to the units
|
|
|
|
*/
|
|
|
|
export function units(value, to = 'metric') {
|
|
|
|
if (to === 'imperial') return round(value / 25.4) + '"'
|
|
|
|
else return round(value / 10) + 'cm'
|
2022-09-09 20:20:38 +02:00
|
|
|
}
|
2022-08-15 16:01:42 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
//////////////////////////////////////////////
|
|
|
|
// PRIVATE METHODS //
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a non-enumerable property to an object
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Object} obj - The object to add the property to
|
|
|
|
* @param {string} name - The name of the property
|
|
|
|
* @param {mixed} value - The value of the property
|
|
|
|
* @return {object} obj - The mutated object
|
|
|
|
*/
|
|
|
|
export function __addNonEnumProp(obj, name, value) {
|
2022-09-09 20:20:38 +02:00
|
|
|
Object.defineProperty(obj, name, {
|
|
|
|
enumerable: false,
|
|
|
|
configurable: false,
|
|
|
|
writable: true,
|
|
|
|
value,
|
2022-08-15 16:01:42 +02:00
|
|
|
})
|
2022-08-13 15:11:33 +02:00
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
return obj
|
2022-08-13 15:11:33 +02:00
|
|
|
}
|
|
|
|
|
2022-10-07 21:41:45 +02:00
|
|
|
/**
|
|
|
|
* Makes sure a passed argument is a number if it can be cast
|
|
|
|
* Will log warnings/errors accordingly
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {mixed} value - The value to check
|
|
|
|
* @param {string} param - The name of the parameter to use in the logs
|
|
|
|
* @param {string} method - The name of the method to use in the logs
|
|
|
|
* @param {object} log - A logging object
|
|
|
|
* @return {bool} result - True if it is a valid coordinate, false when not
|
|
|
|
*/
|
|
|
|
export function __asNumber(value, param, method, log) {
|
|
|
|
if (typeof value === 'number') return value
|
|
|
|
if (typeof value === 'string') {
|
2023-09-05 12:00:05 +02:00
|
|
|
log.warn(
|
2022-10-07 21:41:45 +02:00
|
|
|
`Called \`${method}(${param})\` but \`${param}\` is not a number. Will attempt to cast to Number`
|
|
|
|
)
|
|
|
|
try {
|
|
|
|
value = Number(value)
|
|
|
|
return value
|
|
|
|
} catch {
|
2022-11-15 14:50:14 -06:00
|
|
|
log.error(
|
2022-10-07 21:41:45 +02:00
|
|
|
`Called \`${method}(${param})\` but \`${param}\` is not a number nor can it be cast to one`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else log.error(`Called \`${method}(${param})\` but \`${param}\` is not a number`)
|
|
|
|
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
2022-09-18 15:11:10 +02:00
|
|
|
/**
|
|
|
|
* Checks whether the paramater passed to it is a valid coordinate (x and y attribute)
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {object} value - The object to check
|
|
|
|
* @return {bool} result - True if it is a valid coordinate, false when not
|
|
|
|
*/
|
|
|
|
export function __isCoord(value) {
|
|
|
|
return value === value // NaN does not equal itself
|
|
|
|
? typeof value === 'number'
|
|
|
|
: false
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the internal hook name for a macro
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} name - The macro name
|
|
|
|
* @return {string} macroName - The inernal macroName
|
|
|
|
*/
|
|
|
|
export function __macroName(name) {
|
2023-09-05 20:35:31 +02:00
|
|
|
return `__macro_${name.toLowerCase()}`
|
2022-09-18 15:11:10 +02:00
|
|
|
}
|
2023-04-06 09:50:55 +02:00
|
|
|
|
2023-06-09 20:44:19 +02:00
|
|
|
/**
|
|
|
|
* Returns true if we want to keep the translation
|
|
|
|
* Called by mergeI18n
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} key - The translation key
|
|
|
|
* @param {object} options - The options (for this particular section of the translation file)
|
|
|
|
* @return {bool} result - Whether or not to keep the translation
|
|
|
|
*/
|
|
|
|
function __keepTranslation(key, options) {
|
|
|
|
// Drop it?
|
2023-07-02 13:50:46 +02:00
|
|
|
if (options?.drop && options.drop.includes(key)) return false
|
2023-06-09 20:44:19 +02:00
|
|
|
// Keep only some and not this one?
|
2023-07-02 13:50:46 +02:00
|
|
|
if (options?.keep && !options.keep.includes(key)) return false
|
2023-06-09 20:44:19 +02:00
|
|
|
|
|
|
|
// Keep it
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-04-06 09:50:55 +02:00
|
|
|
/**
|
|
|
|
* Helper method to parse an (SVG) transform string
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} transform - The SVG transform string
|
|
|
|
* @return {object} result - An object with the parts, name, and values
|
|
|
|
*/
|
|
|
|
function __parseTransform(transform) {
|
2023-04-14 15:48:16 -04:00
|
|
|
const parts = transform.match(/(\w+)\(([^)]+)\)/)
|
2023-04-06 09:50:55 +02:00
|
|
|
const name = parts[1]
|
|
|
|
const values = parts[2].split(/,\s*/).map(parseFloat)
|
|
|
|
|
|
|
|
return { parts, name, values }
|
|
|
|
}
|
|
|
|
|
2023-04-28 15:46:33 -04:00
|
|
|
/**
|
|
|
|
* Applies a transformation of the given type to the matrix
|
|
|
|
* @param {String} transformationType the transformation type (tranlate, rotate, scale, skew, etc)
|
|
|
|
* @param {Number[]} matrix the matrix to apply the transform to
|
|
|
|
* @param {Number[]} values the transformation values to apply
|
|
|
|
* @return {Number[]} the transformed matrix
|
|
|
|
*/
|
|
|
|
function matrixTransform(transformationType, matrix, values) {
|
|
|
|
// Update matrix for transform
|
|
|
|
switch (transformationType) {
|
|
|
|
case 'matrix':
|
|
|
|
matrix = [
|
|
|
|
matrix[0] * values[0] + matrix[2] * values[1],
|
|
|
|
matrix[1] * values[0] + matrix[3] * values[1],
|
|
|
|
matrix[0] * values[2] + matrix[2] * values[3],
|
|
|
|
matrix[1] * values[2] + matrix[3] * values[3],
|
|
|
|
matrix[0] * values[4] + matrix[2] * values[5] + matrix[4],
|
|
|
|
matrix[1] * values[4] + matrix[3] * values[5] + matrix[5],
|
|
|
|
]
|
|
|
|
break
|
|
|
|
case 'translate':
|
|
|
|
matrix[4] += matrix[0] * values[0] + matrix[2] * values[1]
|
|
|
|
matrix[5] += matrix[1] * values[0] + matrix[3] * values[1]
|
|
|
|
break
|
|
|
|
case 'scale':
|
|
|
|
matrix[0] *= values[0]
|
|
|
|
matrix[1] *= values[0]
|
|
|
|
matrix[2] *= values[1]
|
|
|
|
matrix[3] *= values[1]
|
|
|
|
break
|
|
|
|
case 'rotate': {
|
|
|
|
const angle = (values[0] * Math.PI) / 180
|
|
|
|
const centerX = values[1]
|
|
|
|
const centerY = values[2]
|
|
|
|
|
|
|
|
// if there's a rotation center, we need to move the origin to that center
|
2023-08-29 00:32:22 +02:00
|
|
|
if (centerX !== undefined) {
|
2023-04-28 15:46:33 -04:00
|
|
|
matrix = matrixTransform('translate', matrix, [centerX, centerY])
|
|
|
|
}
|
|
|
|
|
|
|
|
// rotate
|
|
|
|
const cos = Math.cos(angle)
|
|
|
|
const sin = Math.sin(angle)
|
|
|
|
matrix = [
|
|
|
|
matrix[0] * cos + matrix[2] * sin,
|
|
|
|
matrix[1] * cos + matrix[3] * sin,
|
|
|
|
matrix[0] * -sin + matrix[2] * cos,
|
|
|
|
matrix[1] * -sin + matrix[3] * cos,
|
|
|
|
matrix[4],
|
|
|
|
matrix[5],
|
|
|
|
]
|
|
|
|
|
|
|
|
// move the origin back to origin
|
2023-08-29 00:32:22 +02:00
|
|
|
if (centerX !== undefined) {
|
2023-04-28 15:46:33 -04:00
|
|
|
matrix = matrixTransform('translate', matrix, [-centerX, -centerY])
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
case 'skewX':
|
|
|
|
matrix[2] += matrix[0] * Math.tan((values[0] * Math.PI) / 180)
|
|
|
|
matrix[3] += matrix[1] * Math.tan((values[0] * Math.PI) / 180)
|
|
|
|
break
|
|
|
|
case 'skewY':
|
|
|
|
matrix[0] += matrix[2] * Math.tan((values[0] * Math.PI) / 180)
|
|
|
|
matrix[1] += matrix[3] * Math.tan((values[0] * Math.PI) / 180)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
return matrix
|
|
|
|
}
|
|
|
|
|
2023-04-06 09:50:55 +02:00
|
|
|
/**
|
|
|
|
* Combines an array of (SVG) transforms into a single matrix transform
|
|
|
|
*
|
|
|
|
* @param {array} transorms - The list of transforms to combine
|
|
|
|
* @return {string} matrixTransform - The combined matrix transform
|
|
|
|
*/
|
|
|
|
export function combineTransforms(transforms = []) {
|
|
|
|
// Don't bother if there are no part transforms
|
|
|
|
if (transforms.length < 1) return ''
|
|
|
|
|
|
|
|
// The starting matrix
|
|
|
|
let matrix = [1, 0, 0, 1, 0, 0]
|
|
|
|
|
|
|
|
// Loop through the transforms
|
|
|
|
for (let i = 0; i < transforms.length; i++) {
|
|
|
|
// Parse the transform string
|
2023-04-14 15:48:16 -04:00
|
|
|
const { name, values } = __parseTransform(transforms[i])
|
2023-04-28 15:46:33 -04:00
|
|
|
matrix = matrixTransform(name, matrix, values)
|
2023-04-06 09:50:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return the combined matrix transform
|
2023-04-14 15:43:26 -04:00
|
|
|
return 'matrix(' + matrix.join(', ') + ')'
|
2023-04-06 09:50:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Applies and (SVG) transform to a point's coordinates (x and y)
|
|
|
|
*
|
|
|
|
* @param {string} transorm - The transform to apply
|
|
|
|
* @param {Point} point - The point of which to update the coordinates
|
|
|
|
* @return {Point} point - The point with the transform applied to its coordinates
|
|
|
|
*/
|
|
|
|
export function applyTransformToPoint(transform, point) {
|
|
|
|
// Parse the transform string
|
2023-04-14 15:48:16 -04:00
|
|
|
const { name, values } = __parseTransform(transform)
|
2023-04-06 09:50:55 +02:00
|
|
|
|
|
|
|
// The starting matrix
|
|
|
|
let matrix = [1, 0, 0, 1, 0, 0]
|
2023-08-29 00:32:22 +02:00
|
|
|
matrix = matrixTransform(name, matrix, values)
|
2023-04-06 09:50:55 +02:00
|
|
|
|
|
|
|
// Apply the matrix transform to the coordinates
|
2023-04-14 15:43:26 -04:00
|
|
|
const newX = point.x * matrix[0] + point.y * matrix[2] + matrix[4]
|
|
|
|
const newY = point.x * matrix[1] + point.y * matrix[3] + matrix[5]
|
|
|
|
|
|
|
|
point.x = newX
|
|
|
|
point.y = newY
|
2023-04-06 09:50:55 +02:00
|
|
|
|
|
|
|
return point
|
|
|
|
}
|
2023-04-28 15:46:33 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the bounds of a given object after transforms have been applied
|
|
|
|
* @param {Object} boundsObj any object with `topLeft` and `bottomRight` properties
|
|
|
|
* @param {Boolean|String[]} transforms the transforms to apply to the bounds, structured as they would be for being applied as an svg attribute
|
|
|
|
* @return {Object} `tl` and `br` for the transformed bounds
|
|
|
|
*/
|
|
|
|
export function getTransformedBounds(boundsObj, transforms = false) {
|
|
|
|
if (!boundsObj.topLeft) return {}
|
|
|
|
// get all corners of the part's bounds
|
|
|
|
let tl = boundsObj.topLeft
|
|
|
|
let br = boundsObj.bottomRight
|
|
|
|
let tr = new Point(br.x, tl.y)
|
|
|
|
let bl = new Point(tl.x, br.y)
|
|
|
|
|
|
|
|
// if there are transforms on the part, apply them to the corners so that we have the correct bounds
|
|
|
|
if (transforms) {
|
|
|
|
const combinedTransform = combineTransforms(transforms)
|
|
|
|
|
|
|
|
tl = applyTransformToPoint(combinedTransform, tl.copy())
|
|
|
|
br = applyTransformToPoint(combinedTransform, br.copy())
|
|
|
|
tr = applyTransformToPoint(combinedTransform, tr.copy())
|
|
|
|
bl = applyTransformToPoint(combinedTransform, bl.copy())
|
|
|
|
}
|
|
|
|
|
|
|
|
// now get the top left and bottom right after transforms
|
|
|
|
const transformedTl = new Point(
|
|
|
|
Math.min(tl.x, br.x, bl.x, tr.x),
|
|
|
|
Math.min(tl.y, br.y, bl.y, tr.y)
|
|
|
|
)
|
|
|
|
|
|
|
|
const transformedBr = new Point(
|
|
|
|
Math.max(tl.x, br.x, bl.x, tr.x),
|
|
|
|
Math.max(tl.y, br.y, bl.y, tr.y)
|
|
|
|
)
|
|
|
|
|
|
|
|
return {
|
|
|
|
tl: transformedTl,
|
|
|
|
br: transformedBr,
|
|
|
|
}
|
|
|
|
}
|