don't rely on browsers to compute transforms in layouting
This commit is contained in:
parent
62fb6bc229
commit
c65c08432a
5 changed files with 147 additions and 95 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -682,25 +679,15 @@ function __parseTransform(transform) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines an array of (SVG) transforms into a single matrix transform
|
* Applies a transformation of the given type to the matrix
|
||||||
*
|
* @param {String} transformationType the transformation type (tranlate, rotate, scale, skew, etc)
|
||||||
* @param {array} transorms - The list of transforms to combine
|
* @param {Number[]} matrix the matrix to apply the transform to
|
||||||
* @return {string} matrixTransform - The combined matrix transform
|
* @param {Number[]} values the transformation values to apply
|
||||||
|
* @return {Number[]} the transformed matrix
|
||||||
*/
|
*/
|
||||||
export function combineTransforms(transforms = []) {
|
function matrixTransform(transformationType, matrix, values) {
|
||||||
// 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
|
|
||||||
const { name, values } = __parseTransform(transforms[i])
|
|
||||||
|
|
||||||
// Update matrix for transform
|
// Update matrix for transform
|
||||||
switch (name) {
|
switch (transformationType) {
|
||||||
case 'matrix':
|
case 'matrix':
|
||||||
matrix = [
|
matrix = [
|
||||||
matrix[0] * values[0] + matrix[2] * values[1],
|
matrix[0] * values[0] + matrix[2] * values[1],
|
||||||
|
@ -723,6 +710,15 @@ export function combineTransforms(transforms = []) {
|
||||||
break
|
break
|
||||||
case 'rotate': {
|
case 'rotate': {
|
||||||
const angle = (values[0] * Math.PI) / 180
|
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 cos = Math.cos(angle)
|
||||||
const sin = Math.sin(angle)
|
const sin = Math.sin(angle)
|
||||||
matrix = [
|
matrix = [
|
||||||
|
@ -733,6 +729,11 @@ export function combineTransforms(transforms = []) {
|
||||||
matrix[4],
|
matrix[4],
|
||||||
matrix[5],
|
matrix[5],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// move the origin back to origin
|
||||||
|
if (centerX) {
|
||||||
|
matrix = matrixTransform('translate', matrix, [-centerX, -centerY])
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'skewX':
|
case 'skewX':
|
||||||
|
@ -744,6 +745,28 @@ export function combineTransforms(transforms = []) {
|
||||||
matrix[1] += matrix[3] * Math.tan((values[0] * Math.PI) / 180)
|
matrix[1] += matrix[3] * Math.tan((values[0] * Math.PI) / 180)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
const { name, values } = __parseTransform(transforms[i])
|
||||||
|
matrix = matrixTransform(name, matrix, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue