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}
isSelecting={isSelecting}
parentPoint={noOffset}
parentRotation={0}
/>
))}
</g>

View file

@ -3,7 +3,7 @@ import { useSelector } from 'state'
import styled from 'styles'
import { getShapeUtils } from 'lib/shape-utils'
import { getPage } from 'utils/utils'
import { ShapeType } from 'types'
import { ShapeStyles, ShapeType } from 'types'
import useShapeEvents from 'hooks/useShapeEvents'
import * as vec from 'utils/vec'
import { getShapeStyle } from 'lib/shape-styles'
@ -12,10 +12,9 @@ interface ShapeProps {
id: string
isSelecting: boolean
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 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
// a deleted shape will still be pulled here before the page component
// detects the change and pulls this component.
if (!shape) return null
if (!shape) {
return null
}
const isGroup = shape.type === ShapeType.Group
@ -50,9 +51,7 @@ function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
{...events}
/>
)}
{!shape.isHidden && (
<StyledShape as="use" data-shy={isGroup} href={'#' + id} {...style} />
)}
{!shape.isHidden && <ReadShape isGroup={isGroup} id={id} style={style} />}
{isGroup &&
shape.children.map((shapeId) => (
<Shape
@ -60,13 +59,26 @@ function Shape({ id, isSelecting, parentPoint, parentRotation }: ShapeProps) {
id={shapeId}
isSelecting={isSelecting}
parentPoint={shape.point}
parentRotation={shape.rotation}
/>
))}
</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', {
strokeLinecap: 'round',
strokeLinejoin: 'round',

View file

@ -28,7 +28,7 @@ const group = registerShapeUtils<GroupShape>({
id: uuid(),
type: ShapeType.Group,
isGenerated: false,
name: 'Rectangle',
name: 'Group',
parentId: 'page0',
childIndex: 0,
point: [0, 0],

View file

@ -27,9 +27,9 @@ export default function groupCommand(data: Data) {
let newGroupParentId: string
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(
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
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)
return
} else {
// Make the group inside of the current group
newGroupParentId = parentId
}
}
@ -77,7 +75,8 @@ export default function groupCommand(data: Data) {
parentId: newGroupParentId,
point: [commonBounds.minX, commonBounds.minY],
size: [commonBounds.width, commonBounds.height],
children: selectedShapeIds,
children: initialShapeIds,
childIndex: initialShapes[0].childIndex,
})
history.execute(
@ -85,43 +84,80 @@ export default function groupCommand(data: Data) {
new Command({
name: 'group_shapes',
category: 'canvas',
manualSelection: true,
do(data) {
const { shapes } = getPage(data, currentPageId)
// Remove shapes from old parents
for (const parentId of parentIds) {
if (parentId === currentPageId) continue
// Create the new group
shapes[newGroupShape.id] = newGroupShape
const shape = shapes[parentId] as GroupShape
getShapeUtils(shape).setProperty(
shape,
'children',
shape.children.filter((id) => !selectedIds.has(id))
)
// Assign the group to its new parent
if (newGroupParentId !== data.currentPageId) {
const parent = shapes[newGroupParentId]
getShapeUtils(parent).setProperty(parent, 'children', [
...parent.children,
newGroupShape.id,
])
}
shapes[newGroupShape.id] = newGroupShape
// 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',
oldParent.children.filter((id) => !selectedIds.has(id))
)
}
// 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.add(newGroupShape.id)
initialShapes.forEach(({ id }, i) => {
const shape = shapes[id]
getShapeUtils(shape)
.setProperty(shape, 'parentId', newGroupShape.id)
.setProperty(shape, 'childIndex', i)
})
},
undo(data) {
const { shapes } = getPage(data, currentPageId)
data.selectedIds.clear()
delete shapes[newGroupShape.id]
initialShapes.forEach(({ id, parentId, childIndex }, i) => {
data.selectedIds.add(id)
const group = shapes[newGroupShape.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]
getShapeUtils(shape)
.setProperty(shape, 'parentId', parentId)
.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])
},
undo(data) {
const { id, type, initialShapeBounds } = before
const { id, initialShape } = before
const { shapes } = getPage(data, before.currentPageId)
data.selectedIds.clear()
if (isCreating) {
data.selectedIds.clear()
delete shapes[id]
} else {
const shape = shapes[id]
data.selectedIds.add(id)
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape: after.initialShape,
scaleX: 1,
scaleY: 1,
transformOrigin: [0.5, 0.5],
})
const page = getPage(data)
page.shapes[id] = initialShape
updateParents(data, [id])
data.selectedIds = new Set([id])
}
},
})

