Merge pull request #29 from tldraw/feature-27-round-arrow-angle
Feature 27 round arrow angle
This commit is contained in:
commit
87551353fe
14 changed files with 290 additions and 467 deletions
|
@ -24,8 +24,8 @@
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.@(ts|tsx)": [
|
"*.@(ts|tsx)": [
|
||||||
"yarn lint",
|
"yarn format",
|
||||||
"yarn format"
|
"yarn lint"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"monaco-editor": "^0.25.2",
|
"monaco-editor": "^0.25.2",
|
||||||
"next": "^11.0.1",
|
"next": "^11.0.1",
|
||||||
"next-auth": "^3.27.0",
|
"next-auth": "^3.27.0",
|
||||||
"next-pwa": "^5.2.21",
|
"next-pwa": "^5.2.23",
|
||||||
"perfect-freehand": "^0.4.91",
|
"perfect-freehand": "^0.4.91",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import Command from './command'
|
|
||||||
import history from '../history'
|
|
||||||
import { Data } from 'types'
|
|
||||||
import tld from 'utils/tld'
|
|
||||||
import { ArrowSnapshot } from 'state/sessions/arrow-session'
|
|
||||||
|
|
||||||
export default function arrowCommand(
|
|
||||||
data: Data,
|
|
||||||
before: ArrowSnapshot,
|
|
||||||
after: ArrowSnapshot
|
|
||||||
): void {
|
|
||||||
history.execute(
|
|
||||||
data,
|
|
||||||
new Command({
|
|
||||||
name: 'point_arrow',
|
|
||||||
category: 'canvas',
|
|
||||||
manualSelection: true,
|
|
||||||
do(data, isInitial) {
|
|
||||||
if (isInitial) return
|
|
||||||
|
|
||||||
const { initialShape } = after
|
|
||||||
|
|
||||||
const page = tld.getPage(data)
|
|
||||||
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
|
||||||
|
|
||||||
const selectedIds = tld.getSelectedIds(data)
|
|
||||||
selectedIds.clear()
|
|
||||||
selectedIds.add(initialShape.id)
|
|
||||||
data.hoveredId = undefined
|
|
||||||
data.pointedId = undefined
|
|
||||||
},
|
|
||||||
undo(data) {
|
|
||||||
const { initialShape } = before
|
|
||||||
const shapes = tld.getPage(data).shapes
|
|
||||||
|
|
||||||
delete shapes[initialShape.id]
|
|
||||||
|
|
||||||
const selectedIds = tld.getSelectedIds(data)
|
|
||||||
selectedIds.clear()
|
|
||||||
data.hoveredId = undefined
|
|
||||||
data.pointedId = undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
import Command from './command'
|
|
||||||
import history from '../history'
|
|
||||||
import { Data } from 'types'
|
|
||||||
import tld from 'utils/tld'
|
|
||||||
import { HandleSnapshot } from 'state/sessions/handle-session'
|
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
|
||||||
|
|
||||||
export default function handleCommand(
|
|
||||||
data: Data,
|
|
||||||
before: HandleSnapshot,
|
|
||||||
after: HandleSnapshot
|
|
||||||
): void {
|
|
||||||
history.execute(
|
|
||||||
data,
|
|
||||||
new Command({
|
|
||||||
name: 'moved_handle',
|
|
||||||
category: 'canvas',
|
|
||||||
do(data) {
|
|
||||||
const { initialShape } = after
|
|
||||||
|
|
||||||
const page = tld.getPage(data)
|
|
||||||
const shape = page.shapes[initialShape.id]
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.onHandleChange(shape, initialShape.handles)
|
|
||||||
.onSessionComplete(shape)
|
|
||||||
},
|
|
||||||
undo(data) {
|
|
||||||
const { initialShape } = before
|
|
||||||
|
|
||||||
const page = tld.getPage(data)
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import align from './align'
|
import align from './align'
|
||||||
import arrow from './arrow'
|
|
||||||
import changePage from './change-page'
|
import changePage from './change-page'
|
||||||
import createPage from './create-page'
|
import createPage from './create-page'
|
||||||
import deletePage from './delete-page'
|
import deletePage from './delete-page'
|
||||||
|
@ -11,7 +10,6 @@ import duplicate from './duplicate'
|
||||||
import edit from './edit'
|
import edit from './edit'
|
||||||
import generate from './generate'
|
import generate from './generate'
|
||||||
import group from './group'
|
import group from './group'
|
||||||
import handle from './handle'
|
|
||||||
import move from './move'
|
import move from './move'
|
||||||
import moveToPage from './move-to-page'
|
import moveToPage from './move-to-page'
|
||||||
import mutate from './mutate'
|
import mutate from './mutate'
|
||||||
|
@ -30,7 +28,6 @@ import ungroup from './ungroup'
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
align,
|
align,
|
||||||
arrow,
|
|
||||||
changePage,
|
changePage,
|
||||||
createPage,
|
createPage,
|
||||||
deletePage,
|
deletePage,
|
||||||
|
@ -42,7 +39,6 @@ const commands = {
|
||||||
edit,
|
edit,
|
||||||
generate,
|
generate,
|
||||||
group,
|
group,
|
||||||
handle,
|
|
||||||
move,
|
move,
|
||||||
moveToPage,
|
moveToPage,
|
||||||
mutate,
|
mutate,
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
import { ArrowShape, Data } from 'types'
|
|
||||||
import vec from 'utils/vec'
|
|
||||||
import BaseSession from './base-session'
|
|
||||||
import commands from 'state/commands'
|
|
||||||
import { deepClone, getBoundsFromPoints, setToArray } from 'utils'
|
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
|
||||||
import tld from 'utils/tld'
|
|
||||||
|
|
||||||
export default class ArrowSession extends BaseSession {
|
|
||||||
points: number[][]
|
|
||||||
origin: number[]
|
|
||||||
snapshot: ArrowSnapshot
|
|
||||||
isLocked: boolean
|
|
||||||
lockedDirection: 'horizontal' | 'vertical'
|
|
||||||
|
|
||||||
constructor(data: Data, id: string, point: number[], isLocked: boolean) {
|
|
||||||
super(data)
|
|
||||||
isLocked
|
|
||||||
this.origin = point
|
|
||||||
this.points = [[0, 0]]
|
|
||||||
this.snapshot = getArrowSnapshot(data, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data: Data, point: number[], isLocked = false): void {
|
|
||||||
const { id } = this.snapshot
|
|
||||||
|
|
||||||
const delta = vec.vec(this.origin, point)
|
|
||||||
|
|
||||||
if (isLocked) {
|
|
||||||
if (!this.isLocked && this.points.length > 1) {
|
|
||||||
this.isLocked = true
|
|
||||||
|
|
||||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
|
||||||
this.lockedDirection = 'vertical'
|
|
||||||
} else {
|
|
||||||
this.lockedDirection = 'horizontal'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.isLocked) {
|
|
||||||
this.isLocked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isLocked) {
|
|
||||||
if (this.lockedDirection === 'vertical') {
|
|
||||||
point[0] = this.origin[0]
|
|
||||||
} else {
|
|
||||||
point[1] = this.origin[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
|
||||||
|
|
||||||
getShapeUtils(shape).onHandleChange(shape, {
|
|
||||||
end: {
|
|
||||||
...shape.handles.end,
|
|
||||||
point: vec.sub(point, shape.point),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
tld.updateParents(data, [shape.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(data: Data): void {
|
|
||||||
const { id, initialShape } = this.snapshot
|
|
||||||
|
|
||||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.onHandleChange(shape, { end: initialShape.handles.end })
|
|
||||||
.setProperty(shape, 'point', initialShape.point)
|
|
||||||
|
|
||||||
tld.updateParents(data, [shape.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
complete(data: Data): void {
|
|
||||||
const { id } = this.snapshot
|
|
||||||
|
|
||||||
const shape = tld.getPage(data).shapes[id] as ArrowShape
|
|
||||||
|
|
||||||
const { start, end, bend } = shape.handles
|
|
||||||
|
|
||||||
// Normalize point and handles
|
|
||||||
|
|
||||||
const bounds = getBoundsFromPoints([start.point, end.point])
|
|
||||||
const corner = [bounds.minX, bounds.minY]
|
|
||||||
|
|
||||||
const newPoint = vec.add(shape.point, corner)
|
|
||||||
|
|
||||||
const nextHandles = {
|
|
||||||
start: { ...start, point: vec.sub(start.point, corner) },
|
|
||||||
end: { ...end, point: vec.sub(end.point, corner) },
|
|
||||||
bend: { ...bend, point: vec.sub(bend.point, corner) },
|
|
||||||
}
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.setProperty(shape, 'handles', nextHandles)
|
|
||||||
.setProperty(shape, 'point', newPoint)
|
|
||||||
.onHandleChange(shape, nextHandles)
|
|
||||||
|
|
||||||
commands.arrow(
|
|
||||||
data,
|
|
||||||
this.snapshot,
|
|
||||||
getArrowSnapshot(data, this.snapshot.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function getArrowSnapshot(data: Data, id: string) {
|
|
||||||
const initialShape = deepClone(tld.getPage(data).shapes[id]) as ArrowShape
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
initialShape,
|
|
||||||
selectedIds: setToArray(tld.getSelectedIds(data)),
|
|
||||||
currentPageId: data.currentPageId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArrowSnapshot = ReturnType<typeof getArrowSnapshot>
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Data } from 'types'
|
import { Data, Shape } from 'types'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import BaseSession from './base-session'
|
import BaseSession from './base-session'
|
||||||
import commands from 'state/commands'
|
import commands from 'state/commands'
|
||||||
|
@ -9,73 +9,58 @@ import { deepClone } from 'utils'
|
||||||
export default class HandleSession extends BaseSession {
|
export default class HandleSession extends BaseSession {
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
origin: number[]
|
origin: number[]
|
||||||
snapshot: HandleSnapshot
|
shiftKey: boolean
|
||||||
|
initialShape: Shape
|
||||||
|
handleId: string
|
||||||
|
|
||||||
constructor(data: Data, shapeId: string, handleId: string, point: number[]) {
|
constructor(data: Data, shapeId: string, handleId: string, point: number[]) {
|
||||||
super(data)
|
super(data)
|
||||||
this.origin = point
|
this.origin = point
|
||||||
this.snapshot = getHandleSnapshot(data, shapeId, handleId)
|
this.handleId = handleId
|
||||||
|
this.initialShape = deepClone(tld.getShape(data, shapeId))
|
||||||
}
|
}
|
||||||
|
|
||||||
update(data: Data, point: number[], isAligned: boolean): void {
|
update(
|
||||||
const { handleId, initialShape } = this.snapshot
|
data: Data,
|
||||||
const shape = tld.getPage(data).shapes[initialShape.id]
|
point: number[],
|
||||||
|
shiftKey: boolean,
|
||||||
|
altKey: boolean,
|
||||||
|
metaKey: boolean
|
||||||
|
): void {
|
||||||
|
const shape = tld.getPage(data).shapes[this.initialShape.id]
|
||||||
|
|
||||||
|
this.shiftKey = shiftKey
|
||||||
|
|
||||||
const delta = vec.vec(this.origin, point)
|
const delta = vec.vec(this.origin, point)
|
||||||
|
|
||||||
if (isAligned) {
|
const handles = this.initialShape.handles
|
||||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
|
||||||
delta[0] = 0
|
|
||||||
} else {
|
|
||||||
delta[1] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handles = initialShape.handles
|
getShapeUtils(shape).onHandleChange(
|
||||||
|
shape,
|
||||||
// rotate the delta ?
|
{
|
||||||
// rotate the handle ?
|
[this.handleId]: {
|
||||||
// rotate the shape around the previous center point
|
...handles[this.handleId],
|
||||||
|
point: vec.round(vec.add(handles[this.handleId].point, delta)), // vec.rot(delta, shape.rotation)),
|
||||||
getShapeUtils(shape).onHandleChange(shape, {
|
},
|
||||||
[handleId]: {
|
|
||||||
...handles[handleId],
|
|
||||||
point: vec.add(handles[handleId].point, delta), // vec.rot(delta, shape.rotation)),
|
|
||||||
},
|
},
|
||||||
})
|
{ delta, shiftKey, altKey, metaKey }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const { initialShape } = this.snapshot
|
tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
|
||||||
tld.getPage(data).shapes[initialShape.id] = initialShape
|
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data): void {
|
complete(data: Data): void {
|
||||||
commands.handle(
|
const before = this.initialShape
|
||||||
data,
|
const after = deepClone(tld.getShape(data, before.id))
|
||||||
this.snapshot,
|
commands.mutate(data, [before], [after])
|
||||||
getHandleSnapshot(
|
|
||||||
data,
|
|
||||||
this.snapshot.initialShape.id,
|
|
||||||
this.snapshot.handleId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export function getHandleSnapshot(
|
export function getHandleSnapshot(data: Data, shapeId: string) {
|
||||||
data: Data,
|
return deepClone(tld.getShape(data, shapeId))
|
||||||
shapeId: string,
|
|
||||||
handleId: string
|
|
||||||
) {
|
|
||||||
const initialShape = deepClone(tld.getShape(data, shapeId))
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentPageId: data.currentPageId,
|
|
||||||
handleId,
|
|
||||||
initialShape,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HandleSnapshot = ReturnType<typeof getHandleSnapshot>
|
export type HandleSnapshot = ReturnType<typeof getHandleSnapshot>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import ArrowSession from './arrow-session'
|
|
||||||
import BaseSession from './base-session'
|
import BaseSession from './base-session'
|
||||||
import BrushSession from './brush-session'
|
import BrushSession from './brush-session'
|
||||||
import DirectionSession from './direction-session'
|
import DirectionSession from './direction-session'
|
||||||
|
@ -11,7 +10,6 @@ import HandleSession from './handle-session'
|
||||||
import EditSession from './edit-session'
|
import EditSession from './edit-session'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArrowSession,
|
|
||||||
BaseSession,
|
BaseSession,
|
||||||
BrushSession,
|
BrushSession,
|
||||||
DirectionSession,
|
DirectionSession,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { SVGProps } from 'react'
|
|
||||||
import { ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
|
import { ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
|
||||||
|
|
||||||
export const strokes: Record<ColorStyle, string> = {
|
export const strokes: Record<ColorStyle, string> = {
|
||||||
|
@ -58,9 +57,11 @@ export function getFontStyle(scale: number, style: ShapeStyles): string {
|
||||||
return `${fontSize * scale}px/1.4 Verveine Regular`
|
return `${fontSize * scale}px/1.4 Verveine Regular`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShapeStyle(
|
export function getShapeStyle(style: ShapeStyles): {
|
||||||
style: ShapeStyles
|
stroke: string
|
||||||
): Partial<SVGProps<SVGUseElement>> {
|
fill: string
|
||||||
|
strokeWidth: number
|
||||||
|
} {
|
||||||
const { color, size, isFilled } = style
|
const { color, size, isFilled } = style
|
||||||
|
|
||||||
const strokeWidth = getStrokeWidth(size)
|
const strokeWidth = getStrokeWidth(size)
|
||||||
|
|
|
@ -11,6 +11,9 @@ import {
|
||||||
circleFromThreePoints,
|
circleFromThreePoints,
|
||||||
isAngleBetween,
|
isAngleBetween,
|
||||||
getPerfectDashProps,
|
getPerfectDashProps,
|
||||||
|
clampToRotationToSegments,
|
||||||
|
lerpAngles,
|
||||||
|
clamp,
|
||||||
getFromCache,
|
getFromCache,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +32,7 @@ import getStroke from 'perfect-freehand'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
||||||
const pathCache = new WeakMap<ArrowShape, string>([])
|
const pathCache = new WeakMap<ArrowShape['handles'], string>([])
|
||||||
|
|
||||||
// A cache for semi-expensive circles calculated from three points
|
// A cache for semi-expensive circles calculated from three points
|
||||||
function getCtp(shape: ArrowShape) {
|
function getCtp(shape: ArrowShape) {
|
||||||
|
@ -94,8 +97,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// shape.handles.bend.point = getBendPoint(shape)
|
|
||||||
|
|
||||||
return shape
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -107,36 +108,31 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
const { id, bend, handles, style } = shape
|
const { id, bend, handles, style } = shape
|
||||||
const { start, end, bend: _bend } = handles
|
const { start, end, bend: _bend } = handles
|
||||||
|
|
||||||
const isStraightLine = vec.isEqual(
|
const isStraightLine =
|
||||||
_bend.point,
|
vec.dist(_bend.point, vec.round(vec.med(start.point, end.point))) < 1
|
||||||
vec.med(start.point, end.point)
|
|
||||||
)
|
const isDraw = shape.style.dash === DashStyle.Draw
|
||||||
|
|
||||||
const styles = getShapeStyle(style)
|
const styles = getShapeStyle(style)
|
||||||
|
|
||||||
const strokeWidth = +styles.strokeWidth
|
const { strokeWidth } = styles
|
||||||
|
|
||||||
const sw = strokeWidth * 1.618
|
|
||||||
|
|
||||||
const arrowDist = vec.dist(start.point, end.point)
|
const arrowDist = vec.dist(start.point, end.point)
|
||||||
|
|
||||||
|
const arrowHeadlength = Math.min(arrowDist / 3, strokeWidth * 8)
|
||||||
|
|
||||||
let shaftPath: JSX.Element
|
let shaftPath: JSX.Element
|
||||||
let startAngle: number
|
let insetStart: number[]
|
||||||
let endAngle: number
|
let insetEnd: number[]
|
||||||
|
|
||||||
if (isStraightLine) {
|
if (isStraightLine) {
|
||||||
const straight_sw =
|
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
|
||||||
strokeWidth *
|
|
||||||
(style.dash === DashStyle.Draw && bend === 0 ? 0.5 : 1.618)
|
|
||||||
|
|
||||||
if (shape.style.dash === DashStyle.Draw && !pathCache.has(shape)) {
|
const path = isDraw
|
||||||
renderFreehandArrowShaft(shape)
|
? getFromCache(pathCache, shape.handles, (cache) =>
|
||||||
}
|
cache.set(shape.handles, renderFreehandArrowShaft(shape))
|
||||||
|
)
|
||||||
const path =
|
: 'M' + vec.round(start.point) + 'L' + vec.round(end.point)
|
||||||
shape.style.dash === DashStyle.Draw
|
|
||||||
? pathCache.get(shape)
|
|
||||||
: 'M' + start.point + 'L' + end.point
|
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
arrowDist,
|
arrowDist,
|
||||||
|
@ -145,9 +141,8 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
|
||||||
startAngle = Math.PI
|
insetStart = vec.nudge(start.point, end.point, arrowHeadlength)
|
||||||
|
insetEnd = vec.nudge(end.point, start.point, arrowHeadlength)
|
||||||
endAngle = 0
|
|
||||||
|
|
||||||
// Straight arrow path
|
// Straight arrow path
|
||||||
shaftPath = (
|
shaftPath = (
|
||||||
|
@ -165,7 +160,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
d={path}
|
d={path}
|
||||||
fill={styles.stroke}
|
fill={styles.stroke}
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={straight_sw}
|
strokeWidth={sw}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
|
@ -175,29 +170,39 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
} else {
|
} else {
|
||||||
const circle = getCtp(shape)
|
const circle = getCtp(shape)
|
||||||
|
|
||||||
const path = getArrowArcPath(start, end, circle, bend)
|
const sw = strokeWidth * (isDraw ? 0.618 : 1.618)
|
||||||
|
|
||||||
|
const path = isDraw
|
||||||
|
? getFromCache(pathCache, shape.handles, (cache) =>
|
||||||
|
cache.set(
|
||||||
|
shape.handles,
|
||||||
|
renderCurvedFreehandArrowShaft(shape, circle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: getArrowArcPath(start, end, circle, bend)
|
||||||
|
|
||||||
|
const arcLength = getArcLength(
|
||||||
|
[circle[0], circle[1]],
|
||||||
|
circle[2],
|
||||||
|
start.point,
|
||||||
|
end.point
|
||||||
|
)
|
||||||
|
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
getArcLength(
|
arcLength - 1,
|
||||||
[circle[0], circle[1]],
|
|
||||||
circle[2],
|
|
||||||
start.point,
|
|
||||||
end.point
|
|
||||||
) - 1,
|
|
||||||
sw,
|
sw,
|
||||||
shape.style.dash,
|
shape.style.dash,
|
||||||
2
|
2
|
||||||
)
|
)
|
||||||
|
|
||||||
startAngle =
|
const center = [circle[0], circle[1]]
|
||||||
vec.angle([circle[0], circle[1]], start.point) -
|
const radius = circle[2]
|
||||||
vec.angle(end.point, start.point) +
|
const sa = vec.angle(center, start.point)
|
||||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
const ea = vec.angle(center, end.point)
|
||||||
|
const t = arrowHeadlength / Math.abs(arcLength)
|
||||||
|
|
||||||
endAngle =
|
insetStart = vec.nudgeAtAngle(center, lerpAngles(sa, ea, t), radius)
|
||||||
vec.angle([circle[0], circle[1]], end.point) -
|
insetEnd = vec.nudgeAtAngle(center, lerpAngles(ea, sa, t), radius)
|
||||||
vec.angle(start.point, end.point) +
|
|
||||||
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
|
|
||||||
|
|
||||||
// Curved arrow path
|
// Curved arrow path
|
||||||
shaftPath = (
|
shaftPath = (
|
||||||
|
@ -213,7 +218,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
fill="none"
|
fill={isDraw ? styles.stroke : 'none'}
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={sw}
|
strokeWidth={sw}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
|
@ -229,20 +234,20 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
{shaftPath}
|
{shaftPath}
|
||||||
{shape.decorations.start === Decoration.Arrow && (
|
{shape.decorations.start === Decoration.Arrow && (
|
||||||
<path
|
<path
|
||||||
d={getArrowHeadPath(shape, start.point, startAngle)}
|
d={getArrowHeadPath(shape, start.point, insetStart)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={sw}
|
strokeWidth={strokeWidth * 1.618}
|
||||||
strokeDashoffset="none"
|
strokeDashoffset="none"
|
||||||
strokeDasharray="none"
|
strokeDasharray="none"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shape.decorations.end === Decoration.Arrow && (
|
{shape.decorations.end === Decoration.Arrow && (
|
||||||
<path
|
<path
|
||||||
d={getArrowHeadPath(shape, end.point, endAngle)}
|
d={getArrowHeadPath(shape, end.point, insetEnd)}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={sw}
|
strokeWidth={strokeWidth * 1.618}
|
||||||
strokeDashoffset="none"
|
strokeDashoffset="none"
|
||||||
strokeDasharray="none"
|
strokeDasharray="none"
|
||||||
/>
|
/>
|
||||||
|
@ -258,7 +263,10 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
end.point = vec.rotWith(end.point, mp, delta)
|
end.point = vec.rotWith(end.point, mp, delta)
|
||||||
bend.point = vec.rotWith(bend.point, mp, delta)
|
bend.point = vec.rotWith(bend.point, mp, delta)
|
||||||
|
|
||||||
this.onHandleChange(shape, shape.handles)
|
this.onHandleChange(shape, shape.handles, {
|
||||||
|
delta: [0, 0],
|
||||||
|
shiftKey: false,
|
||||||
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
@ -270,7 +278,10 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
end.point = vec.rotWith(end.point, mp, delta)
|
end.point = vec.rotWith(end.point, mp, delta)
|
||||||
bend.point = vec.rotWith(bend.point, mp, delta)
|
bend.point = vec.rotWith(bend.point, mp, delta)
|
||||||
|
|
||||||
this.onHandleChange(shape, shape.handles)
|
this.onHandleChange(shape, shape.handles, {
|
||||||
|
delta: [0, 0],
|
||||||
|
shiftKey: false,
|
||||||
|
})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
@ -402,42 +413,67 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
onHandleChange(shape, handles) {
|
onHandleChange(shape, handles, { shiftKey }) {
|
||||||
|
// Apple changes to the handles
|
||||||
for (const id in handles) {
|
for (const id in handles) {
|
||||||
const handle = handles[id]
|
const handle = handles[id]
|
||||||
|
|
||||||
shape.handles[handle.id] = handle
|
shape.handles[handle.id] = handle
|
||||||
}
|
}
|
||||||
|
|
||||||
const midPoint = vec.med(shape.handles.start.point, shape.handles.end.point)
|
// If the user is holding shift, we want to snap the handles to angles
|
||||||
|
for (const id in handles) {
|
||||||
|
if ((id === 'start' || id === 'end') && shiftKey) {
|
||||||
|
const point = handles[id].point
|
||||||
|
const other = id === 'start' ? shape.handles.end : shape.handles.start
|
||||||
|
const angle = vec.angle(other.point, point)
|
||||||
|
const distance = vec.dist(other.point, point)
|
||||||
|
const newAngle = clampToRotationToSegments(angle, 24)
|
||||||
|
|
||||||
|
shape.handles[id].point = vec.nudgeAtAngle(
|
||||||
|
other.point,
|
||||||
|
newAngle,
|
||||||
|
distance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is moving the bend handle, we want to move the bend point
|
||||||
if ('bend' in handles) {
|
if ('bend' in handles) {
|
||||||
const { start, end, bend } = shape.handles
|
const { start, end, bend } = shape.handles
|
||||||
|
|
||||||
const dist = vec.dist(start.point, end.point)
|
const distance = vec.dist(start.point, end.point)
|
||||||
|
|
||||||
const midPoint = vec.med(start.point, end.point)
|
const midPoint = vec.med(start.point, end.point)
|
||||||
|
const angle = vec.angle(start.point, end.point)
|
||||||
const u = vec.uni(vec.vec(start.point, end.point))
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
const ap = vec.add(midPoint, vec.mul(vec.per(u), dist / 2))
|
|
||||||
const bp = vec.sub(midPoint, vec.mul(vec.per(u), dist / 2))
|
|
||||||
|
|
||||||
bend.point = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
// Create a line segment perendicular to the line between the start and end points
|
||||||
shape.bend = vec.dist(bend.point, midPoint) / (dist / 2)
|
const ap = vec.add(midPoint, vec.mul(vec.per(u), distance / 2))
|
||||||
|
const bp = vec.sub(midPoint, vec.mul(vec.per(u), distance / 2))
|
||||||
|
|
||||||
const sa = vec.angle(end.point, start.point)
|
const bendPoint = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||||
const la = sa - Math.PI / 2
|
|
||||||
|
|
||||||
if (isAngleBetween(sa, la, vec.angle(end.point, bend.point))) {
|
// Find the distance between the midpoint and the nearest point on the
|
||||||
|
// line segment to the bend handle's dragged point
|
||||||
|
const bendDist = vec.dist(midPoint, bendPoint)
|
||||||
|
|
||||||
|
// The shape's "bend" is the ratio of the bend to the distance between
|
||||||
|
// the start and end points. If the bend is below a certain amount, the
|
||||||
|
// bend should be zero.
|
||||||
|
shape.bend = clamp(bendDist / (distance / 2), -0.99, 0.99)
|
||||||
|
|
||||||
|
// If the point is to the left of the line segment, we make the bend
|
||||||
|
// negative, otherwise it's positive.
|
||||||
|
const angleToBend = vec.angle(start.point, bendPoint)
|
||||||
|
|
||||||
|
if (isAngleBetween(angle, angle + Math.PI, angleToBend)) {
|
||||||
shape.bend *= -1
|
shape.bend *= -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shape.handles.start.point = vec.round(shape.handles.start.point)
|
||||||
|
shape.handles.end.point = vec.round(shape.handles.end.point)
|
||||||
shape.handles.bend.point = getBendPoint(shape)
|
shape.handles.bend.point = getBendPoint(shape)
|
||||||
|
|
||||||
if (vec.isEqual(shape.handles.bend.point, midPoint)) {
|
|
||||||
shape.bend = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -450,9 +486,9 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
|
|
||||||
const { start, end, bend } = shape.handles
|
const { start, end, bend } = shape.handles
|
||||||
|
|
||||||
start.point = vec.sub(start.point, offset)
|
start.point = vec.round(vec.sub(start.point, offset))
|
||||||
end.point = vec.sub(end.point, offset)
|
end.point = vec.round(vec.sub(end.point, offset))
|
||||||
bend.point = vec.sub(bend.point, offset)
|
bend.point = vec.round(vec.sub(bend.point, offset))
|
||||||
|
|
||||||
shape.handles = { ...shape.handles }
|
shape.handles = { ...shape.handles }
|
||||||
|
|
||||||
|
@ -499,9 +535,13 @@ function getBendPoint(shape: ArrowShape) {
|
||||||
const bendDist = (dist / 2) * shape.bend
|
const bendDist = (dist / 2) * shape.bend
|
||||||
const u = vec.uni(vec.vec(start.point, end.point))
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
|
|
||||||
return Math.abs(bendDist) < 10
|
const point = vec.round(
|
||||||
? midPoint
|
Math.abs(bendDist) < 10
|
||||||
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
? midPoint
|
||||||
|
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||||
|
)
|
||||||
|
|
||||||
|
return point
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFreehandArrowShaft(shape: ArrowShape) {
|
function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||||
|
@ -516,11 +556,11 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||||
|
|
||||||
const stroke = getStroke(
|
const stroke = getStroke(
|
||||||
[
|
[
|
||||||
start.point,
|
|
||||||
...vec.pointsBetween(start.point, end.point),
|
...vec.pointsBetween(start.point, end.point),
|
||||||
end.point,
|
end.point,
|
||||||
end.point,
|
end.point,
|
||||||
end.point,
|
end.point,
|
||||||
|
end.point,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
size: strokeWidth / 2,
|
size: strokeWidth / 2,
|
||||||
|
@ -529,43 +569,79 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||||
end: { taper: 1 },
|
end: { taper: 1 },
|
||||||
start: { taper: 1 + 32 * (st * st * st) },
|
start: { taper: 1 + 32 * (st * st * st) },
|
||||||
simulatePressure: true,
|
simulatePressure: true,
|
||||||
|
last: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
pathCache.set(shape, getSvgPathFromStroke(stroke))
|
const path = getSvgPathFromStroke(stroke)
|
||||||
|
|
||||||
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArrowHeadPath(shape: ArrowShape, point: number[], angle = 0) {
|
function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
|
||||||
const { left, right } = getArrowHeadPoints(shape, point, angle)
|
const { style, id } = shape
|
||||||
|
const { start, end } = shape.handles
|
||||||
|
|
||||||
|
const getRandom = rng(id)
|
||||||
|
|
||||||
|
const strokeWidth = +getShapeStyle(style).strokeWidth * 2
|
||||||
|
|
||||||
|
const st = Math.abs(getRandom())
|
||||||
|
|
||||||
|
const center = [circle[0], circle[1]]
|
||||||
|
const radius = circle[2]
|
||||||
|
|
||||||
|
const startAngle = vec.angle(center, start.point)
|
||||||
|
|
||||||
|
const endAngle = vec.angle(center, end.point)
|
||||||
|
|
||||||
|
const points: number[][] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < 21; i++) {
|
||||||
|
const t = i / 20
|
||||||
|
const angle = lerpAngles(startAngle, endAngle, t)
|
||||||
|
points.push(vec.round(vec.nudgeAtAngle(center, angle, radius)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const stroke = getStroke([...points, end.point, end.point], {
|
||||||
|
size: strokeWidth / 2,
|
||||||
|
thinning: 0.5 + getRandom() * 0.3,
|
||||||
|
easing: (t) => t * t,
|
||||||
|
end: {
|
||||||
|
taper: shape.decorations.end ? 1 : 1 + strokeWidth * 5 * (st * st * st),
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
taper: shape.decorations.start ? 1 : 1 + strokeWidth * 5 * (st * st * st),
|
||||||
|
},
|
||||||
|
simulatePressure: true,
|
||||||
|
streamline: 0.01,
|
||||||
|
last: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const path = getSvgPathFromStroke(stroke)
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrowHeadPath(shape: ArrowShape, point: number[], inset: number[]) {
|
||||||
|
const { left, right } = getArrowHeadPoints(shape, point, inset)
|
||||||
return ['M', left, 'L', point, right].join(' ')
|
return ['M', left, 'L', point, right].join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) {
|
function getArrowHeadPoints(
|
||||||
const { start, end } = shape.handles
|
shape: ArrowShape,
|
||||||
|
point: number[],
|
||||||
const stroke = +getShapeStyle(shape.style).strokeWidth * 2
|
inset: number[]
|
||||||
|
) {
|
||||||
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), angle)
|
|
||||||
|
|
||||||
// Use the shape's random seed to create minor offsets for the angles
|
// Use the shape's random seed to create minor offsets for the angles
|
||||||
const getRandom = rng(shape.id)
|
const getRandom = rng(shape.id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: vec.add(
|
left: vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
|
||||||
|
right: vec.rotWith(
|
||||||
|
inset,
|
||||||
point,
|
point,
|
||||||
vec.rot(v, Math.PI / 6 + (Math.PI / 12) * getRandom())
|
-Math.PI / 6 + (Math.PI / 12) * getRandom()
|
||||||
),
|
|
||||||
right: vec.add(
|
|
||||||
point,
|
|
||||||
vec.rot(v, -(Math.PI / 6) + (Math.PI / 12) * getRandom())
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -846,7 +846,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
onExit: 'completeSession',
|
onExit: 'completeSession',
|
||||||
onEnter: 'startArrowSession',
|
onEnter: 'startArrowHandleSession',
|
||||||
on: {
|
on: {
|
||||||
STOPPED_POINTING: [
|
STOPPED_POINTING: [
|
||||||
'completeSession',
|
'completeSession',
|
||||||
|
@ -862,10 +862,10 @@ const state = createState({
|
||||||
to: 'arrow.creating',
|
to: 'arrow.creating',
|
||||||
else: { to: 'selecting' },
|
else: { to: 'selecting' },
|
||||||
},
|
},
|
||||||
PRESSED_SHIFT: 'keyUpdateArrowSession',
|
PRESSED_SHIFT: 'keyUpdateHandleSession',
|
||||||
RELEASED_SHIFT: 'keyUpdateArrowSession',
|
RELEASED_SHIFT: 'keyUpdateHandleSession',
|
||||||
MOVED_POINTER: 'updateArrowSession',
|
MOVED_POINTER: 'updateHandleSession',
|
||||||
PANNED_CAMERA: 'updateArrowSession',
|
PANNED_CAMERA: 'updateHandleSession',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1186,7 +1186,7 @@ const state = createState({
|
||||||
return tld.getShape(data, payload.target) !== undefined
|
return tld.getShape(data, payload.target) !== undefined
|
||||||
},
|
},
|
||||||
isPointingRotationHandle(
|
isPointingRotationHandle(
|
||||||
data,
|
_data,
|
||||||
payload: { target: Edge | Corner | 'rotate' }
|
payload: { target: Edge | Corner | 'rotate' }
|
||||||
) {
|
) {
|
||||||
return payload.target === 'rotate'
|
return payload.target === 'rotate'
|
||||||
|
@ -1347,7 +1347,7 @@ const state = createState({
|
||||||
|
|
||||||
createShape(data, payload, type: ShapeType) {
|
createShape(data, payload, type: ShapeType) {
|
||||||
const style = deepClone(data.currentStyle)
|
const style = deepClone(data.currentStyle)
|
||||||
let point = vec.round(tld.screenToWorld(payload.point, data))
|
let point = tld.screenToWorld(payload.point, data)
|
||||||
|
|
||||||
if (type === ShapeType.Text) {
|
if (type === ShapeType.Text) {
|
||||||
point = vec.sub(point, vec.mul([0, 1], getFontSize(style.size) * 0.8))
|
point = vec.sub(point, vec.mul([0, 1], getFontSize(style.size) * 0.8))
|
||||||
|
@ -1356,7 +1356,7 @@ const state = createState({
|
||||||
const shape = createShape(type, {
|
const shape = createShape(type, {
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
parentId: data.currentPageId,
|
parentId: data.currentPageId,
|
||||||
point,
|
point: vec.round(point),
|
||||||
style: deepClone(data.currentStyle),
|
style: deepClone(data.currentStyle),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1484,19 +1484,23 @@ const state = createState({
|
||||||
},
|
},
|
||||||
keyUpdateHandleSession(
|
keyUpdateHandleSession(
|
||||||
data,
|
data,
|
||||||
payload: { shiftKey: boolean; altKey: boolean }
|
payload: { shiftKey: boolean; altKey: boolean; metaKey: boolean }
|
||||||
) {
|
) {
|
||||||
session.update<Sessions.HandleSession>(
|
session.update<Sessions.HandleSession>(
|
||||||
data,
|
data,
|
||||||
tld.screenToWorld(inputs.pointer.point, data),
|
tld.screenToWorld(inputs.pointer.point, data),
|
||||||
payload.shiftKey
|
payload.shiftKey,
|
||||||
|
payload.altKey,
|
||||||
|
payload.metaKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateHandleSession(data, payload: PointerInfo) {
|
updateHandleSession(data, payload: PointerInfo) {
|
||||||
session.update<Sessions.HandleSession>(
|
session.update<Sessions.HandleSession>(
|
||||||
data,
|
data,
|
||||||
tld.screenToWorld(payload.point, data),
|
tld.screenToWorld(payload.point, data),
|
||||||
payload.shiftKey
|
payload.shiftKey,
|
||||||
|
payload.altKey,
|
||||||
|
payload.metaKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1582,32 +1586,19 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Arrow
|
// Arrow
|
||||||
startArrowSession(data, payload: PointerInfo) {
|
startArrowHandleSession(data) {
|
||||||
const id = Array.from(tld.getSelectedIds(data).values())[0]
|
const shapeId = Array.from(tld.getSelectedIds(data).values())[0]
|
||||||
|
const handleId = 'end'
|
||||||
|
|
||||||
session.begin(
|
session.begin(
|
||||||
new Sessions.ArrowSession(
|
new Sessions.HandleSession(
|
||||||
data,
|
data,
|
||||||
id,
|
shapeId,
|
||||||
tld.screenToWorld(inputs.pointer.origin, data),
|
handleId,
|
||||||
payload.shiftKey
|
tld.screenToWorld(inputs.pointer.origin, data)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
keyUpdateArrowSession(data, payload: PointerInfo) {
|
|
||||||
session.update<Sessions.ArrowSession>(
|
|
||||||
data,
|
|
||||||
tld.screenToWorld(inputs.pointer.point, data),
|
|
||||||
payload.shiftKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
updateArrowSession(data, payload: PointerInfo) {
|
|
||||||
session.update<Sessions.ArrowSession>(
|
|
||||||
data,
|
|
||||||
tld.screenToWorld(payload.point, data),
|
|
||||||
payload.shiftKey
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
/* -------------------- Selection ------------------- */
|
/* -------------------- Selection ------------------- */
|
||||||
|
|
||||||
|
|
8
types.ts
8
types.ts
|
@ -577,7 +577,13 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
onHandleChange(
|
onHandleChange(
|
||||||
this: ShapeUtility<K>,
|
this: ShapeUtility<K>,
|
||||||
shape: Mutable<K>,
|
shape: Mutable<K>,
|
||||||
handle: Partial<K['handles']>
|
handle: Partial<K['handles']>,
|
||||||
|
info?: Partial<{
|
||||||
|
delta: number[]
|
||||||
|
shiftKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
}>
|
||||||
): ShapeUtility<K>
|
): ShapeUtility<K>
|
||||||
|
|
||||||
onDoublePointHandle(
|
onDoublePointHandle(
|
||||||
|
|
|
@ -196,20 +196,6 @@ export function getClosestPointOnCircle(
|
||||||
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
|
return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r))
|
||||||
}
|
}
|
||||||
|
|
||||||
function det(
|
|
||||||
a: number,
|
|
||||||
b: number,
|
|
||||||
c: number,
|
|
||||||
d: number,
|
|
||||||
e: number,
|
|
||||||
f: number,
|
|
||||||
g: number,
|
|
||||||
h: number,
|
|
||||||
i: number
|
|
||||||
): number {
|
|
||||||
return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a circle from three points.
|
* Get a circle from three points.
|
||||||
* @param A
|
* @param A
|
||||||
|
@ -222,47 +208,27 @@ export function circleFromThreePoints(
|
||||||
B: number[],
|
B: number[],
|
||||||
C: number[]
|
C: number[]
|
||||||
): number[] {
|
): number[] {
|
||||||
const a = det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1)
|
const [x1, y1] = A
|
||||||
|
const [x2, y2] = B
|
||||||
|
const [x3, y3] = C
|
||||||
|
|
||||||
const bx = -det(
|
const a = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2
|
||||||
A[0] * A[0] + A[1] * A[1],
|
|
||||||
A[1],
|
|
||||||
1,
|
|
||||||
B[0] * B[0] + B[1] * B[1],
|
|
||||||
B[1],
|
|
||||||
1,
|
|
||||||
C[0] * C[0] + C[1] * C[1],
|
|
||||||
C[1],
|
|
||||||
1
|
|
||||||
)
|
|
||||||
const by = det(
|
|
||||||
A[0] * A[0] + A[1] * A[1],
|
|
||||||
A[0],
|
|
||||||
1,
|
|
||||||
B[0] * B[0] + B[1] * B[1],
|
|
||||||
B[0],
|
|
||||||
1,
|
|
||||||
C[0] * C[0] + C[1] * C[1],
|
|
||||||
C[0],
|
|
||||||
1
|
|
||||||
)
|
|
||||||
const c = -det(
|
|
||||||
A[0] * A[0] + A[1] * A[1],
|
|
||||||
A[0],
|
|
||||||
A[1],
|
|
||||||
B[0] * B[0] + B[1] * B[1],
|
|
||||||
B[0],
|
|
||||||
B[1],
|
|
||||||
C[0] * C[0] + C[1] * C[1],
|
|
||||||
C[0],
|
|
||||||
C[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
const x = -bx / (2 * a)
|
const b =
|
||||||
const y = -by / (2 * a)
|
(x1 * x1 + y1 * y1) * (y3 - y2) +
|
||||||
const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a))
|
(x2 * x2 + y2 * y2) * (y1 - y3) +
|
||||||
|
(x3 * x3 + y3 * y3) * (y2 - y1)
|
||||||
|
|
||||||
return [x, y, r]
|
const c =
|
||||||
|
(x1 * x1 + y1 * y1) * (x2 - x3) +
|
||||||
|
(x2 * x2 + y2 * y2) * (x3 - x1) +
|
||||||
|
(x3 * x3 + y3 * y3) * (x1 - x2)
|
||||||
|
|
||||||
|
const x = -b / (2 * a)
|
||||||
|
|
||||||
|
const y = -c / (2 * a)
|
||||||
|
|
||||||
|
return [x, y, Math.hypot(x - x1, y - y1)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -281,7 +247,12 @@ export function perimeterOfEllipse(rx: number, ry: number): number {
|
||||||
* @param a0
|
* @param a0
|
||||||
* @param a1
|
* @param a1
|
||||||
*/
|
*/
|
||||||
export function shortAngleDist(a0: number, a1: number): number {
|
export function shortAngleDist(a0: number, a1: number, clamp = true): number {
|
||||||
|
if (!clamp) {
|
||||||
|
const da = a1 - a0
|
||||||
|
return 2 * da - da
|
||||||
|
}
|
||||||
|
|
||||||
const max = Math.PI * 2
|
const max = Math.PI * 2
|
||||||
const da = (a1 - a0) % max
|
const da = (a1 - a0) % max
|
||||||
return ((2 * da) % max) - da
|
return ((2 * da) % max) - da
|
||||||
|
@ -303,7 +274,7 @@ export function longAngleDist(a0: number, a1: number): number {
|
||||||
* @param t
|
* @param t
|
||||||
*/
|
*/
|
||||||
export function lerpAngles(a0: number, a1: number, t: number): number {
|
export function lerpAngles(a0: number, a1: number, t: number): number {
|
||||||
return a0 + shortAngleDist(a0, a1) * t
|
return a0 + shortAngleDist(a0, a1, true) * t
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1753,14 +1724,17 @@ export function getSvgPathFromStroke(stroke: number[][]): string {
|
||||||
const d = stroke.reduce(
|
const d = stroke.reduce(
|
||||||
(acc, [x0, y0], i, arr) => {
|
(acc, [x0, y0], i, arr) => {
|
||||||
const [x1, y1] = arr[(i + 1) % arr.length]
|
const [x1, y1] = arr[(i + 1) % arr.length]
|
||||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
|
acc.push(` ${x0},${y0} ${(x0 + x1) / 2},${(y0 + y1) / 2}`)
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
['M', ...stroke[0], 'Q']
|
['M ', `${stroke[0][0]},${stroke[0][1]}`, ' Q']
|
||||||
)
|
)
|
||||||
|
|
||||||
d.push('Z')
|
d.push(' Z')
|
||||||
return d.join(' ').replaceAll(/(\s[0-9]*\.[0-9]{2})([0-9]*)\b/g, '$1')
|
|
||||||
|
return d
|
||||||
|
.join('')
|
||||||
|
.replaceAll(/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g, '$1')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||||
|
|
|
@ -515,6 +515,6 @@ export default class Vec {
|
||||||
const t = i / steps
|
const t = i / steps
|
||||||
return t * t * t
|
return t * t * t
|
||||||
})
|
})
|
||||||
.map((t) => [...Vec.lrp(a, b, t), (1 - t) / 2])
|
.map((t) => Vec.round([...Vec.lrp(a, b, t), (1 - t) / 2]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
yarn.lock
32
yarn.lock
|
@ -5353,7 +5353,7 @@ globalyzer@0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
|
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
|
||||||
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
|
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
|
||||||
|
|
||||||
globby@^11.0.3:
|
globby@^11.0.3, globby@^11.0.4:
|
||||||
version "11.0.4"
|
version "11.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
|
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
|
||||||
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
|
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
|
||||||
|
@ -7887,15 +7887,15 @@ next-auth@^3.27.0:
|
||||||
preact-render-to-string "^5.1.14"
|
preact-render-to-string "^5.1.14"
|
||||||
querystring "^0.2.0"
|
querystring "^0.2.0"
|
||||||
|
|
||||||
next-pwa@^5.2.21:
|
next-pwa@^5.2.23:
|
||||||
version "5.2.21"
|
version "5.2.23"
|
||||||
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.2.21.tgz#fb71ba35b1a984ec6641c5def64ca8c0ab9c2b0f"
|
resolved "https://registry.yarnpkg.com/next-pwa/-/next-pwa-5.2.23.tgz#581a50bc9892c9d3ae7be5c9bbf590becf376f0e"
|
||||||
integrity sha512-jL782UGX0E59TmmYi1xe5kgImeGPBO3me/b3RX7bLP0eG8oJeZiJqTVkc5DJNmXgdwOc6RSvRSOUdBQ9zLu4RA==
|
integrity sha512-nGYp0zliA0lJBoKETEZgW4kaiBckF8m9xpBw3VQ+EyVJ57Bn5HfFeNZUvFi2mvDiiwBuqf34baQIt8EhiN8k+A==
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-loader "^8.2.2"
|
babel-loader "^8.2.2"
|
||||||
clean-webpack-plugin "^3.0.0"
|
clean-webpack-plugin "^3.0.0"
|
||||||
globby "^11.0.3"
|
globby "^11.0.4"
|
||||||
terser-webpack-plugin "^5.1.1"
|
terser-webpack-plugin "^5.1.4"
|
||||||
workbox-webpack-plugin "^6.1.5"
|
workbox-webpack-plugin "^6.1.5"
|
||||||
workbox-window "^6.1.5"
|
workbox-window "^6.1.5"
|
||||||
|
|
||||||
|
@ -9537,10 +9537,10 @@ serialize-javascript@^4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
randombytes "^2.1.0"
|
randombytes "^2.1.0"
|
||||||
|
|
||||||
serialize-javascript@^5.0.1:
|
serialize-javascript@^6.0.0:
|
||||||
version "5.0.1"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
|
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
|
||||||
integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
|
integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
|
||||||
dependencies:
|
dependencies:
|
||||||
randombytes "^2.1.0"
|
randombytes "^2.1.0"
|
||||||
|
|
||||||
|
@ -10225,15 +10225,15 @@ terminal-link@^2.0.0:
|
||||||
ansi-escapes "^4.2.1"
|
ansi-escapes "^4.2.1"
|
||||||
supports-hyperlinks "^2.0.0"
|
supports-hyperlinks "^2.0.0"
|
||||||
|
|
||||||
terser-webpack-plugin@^5.1.1:
|
terser-webpack-plugin@^5.1.4:
|
||||||
version "5.1.3"
|
version "5.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.3.tgz#30033e955ca28b55664f1e4b30a1347e61aa23af"
|
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
|
||||||
integrity sha512-cxGbMqr6+A2hrIB5ehFIF+F/iST5ZOxvOmy9zih9ySbP1C2oEWQSOUS+2SNBTjzx5xLKO4xnod9eywdfq1Nb9A==
|
integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
jest-worker "^27.0.2"
|
jest-worker "^27.0.2"
|
||||||
p-limit "^3.1.0"
|
p-limit "^3.1.0"
|
||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
serialize-javascript "^5.0.1"
|
serialize-javascript "^6.0.0"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
terser "^5.7.0"
|
terser "^5.7.0"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue