1
0
Fork 0

don't rely on browsers to compute transforms in layouting

This commit is contained in:
Enoch Riese 2023-04-28 15:46:33 -04:00
parent 62fb6bc229
commit c65c08432a
5 changed files with 147 additions and 95 deletions

View file

@ -21,6 +21,7 @@ import {
curvesIntersect, curvesIntersect,
deg2rad, deg2rad,
generateStackTransform, generateStackTransform,
getTransformedBounds,
lineIntersectsCircle, lineIntersectsCircle,
lineIntersectsCurve, lineIntersectsCurve,
linesIntersect, linesIntersect,
@ -63,6 +64,7 @@ export {
curvesIntersect, curvesIntersect,
deg2rad, deg2rad,
generateStackTransform, generateStackTransform,
getTransformedBounds,
lineIntersectsCircle, lineIntersectsCircle,
lineIntersectsCurve, lineIntersectsCurve,
linesIntersect, linesIntersect,

View file

@ -52,29 +52,18 @@ Stack.prototype.home = function () {
for (const part of this.getPartList()) { for (const part of this.getPartList()) {
part.__boundary() part.__boundary()
// get all corners of the part's bounds const { tl, br } = utils.getTransformedBounds(part, part.attributes.getAsArray('transform'))
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)
// if there are transforms on the part, apply them to the corners so that we have the correct bounds if (!tl) {
const transforms = part.attributes.getAsArray('transform') continue
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())
} }
// get the top left, the minimum x and y values of any corner // 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.x = Math.min(this.topLeft.x, tl.x)
this.topLeft.y = Math.min(this.topLeft.y, tl.y, br.y, bl.y, tr.y) this.topLeft.y = Math.min(this.topLeft.y, tl.y)
// get the bottom right, the maximum x and y values of any corner // 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.x = Math.max(this.bottomRight.x, br.x)
this.bottomRight.y = Math.max(this.bottomRight.y, tl.y, br.y, bl.y, tr.y) this.bottomRight.y = Math.max(this.bottomRight.y, br.y)
} }
// Fix infinity if it's not overwritten // Fix infinity if it's not overwritten
@ -142,14 +131,22 @@ Stack.prototype.attr = function (name, value, overwrite = false) {
return this 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) { Stack.prototype.generateTransform = function (transforms) {
const { move, rotate, flipX, flipY } = transforms const { move, rotate, flipX, flipY } = transforms
const generated = utils.generateStackTransform(move?.x, move?.y, rotate, flipX, flipY, this) const generated = utils.generateStackTransform(move?.x, move?.y, rotate, flipX, flipY, this)
for (var t in generated) { this.attributes.remove('transform')
this.attr(t, generated[t], true) generated.forEach((t) => this.attr('transform', t))
}
return this
} }
export default Stack export default Stack

View file

@ -288,16 +288,16 @@ export function deg2rad(degrees) {
* @param {bool} flipX - Whether or not to flip/mirror along the X-axis * @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 {bool} flipY - Whether or not to flip/mirror along the Y-axis
* @param {Stack} stack - The Stack instance * @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, x = 0,
y = 0, y = 0,
rotate = 0, rotate = 0,
flipX = false, flipX = false,
flipY = false, flipY = false,
stack stack
) => { ) {
const transforms = [] const transforms = []
let xTotal = x || 0 let xTotal = x || 0
let yTotal = y || 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 // 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})`) if (xTotal !== 0 || yTotal !== 0) transforms.unshift(`translate(${xTotal}, ${yTotal})`)
return { return transforms
transform: transforms.join(' '),
// 'transform-origin': `${center.x} ${center.y}`
}
} }
/** /**
@ -681,6 +678,77 @@ function __parseTransform(transform) {
return { parts, name, values } 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 * 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++) { for (let i = 0; i < transforms.length; i++) {
// Parse the transform string // Parse the transform string
const { name, values } = __parseTransform(transforms[i]) const { name, values } = __parseTransform(transforms[i])
matrix = matrixTransform(name, matrix, values)
// 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
}
} }
// Return the combined matrix transform // Return the combined matrix transform
@ -802,3 +825,44 @@ export function applyTransformToPoint(transform, point) {
return 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,
}
}

View file

@ -492,6 +492,6 @@ describe('Utils', () => {
const pattern = new design() const pattern = new design()
const props = pattern.draft().getRenderProps() const props = pattern.draft().getRenderProps()
const transform = generateStackTransform(30, 60, 90, true, true, props.stacks.test) 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)')
}) })
}) })

View file

@ -44,7 +44,7 @@
* how custom layouts are supported in the core. And I would like to discuss this with the core team. * 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 { useRef, useState, useEffect } from 'react'
import { generateStackTransform } from '@freesewing/core' import { generateStackTransform, getTransformedBounds } from '@freesewing/core'
import { Part } from '../../draft/part.mjs' import { Part } from '../../draft/part.mjs'
import { getProps, angle } from '../../draft/utils.mjs' import { getProps, angle } from '../../draft/utils.mjs'
import { drag } from 'd3-drag' import { drag } from 'd3-drag'
@ -108,9 +108,9 @@ export const Stack = (props) => {
const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack) const transforms = generateStackTransform(translateX, translateY, rotation, flipX, flipY, stack)
const me = select(stackRef.current) const me = select(stackRef.current)
for (var t in transforms) { me.attr('transform', transforms.join(' '))
me.attr(t, transforms[t])
} return transforms
} }
let didDrag = false let didDrag = false
@ -176,22 +176,11 @@ export const Stack = (props) => {
/** don't mess with what we don't lay out */ /** don't mess with what we don't lay out */
if (!stackRef.current || props.isLayoutPart) return if (!stackRef.current || props.isLayoutPart) return
// set the transforms on the part in order to calculate from the latest position // set the transforms on the stack in order to calculate from the latest position
setTransforms() const transforms = setTransforms()
// get the bounding box and the svg's current transform matrix // apply the transforms to the bounding box to get the new extents of the stack
const stackRect = innerRef.current.getBoundingClientRect() const { tl, br } = getTransformedBounds(stack, [transforms])
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 })
// update it on the draft component // update it on the draft component
props.updateLayout( props.updateLayout(