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 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'
|
||||
|
||||
const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||
const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||
|
||||
// Mount the state and load the test file from json
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Data } from 'types'
|
||||
import { getSelectedIds } from 'utils'
|
||||
|
||||
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||
|
||||
interface PointerOptions {
|
||||
id?: string
|
||||
x?: number
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { useSelector } from 'state'
|
||||
import { Bounds, PageState } from 'types'
|
||||
import { boundsCollide, boundsContain } from 'utils/bounds'
|
||||
import { deepCompareArrays, getPage, getViewport } from 'utils'
|
||||
import {
|
||||
deepCompareArrays,
|
||||
getPage,
|
||||
getViewport,
|
||||
boundsCollide,
|
||||
boundsContain,
|
||||
} from 'utils'
|
||||
import Shape from './shape'
|
||||
|
||||
/*
|
||||
|
|
|
@ -94,11 +94,11 @@ class Clipboard {
|
|||
try {
|
||||
navigator.clipboard.writeText(svgString)
|
||||
} catch (e) {
|
||||
Clipboard.copyStringToClipboard(svgString)
|
||||
this.copyStringToClipboard(svgString)
|
||||
}
|
||||
}
|
||||
|
||||
static copyStringToClipboard(string: string) {
|
||||
copyStringToClipboard = (string: string) => {
|
||||
let result: boolean | null
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
import { Bounds } from 'types'
|
||||
import { ease } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
|
||||
/**
|
||||
* ## 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(
|
||||
p0: number[],
|
||||
n0: number[],
|
||||
|
|
|
@ -5,7 +5,6 @@ import { getCommonBounds, getPage, getSelectedShapes } from 'utils'
|
|||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
export default function alignCommand(data: Data, type: AlignType): void {
|
||||
const { currentPageId } = data
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
const entries = selectedShapes.map(
|
||||
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
|
||||
|
@ -21,7 +20,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
|
|||
name: 'aligned',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
switch (type) {
|
||||
case AlignType.Top: {
|
||||
|
@ -87,7 +86,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
for (const id in boundsForShapes) {
|
||||
const shape = shapes[id]
|
||||
const initialBounds = boundsForShapes[id]
|
||||
|
|
|
@ -18,9 +18,9 @@ export default function arrowCommand(
|
|||
do(data, isInitial) {
|
||||
if (isInitial) return
|
||||
|
||||
const { initialShape, currentPageId } = after
|
||||
const { initialShape } = after
|
||||
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
|
||||
|
@ -31,8 +31,8 @@ export default function arrowCommand(
|
|||
data.pointedId = undefined
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShape, currentPageId } = before
|
||||
const shapes = getPage(data, currentPageId).shapes
|
||||
const { initialShape } = before
|
||||
const shapes = getPage(data).shapes
|
||||
|
||||
delete shapes[initialShape.id]
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import Command from './command'
|
|||
import history from '../history'
|
||||
import { Data, Page, PageState } from 'types'
|
||||
import { uniqueId } from 'utils'
|
||||
import { current } from 'immer'
|
||||
import storage from 'state/storage'
|
||||
|
||||
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) {
|
||||
const { currentPageId } = current(data)
|
||||
const { currentPageId } = data
|
||||
|
||||
const pages = Object.values(data.document.pages)
|
||||
const unchanged = pages.filter((page) => page.name.startsWith('Page '))
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import { current } from 'immer'
|
||||
import storage from 'state/storage'
|
||||
import { deepClone, getPage, getPageState } from 'utils'
|
||||
|
||||
export default function deletePage(data: Data, pageId: string): void {
|
||||
const snapshot = getSnapshot(data, pageId)
|
||||
|
@ -29,23 +29,17 @@ export default function deletePage(data: Data, pageId: string): void {
|
|||
}
|
||||
|
||||
function getSnapshot(data: Data, pageId: string) {
|
||||
const cData = current(data)
|
||||
const { currentPageId, document } = cData
|
||||
const { currentPageId, document } = data
|
||||
|
||||
const page = document.pages[pageId]
|
||||
const pageState = cData.pageStates[pageId]
|
||||
const page = deepClone(getPage(data))
|
||||
|
||||
const isCurrent = currentPageId === pageId
|
||||
const pageState = deepClone(getPageState(data))
|
||||
|
||||
// const nextIndex = isCurrent
|
||||
// ? page.childIndex === 0
|
||||
// ? 1
|
||||
// : page.childIndex - 1
|
||||
// : document.pages[currentPageId].childIndex
|
||||
const isCurrent = data.currentPageId === pageId
|
||||
|
||||
const nextPageId = isCurrent
|
||||
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
|
||||
: cData.currentPageId
|
||||
: currentPageId
|
||||
|
||||
return {
|
||||
nextPageId,
|
||||
|
|
|
@ -2,28 +2,26 @@ import Command from './command'
|
|||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import {
|
||||
deepClone,
|
||||
getDocumentBranch,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
setSelectedIds,
|
||||
} from 'utils'
|
||||
import { current } from 'immer'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
export default function deleteSelected(data: Data): void {
|
||||
const { currentPageId } = data
|
||||
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
|
||||
const selectedIdsArr = selectedShapes
|
||||
.filter((shape) => !shape.isLocked)
|
||||
.map((shape) => shape.id)
|
||||
|
||||
const page = getPage(current(data))
|
||||
const page = getPage(data)
|
||||
|
||||
const childrenToDelete = selectedIdsArr
|
||||
.flatMap((id) => getDocumentBranch(data, id))
|
||||
.map((id) => page.shapes[id])
|
||||
.map((id) => deepClone(page.shapes[id]))
|
||||
|
||||
const remainingIds = selectedShapes
|
||||
.filter((shape) => shape.isLocked)
|
||||
|
@ -36,7 +34,7 @@ export default function deleteSelected(data: Data): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
for (const id of selectedIdsArr) {
|
||||
const shape = page.shapes[id]
|
||||
|
@ -67,7 +65,7 @@ export default function deleteSelected(data: Data): void {
|
|||
setSelectedIds(data, remainingIds)
|
||||
},
|
||||
undo(data) {
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
for (const shape of childrenToDelete) {
|
||||
page.shapes[shape.id] = shape
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function directCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const { id, direction } of after.shapes) {
|
||||
const shape = shapes[id] as RayShape | LineShape
|
||||
|
|
|
@ -13,8 +13,6 @@ export default function distributeCommand(
|
|||
data: Data,
|
||||
type: DistributeType
|
||||
): void {
|
||||
const { currentPageId } = data
|
||||
|
||||
const selectedShapes = getSelectedShapes(data).filter(
|
||||
(shape) => !shape.isLocked
|
||||
)
|
||||
|
@ -40,7 +38,7 @@ export default function distributeCommand(
|
|||
name: 'distribute_shapes',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
const len = entries.length
|
||||
|
||||
switch (type) {
|
||||
|
@ -132,7 +130,7 @@ export default function distributeCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
for (const id in boundsForShapes) {
|
||||
const shape = shapes[id]
|
||||
const initialBounds = boundsForShapes[id]
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, DrawShape } from 'types'
|
||||
import { getPage, setSelectedIds } from 'utils'
|
||||
import { current } from 'immer'
|
||||
import { deepClone, getPage, getShape, setSelectedIds } from 'utils'
|
||||
|
||||
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(
|
||||
data,
|
||||
|
|
|
@ -2,18 +2,18 @@ import Command from './command'
|
|||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import {
|
||||
deepClone,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
setSelectedIds,
|
||||
} from 'utils'
|
||||
import { uniqueId } from 'utils'
|
||||
import { current } from 'immer'
|
||||
import vec from 'utils/vec'
|
||||
|
||||
export default function duplicateCommand(data: Data): void {
|
||||
const { currentPageId } = data
|
||||
const selectedShapes = getSelectedShapes(current(data))
|
||||
const selectedShapes = getSelectedShapes(data).map(deepClone)
|
||||
|
||||
const duplicates = selectedShapes.map((shape) => ({
|
||||
...shape,
|
||||
id: uniqueId(),
|
||||
|
@ -28,7 +28,7 @@ export default function duplicateCommand(data: Data): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
shapes[duplicate.id] = duplicate
|
||||
|
@ -40,7 +40,7 @@ export default function duplicateCommand(data: Data): void {
|
|||
)
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
delete shapes[duplicate.id]
|
||||
|
|
|
@ -16,9 +16,9 @@ export default function editCommand(
|
|||
name: 'edit_shape',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { initialShape, currentPageId } = after
|
||||
const { initialShape } = after
|
||||
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
|
||||
|
@ -29,9 +29,9 @@ export default function editCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShape, currentPageId } = before
|
||||
const { initialShape } = before
|
||||
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
},
|
||||
|
|
|
@ -1,34 +1,15 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, Shape } from 'types'
|
||||
import { current } from 'immer'
|
||||
import { getPage, setSelectedIds } from 'utils'
|
||||
import { deepClone, getPage, getShapes, setSelectedIds } from 'utils'
|
||||
|
||||
export default function generateCommand(
|
||||
data: Data,
|
||||
currentPageId: string,
|
||||
generatedShapes: Shape[]
|
||||
): void {
|
||||
const cData = current(data)
|
||||
const page = getPage(cData)
|
||||
|
||||
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
|
||||
}
|
||||
const initialShapes = getShapes(data)
|
||||
.filter((shape) => shape.isGenerated)
|
||||
.map(deepClone)
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
|
@ -37,35 +18,15 @@ export default function generateCommand(
|
|||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
initialShapes.forEach((shape) => delete shapes[shape.id])
|
||||
generatedShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||
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) {
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
// Remove generated shapes
|
||||
for (const id in shapes) {
|
||||
if (shapes[id].isGenerated) {
|
||||
delete shapes[id]
|
||||
}
|
||||
}
|
||||
|
||||
// Restore previous generated shapes
|
||||
for (const shape of prevGeneratedShapes) {
|
||||
shapes[shape.id] = shape
|
||||
}
|
||||
generatedShapes.forEach((shape) => delete shapes[shape.id])
|
||||
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||
setSelectedIds(data, [])
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -82,7 +82,7 @@ export default function groupCommand(data: Data): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
// Create the new group
|
||||
shapes[newGroupShape.id] = newGroupShape
|
||||
|
@ -118,7 +118,7 @@ export default function groupCommand(data: Data): void {
|
|||
setSelectedIds(data, [newGroupShape.id])
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
const group = shapes[newGroupShape.id]
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ export default function handleCommand(
|
|||
name: 'moved_handle',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { initialShape, currentPageId } = after
|
||||
const { initialShape } = after
|
||||
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
const shape = page.shapes[initialShape.id]
|
||||
|
||||
getShapeUtils(shape)
|
||||
|
@ -26,9 +26,9 @@ export default function handleCommand(
|
|||
.onSessionComplete(shape)
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShape, currentPageId } = before
|
||||
const { initialShape } = before
|
||||
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
page.shapes[initialShape.id] = initialShape
|
||||
},
|
||||
})
|
||||
|
|
|
@ -39,7 +39,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
|||
const fromPageId = oldPageId
|
||||
const toPageId = newPageId
|
||||
|
||||
const fromPage = getPage(data, fromPageId)
|
||||
const fromPage = getPage(data)
|
||||
|
||||
// Get all of the selected shapes and their descendents
|
||||
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
|
||||
getPageState(data, fromPageId).selectedIds.clear()
|
||||
getPageState(data).selectedIds.clear()
|
||||
|
||||
// Save the "from" page
|
||||
storage.savePage(data, data.document.id, fromPageId)
|
||||
|
@ -74,7 +74,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
|||
storage.loadPage(data, toPageId)
|
||||
|
||||
// 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.
|
||||
shapesToMove.forEach((shape) => {
|
||||
|
@ -89,7 +89,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
|||
})
|
||||
|
||||
// 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
|
||||
data.currentPageId = toPageId
|
||||
|
@ -98,7 +98,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
|
|||
const fromPageId = newPageId
|
||||
const toPageId = oldPageId
|
||||
|
||||
const fromPage = getPage(data, fromPageId)
|
||||
const fromPage = getPage(data)
|
||||
|
||||
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]
|
||||
})
|
||||
|
||||
getPageState(data, fromPageId).selectedIds.clear()
|
||||
getPageState(data).selectedIds.clear()
|
||||
|
||||
storage.savePage(data, data.document.id, fromPageId)
|
||||
|
||||
storage.loadPage(data, toPageId)
|
||||
|
||||
const toPage = getPage(data, toPageId)
|
||||
const toPage = getPage(data)
|
||||
|
||||
shapesToMove.forEach((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
|
||||
},
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
export default function moveCommand(data: Data, type: MoveType): void {
|
||||
const { currentPageId } = data
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
const selectedIds = setToArray(getSelectedIds(data))
|
||||
|
@ -28,7 +26,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const page = getPage(data, currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
const shapes = selectedIds.map((id) => page.shapes[id])
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import { getShapeUtils } from 'state/shape-utils'
|
|||
import vec from 'utils/vec'
|
||||
|
||||
export default function nudgeCommand(data: Data, delta: number[]): void {
|
||||
const { currentPageId } = data
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
const shapeBounds = Object.fromEntries(
|
||||
selectedShapes.map(
|
||||
|
@ -20,7 +19,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
|
|||
name: 'nudge_shapes',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in shapeBounds) {
|
||||
const shape = shapes[id]
|
||||
|
@ -32,7 +31,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in shapeBounds) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -15,8 +15,6 @@ import { getShapeUtils } from 'state/shape-utils'
|
|||
import state from 'state/state'
|
||||
|
||||
export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
||||
const { currentPageId } = data
|
||||
|
||||
const center = screenToWorld(
|
||||
[window.innerWidth / 2, window.innerHeight / 2],
|
||||
data
|
||||
|
@ -43,7 +41,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
let childIndex =
|
||||
(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))
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
Object.values(newIdMap).forEach((id) => delete shapes[id])
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getShapeUtils } from 'state/shape-utils'
|
|||
const PI2 = Math.PI * 2
|
||||
|
||||
export default function rotateCcwCommand(data: Data): void {
|
||||
const { currentPageId, boundsRotation } = data
|
||||
const { boundsRotation } = data
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
|
@ -63,7 +63,7 @@ export default function rotateCcwCommand(data: Data): void {
|
|||
name: 'rotate_ccw',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in nextShapes) {
|
||||
const shape = shapes[id]
|
||||
|
@ -77,7 +77,7 @@ export default function rotateCcwCommand(data: Data): void {
|
|||
data.boundsRotation = nextboundsRotation
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in initialShapes) {
|
||||
const { point, rotation } = initialShapes[id]
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function rotateCommand(
|
|||
data.boundsRotation = after.boundsRotation
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const { id, point, rotation } of before.initialShapes) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -5,8 +5,6 @@ import { deepClone, getCommonBounds, getPage, getSelectedShapes } from 'utils'
|
|||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
||||
export default function stretchCommand(data: Data, type: StretchType): void {
|
||||
const { currentPageId } = data
|
||||
|
||||
const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape))
|
||||
|
||||
const snapshot = Object.fromEntries(
|
||||
|
@ -29,7 +27,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
|
|||
name: 'stretched_shapes',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
switch (type) {
|
||||
case StretchType.Horizontal: {
|
||||
|
@ -77,7 +75,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
initialShapes.forEach((shape) => (shapes[shape.id] = shape))
|
||||
},
|
||||
})
|
||||
|
|
|
@ -11,7 +11,6 @@ export default function styleCommand(
|
|||
): void {
|
||||
const cData = current(data)
|
||||
const page = getPage(cData)
|
||||
const { currentPageId } = cData
|
||||
|
||||
const selectedIds = setToArray(getSelectedIds(data))
|
||||
|
||||
|
@ -26,7 +25,7 @@ export default function styleCommand(
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const { id } of shapesToStyle) {
|
||||
const shape = shapes[id]
|
||||
|
@ -34,7 +33,7 @@ export default function styleCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const { id, style } of shapesToStyle) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -9,7 +9,6 @@ export default function toggleCommand(
|
|||
data: Data,
|
||||
prop: PropsOfType<Shape>
|
||||
): void {
|
||||
const { currentPageId } = data
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
const isAllToggled = selectedShapes.every((shape) => shape[prop])
|
||||
const initialShapes = Object.fromEntries(
|
||||
|
@ -22,7 +21,7 @@ export default function toggleCommand(
|
|||
name: 'toggle_prop',
|
||||
category: 'canvas',
|
||||
do(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in initialShapes) {
|
||||
const shape = shapes[id]
|
||||
|
@ -34,7 +33,7 @@ export default function toggleCommand(
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const id in initialShapes) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function transformSingleCommand(
|
|||
after: TransformSingleSnapshot,
|
||||
isCreating: boolean
|
||||
): void {
|
||||
const shape = current(getPage(data, after.currentPageId).shapes[after.id])
|
||||
const shape = current(getPage(data).shapes[after.id])
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
|
@ -22,7 +22,7 @@ export default function transformSingleCommand(
|
|||
do(data) {
|
||||
const { id } = after
|
||||
|
||||
const { shapes } = getPage(data, after.currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
setSelectedIds(data, [id])
|
||||
|
||||
|
@ -33,7 +33,7 @@ export default function transformSingleCommand(
|
|||
undo(data) {
|
||||
const { id, initialShape } = before
|
||||
|
||||
const { shapes } = getPage(data, before.currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
if (isCreating) {
|
||||
setSelectedIds(data, [])
|
||||
|
|
|
@ -25,8 +25,8 @@ export default function translateCommand(
|
|||
do(data, initial) {
|
||||
if (initial) return
|
||||
|
||||
const { initialShapes, currentPageId } = after
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { initialShapes } = after
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
// Restore clones to document
|
||||
if (isCloning) {
|
||||
|
@ -66,8 +66,8 @@ export default function translateCommand(
|
|||
)
|
||||
},
|
||||
undo(data) {
|
||||
const { initialShapes, clones, currentPageId, initialParents } = before
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { initialShapes, clones, initialParents } = before
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
// Move shapes back to where they started
|
||||
for (const { id, point } of initialShapes) {
|
||||
|
|
|
@ -68,7 +68,7 @@ export default function ungroupCommand(data: Data): void {
|
|||
}
|
||||
},
|
||||
undo(data) {
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
selectedGroups.forEach((group) => {
|
||||
shapes[group.id] = group
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class DirectionSession extends BaseSession {
|
|||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const page = getPage(data, this.snapshot.currentPageId)
|
||||
const page = getPage(data)
|
||||
|
||||
for (const { id, direction } of this.snapshot.shapes) {
|
||||
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 {
|
||||
const { currentPageId, handleId, initialShape } = this.snapshot
|
||||
const shape = getPage(data, currentPageId).shapes[initialShape.id]
|
||||
const { handleId, initialShape } = this.snapshot
|
||||
const shape = getPage(data).shapes[initialShape.id]
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
|
@ -46,8 +46,8 @@ export default class HandleSession extends BaseSession {
|
|||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const { currentPageId, initialShape } = this.snapshot
|
||||
getPage(data, currentPageId).shapes[initialShape.id] = initialShape
|
||||
const { initialShape } = this.snapshot
|
||||
getPage(data).shapes[initialShape.id] = initialShape
|
||||
}
|
||||
|
||||
complete(data: Data): void {
|
||||
|
|
|
@ -76,8 +76,8 @@ export default class RotateSession extends BaseSession {
|
|||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const { currentPageId, initialShapes } = this.snapshot
|
||||
const page = getPage(data, currentPageId)
|
||||
const { initialShapes } = this.snapshot
|
||||
const page = getPage(data)
|
||||
|
||||
for (const { id, point, rotation } of initialShapes) {
|
||||
const shape = page.shapes[id]
|
||||
|
|
|
@ -80,9 +80,9 @@ export default class TransformSession extends BaseSession {
|
|||
}
|
||||
|
||||
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) {
|
||||
const shape = shapes[id]
|
||||
|
|
|
@ -36,10 +36,9 @@ export default class TransformSingleSession extends BaseSession {
|
|||
update(data: Data, point: number[], isAspectRatioLocked = false): void {
|
||||
const { transformType } = this
|
||||
|
||||
const { initialShapeBounds, currentPageId, initialShape, id } =
|
||||
this.snapshot
|
||||
const { initialShapeBounds, initialShape, id } = this.snapshot
|
||||
|
||||
const shape = getShape(data, id, currentPageId)
|
||||
const shape = getShape(data, id)
|
||||
|
||||
const newBoundingBox = getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
|
|
|
@ -33,9 +33,8 @@ export default class TranslateSession extends BaseSession {
|
|||
isAligned: boolean,
|
||||
isCloning: boolean
|
||||
): void {
|
||||
const { currentPageId, clones, initialShapes, initialParents } =
|
||||
this.snapshot
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { clones, initialShapes, initialParents } = this.snapshot
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
|
@ -143,9 +142,8 @@ export default class TranslateSession extends BaseSession {
|
|||
}
|
||||
|
||||
cancel(data: Data): void {
|
||||
const { initialShapes, initialParents, clones, currentPageId } =
|
||||
this.snapshot
|
||||
const { shapes } = getPage(data, currentPageId)
|
||||
const { initialShapes, initialParents, clones } = this.snapshot
|
||||
const { shapes } = getPage(data)
|
||||
|
||||
for (const { id } of initialShapes) {
|
||||
getDocumentBranch(data, id).forEach((id) => {
|
||||
|
|
|
@ -5,7 +5,8 @@ import {
|
|||
rng,
|
||||
getBoundsFromPoints,
|
||||
translateBounds,
|
||||
pointsBetween,
|
||||
pointInBounds,
|
||||
pointInCircle,
|
||||
} from 'utils'
|
||||
import {
|
||||
ArrowShape,
|
||||
|
@ -15,12 +16,10 @@ import {
|
|||
ShapeType,
|
||||
} from 'types'
|
||||
import { circleFromThreePoints, isAngleBetween } from 'utils'
|
||||
import { pointInBounds } from 'utils/hitTests'
|
||||
import {
|
||||
intersectArcBounds,
|
||||
intersectLineSegmentBounds,
|
||||
} from 'utils/intersections'
|
||||
import { pointInCircle } from 'utils/hitTests'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import React from 'react'
|
||||
|
@ -502,8 +501,8 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
|
|||
|
||||
const stroke = getStroke(
|
||||
[
|
||||
...pointsBetween(start.point, m),
|
||||
...pointsBetween(m, end.point),
|
||||
...vec.pointsBetween(start.point, m),
|
||||
...vec.pointsBetween(m, end.point),
|
||||
end.point,
|
||||
end.point,
|
||||
end.point,
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { DotShape, ShapeType } from 'types'
|
||||
import { boundsContained } from 'utils/bounds'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { translateBounds } from 'utils'
|
||||
import { boundsContained, translateBounds } from 'utils'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ import { uniqueId } from 'utils'
|
|||
import vec from 'utils/vec'
|
||||
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
import { boundsContain } from 'utils/bounds'
|
||||
import getStroke, { getStrokePoints } from 'perfect-freehand'
|
||||
import {
|
||||
getBoundsCenter,
|
||||
getBoundsFromPoints,
|
||||
getSvgPathFromStroke,
|
||||
translateBounds,
|
||||
boundsContain,
|
||||
} from 'utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
|
|
@ -2,15 +2,15 @@ import { getPerfectDashProps } from 'utils/dashes'
|
|||
import vec from 'utils/vec'
|
||||
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
||||
import { getShapeUtils } from './index'
|
||||
import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
|
||||
import { intersectEllipseBounds } from 'utils/intersections'
|
||||
import { pointInEllipse } from 'utils/hitTests'
|
||||
import {
|
||||
uniqueId,
|
||||
ease,
|
||||
getSvgPathFromStroke,
|
||||
rng,
|
||||
translateBounds,
|
||||
pointInEllipse,
|
||||
boundsContained,
|
||||
getRotatedEllipseBounds,
|
||||
} from 'utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import getStroke from 'perfect-freehand'
|
||||
|
@ -207,7 +207,8 @@ function renderPath(shape: EllipseShape) {
|
|||
}
|
||||
|
||||
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 y = ry * Math.sin(rads) + center[1]
|
||||
points.push([x, y])
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
import { LineShape, ShapeType } from 'types'
|
||||
import { boundsContained } from 'utils/bounds'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { ThinLine } from 'components/canvas/misc'
|
||||
import { translateBounds } from 'utils'
|
||||
import { translateBounds, boundsContained } from 'utils'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@ import { uniqueId } from 'utils'
|
|||
import vec from 'utils/vec'
|
||||
import { PolylineShape, ShapeType } from 'types'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
import { boundsContainPolygon } from 'utils/bounds'
|
||||
import { getBoundsFromPoints, translateBounds } from 'utils'
|
||||
import {
|
||||
boundsContainPolygon,
|
||||
getBoundsFromPoints,
|
||||
translateBounds,
|
||||
} from 'utils'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
import { RayShape, ShapeType } from 'types'
|
||||
import { boundsContained } from 'utils/bounds'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { ThinLine } from 'components/canvas/misc'
|
||||
import { translateBounds } from 'utils'
|
||||
import { translateBounds, boundsContained } from 'utils'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
||||
import {
|
||||
getSvgPathFromStroke,
|
||||
translateBounds,
|
||||
rng,
|
||||
shuffleArr,
|
||||
pointsBetween,
|
||||
} from 'utils'
|
||||
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import { registerShapeUtils } from './register'
|
||||
|
@ -203,10 +197,10 @@ function renderPath(shape: RectangleShape) {
|
|||
|
||||
const lines = shuffleArr(
|
||||
[
|
||||
pointsBetween(tr, br),
|
||||
pointsBetween(br, bl),
|
||||
pointsBetween(bl, tl),
|
||||
pointsBetween(tl, tr),
|
||||
vec.pointsBetween(tr, br),
|
||||
vec.pointsBetween(br, bl),
|
||||
vec.pointsBetween(bl, tl),
|
||||
vec.pointsBetween(tl, tr),
|
||||
],
|
||||
Math.floor(5 + getRandom() * 4)
|
||||
)
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { Shape, ShapeUtility } from 'types'
|
||||
import vec from 'utils/vec'
|
||||
import { getBoundsCenter, getBoundsFromPoints, getRotatedCorners } from 'utils'
|
||||
import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds'
|
||||
import {
|
||||
pointInBounds,
|
||||
getBoundsCenter,
|
||||
getBoundsFromPoints,
|
||||
getRotatedCorners,
|
||||
boundsCollidePolygon,
|
||||
boundsContainPolygon,
|
||||
} from 'utils'
|
||||
import { uniqueId } from 'utils'
|
||||
import React from 'react'
|
||||
import { pointInBounds } from 'utils/hitTests'
|
||||
|
||||
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||
return {
|
||||
|
|
|
@ -11,7 +11,6 @@ import commands from './commands'
|
|||
import {
|
||||
getChildren,
|
||||
getCommonBounds,
|
||||
getCurrent,
|
||||
getCurrentCamera,
|
||||
getPage,
|
||||
getSelectedBounds,
|
||||
|
@ -27,8 +26,10 @@ import {
|
|||
setSelectedIds,
|
||||
getPageState,
|
||||
setToArray,
|
||||
copyToClipboard,
|
||||
deepClone,
|
||||
pointInBounds,
|
||||
} from 'utils'
|
||||
|
||||
import {
|
||||
Data,
|
||||
PointerInfo,
|
||||
|
@ -47,7 +48,6 @@ import {
|
|||
ColorStyle,
|
||||
} from 'types'
|
||||
import session from './session'
|
||||
import { pointInBounds } from 'utils/hitTests'
|
||||
|
||||
const initialData: Data = {
|
||||
isReadOnly: false,
|
||||
|
@ -335,8 +335,6 @@ const state = createState({
|
|||
selecting: {
|
||||
onEnter: ['setActiveToolSelect', 'clearInputs'],
|
||||
on: {
|
||||
UNDO: 'undo',
|
||||
REDO: 'redo',
|
||||
SAVED: 'forceSave',
|
||||
DELETED: {
|
||||
unless: 'isReadOnly',
|
||||
|
@ -773,8 +771,6 @@ const state = createState({
|
|||
do: 'createShape',
|
||||
to: 'arrow.editing',
|
||||
},
|
||||
UNDO: { do: 'undo' },
|
||||
REDO: { do: 'redo' },
|
||||
},
|
||||
},
|
||||
editing: {
|
||||
|
@ -1144,7 +1140,7 @@ const state = createState({
|
|||
const shape = createShape(type, {
|
||||
parentId: data.currentPageId,
|
||||
point: vec.round(screenToWorld(payload.point, data)),
|
||||
style: getCurrent(data.currentStyle),
|
||||
style: deepClone(data.currentStyle),
|
||||
})
|
||||
|
||||
const siblings = getChildren(data, shape.parentId)
|
||||
|
@ -1748,7 +1744,7 @@ const state = createState({
|
|||
data,
|
||||
payload: { shapes: Shape[]; controls: CodeControl[] }
|
||||
) {
|
||||
commands.generate(data, data.currentPageId, payload.shapes)
|
||||
commands.generate(data, payload.shapes)
|
||||
},
|
||||
setCodeControls(data, payload: { controls: CodeControl[] }) {
|
||||
data.codeControls = Object.fromEntries(
|
||||
|
@ -1776,7 +1772,7 @@ const state = createState({
|
|||
data.document.code[data.currentCodeFileId].code
|
||||
)
|
||||
|
||||
commands.generate(data, data.currentPageId, shapes)
|
||||
commands.generate(data, shapes)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
@ -1807,7 +1803,7 @@ const state = createState({
|
|||
},
|
||||
|
||||
copyStateToClipboard(data) {
|
||||
copyToClipboard(JSON.stringify(data))
|
||||
clipboard.copyStringToClipboard(JSON.stringify(data))
|
||||
},
|
||||
|
||||
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 => {
|
||||
;(window as any).gtag('config', GA_TRACKING_ID, {
|
||||
if ('gtag' in window) {
|
||||
;(window as any)?.gtag('config', GA_TRACKING_ID, {
|
||||
page_path: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const event = ({ action, category, label, value }: GTagEvent): void => {
|
||||
;(window as any).gtag('event', action, {
|
||||
if ('gtag' in window) {
|
||||
;(window as any)?.gtag('event', action, {
|
||||
event_category: category,
|
||||
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 }
|
||||
}
|
||||
|
||||
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(
|
||||
a1: number[],
|
||||
a2: number[],
|
||||
|
@ -45,6 +70,27 @@ export function intersectLineSegments(
|
|||
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(
|
||||
c: 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 B
|
||||
* @param d
|
||||
|
@ -482,6 +482,16 @@ export default class Vec {
|
|||
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.
|
||||
* @param a
|
||||
|
@ -490,4 +500,19 @@ export default class Vec {
|
|||
static toPrecision = (a: number[], n = 4): number[] => {
|
||||
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