pull/3/head
Steve Ruiz 2021-06-21 14:13:16 +01:00
rodzic fc2e3b3c4c
commit daa44f9911
20 zmienionych plików z 1457 dodań i 794 usunięć

7
.babelrc 100644
Wyświetl plik

@ -0,0 +1,7 @@
{
"env": {
"test": {
"presets": ["next/babel"]
}
}
}

Wyświetl plik

@ -1 +0,0 @@
{}

Wyświetl plik

@ -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"
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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) => { ... })

Wyświetl plik

@ -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')

Wyświetl plik

@ -38,9 +38,9 @@ const strokeWidths = {
}
const dashArrays = {
[DashStyle.Solid]: () => 'none',
[DashStyle.Dashed]: (sw: number) => `${sw} ${sw * 2}`,
[DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
[DashStyle.Solid]: () => [1],
[DashStyle.Dashed]: (sw: number) => [sw * 2, sw * 4],
[DashStyle.Dotted]: (sw: number) => [0, sw * 3],
}
const fontSizes = {
@ -50,11 +50,11 @@ const fontSizes = {
auto: 'auto',
}
function getStrokeWidth(size: SizeStyle) {
export function getStrokeWidth(size: SizeStyle) {
return strokeWidths[size]
}
function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
export function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
return dashArrays[dash](strokeWidth)
}
@ -74,7 +74,7 @@ export function getShapeStyle(
const { color, size, dash, isFilled } = style
const strokeWidth = getStrokeWidth(size)
const strokeDasharray = getStrokeDashArray(dash, strokeWidth)
const strokeDasharray = getStrokeDashArray(dash, strokeWidth).join()
return {
stroke: strokes[color],

Wyświetl plik

@ -1,4 +1,4 @@
import { uniqueId } from 'utils/utils'
import { getArcLength, lerp, uniqueId } from 'utils/utils'
import vec from 'utils/vec'
import {
getSvgPathFromStroke,
@ -7,7 +7,7 @@ import {
translateBounds,
pointsBetween,
} from 'utils/utils'
import { ArrowShape, Bounds, ShapeHandle, ShapeType } from 'types'
import { ArrowShape, Bounds, DashStyle, ShapeHandle, ShapeType } from 'types'
import { registerShapeUtils } from './index'
import { circleFromThreePoints, isAngleBetween } from 'utils/utils'
import { pointInBounds } from 'utils/bounds'
@ -16,22 +16,20 @@ import {
intersectLineSegmentBounds,
} from 'utils/intersections'
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 React from 'react'
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
const pathCache = new WeakMap<ArrowShape, string>([])
// A cache for semi-expensive circles calculated from three points
function getCtp(shape: ArrowShape) {
if (!ctpCache.has(shape.handles)) {
const { start, end, bend } = shape.handles
ctpCache.set(
shape.handles,
circleFromThreePoints(start.point, end.point, bend.point)
)
}
return ctpCache.get(shape.handles)
const { start, end, bend } = shape.handles
return circleFromThreePoints(start.point, end.point, bend.point)
}
const arrow = registerShapeUtils<ArrowShape>({
@ -40,10 +38,6 @@ const arrow = registerShapeUtils<ArrowShape>({
create(props) {
const {
point = [0, 0],
points = [
[0, 0],
[0, 1],
],
handles = {
start: {
id: 'start',
@ -77,7 +71,6 @@ const arrow = registerShapeUtils<ArrowShape>({
isLocked: false,
isHidden: false,
bend: 0,
points,
handles,
decorations: {
start: null,
@ -94,63 +87,123 @@ const arrow = registerShapeUtils<ArrowShape>({
},
render(shape) {
const { id, bend, handles } = shape
const { id, bend, handles, style } = shape
const { start, end, bend: _bend } = handles
const arrowDist = vec.dist(start.point, end.point)
const showCircle = !vec.isEqual(
const isStraightLine = vec.isEqual(
_bend.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 (!ctpCache.has(handles)) {
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 (isStraightLine) {
// Render a straight arrow as a freehand path.
if (!pathCache.has(shape)) {
renderPath(shape)
}
const offset = -vec.dist(start.point, end.point) + strokeWidth
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) {
@ -179,17 +232,20 @@ const arrow = registerShapeUtils<ArrowShape>({
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const { start, end } = shape.handles
this.boundsCache.set(shape, getBoundsFromPoints([start.point, end.point]))
const { start, bend, end } = shape.handles
this.boundsCache.set(
shape,
getBoundsFromPoints([start.point, bend.point, end.point])
)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
const { start, end } = shape.handles
const { start, bend, end } = shape.handles
return translateBounds(
getBoundsFromPoints([start.point, end.point], shape.rotation),
getBoundsFromPoints([start.point, bend.point, end.point], shape.rotation),
shape.point
)
},
@ -200,7 +256,7 @@ const arrow = registerShapeUtils<ArrowShape>({
},
hitTest(shape, point) {
const { start, end, bend } = shape.handles
const { start, end } = shape.handles
if (shape.bend === 0) {
return (
vec.distanceToLineSegment(
@ -239,33 +295,42 @@ const arrow = registerShapeUtils<ArrowShape>({
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
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.points = shape.points.map((_, i) => {
const [x, y] = initialShape.points[i]
const handles = ['start', 'end']
handles.forEach((handle) => {
const [x, y] = initialShape.handles[handle].point
let nw = x / initialShapeBounds.width
let nh = y / initialShapeBounds.height
if (i === 1) {
let [x0, y0] = initialShape.points[0]
if (x0 === x) nw = 1
if (y0 === y) nh = 1
}
return [
shape.handles[handle].point = [
bounds.width * (scaleX < 0 ? 1 - nw : nw),
bounds.height * (scaleY < 0 ? 1 - nh : nh),
]
})
const { start, end, bend } = shape.handles
const { start, bend, end } = shape.handles
start.point = shape.points[0]
end.point = shape.points[1]
const dist = vec.dist(start.point, end.point)
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
},
@ -279,10 +344,6 @@ const arrow = registerShapeUtils<ArrowShape>({
shape.handles[handle.id] = handle
if (handle.index < 2) {
shape.points[handle.index] = handle.point
}
const { start, end, bend } = shape.handles
const dist = vec.dist(start.point, end.point)
@ -327,6 +388,8 @@ const arrow = registerShapeUtils<ArrowShape>({
end.point = vec.sub(end.point, offset)
bend.point = vec.sub(bend.point, offset)
shape.handles = { ...shape.handles }
return this
},
@ -375,47 +438,6 @@ function getBendPoint(shape: ArrowShape) {
: 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) {
const { style, id } = shape
const { start, end } = shape.handles
@ -424,42 +446,34 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
const arrowDist = vec.dist(start.point, end.point)
const styles = getShapeStyle(shape.style)
const sw = +styles.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)
const sw = strokeWidth
// Start
const a = start.point
// End
const b = end.point
// Middle
const m = vec.add(
vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2),
[getRandom() * sw, getRandom() * sw]
)
// End
const b = end.point
// Left and right sides of the arrowhead
let { left: c, right: d } = getArrowHeadPoints(shape, endAngle)
// Left
let c = vec.add(
end.point,
vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())
)
// Switch which side of the arrow is drawn first
if (getRandom() > 0) [c, d] = [d, c]
// Right
let d = vec.add(
end.point,
vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom())
)
if (getRandom() > 0.5) {
;[c, d] = [d, c]
if (style.dash !== DashStyle.Solid) {
pathCache.set(
shape,
(endAngle ? ['M', c, 'L', b, d] : ['M', a, 'L', b]).join(' ')
)
return
}
const points = endAngle
@ -471,7 +485,7 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
...pointsBetween(d, b),
]
: [
// The shaft too
// The arrow shaft
b,
a,
...pointsBetween(a, m),
@ -493,3 +507,60 @@ function renderPath(shape: ArrowShape, endAngle = 0) {
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
}

Wyświetl plik

@ -2,7 +2,12 @@ import { uniqueId, isMobile } from 'utils/utils'
import vec from 'utils/vec'
import { TextShape, ShapeType, FontSize, SizeStyle } from 'types'
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 state from 'state'
import { useEffect, useRef } from 'react'
@ -98,6 +103,32 @@ const text = registerShapeUtils<TextShape>({
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 (
<foreignObject
id={id}
@ -107,37 +138,26 @@ const text = registerShapeUtils<TextShape>({
height={bounds.height}
pointerEvents="none"
>
{isEditing ? (
<StyledTextArea
ref={ref}
style={{
font,
color: styles.stroke,
}}
value={text}
tabIndex={0}
autoComplete="false"
autoCapitalize="false"
autoCorrect="false"
autoSave="false"
placeholder=""
name="text"
autoFocus={isMobile() ? true : false}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={handleChange}
/>
) : (
<StyledText
style={{
font,
color: styles.stroke,
}}
>
{text}
</StyledText>
)}
<StyledTextArea
ref={ref}
style={{
font,
color: styles.stroke,
}}
value={text}
tabIndex={0}
autoComplete="false"
autoCapitalize="false"
autoCorrect="false"
autoSave="false"
placeholder=""
name="text"
autoFocus={isMobile() ? true : false}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onChange={handleChange}
/>
</foreignObject>
)
},

Wyświetl plik

@ -5,7 +5,10 @@
"scripts": {
"dev": "next dev",
"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": {
"@monaco-editor/react": "^4.1.3",
@ -46,7 +49,8 @@
"@types/react": "^17.0.5",
"@types/react-dom": "^17.0.3",
"@types/uuid": "^8.3.0",
"cypress": "^7.3.0",
"babel-jest": "^27.0.2",
"jest": "^27.0.4",
"monaco-editor": "^0.24.0",
"typescript": "^4.2.4"
}

Wyświetl plik

@ -19,6 +19,7 @@ import paste from './paste'
import rotateCcw from './rotate-ccw'
import stretch from './stretch'
import style from './style'
import mutate from './mutate'
import toggle from './toggle'
import transform from './transform'
import transformSingle from './transform-single'
@ -28,6 +29,7 @@ import edit from './edit'
import resetBounds from './reset-bounds'
const commands = {
mutate,
align,
arrow,
changePage,

Wyświetl plik

@ -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)
)
},
})
)
}

Wyświetl plik

@ -18,24 +18,12 @@ export default function transformCommand(
name: 'transform_shapes',
category: 'canvas',
do(data) {
const { type, shapeBounds } = after
const { shapeBounds } = after
const { shapes } = getPage(data)
for (let id in shapeBounds) {
const { initialShapeBounds: bounds } = after.shapeBounds[id]
const { initialShape, transformOrigin } = before.shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape)
.transform(shape, bounds, {
type,
initialShape,
transformOrigin,
scaleX,
scaleY,
})
.onSessionComplete(shape)
shapes[id] = shapeBounds[id].initialShape
}
updateParents(data, Object.keys(shapeBounds))

Wyświetl plik

@ -5,6 +5,7 @@ import commands from 'state/commands'
import { current, freeze } from 'immer'
import { getShapeUtils } from 'lib/shape-utils'
import {
deepClone,
getBoundsCenter,
getBoundsFromPoints,
getCommonBounds,
@ -103,26 +104,28 @@ export default class TransformSession extends BaseSession {
}
complete(data: Data) {
if (!this.snapshot.hasUnlockedShapes) return
const { initialShapes, hasUnlockedShapes } = this.snapshot
commands.transform(
data,
this.snapshot,
getTransformSnapshot(data, this.transformType),
this.scaleX,
this.scaleY
if (!hasUnlockedShapes) return
const page = getPage(data)
const finalShapes = initialShapes.map((shape) =>
deepClone(page.shapes[shape.id])
)
commands.mutate(data, initialShapes, finalShapes)
}
}
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
const cData = current(data)
const { currentPageId } = cData
const page = getPage(cData)
const { currentPageId } = data
const page = getPage(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)
.map((shape) => deepClone(shape))
const hasUnlockedShapes = initialShapes.length > 0
@ -151,6 +154,7 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
hasUnlockedShapes,
isAllAspectRatioLocked,
currentPageId,
initialShapes,
initialBounds: commonBounds,
shapeBounds: Object.fromEntries(
initialShapes.map((shape) => {

Wyświetl plik

@ -66,7 +66,6 @@ const initialData: Data = {
size: SizeStyle.Medium,
color: ColorStyle.Black,
dash: DashStyle.Solid,
fontSize: FontSize.Medium,
isFilled: false,
},
activeTool: 'select',
@ -131,7 +130,7 @@ const state = createState({
wait: 0.01,
if: 'hasSelection',
do: 'zoomCameraToSelectionActual',
else: ['zoomCameraToFit', 'zoomCameraToActual'],
else: ['zoomCameraToActual'],
},
on: {
COPIED: { if: 'hasSelection', do: 'copyToClipboard' },

Wyświetl plik

@ -47,5 +47,10 @@
## Clipboard
- [ ] Copy
- [ ] Paste
- [x] Copy
- [x] Paste shapes
- [ ] Paste as text
## Copy to SVG
- [ ] Copy to SVG

Wyświetl plik

@ -16,7 +16,7 @@ export interface Data {
isToolLocked: boolean
isPenLocked: boolean
}
currentStyle: ShapeStyles & TextStyles
currentStyle: ShapeStyles
activeTool: ShapeType | 'select'
brush?: Bounds
boundsRotation: number
@ -114,10 +114,6 @@ export type ShapeStyles = {
isFilled: boolean
}
export type TextStyles = {
fontSize: FontSize
}
export interface BaseShape {
id: string
seed: number
@ -180,7 +176,6 @@ export interface DrawShape extends BaseShape {
export interface ArrowShape extends BaseShape {
type: ShapeType.Arrow
points: number[][]
handles: Record<string, ShapeHandle>
bend: number
decorations?: {

Wyświetl plik

@ -1,8 +1,8 @@
import { Bounds } from "types"
import { Bounds } from 'types'
import {
intersectPolygonBounds,
intersectPolylineBounds,
} from "./intersections"
} from './intersections'
/**
* Get whether two bounds collide.

Wyświetl plik

@ -1850,3 +1850,65 @@ export function decompress(s: string) {
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
}

1526
yarn.lock

Plik diff jest za duży Load Diff