View file

@ -18,8 +18,6 @@ export default function transformCommand(
name: 'translate_shapes',
category: 'canvas',
do(data, isInitial) {
if (isInitial) return
const { type, shapeBounds } = after
const { shapes } = getPage(data)
@ -27,15 +25,18 @@ export default function transformCommand(
for (let id in shapeBounds) {
const { initialShape, initialShapeBounds, transformOrigin } =
shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX,
scaleY,
})
getShapeUtils(shape)
.transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX,
scaleY,
})
.onSessionComplete(shape)
}
updateParents(data, Object.keys(shapeBounds))
@ -50,13 +51,15 @@ export default function transformCommand(
shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX: scaleX < 0 ? scaleX * -1 : scaleX,
scaleY: scaleX < 0 ? scaleX * -1 : scaleX,
})
getShapeUtils(shape)
.transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX: scaleX < 0 ? scaleX * -1 : scaleX,
scaleY: scaleX < 0 ? scaleX * -1 : scaleX,
})
.onSessionComplete(shape)
}
updateParents(data, Object.keys(shapeBounds))

View file

@ -2,7 +2,7 @@ import Command from './command'
import history from '../history'
import { TranslateSnapshot } from 'state/sessions/translate-session'
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 { v4 as uuid } from 'uuid'
@ -43,8 +43,10 @@ export default function translateCommand(
// Move shapes (these initialShapes will include clones if any)
for (const { id, point } of initialShapes) {
const shape = shapes[id]
getShapeUtils(shape).translateTo(shape, point)
getDocumentBranch(data, id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateTo(shape, point)
})
}
// Set selected shapes

View file

@ -67,18 +67,10 @@ export default class TransformSingleSession extends BaseSession {
}
cancel(data: Data) {
const { id, initialShape, initialShapeBounds, currentPageId } =
this.snapshot
const { id, initialShape } = this.snapshot
const shape = getShape(data, id, currentPageId)
getShapeUtils(shape).transform(shape, initialShapeBounds, {
initialShape,
type: this.transformType,
scaleX: this.scaleX,
scaleY: this.scaleY,
transformOrigin: [0.5, 0.5],
})
const page = getPage(data)
page.shapes[id] = initialShape
updateParents(data, [id])
}

View file

@ -6,6 +6,7 @@ import { current } from 'immer'
import { v4 as uuid } from 'uuid'
import {
getChildIndexAbove,
getDocumentBranch,
getPage,
getSelectedShapes,
updateParents,
@ -14,6 +15,7 @@ import { getShapeUtils } from 'lib/shape-utils'
export default class TranslateSession extends BaseSession {
delta = [0, 0]
prev = [0, 0]
origin: number[]
snapshot: TranslateSnapshot
isCloning = false
@ -30,6 +32,9 @@ export default class TranslateSession extends BaseSession {
const { shapes } = getPage(data, currentPageId)
const delta = vec.vec(this.origin, point)
const trueDelta = vec.sub(delta, this.prev)
this.delta = delta
this.prev = delta
if (isAligned) {
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
@ -92,17 +97,10 @@ export default class TranslateSession extends BaseSession {
}
for (const initialShape of initialShapes) {
const shape = shapes[initialShape.id]
const next = vec.add(initialShape.point, delta)
const deltaForShape = vec.sub(next, shape.point)
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)
}
}
getDocumentBranch(data, initialShape.id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateBy(shape, trueDelta)
})
}
updateParents(
@ -117,17 +115,11 @@ export default class TranslateSession extends BaseSession {
this.snapshot
const { shapes } = getPage(data, currentPageId)
for (const { id, point } of initialShapes) {
const shape = shapes[id]
const deltaForShape = vec.sub(point, shape.point)
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 initialShapes) {
getDocumentBranch(data, id).forEach((id) => {
const shape = shapes[id]
getShapeUtils(shape).translateBy(shape, vec.neg(this.delta))
})
}
for (const { id } of clones) {
@ -159,6 +151,14 @@ export default class TranslateSession extends BaseSession {
export function getTranslateSnapshot(data: 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 selectedShapes = getSelectedShapes(cData).filter(
(shape) => !shape.isLocked