Updates arrows
This commit is contained in:
parent
fc2e3b3c4c
commit
daa44f9911
20 changed files with 1457 additions and 794 deletions
7
.babelrc
Normal file
7
.babelrc
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"presets": ["next/babel"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.js can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
|
@ -1,20 +0,0 @@
|
||||||
// ***********************************************************
|
|
||||||
// This example support/index.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands'
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
|
@ -38,9 +38,9 @@ const strokeWidths = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const dashArrays = {
|
const dashArrays = {
|
||||||
[DashStyle.Solid]: () => 'none',
|
[DashStyle.Solid]: () => [1],
|
||||||
[DashStyle.Dashed]: (sw: number) => `${sw} ${sw * 2}`,
|
[DashStyle.Dashed]: (sw: number) => [sw * 2, sw * 4],
|
||||||
[DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
|
[DashStyle.Dotted]: (sw: number) => [0, sw * 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontSizes = {
|
const fontSizes = {
|
||||||
|
@ -50,11 +50,11 @@ const fontSizes = {
|
||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStrokeWidth(size: SizeStyle) {
|
export function getStrokeWidth(size: SizeStyle) {
|
||||||
return strokeWidths[size]
|
return strokeWidths[size]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
|
export function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
|
||||||
return dashArrays[dash](strokeWidth)
|
return dashArrays[dash](strokeWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export function getShapeStyle(
|
||||||
const { color, size, dash, isFilled } = style
|
const { color, size, dash, isFilled } = style
|
||||||
|
|
||||||
const strokeWidth = getStrokeWidth(size)
|
const strokeWidth = getStrokeWidth(size)
|
||||||
const strokeDasharray = getStrokeDashArray(dash, strokeWidth)
|
const strokeDasharray = getStrokeDashArray(dash, strokeWidth).join()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stroke: strokes[color],
|
stroke: strokes[color],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils/utils'
|
import { getArcLength, lerp, uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import {
|
import {
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
|
@ -7,7 +7,7 @@ import {
|
||||||
translateBounds,
|
translateBounds,
|
||||||
pointsBetween,
|
pointsBetween,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import { ArrowShape, Bounds, ShapeHandle, ShapeType } from 'types'
|
import { ArrowShape, Bounds, DashStyle, ShapeHandle, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from './index'
|
import { registerShapeUtils } from './index'
|
||||||
import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
|
import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
|
||||||
import { pointInBounds } from 'utils/bounds'
|
import { pointInBounds } from 'utils/bounds'
|
||||||
|
@ -16,22 +16,20 @@ import {
|
||||||
intersectLineSegmentBounds,
|
intersectLineSegmentBounds,
|
||||||
} from 'utils/intersections'
|
} from 'utils/intersections'
|
||||||
import { pointInCircle } from 'utils/hitTests'
|
import { pointInCircle } from 'utils/hitTests'
|
||||||
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
|
import {
|
||||||
|
defaultStyle,
|
||||||
|
getShapeStyle,
|
||||||
|
getStrokeDashArray,
|
||||||
|
} from 'lib/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
|
|
||||||
const pathCache = new WeakMap<ArrowShape, string>([])
|
const pathCache = new WeakMap<ArrowShape, string>([])
|
||||||
|
|
||||||
|
// A cache for semi-expensive circles calculated from three points
|
||||||
function getCtp(shape: ArrowShape) {
|
function getCtp(shape: ArrowShape) {
|
||||||
if (!ctpCache.has(shape.handles)) {
|
const { start, end, bend } = shape.handles
|
||||||
const { start, end, bend } = shape.handles
|
return circleFromThreePoints(start.point, end.point, bend.point)
|
||||||
ctpCache.set(
|
|
||||||
shape.handles,
|
|
||||||
circleFromThreePoints(start.point, end.point, bend.point)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctpCache.get(shape.handles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrow = registerShapeUtils<ArrowShape>({
|
const arrow = registerShapeUtils<ArrowShape>({
|
||||||
|
@ -40,10 +38,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
create(props) {
|
create(props) {
|
||||||
const {
|
const {
|
||||||
point = [0, 0],
|
point = [0, 0],
|
||||||
points = [
|
|
||||||
[0, 0],
|
|
||||||
[0, 1],
|
|
||||||
],
|
|
||||||
handles = {
|
handles = {
|
||||||
start: {
|
start: {
|
||||||
id: 'start',
|
id: 'start',
|
||||||
|
@ -77,7 +71,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
bend: 0,
|
bend: 0,
|
||||||
points,
|
|
||||||
handles,
|
handles,
|
||||||
decorations: {
|
decorations: {
|
||||||
start: null,
|
start: null,
|
||||||
|
@ -94,63 +87,123 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
render(shape) {
|
render(shape) {
|
||||||
const { id, bend, handles } = shape
|
const { id, bend, handles, style } = shape
|
||||||
const { start, end, bend: _bend } = handles
|
const { start, end, bend: _bend } = handles
|
||||||
|
|
||||||
const arrowDist = vec.dist(start.point, end.point)
|
const isStraightLine = vec.isEqual(
|
||||||
|
|
||||||
const showCircle = !vec.isEqual(
|
|
||||||
_bend.point,
|
_bend.point,
|
||||||
vec.med(start.point, end.point)
|
vec.med(start.point, end.point)
|
||||||
)
|
)
|
||||||
|
|
||||||
const style = getShapeStyle(shape.style)
|
const styles = getShapeStyle(style)
|
||||||
|
|
||||||
let body: JSX.Element
|
const strokeWidth = +styles.strokeWidth
|
||||||
|
|
||||||
if (showCircle) {
|
if (isStraightLine) {
|
||||||
if (!ctpCache.has(handles)) {
|
// Render a straight arrow as a freehand path.
|
||||||
ctpCache.set(
|
|
||||||
handles,
|
|
||||||
circleFromThreePoints(start.point, end.point, _bend.point)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const circle = getCtp(shape)
|
|
||||||
|
|
||||||
if (!pathCache.has(shape)) {
|
|
||||||
renderPath(
|
|
||||||
shape,
|
|
||||||
vec.angle([circle[0], circle[1]], end.point) -
|
|
||||||
vec.angle(start.point, end.point) +
|
|
||||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = pathCache.get(shape)
|
|
||||||
|
|
||||||
body = (
|
|
||||||
<>
|
|
||||||
<path
|
|
||||||
d={getArrowArcPath(start, end, circle, bend)}
|
|
||||||
fill="none"
|
|
||||||
strokeWidth={(+style.strokeWidth * 1.85).toString()}
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
<path d={path} strokeWidth={+style.strokeWidth * 1.5} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (!pathCache.has(shape)) {
|
if (!pathCache.has(shape)) {
|
||||||
renderPath(shape)
|
renderPath(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const offset = -vec.dist(start.point, end.point) + strokeWidth
|
||||||
|
|
||||||
const path = pathCache.get(shape)
|
const path = pathCache.get(shape)
|
||||||
|
|
||||||
body = <path d={path} />
|
return (
|
||||||
|
<g id={id}>
|
||||||
|
{/* Improves hit testing */}
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
stroke="transparent"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="none"
|
||||||
|
/>
|
||||||
|
{/* Arrowshaft */}
|
||||||
|
<circle
|
||||||
|
cx={start.point[0]}
|
||||||
|
cy={start.point[1]}
|
||||||
|
r={strokeWidth}
|
||||||
|
fill={styles.stroke}
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={
|
||||||
|
strokeWidth * (style.dash === DashStyle.Solid ? 1 : 1.618)
|
||||||
|
}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
{/* Arrowhead */}
|
||||||
|
{style.dash !== DashStyle.Solid && (
|
||||||
|
<path
|
||||||
|
d={getArrowHeadPath(shape, 0)}
|
||||||
|
strokeWidth={strokeWidth * 1.618}
|
||||||
|
strokeDasharray="none"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <g id={id}>{body}</g>
|
const circle = getCtp(shape)
|
||||||
|
|
||||||
|
if (!pathCache.has(shape)) {
|
||||||
|
renderPath(
|
||||||
|
shape,
|
||||||
|
vec.angle([circle[0], circle[1]], end.point) -
|
||||||
|
vec.angle(start.point, end.point) +
|
||||||
|
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = getArrowArcPath(start, end, circle, bend)
|
||||||
|
|
||||||
|
const strokeDashOffset = getStrokeDashOffsetForArc(
|
||||||
|
shape,
|
||||||
|
circle,
|
||||||
|
strokeWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g id={id}>
|
||||||
|
{/* Improves hit testing */}
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
stroke="transparent"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={Math.max(8, strokeWidth * 2)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="none"
|
||||||
|
/>
|
||||||
|
{/* Arrow Shaft */}
|
||||||
|
<circle
|
||||||
|
cx={start.point[0]}
|
||||||
|
cy={start.point[1]}
|
||||||
|
r={strokeWidth}
|
||||||
|
fill={styles.stroke}
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d={path}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={strokeWidth * 1.618}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDashoffset={strokeDashOffset}
|
||||||
|
/>
|
||||||
|
{/* Arrowhead */}
|
||||||
|
<path
|
||||||
|
d={pathCache.get(shape)}
|
||||||
|
strokeWidth={strokeWidth * 1.618}
|
||||||
|
strokeDasharray="none"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
rotateBy(shape, delta) {
|
rotateBy(shape, delta) {
|
||||||
|
@ -179,17 +232,20 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (!this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
const { start, end } = shape.handles
|
const { start, bend, end } = shape.handles
|
||||||
this.boundsCache.set(shape, getBoundsFromPoints([start.point, end.point]))
|
this.boundsCache.set(
|
||||||
|
shape,
|
||||||
|
getBoundsFromPoints([start.point, bend.point, end.point])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
},
|
},
|
||||||
|
|
||||||
getRotatedBounds(shape) {
|
getRotatedBounds(shape) {
|
||||||
const { start, end } = shape.handles
|
const { start, bend, end } = shape.handles
|
||||||
return translateBounds(
|
return translateBounds(
|
||||||
getBoundsFromPoints([start.point, end.point], shape.rotation),
|
getBoundsFromPoints([start.point, bend.point, end.point], shape.rotation),
|
||||||
shape.point
|
shape.point
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -200,7 +256,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, point) {
|
hitTest(shape, point) {
|
||||||
const { start, end, bend } = shape.handles
|
const { start, end } = shape.handles
|
||||||
if (shape.bend === 0) {
|
if (shape.bend === 0) {
|
||||||
return (
|
return (
|
||||||
vec.distanceToLineSegment(
|
vec.distanceToLineSegment(
|
||||||
|
@ -239,33 +295,42 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
|
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
|
||||||
const initialShapeBounds = this.getBounds(initialShape)
|
const initialShapeBounds = this.getBounds(initialShape)
|
||||||
|
|
||||||
|
// let nw = initialShape.point[0] / initialShapeBounds.width
|
||||||
|
// let nh = initialShape.point[1] / initialShapeBounds.height
|
||||||
|
|
||||||
|
// shape.point = [
|
||||||
|
// bounds.width * (scaleX < 0 ? 1 - nw : nw),
|
||||||
|
// bounds.height * (scaleY < 0 ? 1 - nh : nh),
|
||||||
|
// ]
|
||||||
|
|
||||||
shape.point = [bounds.minX, bounds.minY]
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
|
||||||
shape.points = shape.points.map((_, i) => {
|
const handles = ['start', 'end']
|
||||||
const [x, y] = initialShape.points[i]
|
|
||||||
|
handles.forEach((handle) => {
|
||||||
|
const [x, y] = initialShape.handles[handle].point
|
||||||
let nw = x / initialShapeBounds.width
|
let nw = x / initialShapeBounds.width
|
||||||
let nh = y / initialShapeBounds.height
|
let nh = y / initialShapeBounds.height
|
||||||
|
|
||||||
if (i === 1) {
|
shape.handles[handle].point = [
|
||||||
let [x0, y0] = initialShape.points[0]
|
|
||||||
if (x0 === x) nw = 1
|
|
||||||
if (y0 === y) nh = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
bounds.width * (scaleX < 0 ? 1 - nw : nw),
|
bounds.width * (scaleX < 0 ? 1 - nw : nw),
|
||||||
bounds.height * (scaleY < 0 ? 1 - nh : nh),
|
bounds.height * (scaleY < 0 ? 1 - nh : nh),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { start, end, bend } = shape.handles
|
const { start, bend, end } = shape.handles
|
||||||
|
|
||||||
start.point = shape.points[0]
|
const dist = vec.dist(start.point, end.point)
|
||||||
end.point = shape.points[1]
|
|
||||||
|
|
||||||
bend.point = getBendPoint(shape)
|
const midPoint = vec.med(start.point, end.point)
|
||||||
|
|
||||||
shape.points = [shape.handles.start.point, shape.handles.end.point]
|
const bendDist = (dist / 2) * initialShape.bend
|
||||||
|
|
||||||
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
|
|
||||||
|
const point = vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||||
|
|
||||||
|
bend.point = Math.abs(bendDist) < 10 ? midPoint : point
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
@ -279,10 +344,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
|
|
||||||
shape.handles[handle.id] = handle
|
shape.handles[handle.id] = handle
|
||||||
|
|
||||||
if (handle.index < 2) {
|
|
||||||
shape.points[handle.index] = handle.point
|
|
||||||
}
|
|
||||||
|
|
||||||
const { start, end, bend } = shape.handles
|
const { start, end, bend } = shape.handles
|
||||||
|
|
||||||
const dist = vec.dist(start.point, end.point)
|
const dist = vec.dist(start.point, end.point)
|
||||||
|
@ -327,6 +388,8 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
end.point = vec.sub(end.point, offset)
|
end.point = vec.sub(end.point, offset)
|
||||||
bend.point = vec.sub(bend.point, offset)
|
bend.point = vec.sub(bend.point, offset)
|
||||||
|
|
||||||
|
shape.handles = { ...shape.handles }
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -375,47 +438,6 @@ function getBendPoint(shape: ArrowShape) {
|
||||||
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResizeOffset(a: Bounds, b: Bounds) {
|
|
||||||
const { minX: x0, minY: y0, width: w0, height: h0 } = a
|
|
||||||
const { minX: x1, minY: y1, width: w1, height: h1 } = b
|
|
||||||
|
|
||||||
let delta: number[]
|
|
||||||
|
|
||||||
if (h0 === h1 && w0 !== w1) {
|
|
||||||
if (x0 !== x1) {
|
|
||||||
// moving left edge, pin right edge
|
|
||||||
delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
|
|
||||||
} else {
|
|
||||||
// moving right edge, pin left edge
|
|
||||||
delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
|
|
||||||
}
|
|
||||||
} else if (h0 !== h1 && w0 === w1) {
|
|
||||||
if (y0 !== y1) {
|
|
||||||
// moving top edge, pin bottom edge
|
|
||||||
delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
|
|
||||||
} else {
|
|
||||||
// moving bottom edge, pin top edge
|
|
||||||
delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
|
|
||||||
}
|
|
||||||
} else if (x0 !== x1) {
|
|
||||||
if (y0 !== y1) {
|
|
||||||
// moving top left, pin bottom right
|
|
||||||
delta = vec.sub([x1, y1], [x0, y0])
|
|
||||||
} else {
|
|
||||||
// moving bottom left, pin top right
|
|
||||||
delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
|
|
||||||
}
|
|
||||||
} else if (y0 !== y1) {
|
|
||||||
// moving top right, pin bottom left
|
|
||||||
delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
|
|
||||||
} else {
|
|
||||||
// moving bottom right, pin top left
|
|
||||||
delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return delta
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPath(shape: ArrowShape, endAngle = 0) {
|
function renderPath(shape: ArrowShape, endAngle = 0) {
|
||||||
const { style, id } = shape
|
const { style, id } = shape
|
||||||
const { start, end } = shape.handles
|
const { start, end } = shape.handles
|
||||||
|
@ -424,42 +446,34 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
|
||||||
|
|
||||||
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
||||||
|
|
||||||
const arrowDist = vec.dist(start.point, end.point)
|
|
||||||
|
|
||||||
const styles = getShapeStyle(shape.style)
|
const styles = getShapeStyle(shape.style)
|
||||||
|
|
||||||
const sw = +styles.strokeWidth
|
const sw = strokeWidth
|
||||||
|
|
||||||
const length = Math.min(arrowDist / 2, 24 + sw * 2)
|
|
||||||
const u = vec.uni(vec.vec(start.point, end.point))
|
|
||||||
const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
|
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
const a = start.point
|
const a = start.point
|
||||||
|
|
||||||
|
// End
|
||||||
|
const b = end.point
|
||||||
|
|
||||||
// Middle
|
// Middle
|
||||||
const m = vec.add(
|
const m = vec.add(
|
||||||
vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
|
vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
|
||||||
[getRandom() * sw, getRandom() * sw]
|
[getRandom() * sw, getRandom() * sw]
|
||||||
)
|
)
|
||||||
|
|
||||||
// End
|
// Left and right sides of the arrowhead
|
||||||
const b = end.point
|
let { left: c, right: d } = getArrowHeadPoints(shape, endAngle)
|
||||||
|
|
||||||
// Left
|
// Switch which side of the arrow is drawn first
|
||||||
let c = vec.add(
|
if (getRandom() > 0) [c, d] = [d, c]
|
||||||
end.point,
|
|
||||||
vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
|
|
||||||
)
|
|
||||||
|
|
||||||
// Right
|
if (style.dash !== DashStyle.Solid) {
|
||||||
let d = vec.add(
|
pathCache.set(
|
||||||
end.point,
|
shape,
|
||||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
|
(endAngle ? ['M', c, 'L', b, d] : ['M', a, 'L', b]).join(' ')
|
||||||
)
|
)
|
||||||
|
return
|
||||||
if (getRandom() > 0.5) {
|
|
||||||
;[c, d] = [d, c]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = endAngle
|
const points = endAngle
|
||||||
|
@ -471,7 +485,7 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
|
||||||
...pointsBetween(d, b),
|
...pointsBetween(d, b),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
// The shaft too
|
// The arrow shaft
|
||||||
b,
|
b,
|
||||||
a,
|
a,
|
||||||
...pointsBetween(a, m),
|
...pointsBetween(a, m),
|
||||||
|
@ -493,3 +507,60 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
|
||||||
|
|
||||||
pathCache.set(shape, getSvgPathFromStroke(stroke))
|
pathCache.set(shape, getSvgPathFromStroke(stroke))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getArrowHeadPath(shape: ArrowShape, endAngle = 0) {
|
||||||
|
const { end } = shape.handles
|
||||||
|
const { left, right } = getArrowHeadPoints(shape, endAngle)
|
||||||
|
return ['M', left, 'L', end.point, right].join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrowHeadPoints(shape: ArrowShape, endAngle = 0) {
|
||||||
|
const { start, end } = shape.handles
|
||||||
|
|
||||||
|
const stroke = +getShapeStyle(shape.style).strokeWidth * 2
|
||||||
|
|
||||||
|
const arrowDist = vec.dist(start.point, end.point)
|
||||||
|
|
||||||
|
const arrowHeadlength = Math.min(arrowDist / 3, stroke * 4)
|
||||||
|
|
||||||
|
// Unit vector from start to end
|
||||||
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
|
|
||||||
|
// The end of the arrowhead wings
|
||||||
|
const v = vec.rot(vec.mul(vec.neg(u), arrowHeadlength), endAngle)
|
||||||
|
|
||||||
|
// Use the shape's random seed to create minor offsets for the angles
|
||||||
|
const getRandom = rng(shape.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: vec.add(
|
||||||
|
end.point,
|
||||||
|
vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
|
||||||
|
),
|
||||||
|
right: vec.add(
|
||||||
|
end.point,
|
||||||
|
vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStrokeDashOffsetForArc(
|
||||||
|
shape: ArrowShape,
|
||||||
|
circle: number[],
|
||||||
|
strokeWidth: number
|
||||||
|
) {
|
||||||
|
const { start, end } = shape.handles
|
||||||
|
|
||||||
|
const sweep = getArcLength(
|
||||||
|
[circle[0], circle[1]],
|
||||||
|
circle[2],
|
||||||
|
start.point,
|
||||||
|
end.point
|
||||||
|
)
|
||||||
|
|
||||||
|
return Math.abs(shape.bend) === 1
|
||||||
|
? -strokeWidth / 2
|
||||||
|
: shape.bend < 0
|
||||||
|
? sweep + strokeWidth
|
||||||
|
: -sweep + strokeWidth
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,12 @@ import { uniqueId, isMobile } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { TextShape, ShapeType, FontSize, SizeStyle } from 'types'
|
import { TextShape, ShapeType, FontSize, SizeStyle } from 'types'
|
||||||
import { registerShapeUtils } from './index'
|
import { registerShapeUtils } from './index'
|
||||||
import { defaultStyle, getFontStyle, getShapeStyle } from 'lib/shape-styles'
|
import {
|
||||||
|
defaultStyle,
|
||||||
|
getFontSize,
|
||||||
|
getFontStyle,
|
||||||
|
getShapeStyle,
|
||||||
|
} from 'lib/shape-styles'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
@ -98,6 +103,32 @@ const text = registerShapeUtils<TextShape>({
|
||||||
state.send('FOCUSED_EDITING_SHAPE')
|
state.send('FOCUSED_EDITING_SHAPE')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontSize = getFontSize(shape.style.size) * shape.scale
|
||||||
|
const gap = fontSize * 0.4
|
||||||
|
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<g id={id} pointerEvents="none">
|
||||||
|
{text.split('\n').map((str, i) => (
|
||||||
|
<text
|
||||||
|
key={i}
|
||||||
|
x={4}
|
||||||
|
y={4 + gap / 2 + i * (fontSize + gap)}
|
||||||
|
fontFamily="Verveine Regular"
|
||||||
|
fontStyle="normal"
|
||||||
|
fontWeight="regular"
|
||||||
|
fontSize={fontSize}
|
||||||
|
width={bounds.width}
|
||||||
|
height={bounds.height}
|
||||||
|
dominant-baseline="hanging"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</text>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<foreignObject
|
<foreignObject
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -107,37 +138,26 @@ const text = registerShapeUtils<TextShape>({
|
||||||
height={bounds.height}
|
height={bounds.height}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
{isEditing ? (
|
<StyledTextArea
|
||||||
<StyledTextArea
|
ref={ref}
|
||||||
ref={ref}
|
style={{
|
||||||
style={{
|
font,
|
||||||
font,
|
color: styles.stroke,
|
||||||
color: styles.stroke,
|
}}
|
||||||
}}
|
value={text}
|
||||||
value={text}
|
tabIndex={0}
|
||||||
tabIndex={0}
|
autoComplete="false"
|
||||||
autoComplete="false"
|
autoCapitalize="false"
|
||||||
autoCapitalize="false"
|
autoCorrect="false"
|
||||||
autoCorrect="false"
|
autoSave="false"
|
||||||
autoSave="false"
|
placeholder=""
|
||||||
placeholder=""
|
name="text"
|
||||||
name="text"
|
autoFocus={isMobile() ? true : false}
|
||||||
autoFocus={isMobile() ? true : false}
|
onFocus={handleFocus}
|
||||||
onFocus={handleFocus}
|
onBlur={handleBlur}
|
||||||
onBlur={handleBlur}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyDown={handleKeyDown}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<StyledText
|
|
||||||
style={{
|
|
||||||
font,
|
|
||||||
color: styles.stroke,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</StyledText>
|
|
||||||
)}
|
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test": "yarn test:app",
|
||||||
|
"test:all": "yarn test:code",
|
||||||
|
"test:update": "yarn test:app --updateSnapshot --watchAll=false"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@monaco-editor/react": "^4.1.3",
|
"@monaco-editor/react": "^4.1.3",
|
||||||
|
@ -46,7 +49,8 @@
|
||||||
"@types/react": "^17.0.5",
|
"@types/react": "^17.0.5",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"cypress": "^7.3.0",
|
"babel-jest": "^27.0.2",
|
||||||
|
"jest": "^27.0.4",
|
||||||
"monaco-editor": "^0.24.0",
|
"monaco-editor": "^0.24.0",
|
||||||
"typescript": "^4.2.4"
|
"typescript": "^4.2.4"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import paste from './paste'
|
||||||
import rotateCcw from './rotate-ccw'
|
import rotateCcw from './rotate-ccw'
|
||||||
import stretch from './stretch'
|
import stretch from './stretch'
|
||||||
import style from './style'
|
import style from './style'
|
||||||
|
import mutate from './mutate'
|
||||||
import toggle from './toggle'
|
import toggle from './toggle'
|
||||||
import transform from './transform'
|
import transform from './transform'
|
||||||
import transformSingle from './transform-single'
|
import transformSingle from './transform-single'
|
||||||
|
@ -28,6 +29,7 @@ import edit from './edit'
|
||||||
import resetBounds from './reset-bounds'
|
import resetBounds from './reset-bounds'
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
|
mutate,
|
||||||
align,
|
align,
|
||||||
arrow,
|
arrow,
|
||||||
changePage,
|
changePage,
|
||||||
|
|
49
state/commands/mutate.ts
Normal file
49
state/commands/mutate.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data, Shape } from 'types'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
import { getPage, updateParents } from 'utils/utils'
|
||||||
|
|
||||||
|
// Used when changing the properties of one or more shapes,
|
||||||
|
// without changing selection or deleting any shapes.
|
||||||
|
|
||||||
|
export default function mutateShapesCommand(
|
||||||
|
data: Data,
|
||||||
|
before: Shape[],
|
||||||
|
after: Shape[],
|
||||||
|
name = 'mutate_shapes'
|
||||||
|
) {
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name,
|
||||||
|
category: 'canvas',
|
||||||
|
do(data) {
|
||||||
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
|
after.forEach((shape) => {
|
||||||
|
shapes[shape.id] = shape
|
||||||
|
getShapeUtils(shape).onSessionComplete(shape)
|
||||||
|
})
|
||||||
|
|
||||||
|
// updateParents(
|
||||||
|
// data,
|
||||||
|
// after.map((shape) => shape.id)
|
||||||
|
// )
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
|
before.forEach((shape) => {
|
||||||
|
shapes[shape.id] = shape
|
||||||
|
getShapeUtils(shape).onSessionComplete(shape)
|
||||||
|
})
|
||||||
|
|
||||||
|
updateParents(
|
||||||
|
data,
|
||||||
|
before.map((shape) => shape.id)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,24 +18,12 @@ export default function transformCommand(
|
||||||
name: 'transform_shapes',
|
name: 'transform_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { type, shapeBounds } = after
|
const { shapeBounds } = after
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (let id in shapeBounds) {
|
for (let id in shapeBounds) {
|
||||||
const { initialShapeBounds: bounds } = after.shapeBounds[id]
|
shapes[id] = shapeBounds[id].initialShape
|
||||||
const { initialShape, transformOrigin } = before.shapeBounds[id]
|
|
||||||
const shape = shapes[id]
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.transform(shape, bounds, {
|
|
||||||
type,
|
|
||||||
initialShape,
|
|
||||||
transformOrigin,
|
|
||||||
scaleX,
|
|
||||||
scaleY,
|
|
||||||
})
|
|
||||||
.onSessionComplete(shape)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParents(data, Object.keys(shapeBounds))
|
updateParents(data, Object.keys(shapeBounds))
|
||||||
|
|
|
@ -5,6 +5,7 @@ import commands from 'state/commands'
|
||||||
import { current, freeze } from 'immer'
|
import { current, freeze } from 'immer'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import {
|
import {
|
||||||
|
deepClone,
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
|
@ -103,26 +104,28 @@ export default class TransformSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data) {
|
complete(data: Data) {
|
||||||
if (!this.snapshot.hasUnlockedShapes) return
|
const { initialShapes, hasUnlockedShapes } = this.snapshot
|
||||||
|
|
||||||
commands.transform(
|
if (!hasUnlockedShapes) return
|
||||||
data,
|
|
||||||
this.snapshot,
|
const page = getPage(data)
|
||||||
getTransformSnapshot(data, this.transformType),
|
|
||||||
this.scaleX,
|
const finalShapes = initialShapes.map((shape) =>
|
||||||
this.scaleY
|
deepClone(page.shapes[shape.id])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
commands.mutate(data, initialShapes, finalShapes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
const cData = current(data)
|
const { currentPageId } = data
|
||||||
const { currentPageId } = cData
|
const page = getPage(data)
|
||||||
const page = getPage(cData)
|
|
||||||
|
|
||||||
const initialShapes = setToArray(getSelectedIds(data))
|
const initialShapes = setToArray(getSelectedIds(data))
|
||||||
.flatMap((id) => getDocumentBranch(cData, id).map((id) => page.shapes[id]))
|
.flatMap((id) => getDocumentBranch(data, id).map((id) => page.shapes[id]))
|
||||||
.filter((shape) => !shape.isLocked)
|
.filter((shape) => !shape.isLocked)
|
||||||
|
.map((shape) => deepClone(shape))
|
||||||
|
|
||||||
const hasUnlockedShapes = initialShapes.length > 0
|
const hasUnlockedShapes = initialShapes.length > 0
|
||||||
|
|
||||||
|
@ -151,6 +154,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
hasUnlockedShapes,
|
hasUnlockedShapes,
|
||||||
isAllAspectRatioLocked,
|
isAllAspectRatioLocked,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
|
initialShapes,
|
||||||
initialBounds: commonBounds,
|
initialBounds: commonBounds,
|
||||||
shapeBounds: Object.fromEntries(
|
shapeBounds: Object.fromEntries(
|
||||||
initialShapes.map((shape) => {
|
initialShapes.map((shape) => {
|
||||||
|
|
|
@ -66,7 +66,6 @@ const initialData: Data = {
|
||||||
size: SizeStyle.Medium,
|
size: SizeStyle.Medium,
|
||||||
color: ColorStyle.Black,
|
color: ColorStyle.Black,
|
||||||
dash: DashStyle.Solid,
|
dash: DashStyle.Solid,
|
||||||
fontSize: FontSize.Medium,
|
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
},
|
},
|
||||||
activeTool: 'select',
|
activeTool: 'select',
|
||||||
|
@ -131,7 +130,7 @@ const state = createState({
|
||||||
wait: 0.01,
|
wait: 0.01,
|
||||||
if: 'hasSelection',
|
if: 'hasSelection',
|
||||||
do: 'zoomCameraToSelectionActual',
|
do: 'zoomCameraToSelectionActual',
|
||||||
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
else: ['zoomCameraToActual'],
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
|
COPIED: { if: 'hasSelection', do: 'copyToClipboard' },
|
||||||
|
|
9
todo.md
9
todo.md
|
@ -47,5 +47,10 @@
|
||||||
|
|
||||||
## Clipboard
|
## Clipboard
|
||||||
|
|
||||||
- [ ] Copy
|
- [x] Copy
|
||||||
- [ ] Paste
|
- [x] Paste shapes
|
||||||
|
- [ ] Paste as text
|
||||||
|
|
||||||
|
## Copy to SVG
|
||||||
|
|
||||||
|
- [ ] Copy to SVG
|
||||||
|
|
7
types.ts
7
types.ts
|
@ -16,7 +16,7 @@ export interface Data {
|
||||||
isToolLocked: boolean
|
isToolLocked: boolean
|
||||||
isPenLocked: boolean
|
isPenLocked: boolean
|
||||||
}
|
}
|
||||||
currentStyle: ShapeStyles & TextStyles
|
currentStyle: ShapeStyles
|
||||||
activeTool: ShapeType | 'select'
|
activeTool: ShapeType | 'select'
|
||||||
brush?: Bounds
|
brush?: Bounds
|
||||||
boundsRotation: number
|
boundsRotation: number
|
||||||
|
@ -114,10 +114,6 @@ export type ShapeStyles = {
|
||||||
isFilled: boolean
|
isFilled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextStyles = {
|
|
||||||
fontSize: FontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseShape {
|
export interface BaseShape {
|
||||||
id: string
|
id: string
|
||||||
seed: number
|
seed: number
|
||||||
|
@ -180,7 +176,6 @@ export interface DrawShape extends BaseShape {
|
||||||
|
|
||||||
export interface ArrowShape extends BaseShape {
|
export interface ArrowShape extends BaseShape {
|
||||||
type: ShapeType.Arrow
|
type: ShapeType.Arrow
|
||||||
points: number[][]
|
|
||||||
handles: Record<string, ShapeHandle>
|
handles: Record<string, ShapeHandle>
|
||||||
bend: number
|
bend: number
|
||||||
decorations?: {
|
decorations?: {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Bounds } from "types"
|
import { Bounds } from 'types'
|
||||||
import {
|
import {
|
||||||
intersectPolygonBounds,
|
intersectPolygonBounds,
|
||||||
intersectPolylineBounds,
|
intersectPolylineBounds,
|
||||||
} from "./intersections"
|
} from './intersections'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether two bounds collide.
|
* Get whether two bounds collide.
|
||||||
|
|
|
@ -1850,3 +1850,65 @@ export function decompress(s: string) {
|
||||||
|
|
||||||
return out.join('')
|
return out.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResizeOffset(a: Bounds, b: Bounds) {
|
||||||
|
const { minX: x0, minY: y0, width: w0, height: h0 } = a
|
||||||
|
const { minX: x1, minY: y1, width: w1, height: h1 } = b
|
||||||
|
|
||||||
|
let delta: number[]
|
||||||
|
|
||||||
|
if (h0 === h1 && w0 !== w1) {
|
||||||
|
if (x0 !== x1) {
|
||||||
|
// moving left edge, pin right edge
|
||||||
|
delta = vec.sub([x1, y1 + h1 / 2], [x0, y0 + h0 / 2])
|
||||||
|
} else {
|
||||||
|
// moving right edge, pin left edge
|
||||||
|
delta = vec.sub([x1 + w1, y1 + h1 / 2], [x0 + w0, y0 + h0 / 2])
|
||||||
|
}
|
||||||
|
} else if (h0 !== h1 && w0 === w1) {
|
||||||
|
if (y0 !== y1) {
|
||||||
|
// moving top edge, pin bottom edge
|
||||||
|
delta = vec.sub([x1 + w1 / 2, y1], [x0 + w0 / 2, y0])
|
||||||
|
} else {
|
||||||
|
// moving bottom edge, pin top edge
|
||||||
|
delta = vec.sub([x1 + w1 / 2, y1 + h1], [x0 + w0 / 2, y0 + h0])
|
||||||
|
}
|
||||||
|
} else if (x0 !== x1) {
|
||||||
|
if (y0 !== y1) {
|
||||||
|
// moving top left, pin bottom right
|
||||||
|
delta = vec.sub([x1, y1], [x0, y0])
|
||||||
|
} else {
|
||||||
|
// moving bottom left, pin top right
|
||||||
|
delta = vec.sub([x1, y1 + h1], [x0, y0 + h0])
|
||||||
|
}
|
||||||
|
} else if (y0 !== y1) {
|
||||||
|
// moving top right, pin bottom left
|
||||||
|
delta = vec.sub([x1 + w1, y1], [x0 + w0, y0])
|
||||||
|
} else {
|
||||||
|
// moving bottom right, pin top left
|
||||||
|
delta = vec.sub([x1 + w1, y1 + h1], [x0 + w0, y0 + h0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepClone<T extends unknown[] | object>(obj: T): T {
|
||||||
|
if (obj === null) return null
|
||||||
|
|
||||||
|
let clone = { ...obj }
|
||||||
|
|
||||||
|
Object.keys(obj).forEach(
|
||||||
|
(key) =>
|
||||||
|
(clone[key] =
|
||||||
|
typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
// @ts-ignore
|
||||||
|
clone.length = obj.length
|
||||||
|
// @ts-ignore
|
||||||
|
return Array.from(clone) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue