Fixes nesting groups
This commit is contained in:
parent
72accc5f44
commit
a52e91459f
9 changed files with 138 additions and 102 deletions
|
@ -29,7 +29,6 @@ export default function Page() {
|
||||||
id={shapeId}
|
id={shapeId}
|
||||||
isSelecting={isSelecting}
|
isSelecting={isSelecting}
|
||||||
parentPoint={noOffset}
|
parentPoint={noOffset}
|
||||||
parentRotation={0}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getPage } from 'utils/utils'
|
import { getPage } from 'utils/utils'
|
||||||
import { ShapeType } from 'types'
|
import { ShapeStyles, ShapeType } from 'types'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import { getShapeStyle } from 'lib/shape-styles'
|
import { getShapeStyle } from 'lib/shape-styles'
|
||||||
|
@ -12,10 +12,9 @@ interface ShapeProps {
|
||||||
id: string
|
id: string
|
||||||
isSelecting: boolean
|
isSelecting: boolean
|
||||||
parentPoint: number[]
|
parentPoint: number[]
|
||||||
parentRotation: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
|
function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||||
|
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
@ -26,7 +25,9 @@ function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
|
||||||
// may sometimes run before the hook in the Page component, which means
|
// may sometimes run before the hook in the Page component, which means
|
||||||
// a deleted shape will still be pulled here before the page component
|
// a deleted shape will still be pulled here before the page component
|
||||||
// detects the change and pulls this component.
|
// detects the change and pulls this component.
|
||||||
if (!shape) return null
|
if (!shape) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const isGroup = shape.type === ShapeType.Group
|
const isGroup = shape.type === ShapeType.Group
|
||||||
|
|
||||||
|
@ -50,9 +51,7 @@ function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
|
||||||
{...events}
|
{...events}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shape.isHidden && (
|
{!shape.isHidden && <ReadShape isGroup={isGroup} id={id} style={style} />}
|
||||||
<StyledShape as="use" data-shy={isGroup} href={'#' + id} {...style} />
|
|
||||||
)}
|
|
||||||
{isGroup &&
|
{isGroup &&
|
||||||
shape.children.map((shapeId) => (
|
shape.children.map((shapeId) => (
|
||||||
<Shape
|
<Shape
|
||||||
|
@ -60,13 +59,26 @@ function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
|
||||||
id={shapeId}
|
id={shapeId}
|
||||||
isSelecting={isSelecting}
|
isSelecting={isSelecting}
|
||||||
parentPoint={shape.point}
|
parentPoint={shape.point}
|
||||||
parentRotation={shape.rotation}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RealShapeProps {
|
||||||
|
isGroup: boolean
|
||||||
|
id: string
|
||||||
|
style: Partial<React.SVGProps<SVGUseElement>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReadShape = memo(function RealShape({
|
||||||
|
isGroup,
|
||||||
|
id,
|
||||||
|
style,
|
||||||
|
}: RealShapeProps) {
|
||||||
|
return <StyledShape as="use" data-shy={isGroup} href={'#' + id} {...style} />
|
||||||
|
})
|
||||||
|
|
||||||
const StyledShape = styled('path', {
|
const StyledShape = styled('path', {
|
||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
|
|
|
@ -28,7 +28,7 @@ const group = registerShapeUtils<GroupShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Group,
|
type: ShapeType.Group,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Rectangle',
|
name: 'Group',
|
||||||
parentId: 'page0',
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
|
|
@ -27,9 +27,9 @@ export default function groupCommand(data: Data) {
|
||||||
|
|
||||||
let newGroupParentId: string
|
let newGroupParentId: string
|
||||||
let newGroupShape: GroupShape
|
let newGroupShape: GroupShape
|
||||||
let oldGroupShape: GroupShape
|
let newGroupChildIndex: number
|
||||||
|
|
||||||
const selectedShapeIds = initialShapes.map((s) => s.id)
|
const initialShapeIds = initialShapes.map((s) => s.id)
|
||||||
|
|
||||||
const parentIds = Array.from(
|
const parentIds = Array.from(
|
||||||
new Set(initialShapes.map((s) => s.parentId)).values()
|
new Set(initialShapes.map((s) => s.parentId)).values()
|
||||||
|
@ -50,13 +50,11 @@ export default function groupCommand(data: Data) {
|
||||||
const parent = getShape(data, parentId) as GroupShape
|
const parent = getShape(data, parentId) as GroupShape
|
||||||
|
|
||||||
if (parent.children.length === initialShapes.length) {
|
if (parent.children.length === initialShapes.length) {
|
||||||
// !
|
// !!! Hey! We're not going any further. We need to ungroup those shapes.
|
||||||
// !
|
|
||||||
// !
|
|
||||||
// Hey! We're not going any further. We need to ungroup those shapes.
|
|
||||||
commands.ungroup(data)
|
commands.ungroup(data)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
// Make the group inside of the current group
|
||||||
newGroupParentId = parentId
|
newGroupParentId = parentId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +75,8 @@ export default function groupCommand(data: Data) {
|
||||||
parentId: newGroupParentId,
|
parentId: newGroupParentId,
|
||||||
point: [commonBounds.minX, commonBounds.minY],
|
point: [commonBounds.minX, commonBounds.minY],
|
||||||
size: [commonBounds.width, commonBounds.height],
|
size: [commonBounds.width, commonBounds.height],
|
||||||
children: selectedShapeIds,
|
children: initialShapeIds,
|
||||||
|
childIndex: initialShapes[0].childIndex,
|
||||||
})
|
})
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
|
@ -85,43 +84,80 @@ export default function groupCommand(data: Data) {
|
||||||
new Command({
|
new Command({
|
||||||
name: 'group_shapes',
|
name: 'group_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
// Remove shapes from old parents
|
// Create the new group
|
||||||
for (const parentId of parentIds) {
|
shapes[newGroupShape.id] = newGroupShape
|
||||||
if (parentId === currentPageId) continue
|
|
||||||
|
|
||||||
const shape = shapes[parentId] as GroupShape
|
// Assign the group to its new parent
|
||||||
getShapeUtils(shape).setProperty(
|
if (newGroupParentId !== data.currentPageId) {
|
||||||
shape,
|
const parent = shapes[newGroupParentId]
|
||||||
|
getShapeUtils(parent).setProperty(parent, 'children', [
|
||||||
|
...parent.children,
|
||||||
|
newGroupShape.id,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the shapes to their new parent
|
||||||
|
initialShapes.forEach((initialShape, i) => {
|
||||||
|
// Remove shape from its old parent
|
||||||
|
if (initialShape.parentId !== currentPageId) {
|
||||||
|
const oldParent = shapes[initialShape.parentId] as GroupShape
|
||||||
|
getShapeUtils(oldParent).setProperty(
|
||||||
|
oldParent,
|
||||||
'children',
|
'children',
|
||||||
shape.children.filter((id) => !selectedIds.has(id))
|
oldParent.children.filter((id) => !selectedIds.has(id))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
shapes[newGroupShape.id] = newGroupShape
|
// Assign the shape to its new parent, with its new childIndex
|
||||||
|
const shape = shapes[initialShape.id]
|
||||||
|
getShapeUtils(shape)
|
||||||
|
.setProperty(shape, 'childIndex', i)
|
||||||
|
.setProperty(shape, 'parentId', newGroupShape.id)
|
||||||
|
})
|
||||||
|
|
||||||
data.selectedIds.clear()
|
data.selectedIds.clear()
|
||||||
data.selectedIds.add(newGroupShape.id)
|
data.selectedIds.add(newGroupShape.id)
|
||||||
initialShapes.forEach(({ id }, i) => {
|
|
||||||
const shape = shapes[id]
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.setProperty(shape, 'parentId', newGroupShape.id)
|
|
||||||
.setProperty(shape, 'childIndex', i)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
delete shapes[newGroupShape.id]
|
const group = shapes[newGroupShape.id]
|
||||||
initialShapes.forEach(({ id, parentId, childIndex }, i) => {
|
|
||||||
data.selectedIds.add(id)
|
// remove the group from its parent
|
||||||
|
if (group.parentId !== data.currentPageId) {
|
||||||
|
const parent = shapes[group.parentId]
|
||||||
|
getShapeUtils(parent).setProperty(
|
||||||
|
parent,
|
||||||
|
'children',
|
||||||
|
parent.children.filter((id) => id !== newGroupShape.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the shapes back to their previous parent / childIndex
|
||||||
|
initialShapes.forEach(({ id, parentId, childIndex }) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
getShapeUtils(shape)
|
getShapeUtils(shape)
|
||||||
.setProperty(shape, 'parentId', parentId)
|
.setProperty(shape, 'parentId', parentId)
|
||||||
.setProperty(shape, 'childIndex', childIndex)
|
.setProperty(shape, 'childIndex', childIndex)
|
||||||
|
|
||||||
|
if (parentId !== data.currentPageId) {
|
||||||
|
const parent = shapes[parentId]
|
||||||
|
getShapeUtils(parent).setProperty(parent, 'children', [
|
||||||
|
...parent.children,
|
||||||
|
id,
|
||||||
|
])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Delete the group
|
||||||
|
delete shapes[newGroupShape.id]
|
||||||
|
|
||||||
|
// Reselect the children of the group
|
||||||
|
data.selectedIds = new Set(initialShapeIds)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,26 +33,18 @@ export default function transformSingleCommand(
|
||||||
updateParents(data, [id])
|
updateParents(data, [id])
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { id, type, initialShapeBounds } = before
|
const { id, initialShape } = before
|
||||||
|
|
||||||
const { shapes } = getPage(data, before.currentPageId)
|
const { shapes } = getPage(data, before.currentPageId)
|
||||||
|
|
||||||
data.selectedIds.clear()
|
|
||||||
|
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
|
data.selectedIds.clear()
|
||||||
delete shapes[id]
|
delete shapes[id]
|
||||||
} else {
|
} else {
|
||||||
const shape = shapes[id]
|
const page = getPage(data)
|
||||||
data.selectedIds.add(id)
|
page.shapes[id] = initialShape
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
|
||||||
type,
|
|
||||||
initialShape: after.initialShape,
|
|
||||||
scaleX: 1,
|
|
||||||
scaleY: 1,
|
|
||||||
transformOrigin: [0.5, 0.5],
|
|
||||||
})
|
|
||||||
updateParents(data, [id])
|
updateParents(data, [id])
|
||||||
|
data.selectedIds = new Set([id])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,8 +18,6 @@ export default function transformCommand(
|
||||||
name: 'translate_shapes',
|
name: 'translate_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data, isInitial) {
|
do(data, isInitial) {
|
||||||
if (isInitial) return
|
|
||||||
|
|
||||||
const { type, shapeBounds } = after
|
const { type, shapeBounds } = after
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
@ -27,15 +25,18 @@ export default function transformCommand(
|
||||||
for (let id in shapeBounds) {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShape, initialShapeBounds, transformOrigin } =
|
||||||
shapeBounds[id]
|
shapeBounds[id]
|
||||||
|
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
getShapeUtils(shape)
|
||||||
|
.transform(shape, initialShapeBounds, {
|
||||||
type,
|
type,
|
||||||
initialShape,
|
initialShape,
|
||||||
transformOrigin,
|
transformOrigin,
|
||||||
scaleX,
|
scaleX,
|
||||||
scaleY,
|
scaleY,
|
||||||
})
|
})
|
||||||
|
.onSessionComplete(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParents(data, Object.keys(shapeBounds))
|
updateParents(data, Object.keys(shapeBounds))
|
||||||
|
@ -50,13 +51,15 @@ export default function transformCommand(
|
||||||
shapeBounds[id]
|
shapeBounds[id]
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
getShapeUtils(shape)
|
||||||
|
.transform(shape, initialShapeBounds, {
|
||||||
type,
|
type,
|
||||||
initialShape,
|
initialShape,
|
||||||
transformOrigin,
|
transformOrigin,
|
||||||
scaleX: scaleX < 0 ? scaleX * -1 : scaleX,
|
scaleX: scaleX < 0 ? scaleX * -1 : scaleX,
|
||||||
scaleY: scaleX < 0 ? scaleX * -1 : scaleX,
|
scaleY: scaleX < 0 ? scaleX * -1 : scaleX,
|
||||||
})
|
})
|
||||||
|
.onSessionComplete(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParents(data, Object.keys(shapeBounds))
|
updateParents(data, Object.keys(shapeBounds))
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
import { TranslateSnapshot } from 'state/sessions/translate-session'
|
||||||
import { Data, GroupShape, Shape, ShapeType } from 'types'
|
import { Data, GroupShape, Shape, ShapeType } from 'types'
|
||||||
import { getPage, updateParents } from 'utils/utils'
|
import { getDocumentBranch, getPage, updateParents } from 'utils/utils'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
|
||||||
|
@ -43,8 +43,10 @@ export default function translateCommand(
|
||||||
|
|
||||||
// Move shapes (these initialShapes will include clones if any)
|
// Move shapes (these initialShapes will include clones if any)
|
||||||
for (const { id, point } of initialShapes) {
|
for (const { id, point } of initialShapes) {
|
||||||
|
getDocumentBranch(data, id).forEach((id) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
getShapeUtils(shape).translateTo(shape, point)
|
getShapeUtils(shape).translateTo(shape, point)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set selected shapes
|
// Set selected shapes
|
||||||
|
|
|
@ -67,18 +67,10 @@ export default class TransformSingleSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data) {
|
cancel(data: Data) {
|
||||||
const { id, initialShape, initialShapeBounds, currentPageId } =
|
const { id, initialShape } = this.snapshot
|
||||||
this.snapshot
|
|
||||||
|
|
||||||
const shape = getShape(data, id, currentPageId)
|
const page = getPage(data)
|
||||||
|
page.shapes[id] = initialShape
|
||||||
getShapeUtils(shape).transform(shape, initialShapeBounds, {
|
|
||||||
initialShape,
|
|
||||||
type: this.transformType,
|
|
||||||
scaleX: this.scaleX,
|
|
||||||
scaleY: this.scaleY,
|
|
||||||
transformOrigin: [0.5, 0.5],
|
|
||||||
})
|
|
||||||
|
|
||||||
updateParents(data, [id])
|
updateParents(data, [id])
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { current } from 'immer'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import {
|
import {
|
||||||
getChildIndexAbove,
|
getChildIndexAbove,
|
||||||
|
getDocumentBranch,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
updateParents,
|
updateParents,
|
||||||
|
@ -14,6 +15,7 @@ import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export default class TranslateSession extends BaseSession {
|
export default class TranslateSession extends BaseSession {
|
||||||
delta = [0, 0]
|
delta = [0, 0]
|
||||||
|
prev = [0, 0]
|
||||||
origin: number[]
|
origin: number[]
|
||||||
snapshot: TranslateSnapshot
|
snapshot: TranslateSnapshot
|
||||||
isCloning = false
|
isCloning = false
|
||||||
|
@ -30,6 +32,9 @@ export default class TranslateSession extends BaseSession {
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
const delta = vec.vec(this.origin, point)
|
const delta = vec.vec(this.origin, point)
|
||||||
|
const trueDelta = vec.sub(delta, this.prev)
|
||||||
|
this.delta = delta
|
||||||
|
this.prev = delta
|
||||||
|
|
||||||
if (isAligned) {
|
if (isAligned) {
|
||||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||||
|
@ -92,17 +97,10 @@ export default class TranslateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const initialShape of initialShapes) {
|
for (const initialShape of initialShapes) {
|
||||||
const shape = shapes[initialShape.id]
|
getDocumentBranch(data, initialShape.id).forEach((id) => {
|
||||||
const next = vec.add(initialShape.point, delta)
|
const shape = shapes[id]
|
||||||
const deltaForShape = vec.sub(next, shape.point)
|
getShapeUtils(shape).translateBy(shape, trueDelta)
|
||||||
getShapeUtils(shape).translateTo(shape, next)
|
})
|
||||||
|
|
||||||
if (shape.type === ShapeType.Group) {
|
|
||||||
for (let childId of shape.children) {
|
|
||||||
const childShape = shapes[childId]
|
|
||||||
getShapeUtils(childShape).translateBy(childShape, deltaForShape)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParents(
|
updateParents(
|
||||||
|
@ -117,17 +115,11 @@ export default class TranslateSession extends BaseSession {
|
||||||
this.snapshot
|
this.snapshot
|
||||||
const { shapes } = getPage(data, currentPageId)
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
for (const { id, point } of initialShapes) {
|
for (const { id } of initialShapes) {
|
||||||
|
getDocumentBranch(data, id).forEach((id) => {
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
const deltaForShape = vec.sub(point, shape.point)
|
getShapeUtils(shape).translateBy(shape, vec.neg(this.delta))
|
||||||
getShapeUtils(shape).translateTo(shape, point)
|
})
|
||||||
|
|
||||||
if (shape.type === ShapeType.Group) {
|
|
||||||
for (let childId of shape.children) {
|
|
||||||
const childShape = shapes[childId]
|
|
||||||
getShapeUtils(childShape).translateBy(childShape, deltaForShape)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { id } of clones) {
|
for (const { id } of clones) {
|
||||||
|
@ -159,6 +151,14 @@ export default class TranslateSession extends BaseSession {
|
||||||
|
|
||||||
export function getTranslateSnapshot(data: Data) {
|
export function getTranslateSnapshot(data: Data) {
|
||||||
const cData = current(data)
|
const cData = current(data)
|
||||||
|
|
||||||
|
// Get selected shapes
|
||||||
|
// Filter out the locked shapes
|
||||||
|
// Collect the branch children for each remaining shape
|
||||||
|
// Filter out doubles using a set
|
||||||
|
// End up with an array of ids for all of the shapes that will change
|
||||||
|
// Map into shapes from data snapshot
|
||||||
|
|
||||||
const page = getPage(cData)
|
const page = getPage(cData)
|
||||||
const selectedShapes = getSelectedShapes(cData).filter(
|
const selectedShapes = getSelectedShapes(cData).filter(
|
||||||
(shape) => !shape.isLocked
|
(shape) => !shape.isLocked
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue