Fixes nesting groups

This commit is contained in:
Steve Ruiz 2021-06-05 20:36:46 +01:00
parent 72accc5f44
commit a52e91459f
9 changed files with 138 additions and 102 deletions

View file

@ -29,7 +29,6 @@ export default function Page() {
id={shapeId} id={shapeId}
isSelecting={isSelecting} isSelecting={isSelecting}
parentPoint={noOffset} parentPoint={noOffset}
parentRotation={0}
/> />
))} ))}
</g> </g>

View file

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

View file

@ -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],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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