diff --git a/packages/core/src/index.mjs b/packages/core/src/index.mjs index 38657c84c0d..fa5d133c7a5 100644 --- a/packages/core/src/index.mjs +++ b/packages/core/src/index.mjs @@ -21,6 +21,7 @@ import { curvesIntersect, deg2rad, generateStackTransform, + getTransformedBounds, lineIntersectsCircle, lineIntersectsCurve, linesIntersect, @@ -63,6 +64,7 @@ export { curvesIntersect, deg2rad, generateStackTransform, + getTransformedBounds, lineIntersectsCircle, lineIntersectsCurve, linesIntersect, diff --git a/packages/core/src/stack.mjs b/packages/core/src/stack.mjs index 1a60c7ab5c3..9238f4b7667 100644 --- a/packages/core/src/stack.mjs +++ b/packages/core/src/stack.mjs @@ -52,29 +52,18 @@ Stack.prototype.home = function () { for (const part of this.getPartList()) { part.__boundary() - // get all corners of the part's bounds - let tl = part.topLeft || this.topLeft - let br = part.bottomRight || this.bottomRight - let tr = new Point(br.x, tl.y) - let bl = new Point(tl.x, br.y) + const { tl, br } = utils.getTransformedBounds(part, part.attributes.getAsArray('transform')) - // if there are transforms on the part, apply them to the corners so that we have the correct bounds - const transforms = part.attributes.getAsArray('transform') - if (transforms) { - const combinedTransform = utils.combineTransforms(transforms) - - tl = utils.applyTransformToPoint(combinedTransform, tl.copy()) - br = utils.applyTransformToPoint(combinedTransform, br.copy()) - tr = utils.applyTransformToPoint(combinedTransform, tr.copy()) - bl = utils.applyTransformToPoint(combinedTransform, bl.copy()) + if (!tl) { + continue } // get the top left, the minimum x and y values of any corner - this.topLeft.x = Math.min(this.topLeft.x, tl.x, br.x, bl.x, tr.x) - this.topLeft.y = Math.min(this.topLeft.y, tl.y, br.y, bl.y, tr.y) + this.topLeft.x = Math.min(this.topLeft.x, tl.x) + this.topLeft.y = Math.min(this.topLeft.y, tl.y) // get the bottom right, the maximum x and y values of any corner - this.bottomRight.x = Math.max(this.bottomRight.x, tl.x, br.x, bl.x, tr.x) - this.bottomRight.y = Math.max(this.bottomRight.y, tl.y, br.y, bl.y, tr.y) + this.bottomRight.x = Math.max(this.bottomRight.x, br.x) + this.bottomRight.y = Math.max(this.bottomRight.y, br.y) } // Fix infinity if it's not overwritten @@ -142,14 +131,22 @@ Stack.prototype.attr = function (name, value, overwrite = false) { return this } -/** Generates the transform for a stack */ +/** + * Generates the transforms for a stack and sets them as attributes + * @param {Object} transforms a transform config object + * @param {Object} transforms.move x and y coordinates for how far to translate the stack + * @param {Number} transfroms.rotate the number of degrees to rotate the stack around its center + * @param {Boolean} tranforms.flipX whether to flip the stack along the X axis + * @param {Boolean} transforms.flipY whether to flip the stack along the Y axis + */ Stack.prototype.generateTransform = function (transforms) { const { move, rotate, flipX, flipY } = transforms const generated = utils.generateStackTransform(move?.x, move?.y, rotate, flipX, flipY, this) - for (var t in generated) { - this.attr(t, generated[t], true) - } + this.attributes.remove('transform') + generated.forEach((t) => this.attr('transform', t)) + + return this } export default Stack diff --git a/packages/core/src/utils.mjs b/packages/core/src/utils.mjs index aea6af3d686..5b8b68c0a20 100644 --- a/packages/core/src/utils.mjs +++ b/packages/core/src/utils.mjs @@ -288,16 +288,16 @@ export function deg2rad(degrees) { * @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 - * @return {string} transform - The SVG transform value + * @return {String[]} transform - An array of SVG transform values */ -export const generateStackTransform = ( +export function generateStackTransform( x = 0, y = 0, rotate = 0, flipX = false, flipY = false, stack -) => { +) { const transforms = [] let xTotal = x || 0 let yTotal = y || 0 @@ -337,10 +337,7 @@ export const generateStackTransform = ( // put the translation before any other transforms to avoid having to make complex calculations once the matrix has been rotated or scaled if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal}, ${yTotal})`) - return { - transform: transforms.join(' '), - // 'transform-origin': `${center.x} ${center.y}` - } + return transforms } /** @@ -681,6 +678,77 @@ function __parseTransform(transform) { return { parts, name, values } } +/** + * 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 + if (centerX) { + 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 + if (centerX) { + 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 +} + /** * Combines an array of (SVG) transforms into a single matrix transform * @@ -698,52 +766,7 @@ export function combineTransforms(transforms = []) { for (let i = 0; i < transforms.length; i++) { // Parse the transform string const { name, values } = __parseTransform(transforms[i]) - - // Update matrix for transform - switch (name) { - 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 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], - ] - 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 - } + matrix = matrixTransform(name, matrix, values) } // Return the combined matrix transform @@ -802,3 +825,44 @@ export function applyTransformToPoint(transform, point) { return point } + +/** + * 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, + } +} diff --git a/packages/core/tests/utils.test.mjs b/packages/core/tests/utils.test.mjs index bd0817f0ee6..729ef21522a 100644 --- a/packages/core/tests/utils.test.mjs +++ b/packages/core/tests/utils.test.mjs @@ -492,6 +492,6 @@ describe('Utils', () => { const pattern = new design() const props = pattern.draft().getRenderProps() const transform = generateStackTransform(30, 60, 90, true, true, props.stacks.test) - expect(transform.transform).to.equal('translate(51, 138) scale(-1, -1) rotate(90, 10.5, 39)') + expect(transform.join(' ')).to.equal('translate(51, 138) scale(-1, -1) rotate(90, 10.5, 39)') }) }) diff --git a/sites/shared/components/workbench/layout/draft/stack.mjs b/sites/shared/components/workbench/layout/draft/stack.mjs index d5b3469d98b..5e5f91de0df 100644 --- a/sites/shared/components/workbench/layout/draft/stack.mjs +++ b/sites/shared/components/workbench/layout/draft/stack.mjs @@ -44,7 +44,7 @@ * how custom layouts are supported in the core. And I would like to discuss this with the core team. */ import { useRef, useState, useEffect } from 'react' -import { generateStackTransform } from '@freesewing/core' +import { generateStackTransform, getTransformedBounds } from '@freesewing/core' import { Part } from '../../draft/part.mjs' import { getProps, angle } from '../../draft/utils.mjs' import { drag } from 'd3-drag' @@ -108,9 +108,9 @@ export const Stack = (props) => { const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack) const me = select(stackRef.current) - for (var t in transforms) { - me.attr(t, transforms[t]) - } + me.attr('transform', transforms.join(' ')) + + return transforms } let didDrag = false @@ -176,22 +176,11 @@ export const Stack = (props) => { /** don't mess with what we don't lay out */ if (!stackRef.current || props.isLayoutPart) return - // set the transforms on the part in order to calculate from the latest position - setTransforms() + // set the transforms on the stack in order to calculate from the latest position + const transforms = setTransforms() - // get the bounding box and the svg's current transform matrix - const stackRect = innerRef.current.getBoundingClientRect() - const matrix = innerRef.current.ownerSVGElement.getScreenCTM().inverse() - - // a function to convert dom space to svg space - const domToSvg = (point) => { - const { x, y } = DOMPointReadOnly.fromPoint(point).matrixTransform(matrix) - return { x, y } - } - - // include the new top left and bottom right to ease calculating the pattern width and height - const tl = domToSvg({ x: stackRect.left, y: stackRect.top }) - const br = domToSvg({ x: stackRect.right, y: props.isLayoutPart ? 0 : stackRect.bottom }) + // apply the transforms to the bounding box to get the new extents of the stack + const { tl, br } = getTransformedBounds(stack, [transforms]) // update it on the draft component props.updateLayout(