Fix double undos, cleans up utils
This commit is contained in:
parent
8271e6d431
commit
bdafae3db6
53 changed files with 1676 additions and 1515 deletions
31
__tests__/delete.test.ts
Normal file
31
__tests__/delete.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import state from 'state'
|
||||||
|
import inputs from 'state/inputs'
|
||||||
|
import { getShape } from 'utils'
|
||||||
|
import { idsAreSelected, point, rectangleId } from './test-utils'
|
||||||
|
import * as json from './__mocks__/document.json'
|
||||||
|
|
||||||
|
state.reset()
|
||||||
|
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||||
|
|
||||||
|
describe('selection', () => {
|
||||||
|
it('deletes a shape and undoes the delete', () => {
|
||||||
|
state
|
||||||
|
.send('CANCELED')
|
||||||
|
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||||
|
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||||
|
.send('DELETED')
|
||||||
|
|
||||||
|
expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||||
|
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||||
|
|
||||||
|
state.send('UNDO')
|
||||||
|
|
||||||
|
expect(getShape(state.data, rectangleId)).toBeTruthy()
|
||||||
|
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||||
|
|
||||||
|
state.send('REDO')
|
||||||
|
|
||||||
|
expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||||
|
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,11 +1,8 @@
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
import inputs from 'state/inputs'
|
import inputs from 'state/inputs'
|
||||||
import { idsAreSelected, point } from './test-utils'
|
import { idsAreSelected, point, rectangleId, arrowId } from './test-utils'
|
||||||
import * as json from './__mocks__/document.json'
|
import * as json from './__mocks__/document.json'
|
||||||
|
|
||||||
const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
|
||||||
const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
|
||||||
|
|
||||||
// Mount the state and load the test file from json
|
// Mount the state and load the test file from json
|
||||||
state.reset()
|
state.reset()
|
||||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { getSelectedIds } from 'utils'
|
import { getSelectedIds } from 'utils'
|
||||||
|
|
||||||
|
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||||
|
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||||
|
|
||||||
interface PointerOptions {
|
interface PointerOptions {
|
||||||
id?: string
|
id?: string
|
||||||
x?: number
|
x?: number
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import { useSelector } from 'state'
|
import { useSelector } from 'state'
|
||||||
import { Bounds, PageState } from 'types'
|
import { Bounds, PageState } from 'types'
|
||||||
import { boundsCollide, boundsContain } from 'utils/bounds'
|
import {
|
||||||
import { deepCompareArrays, getPage, getViewport } from 'utils'
|
deepCompareArrays,
|
||||||
|
getPage,
|
||||||
|
getViewport,
|
||||||
|
boundsCollide,
|
||||||
|
boundsContain,
|
||||||
|
} from 'utils'
|
||||||
import Shape from './shape'
|
import Shape from './shape'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -94,11 +94,11 @@ class Clipboard {
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(svgString)
|
navigator.clipboard.writeText(svgString)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Clipboard.copyStringToClipboard(svgString)
|
this.copyStringToClipboard(svgString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static copyStringToClipboard(string: string) {
|
copyStringToClipboard = (string: string) => {
|
||||||
let result: boolean | null
|
let result: boolean | null
|
||||||
|
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import { Bounds } from 'types'
|
import { Bounds } from 'types'
|
||||||
import { ease } from 'utils'
|
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## Utils
|
* ## Utils
|
||||||
*/
|
*/
|
||||||
export default class Utils {
|
export default class Utils {
|
||||||
static pointsBetween(a: number[], b: number[], steps = 6): number[][] {
|
|
||||||
return Array.from(Array(steps))
|
|
||||||
.map((_, i) => ease(i / steps))
|
|
||||||
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
|
|
||||||
}
|
|
||||||
|
|
||||||
static getRayRayIntersection(
|
static getRayRayIntersection(
|
||||||
p0: number[],
|
p0: number[],
|
||||||
n0: number[],
|
n0: number[],
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { getCommonBounds, getPage, getSelectedShapes } from 'utils'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
||||||
export default function alignCommand(data: Data, type: AlignType): void {
|
export default function alignCommand(data: Data, type: AlignType): void {
|
||||||
const { currentPageId } = data
|
|
||||||
const selectedShapes = getSelectedShapes(data)
|
const selectedShapes = getSelectedShapes(data)
|
||||||
const entries = selectedShapes.map(
|
const entries = selectedShapes.map(
|
||||||
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
|
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
|
||||||
|
@ -21,7 +20,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
|
||||||
name: 'aligned',
|
name: 'aligned',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case AlignType.Top: {
|
case AlignType.Top: {
|
||||||
|
@ -87,7 +86,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
for (const id in boundsForShapes) {
|
for (const id in boundsForShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
const initialBounds = boundsForShapes[id]
|
const initialBounds = boundsForShapes[id]
|
||||||
|
|
|
@ -18,9 +18,9 @@ export default function arrowCommand(
|
||||||
do(data, isInitial) {
|
do(data, isInitial) {
|
||||||
if (isInitial) return
|
if (isInitial) return
|
||||||
|
|
||||||
const { initialShape, currentPageId } = after
|
const { initialShape } = after
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ export default function arrowCommand(
|
||||||
data.pointedId = undefined
|
data.pointedId = undefined
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { initialShape, currentPageId } = before
|
const { initialShape } = before
|
||||||
const shapes = getPage(data, currentPageId).shapes
|
const shapes = getPage(data).shapes
|
||||||
|
|
||||||
delete shapes[initialShape.id]
|
delete shapes[initialShape.id]
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, Page, PageState } from 'types'
|
import { Data, Page, PageState } from 'types'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { current } from 'immer'
|
|
||||||
import storage from 'state/storage'
|
import storage from 'state/storage'
|
||||||
|
|
||||||
export default function createPage(data: Data, goToPage = true): void {
|
export default function createPage(data: Data, goToPage = true): void {
|
||||||
|
@ -40,7 +39,7 @@ export default function createPage(data: Data, goToPage = true): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnapshot(data: Data) {
|
function getSnapshot(data: Data) {
|
||||||
const { currentPageId } = current(data)
|
const { currentPageId } = data
|
||||||
|
|
||||||
const pages = Object.values(data.document.pages)
|
const pages = Object.values(data.document.pages)
|
||||||
const unchanged = pages.filter((page) => page.name.startsWith('Page '))
|
const unchanged = pages.filter((page) => page.name.startsWith('Page '))
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { current } from 'immer'
|
|
||||||
import storage from 'state/storage'
|
import storage from 'state/storage'
|
||||||
|
import { deepClone, getPage, getPageState } from 'utils'
|
||||||
|
|
||||||
export default function deletePage(data: Data, pageId: string): void {
|
export default function deletePage(data: Data, pageId: string): void {
|
||||||
const snapshot = getSnapshot(data, pageId)
|
const snapshot = getSnapshot(data, pageId)
|
||||||
|
@ -29,23 +29,17 @@ export default function deletePage(data: Data, pageId: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnapshot(data: Data, pageId: string) {
|
function getSnapshot(data: Data, pageId: string) {
|
||||||
const cData = current(data)
|
const { currentPageId, document } = data
|
||||||
const { currentPageId, document } = cData
|
|
||||||
|
|
||||||
const page = document.pages[pageId]
|
const page = deepClone(getPage(data))
|
||||||
const pageState = cData.pageStates[pageId]
|
|
||||||
|
|
||||||
const isCurrent = currentPageId === pageId
|
const pageState = deepClone(getPageState(data))
|
||||||
|
|
||||||
// const nextIndex = isCurrent
|
const isCurrent = data.currentPageId === pageId
|
||||||
// ? page.childIndex === 0
|
|
||||||
// ? 1
|
|
||||||
// : page.childIndex - 1
|
|
||||||
// : document.pages[currentPageId].childIndex
|
|
||||||
|
|
||||||
const nextPageId = isCurrent
|
const nextPageId = isCurrent
|
||||||
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
|
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
|
||||||
: cData.currentPageId
|
: currentPageId
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nextPageId,
|
nextPageId,
|
||||||
|
|
|
@ -2,28 +2,26 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import {
|
import {
|
||||||
|
deepClone,
|
||||||
getDocumentBranch,
|
getDocumentBranch,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { current } from 'immer'
|
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
||||||
export default function deleteSelected(data: Data): void {
|
export default function deleteSelected(data: Data): void {
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
const selectedShapes = getSelectedShapes(data)
|
const selectedShapes = getSelectedShapes(data)
|
||||||
|
|
||||||
const selectedIdsArr = selectedShapes
|
const selectedIdsArr = selectedShapes
|
||||||
.filter((shape) => !shape.isLocked)
|
.filter((shape) => !shape.isLocked)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
|
|
||||||
const page = getPage(current(data))
|
const page = getPage(data)
|
||||||
|
|
||||||
const childrenToDelete = selectedIdsArr
|
const childrenToDelete = selectedIdsArr
|
||||||
.flatMap((id) => getDocumentBranch(data, id))
|
.flatMap((id) => getDocumentBranch(data, id))
|
||||||
.map((id) => page.shapes[id])
|
.map((id) => deepClone(page.shapes[id]))
|
||||||
|
|
||||||
const remainingIds = selectedShapes
|
const remainingIds = selectedShapes
|
||||||
.filter((shape) => shape.isLocked)
|
.filter((shape) => shape.isLocked)
|
||||||
|
@ -36,7 +34,7 @@ export default function deleteSelected(data: Data): void {
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
for (const id of selectedIdsArr) {
|
for (const id of selectedIdsArr) {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
|
@ -67,7 +65,7 @@ export default function deleteSelected(data: Data): void {
|
||||||
setSelectedIds(data, remainingIds)
|
setSelectedIds(data, remainingIds)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
for (const shape of childrenToDelete) {
|
for (const shape of childrenToDelete) {
|
||||||
page.shapes[shape.id] = shape
|
page.shapes[shape.id] = shape
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function directCommand(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, before.currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const { id, direction } of after.shapes) {
|
for (const { id, direction } of after.shapes) {
|
||||||
const shape = shapes[id] as RayShape | LineShape
|
const shape = shapes[id] as RayShape | LineShape
|
||||||
|
|
|
@ -13,8 +13,6 @@ export default function distributeCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
type: DistributeType
|
type: DistributeType
|
||||||
): void {
|
): void {
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
const selectedShapes = getSelectedShapes(data).filter(
|
const selectedShapes = getSelectedShapes(data).filter(
|
||||||
(shape) => !shape.isLocked
|
(shape) => !shape.isLocked
|
||||||
)
|
)
|
||||||
|
@ -40,7 +38,7 @@ export default function distributeCommand(
|
||||||
name: 'distribute_shapes',
|
name: 'distribute_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
const len = entries.length
|
const len = entries.length
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -132,7 +130,7 @@ export default function distributeCommand(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
for (const id in boundsForShapes) {
|
for (const id in boundsForShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
const initialBounds = boundsForShapes[id]
|
const initialBounds = boundsForShapes[id]
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, DrawShape } from 'types'
|
import { Data, DrawShape } from 'types'
|
||||||
import { getPage, setSelectedIds } from 'utils'
|
import { deepClone, getPage, getShape, setSelectedIds } from 'utils'
|
||||||
import { current } from 'immer'
|
|
||||||
|
|
||||||
export default function drawCommand(data: Data, id: string): void {
|
export default function drawCommand(data: Data, id: string): void {
|
||||||
const restoreShape = getPage(current(data)).shapes[id] as DrawShape
|
const restoreShape = deepClone(getShape(data, id)) as DrawShape
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -2,18 +2,18 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import {
|
import {
|
||||||
|
deepClone,
|
||||||
getCurrentCamera,
|
getCurrentCamera,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { current } from 'immer'
|
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
|
|
||||||
export default function duplicateCommand(data: Data): void {
|
export default function duplicateCommand(data: Data): void {
|
||||||
const { currentPageId } = data
|
const selectedShapes = getSelectedShapes(data).map(deepClone)
|
||||||
const selectedShapes = getSelectedShapes(current(data))
|
|
||||||
const duplicates = selectedShapes.map((shape) => ({
|
const duplicates = selectedShapes.map((shape) => ({
|
||||||
...shape,
|
...shape,
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
|
@ -28,7 +28,7 @@ export default function duplicateCommand(data: Data): void {
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
for (const duplicate of duplicates) {
|
||||||
shapes[duplicate.id] = duplicate
|
shapes[duplicate.id] = duplicate
|
||||||
|
@ -40,7 +40,7 @@ export default function duplicateCommand(data: Data): void {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const duplicate of duplicates) {
|
for (const duplicate of duplicates) {
|
||||||
delete shapes[duplicate.id]
|
delete shapes[duplicate.id]
|
||||||
|
|
|
@ -16,9 +16,9 @@ export default function editCommand(
|
||||||
name: 'edit_shape',
|
name: 'edit_shape',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { initialShape, currentPageId } = after
|
const { initialShape } = after
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ export default function editCommand(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { initialShape, currentPageId } = before
|
const { initialShape } = before
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
page.shapes[initialShape.id] = initialShape
|
page.shapes[initialShape.id] = initialShape
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,34 +1,15 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, Shape } from 'types'
|
import { Data, Shape } from 'types'
|
||||||
import { current } from 'immer'
|
import { deepClone, getPage, getShapes, setSelectedIds } from 'utils'
|
||||||
import { getPage, setSelectedIds } from 'utils'
|
|
||||||
|
|
||||||
export default function generateCommand(
|
export default function generateCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
currentPageId: string,
|
|
||||||
generatedShapes: Shape[]
|
generatedShapes: Shape[]
|
||||||
): void {
|
): void {
|
||||||
const cData = current(data)
|
const initialShapes = getShapes(data)
|
||||||
const page = getPage(cData)
|
.filter((shape) => shape.isGenerated)
|
||||||
|
.map(deepClone)
|
||||||
const currentShapes = page.shapes
|
|
||||||
|
|
||||||
const prevGeneratedShapes = Object.values(currentShapes).filter(
|
|
||||||
(shape) => shape.isGenerated
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remove previous generated shapes
|
|
||||||
for (const id in currentShapes) {
|
|
||||||
if (currentShapes[id].isGenerated) {
|
|
||||||
delete currentShapes[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new ones
|
|
||||||
for (const shape of generatedShapes) {
|
|
||||||
currentShapes[shape.id] = shape
|
|
||||||
}
|
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
|
@ -37,35 +18,15 @@ export default function generateCommand(
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
initialShapes.forEach((shape) => delete shapes[shape.id])
|
||||||
|
generatedShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||||
setSelectedIds(data, [])
|
setSelectedIds(data, [])
|
||||||
|
|
||||||
// Remove previous generated shapes
|
|
||||||
for (const id in shapes) {
|
|
||||||
if (shapes[id].isGenerated) {
|
|
||||||
delete shapes[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new generated shapes
|
|
||||||
for (const shape of generatedShapes) {
|
|
||||||
shapes[shape.id] = shape
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
generatedShapes.forEach((shape) => delete shapes[shape.id])
|
||||||
// Remove generated shapes
|
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||||
for (const id in shapes) {
|
setSelectedIds(data, [])
|
||||||
if (shapes[id].isGenerated) {
|
|
||||||
delete shapes[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore previous generated shapes
|
|
||||||
for (const shape of prevGeneratedShapes) {
|
|
||||||
shapes[shape.id] = shape
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default function groupCommand(data: Data): void {
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
// Create the new group
|
// Create the new group
|
||||||
shapes[newGroupShape.id] = newGroupShape
|
shapes[newGroupShape.id] = newGroupShape
|
||||||
|
@ -118,7 +118,7 @@ export default function groupCommand(data: Data): void {
|
||||||
setSelectedIds(data, [newGroupShape.id])
|
setSelectedIds(data, [newGroupShape.id])
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
const group = shapes[newGroupShape.id]
|
const group = shapes[newGroupShape.id]
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ export default function handleCommand(
|
||||||
name: 'moved_handle',
|
name: 'moved_handle',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { initialShape, currentPageId } = after
|
const { initialShape } = after
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
const shape = page.shapes[initialShape.id]
|
const shape = page.shapes[initialShape.id]
|
||||||
|
|
||||||
getShapeUtils(shape)
|
getShapeUtils(shape)
|
||||||
|
@ -26,9 +26,9 @@ export default function handleCommand(
|
||||||
.onSessionComplete(shape)
|
.onSessionComplete(shape)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { initialShape, currentPageId } = before
|
const { initialShape } = before
|
||||||
|
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
page.shapes[initialShape.id] = initialShape
|
page.shapes[initialShape.id] = initialShape
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
const fromPageId = oldPageId
|
const fromPageId = oldPageId
|
||||||
const toPageId = newPageId
|
const toPageId = newPageId
|
||||||
|
|
||||||
const fromPage = getPage(data, fromPageId)
|
const fromPage = getPage(data)
|
||||||
|
|
||||||
// Get all of the selected shapes and their descendents
|
// Get all of the selected shapes and their descendents
|
||||||
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
|
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
|
||||||
|
@ -65,7 +65,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear the current page state's selected ids
|
// Clear the current page state's selected ids
|
||||||
getPageState(data, fromPageId).selectedIds.clear()
|
getPageState(data).selectedIds.clear()
|
||||||
|
|
||||||
// Save the "from" page
|
// Save the "from" page
|
||||||
storage.savePage(data, data.document.id, fromPageId)
|
storage.savePage(data, data.document.id, fromPageId)
|
||||||
|
@ -74,7 +74,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
storage.loadPage(data, toPageId)
|
storage.loadPage(data, toPageId)
|
||||||
|
|
||||||
// The page we're moving the shapes to
|
// The page we're moving the shapes to
|
||||||
const toPage = getPage(data, toPageId)
|
const toPage = getPage(data)
|
||||||
|
|
||||||
// Add all of the selected shapes to the "from" page.
|
// Add all of the selected shapes to the "from" page.
|
||||||
shapesToMove.forEach((shape) => {
|
shapesToMove.forEach((shape) => {
|
||||||
|
@ -89,7 +89,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select the selected ids on the new page
|
// Select the selected ids on the new page
|
||||||
getPageState(data, toPageId).selectedIds = new Set(selectedIds)
|
getPageState(data).selectedIds = new Set(selectedIds)
|
||||||
|
|
||||||
// Move to the new page
|
// Move to the new page
|
||||||
data.currentPageId = toPageId
|
data.currentPageId = toPageId
|
||||||
|
@ -98,7 +98,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
const fromPageId = newPageId
|
const fromPageId = newPageId
|
||||||
const toPageId = oldPageId
|
const toPageId = oldPageId
|
||||||
|
|
||||||
const fromPage = getPage(data, fromPageId)
|
const fromPage = getPage(data)
|
||||||
|
|
||||||
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
|
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
|
||||||
|
|
||||||
|
@ -119,13 +119,13 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
delete fromPage.shapes[shape.id]
|
delete fromPage.shapes[shape.id]
|
||||||
})
|
})
|
||||||
|
|
||||||
getPageState(data, fromPageId).selectedIds.clear()
|
getPageState(data).selectedIds.clear()
|
||||||
|
|
||||||
storage.savePage(data, data.document.id, fromPageId)
|
storage.savePage(data, data.document.id, fromPageId)
|
||||||
|
|
||||||
storage.loadPage(data, toPageId)
|
storage.loadPage(data, toPageId)
|
||||||
|
|
||||||
const toPage = getPage(data, toPageId)
|
const toPage = getPage(data)
|
||||||
|
|
||||||
shapesToMove.forEach((shape) => {
|
shapesToMove.forEach((shape) => {
|
||||||
toPage.shapes[shape.id] = shape
|
toPage.shapes[shape.id] = shape
|
||||||
|
@ -144,7 +144,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
getPageState(data, toPageId).selectedIds = new Set(selectedIds)
|
getPageState(data).selectedIds = new Set(selectedIds)
|
||||||
|
|
||||||
data.currentPageId = toPageId
|
data.currentPageId = toPageId
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,8 +11,6 @@ import {
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
||||||
export default function moveCommand(data: Data, type: MoveType): void {
|
export default function moveCommand(data: Data, type: MoveType): void {
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
|
|
||||||
const selectedIds = setToArray(getSelectedIds(data))
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
|
@ -28,7 +26,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
const shapes = selectedIds.map((id) => page.shapes[id])
|
const shapes = selectedIds.map((id) => page.shapes[id])
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { getShapeUtils } from 'state/shape-utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
|
|
||||||
export default function nudgeCommand(data: Data, delta: number[]): void {
|
export default function nudgeCommand(data: Data, delta: number[]): void {
|
||||||
const { currentPageId } = data
|
|
||||||
const selectedShapes = getSelectedShapes(data)
|
const selectedShapes = getSelectedShapes(data)
|
||||||
const shapeBounds = Object.fromEntries(
|
const shapeBounds = Object.fromEntries(
|
||||||
selectedShapes.map(
|
selectedShapes.map(
|
||||||
|
@ -20,7 +19,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
|
||||||
name: 'nudge_shapes',
|
name: 'nudge_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in shapeBounds) {
|
for (const id in shapeBounds) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
@ -32,7 +31,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in shapeBounds) {
|
for (const id in shapeBounds) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
|
@ -15,8 +15,6 @@ import { getShapeUtils } from 'state/shape-utils'
|
||||||
import state from 'state/state'
|
import state from 'state/state'
|
||||||
|
|
||||||
export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
const center = screenToWorld(
|
const center = screenToWorld(
|
||||||
[window.innerWidth / 2, window.innerHeight / 2],
|
[window.innerWidth / 2, window.innerHeight / 2],
|
||||||
data
|
data
|
||||||
|
@ -43,7 +41,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
let childIndex =
|
let childIndex =
|
||||||
(state.values.currentShapes[state.values.currentShapes.length - 1]
|
(state.values.currentShapes[state.values.currentShapes.length - 1]
|
||||||
|
@ -67,7 +65,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
||||||
setSelectedIds(data, Object.values(newIdMap))
|
setSelectedIds(data, Object.values(newIdMap))
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
Object.values(newIdMap).forEach((id) => delete shapes[id])
|
Object.values(newIdMap).forEach((id) => delete shapes[id])
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { getShapeUtils } from 'state/shape-utils'
|
||||||
const PI2 = Math.PI * 2
|
const PI2 = Math.PI * 2
|
||||||
|
|
||||||
export default function rotateCcwCommand(data: Data): void {
|
export default function rotateCcwCommand(data: Data): void {
|
||||||
const { currentPageId, boundsRotation } = data
|
const { boundsRotation } = data
|
||||||
|
|
||||||
const page = getPage(data)
|
const page = getPage(data)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export default function rotateCcwCommand(data: Data): void {
|
||||||
name: 'rotate_ccw',
|
name: 'rotate_ccw',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in nextShapes) {
|
for (const id in nextShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
@ -77,7 +77,7 @@ export default function rotateCcwCommand(data: Data): void {
|
||||||
data.boundsRotation = nextboundsRotation
|
data.boundsRotation = nextboundsRotation
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in initialShapes) {
|
for (const id in initialShapes) {
|
||||||
const { point, rotation } = initialShapes[id]
|
const { point, rotation } = initialShapes[id]
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default function rotateCommand(
|
||||||
data.boundsRotation = after.boundsRotation
|
data.boundsRotation = after.boundsRotation
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, before.currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const { id, point, rotation } of before.initialShapes) {
|
for (const { id, point, rotation } of before.initialShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
|
@ -5,8 +5,6 @@ import { deepClone, getCommonBounds, getPage, getSelectedShapes } from 'utils'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
||||||
export default function stretchCommand(data: Data, type: StretchType): void {
|
export default function stretchCommand(data: Data, type: StretchType): void {
|
||||||
const { currentPageId } = data
|
|
||||||
|
|
||||||
const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape))
|
const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape))
|
||||||
|
|
||||||
const snapshot = Object.fromEntries(
|
const snapshot = Object.fromEntries(
|
||||||
|
@ -29,7 +27,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
|
||||||
name: 'stretched_shapes',
|
name: 'stretched_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case StretchType.Horizontal: {
|
case StretchType.Horizontal: {
|
||||||
|
@ -77,7 +75,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
|
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,6 @@ export default function styleCommand(
|
||||||
): void {
|
): void {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
const page = getPage(cData)
|
const page = getPage(cData)
|
||||||
const { currentPageId } = cData
|
|
||||||
|
|
||||||
const selectedIds = setToArray(getSelectedIds(data))
|
const selectedIds = setToArray(getSelectedIds(data))
|
||||||
|
|
||||||
|
@ -26,7 +25,7 @@ export default function styleCommand(
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const { id } of shapesToStyle) {
|
for (const { id } of shapesToStyle) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
@ -34,7 +33,7 @@ export default function styleCommand(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const { id, style } of shapesToStyle) {
|
for (const { id, style } of shapesToStyle) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
|
@ -9,7 +9,6 @@ export default function toggleCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
prop: PropsOfType<Shape>
|
prop: PropsOfType<Shape>
|
||||||
): void {
|
): void {
|
||||||
const { currentPageId } = data
|
|
||||||
const selectedShapes = getSelectedShapes(data)
|
const selectedShapes = getSelectedShapes(data)
|
||||||
const isAllToggled = selectedShapes.every((shape) => shape[prop])
|
const isAllToggled = selectedShapes.every((shape) => shape[prop])
|
||||||
const initialShapes = Object.fromEntries(
|
const initialShapes = Object.fromEntries(
|
||||||
|
@ -22,7 +21,7 @@ export default function toggleCommand(
|
||||||
name: 'toggle_prop',
|
name: 'toggle_prop',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in initialShapes) {
|
for (const id in initialShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
@ -34,7 +33,7 @@ export default function toggleCommand(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in initialShapes) {
|
for (const id in initialShapes) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function transformSingleCommand(
|
||||||
after: TransformSingleSnapshot,
|
after: TransformSingleSnapshot,
|
||||||
isCreating: boolean
|
isCreating: boolean
|
||||||
): void {
|
): void {
|
||||||
const shape = current(getPage(data, after.currentPageId).shapes[after.id])
|
const shape = current(getPage(data).shapes[after.id])
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
|
@ -22,7 +22,7 @@ export default function transformSingleCommand(
|
||||||
do(data) {
|
do(data) {
|
||||||
const { id } = after
|
const { id } = after
|
||||||
|
|
||||||
const { shapes } = getPage(data, after.currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
setSelectedIds(data, [id])
|
setSelectedIds(data, [id])
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export default function transformSingleCommand(
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { id, initialShape } = before
|
const { id, initialShape } = before
|
||||||
|
|
||||||
const { shapes } = getPage(data, before.currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
setSelectedIds(data, [])
|
setSelectedIds(data, [])
|
||||||
|
|
|
@ -25,8 +25,8 @@ export default function translateCommand(
|
||||||
do(data, initial) {
|
do(data, initial) {
|
||||||
if (initial) return
|
if (initial) return
|
||||||
|
|
||||||
const { initialShapes, currentPageId } = after
|
const { initialShapes } = after
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
// Restore clones to document
|
// Restore clones to document
|
||||||
if (isCloning) {
|
if (isCloning) {
|
||||||
|
@ -66,8 +66,8 @@ export default function translateCommand(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { initialShapes, clones, currentPageId, initialParents } = before
|
const { initialShapes, clones, initialParents } = before
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
// Move shapes back to where they started
|
// Move shapes back to where they started
|
||||||
for (const { id, point } of initialShapes) {
|
for (const { id, point } of initialShapes) {
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default function ungroupCommand(data: Data): void {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
selectedGroups.forEach((group) => {
|
selectedGroups.forEach((group) => {
|
||||||
shapes[group.id] = group
|
shapes[group.id] = group
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default class DirectionSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const page = getPage(data, this.snapshot.currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
for (const { id, direction } of this.snapshot.shapes) {
|
for (const { id, direction } of this.snapshot.shapes) {
|
||||||
const shape = page.shapes[id] as RayShape | LineShape
|
const shape = page.shapes[id] as RayShape | LineShape
|
||||||
|
|
|
@ -18,8 +18,8 @@ export default class HandleSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
update(data: Data, point: number[], isAligned: boolean): void {
|
update(data: Data, point: number[], isAligned: boolean): void {
|
||||||
const { currentPageId, handleId, initialShape } = this.snapshot
|
const { handleId, initialShape } = this.snapshot
|
||||||
const shape = getPage(data, currentPageId).shapes[initialShape.id]
|
const shape = getPage(data).shapes[initialShape.id]
|
||||||
|
|
||||||
const delta = vec.vec(this.origin, point)
|
const delta = vec.vec(this.origin, point)
|
||||||
|
|
||||||
|
@ -46,8 +46,8 @@ export default class HandleSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const { currentPageId, initialShape } = this.snapshot
|
const { initialShape } = this.snapshot
|
||||||
getPage(data, currentPageId).shapes[initialShape.id] = initialShape
|
getPage(data).shapes[initialShape.id] = initialShape
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data): void {
|
complete(data: Data): void {
|
||||||
|
|
|
@ -76,8 +76,8 @@ export default class RotateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const { currentPageId, initialShapes } = this.snapshot
|
const { initialShapes } = this.snapshot
|
||||||
const page = getPage(data, currentPageId)
|
const page = getPage(data)
|
||||||
|
|
||||||
for (const { id, point, rotation } of initialShapes) {
|
for (const { id, point, rotation } of initialShapes) {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
|
|
|
@ -80,9 +80,9 @@ export default class TransformSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const { currentPageId, shapeBounds } = this.snapshot
|
const { shapeBounds } = this.snapshot
|
||||||
|
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (const id in shapeBounds) {
|
for (const id in shapeBounds) {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
|
@ -36,10 +36,9 @@ export default class TransformSingleSession extends BaseSession {
|
||||||
update(data: Data, point: number[], isAspectRatioLocked = false): void {
|
update(data: Data, point: number[], isAspectRatioLocked = false): void {
|
||||||
const { transformType } = this
|
const { transformType } = this
|
||||||
|
|
||||||
const { initialShapeBounds, currentPageId, initialShape, id } =
|
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||||
this.snapshot
|
|
||||||
|
|
||||||
const shape = getShape(data, id, currentPageId)
|
const shape = getShape(data, id)
|
||||||
|
|
||||||
const newBoundingBox = getTransformedBoundingBox(
|
const newBoundingBox = getTransformedBoundingBox(
|
||||||
initialShapeBounds,
|
initialShapeBounds,
|
||||||
|
|
|
@ -33,9 +33,8 @@ export default class TranslateSession extends BaseSession {
|
||||||
isAligned: boolean,
|
isAligned: boolean,
|
||||||
isCloning: boolean
|
isCloning: boolean
|
||||||
): void {
|
): void {
|
||||||
const { currentPageId, clones, initialShapes, initialParents } =
|
const { clones, initialShapes, initialParents } = this.snapshot
|
||||||
this.snapshot
|
const { shapes } = getPage(data)
|
||||||
const { shapes } = getPage(data, currentPageId)
|
|
||||||
|
|
||||||
const delta = vec.vec(this.origin, point)
|
const delta = vec.vec(this.origin, point)
|
||||||
|
|
||||||
|
@ -143,9 +142,8 @@ export default class TranslateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
const { initialShapes, initialParents, clones, currentPageId } =
|
const { initialShapes, initialParents, clones } = this.snapshot
|
||||||
this.snapshot
|
const { shapes } = getPage(data)
|
||||||
const { shapes } = getPage(data, currentPageId)
|
|
||||||
|
|
||||||
for (const { id } of initialShapes) {
|
for (const { id } of initialShapes) {
|
||||||
getDocumentBranch(data, id).forEach((id) => {
|
getDocumentBranch(data, id).forEach((id) => {
|
||||||
|
|
|
@ -5,7 +5,8 @@ import {
|
||||||
rng,
|
rng,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
pointsBetween,
|
pointInBounds,
|
||||||
|
pointInCircle,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
|
@ -15,12 +16,10 @@ import {
|
||||||
ShapeType,
|
ShapeType,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { circleFromThreePoints, isAngleBetween } from 'utils'
|
import { circleFromThreePoints, isAngleBetween } from 'utils'
|
||||||
import { pointInBounds } from 'utils/hitTests'
|
|
||||||
import {
|
import {
|
||||||
intersectArcBounds,
|
intersectArcBounds,
|
||||||
intersectLineSegmentBounds,
|
intersectLineSegmentBounds,
|
||||||
} from 'utils/intersections'
|
} from 'utils/intersections'
|
||||||
import { pointInCircle } from 'utils/hitTests'
|
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
@ -502,8 +501,8 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
||||||
|
|
||||||
const stroke = getStroke(
|
const stroke = getStroke(
|
||||||
[
|
[
|
||||||
...pointsBetween(start.point, m),
|
...vec.pointsBetween(start.point, m),
|
||||||
...pointsBetween(m, end.point),
|
...vec.pointsBetween(m, end.point),
|
||||||
end.point,
|
end.point,
|
||||||
end.point,
|
end.point,
|
||||||
end.point,
|
end.point,
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import { DotShape, ShapeType } from 'types'
|
import { DotShape, ShapeType } from 'types'
|
||||||
import { boundsContained } from 'utils/bounds'
|
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { translateBounds } from 'utils'
|
import { boundsContained, translateBounds } from 'utils'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { uniqueId } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
||||||
import { intersectPolylineBounds } from 'utils/intersections'
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
import { boundsContain } from 'utils/bounds'
|
|
||||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||||
import {
|
import {
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
|
boundsContain,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
|
@ -2,15 +2,15 @@ import { getPerfectDashProps } from 'utils/dashes'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
||||||
import { getShapeUtils } from './index'
|
import { getShapeUtils } from './index'
|
||||||
import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
|
|
||||||
import { intersectEllipseBounds } from 'utils/intersections'
|
import { intersectEllipseBounds } from 'utils/intersections'
|
||||||
import { pointInEllipse } from 'utils/hitTests'
|
|
||||||
import {
|
import {
|
||||||
uniqueId,
|
uniqueId,
|
||||||
ease,
|
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
rng,
|
rng,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
|
pointInEllipse,
|
||||||
|
boundsContained,
|
||||||
|
getRotatedEllipseBounds,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
|
@ -207,7 +207,8 @@ function renderPath(shape: EllipseShape) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 5; i < 32; i++) {
|
for (let i = 5; i < 32; i++) {
|
||||||
const rads = start + overlap * 2 + Math.PI * 2.5 * ease(i / 35)
|
const t = i / 35
|
||||||
|
const rads = start + overlap * 2 + Math.PI * 2.5 * (t * t * t)
|
||||||
const x = rx * Math.cos(rads) + center[0]
|
const x = rx * Math.cos(rads) + center[0]
|
||||||
const y = ry * Math.sin(rads) + center[1]
|
const y = ry * Math.sin(rads) + center[1]
|
||||||
points.push([x, y])
|
points.push([x, y])
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { LineShape, ShapeType } from 'types'
|
import { LineShape, ShapeType } from 'types'
|
||||||
import { boundsContained } from 'utils/bounds'
|
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { ThinLine } from 'components/canvas/misc'
|
import { ThinLine } from 'components/canvas/misc'
|
||||||
import { translateBounds } from 'utils'
|
import { translateBounds, boundsContained } from 'utils'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,11 @@ import { uniqueId } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { PolylineShape, ShapeType } from 'types'
|
import { PolylineShape, ShapeType } from 'types'
|
||||||
import { intersectPolylineBounds } from 'utils/intersections'
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
import { boundsContainPolygon } from 'utils/bounds'
|
import {
|
||||||
import { getBoundsFromPoints, translateBounds } from 'utils'
|
boundsContainPolygon,
|
||||||
|
getBoundsFromPoints,
|
||||||
|
translateBounds,
|
||||||
|
} from 'utils'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { RayShape, ShapeType } from 'types'
|
import { RayShape, ShapeType } from 'types'
|
||||||
import { boundsContained } from 'utils/bounds'
|
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { ThinLine } from 'components/canvas/misc'
|
import { ThinLine } from 'components/canvas/misc'
|
||||||
import { translateBounds } from 'utils'
|
import { translateBounds, boundsContained } from 'utils'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
||||||
import {
|
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
|
||||||
getSvgPathFromStroke,
|
|
||||||
translateBounds,
|
|
||||||
rng,
|
|
||||||
shuffleArr,
|
|
||||||
pointsBetween,
|
|
||||||
} from 'utils'
|
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
|
@ -203,10 +197,10 @@ function renderPath(shape: RectangleShape) {
|
||||||
|
|
||||||
const lines = shuffleArr(
|
const lines = shuffleArr(
|
||||||
[
|
[
|
||||||
pointsBetween(tr, br),
|
vec.pointsBetween(tr, br),
|
||||||
pointsBetween(br, bl),
|
vec.pointsBetween(br, bl),
|
||||||
pointsBetween(bl, tl),
|
vec.pointsBetween(bl, tl),
|
||||||
pointsBetween(tl, tr),
|
vec.pointsBetween(tl, tr),
|
||||||
],
|
],
|
||||||
Math.floor(5 + getRandom() * 4)
|
Math.floor(5 + getRandom() * 4)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Shape, ShapeUtility } from 'types'
|
import { Shape, ShapeUtility } from 'types'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { getBoundsCenter, getBoundsFromPoints, getRotatedCorners } from 'utils'
|
import {
|
||||||
import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds'
|
pointInBounds,
|
||||||
|
getBoundsCenter,
|
||||||
|
getBoundsFromPoints,
|
||||||
|
getRotatedCorners,
|
||||||
|
boundsCollidePolygon,
|
||||||
|
boundsContainPolygon,
|
||||||
|
} from 'utils'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { pointInBounds } from 'utils/hitTests'
|
|
||||||
|
|
||||||
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import commands from './commands'
|
||||||
import {
|
import {
|
||||||
getChildren,
|
getChildren,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getCurrent,
|
|
||||||
getCurrentCamera,
|
getCurrentCamera,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedBounds,
|
getSelectedBounds,
|
||||||
|
@ -27,8 +26,10 @@ import {
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
getPageState,
|
getPageState,
|
||||||
setToArray,
|
setToArray,
|
||||||
copyToClipboard,
|
deepClone,
|
||||||
|
pointInBounds,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
PointerInfo,
|
PointerInfo,
|
||||||
|
@ -47,7 +48,6 @@ import {
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import session from './session'
|
import session from './session'
|
||||||
import { pointInBounds } from 'utils/hitTests'
|
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
@ -335,8 +335,6 @@ const state = createState({
|
||||||
selecting: {
|
selecting: {
|
||||||
onEnter: ['setActiveToolSelect', 'clearInputs'],
|
onEnter: ['setActiveToolSelect', 'clearInputs'],
|
||||||
on: {
|
on: {
|
||||||
UNDO: 'undo',
|
|
||||||
REDO: 'redo',
|
|
||||||
SAVED: 'forceSave',
|
SAVED: 'forceSave',
|
||||||
DELETED: {
|
DELETED: {
|
||||||
unless: 'isReadOnly',
|
unless: 'isReadOnly',
|
||||||
|
@ -773,8 +771,6 @@ const state = createState({
|
||||||
do: 'createShape',
|
do: 'createShape',
|
||||||
to: 'arrow.editing',
|
to: 'arrow.editing',
|
||||||
},
|
},
|
||||||
UNDO: { do: 'undo' },
|
|
||||||
REDO: { do: 'redo' },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
|
@ -1144,7 +1140,7 @@ const state = createState({
|
||||||
const shape = createShape(type, {
|
const shape = createShape(type, {
|
||||||
parentId: data.currentPageId,
|
parentId: data.currentPageId,
|
||||||
point: vec.round(screenToWorld(payload.point, data)),
|
point: vec.round(screenToWorld(payload.point, data)),
|
||||||
style: getCurrent(data.currentStyle),
|
style: deepClone(data.currentStyle),
|
||||||
})
|
})
|
||||||
|
|
||||||
const siblings = getChildren(data, shape.parentId)
|
const siblings = getChildren(data, shape.parentId)
|
||||||
|
@ -1748,7 +1744,7 @@ const state = createState({
|
||||||
data,
|
data,
|
||||||
payload: { shapes: Shape[]; controls: CodeControl[] }
|
payload: { shapes: Shape[]; controls: CodeControl[] }
|
||||||
) {
|
) {
|
||||||
commands.generate(data, data.currentPageId, payload.shapes)
|
commands.generate(data, payload.shapes)
|
||||||
},
|
},
|
||||||
setCodeControls(data, payload: { controls: CodeControl[] }) {
|
setCodeControls(data, payload: { controls: CodeControl[] }) {
|
||||||
data.codeControls = Object.fromEntries(
|
data.codeControls = Object.fromEntries(
|
||||||
|
@ -1776,7 +1772,7 @@ const state = createState({
|
||||||
data.document.code[data.currentCodeFileId].code
|
data.document.code[data.currentCodeFileId].code
|
||||||
)
|
)
|
||||||
|
|
||||||
commands.generate(data, data.currentPageId, shapes)
|
commands.generate(data, shapes)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
@ -1807,7 +1803,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
copyStateToClipboard(data) {
|
copyStateToClipboard(data) {
|
||||||
copyToClipboard(JSON.stringify(data))
|
clipboard.copyStringToClipboard(JSON.stringify(data))
|
||||||
},
|
},
|
||||||
|
|
||||||
pasteFromClipboard() {
|
pasteFromClipboard() {
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
import { Bounds } from 'types'
|
|
||||||
import { pointInBounds } from './hitTests'
|
|
||||||
import { intersectPolygonBounds } from './intersections'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether two bounds collide.
|
|
||||||
* @param a Bounds
|
|
||||||
* @param b Bounds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function boundsCollide(a: Bounds, b: Bounds): boolean {
|
|
||||||
return !(
|
|
||||||
a.maxX < b.minX ||
|
|
||||||
a.minX > b.maxX ||
|
|
||||||
a.maxY < b.minY ||
|
|
||||||
a.minY > b.maxY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether the bounds of A contain the bounds of B. A perfect match will return true.
|
|
||||||
* @param a Bounds
|
|
||||||
* @param b Bounds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function boundsContain(a: Bounds, b: Bounds): boolean {
|
|
||||||
return (
|
|
||||||
a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether the bounds of A are contained by the bounds of B.
|
|
||||||
* @param a Bounds
|
|
||||||
* @param b Bounds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function boundsContained(a: Bounds, b: Bounds): boolean {
|
|
||||||
return boundsContain(b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether a set of points are all contained by a bounding box.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function boundsContainPolygon(a: Bounds, points: number[][]): boolean {
|
|
||||||
return points.every((point) => pointInBounds(point, a))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether a polygon collides a bounding box.
|
|
||||||
* @param points
|
|
||||||
* @param b
|
|
||||||
*/
|
|
||||||
export function boundsCollidePolygon(a: Bounds, points: number[][]): boolean {
|
|
||||||
return intersectPolygonBounds(points, a).length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether two bounds are identical.
|
|
||||||
* @param a Bounds
|
|
||||||
* @param b Bounds
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function boundsAreEqual(a: Bounds, b: Bounds): boolean {
|
|
||||||
return !(
|
|
||||||
b.maxX !== a.maxX ||
|
|
||||||
b.minX !== a.minX ||
|
|
||||||
b.maxY !== a.maxY ||
|
|
||||||
b.minY !== a.minY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRotatedEllipseBounds(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
rx: number,
|
|
||||||
ry: number,
|
|
||||||
rotation: number
|
|
||||||
): Bounds {
|
|
||||||
const c = Math.cos(rotation)
|
|
||||||
const s = Math.sin(rotation)
|
|
||||||
const w = Math.hypot(rx * c, ry * s)
|
|
||||||
const h = Math.hypot(rx * s, ry * c)
|
|
||||||
|
|
||||||
return {
|
|
||||||
minX: x + rx - w,
|
|
||||||
minY: y + ry - h,
|
|
||||||
maxX: x + rx + w,
|
|
||||||
maxY: y + ry + h,
|
|
||||||
width: w * 2,
|
|
||||||
height: h * 2,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,15 +8,19 @@ type GTagEvent = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pageview = (url: URL): void => {
|
export const pageview = (url: URL): void => {
|
||||||
;(window as any).gtag('config', GA_TRACKING_ID, {
|
if ('gtag' in window) {
|
||||||
page_path: url,
|
;(window as any)?.gtag('config', GA_TRACKING_ID, {
|
||||||
})
|
page_path: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const event = ({ action, category, label, value }: GTagEvent): void => {
|
export const event = ({ action, category, label, value }: GTagEvent): void => {
|
||||||
;(window as any).gtag('event', action, {
|
if ('gtag' in window) {
|
||||||
event_category: category,
|
;(window as any)?.gtag('event', action, {
|
||||||
event_label: label,
|
event_category: category,
|
||||||
value: value,
|
event_label: label,
|
||||||
})
|
value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { Bounds } from 'types'
|
|
||||||
import vec from './vec'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether a point is inside of a bounds.
|
|
||||||
* @param A
|
|
||||||
* @param b
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function pointInBounds(A: number[], b: Bounds): boolean {
|
|
||||||
return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether a point is inside of a circle.
|
|
||||||
* @param A
|
|
||||||
* @param b
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function pointInCircle(A: number[], C: number[], r: number): boolean {
|
|
||||||
return vec.dist(A, C) <= r
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get whether a point is inside of an ellipse.
|
|
||||||
* @param point
|
|
||||||
* @param center
|
|
||||||
* @param rx
|
|
||||||
* @param ry
|
|
||||||
* @param rotation
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function pointInEllipse(
|
|
||||||
A: number[],
|
|
||||||
C: number[],
|
|
||||||
rx: number,
|
|
||||||
ry: number,
|
|
||||||
rotation = 0
|
|
||||||
): boolean {
|
|
||||||
rotation = rotation || 0
|
|
||||||
const cos = Math.cos(rotation)
|
|
||||||
const sin = Math.sin(rotation)
|
|
||||||
const delta = vec.sub(A, C)
|
|
||||||
const tdx = cos * delta[0] + sin * delta[1]
|
|
||||||
const tdy = sin * delta[0] - cos * delta[1]
|
|
||||||
|
|
||||||
return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1
|
|
||||||
}
|
|
|
@ -12,6 +12,31 @@ function getIntersection(message: string, ...points: number[][]) {
|
||||||
return { didIntersect: points.length > 0, message, points }
|
return { didIntersect: points.length > 0, message, points }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function intersectRays(
|
||||||
|
p0: number[],
|
||||||
|
n0: number[],
|
||||||
|
p1: number[],
|
||||||
|
n1: number[]
|
||||||
|
): Intersection {
|
||||||
|
const dx = p1[0] - p0[0]
|
||||||
|
const dy = p1[1] - p0[1]
|
||||||
|
const det = n1[0] * n0[1] - n1[1] * n0[0]
|
||||||
|
const u = (dy * n1[0] - dx * n1[1]) / det
|
||||||
|
const v = (dy * n0[0] - dx * n0[1]) / det
|
||||||
|
if (u < 0 || v < 0) return getIntersection('miss')
|
||||||
|
|
||||||
|
const m0 = n0[1] / n0[0]
|
||||||
|
const m1 = n1[1] / n1[0]
|
||||||
|
const b0 = p0[1] - m0 * p0[0]
|
||||||
|
const b1 = p1[1] - m1 * p1[0]
|
||||||
|
const x = (b1 - b0) / (m0 - m1)
|
||||||
|
const y = m0 * x + b0
|
||||||
|
|
||||||
|
return Number.isFinite(x)
|
||||||
|
? getIntersection('intersection', [x, y])
|
||||||
|
: getIntersection('parallel')
|
||||||
|
}
|
||||||
|
|
||||||
export function intersectLineSegments(
|
export function intersectLineSegments(
|
||||||
a1: number[],
|
a1: number[],
|
||||||
a2: number[],
|
a2: number[],
|
||||||
|
@ -45,6 +70,27 @@ export function intersectLineSegments(
|
||||||
return getIntersection('no intersection')
|
return getIntersection('no intersection')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function intersectCircleCircle(a: number[], b: number[]): Intersection {
|
||||||
|
const R = a[2],
|
||||||
|
r = b[2]
|
||||||
|
|
||||||
|
let dx = b[0] - a[0],
|
||||||
|
dy = b[1] - a[1]
|
||||||
|
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy),
|
||||||
|
x = (d * d - r * r + R * R) / (2 * d),
|
||||||
|
y = Math.sqrt(R * R - x * x)
|
||||||
|
|
||||||
|
dx /= d
|
||||||
|
dy /= d
|
||||||
|
|
||||||
|
return getIntersection(
|
||||||
|
'intersection',
|
||||||
|
[a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
|
||||||
|
[a[0] + dx * x + dy * y, a[1] + dy * x - dx * y]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function intersectCircleLineSegment(
|
export function intersectCircleLineSegment(
|
||||||
c: number[],
|
c: number[],
|
||||||
r: number,
|
r: number,
|
||||||
|
|
2566
utils/utils.ts
2566
utils/utils.ts
File diff suppressed because it is too large
Load diff
27
utils/vec.ts
27
utils/vec.ts
|
@ -472,7 +472,7 @@ export default class Vec {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a vector d distance from A towards B.
|
* Push a point A towards point B by a given distance.
|
||||||
* @param A
|
* @param A
|
||||||
* @param B
|
* @param B
|
||||||
* @param d
|
* @param d
|
||||||
|
@ -482,6 +482,16 @@ export default class Vec {
|
||||||
return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d))
|
return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push a point in a given angle by a given distance.
|
||||||
|
* @param A
|
||||||
|
* @param B
|
||||||
|
* @param d
|
||||||
|
*/
|
||||||
|
static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
|
||||||
|
return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Round a vector to a precision length.
|
* Round a vector to a precision length.
|
||||||
* @param a
|
* @param a
|
||||||
|
@ -490,4 +500,19 @@ export default class Vec {
|
||||||
static toPrecision = (a: number[], n = 4): number[] => {
|
static toPrecision = (a: number[], n = 4): number[] => {
|
||||||
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a number of points between two points.
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @param steps
|
||||||
|
*/
|
||||||
|
static pointsBetween = (a: number[], b: number[], steps = 6): number[][] => {
|
||||||
|
return Array.from(Array(steps))
|
||||||
|
.map((_, i) => {
|
||||||
|
const t = i / steps
|
||||||
|
return t * t * t
|
||||||
|
})
|
||||||
|
.map((t) => [...Vec.lrp(a, b, t), (1 - t) / 2])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue