Starts implementing locked shapes

This commit is contained in:
Steve Ruiz 2021-05-29 13:40:41 +01:00
parent 3329c16e57
commit 6118dfcc2c
9 changed files with 138 additions and 87 deletions

View file

@ -1,7 +1,7 @@
import * as React from 'react'
import { Edge, Corner } from 'types'
import { useSelector } from 'state'
import { getSelectedShapes, isMobile } from 'utils/utils'
import { getPage, getSelectedShapes, isMobile } from 'utils/utils'
import CenterHandle from './center-handle'
import CornerHandle from './corner-handle'
@ -13,10 +13,18 @@ export default function Bounds() {
const isSelecting = useSelector((s) => s.isIn('selecting'))
const zoom = useSelector((s) => s.data.camera.zoom)
const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector(({ data }) =>
data.selectedIds.size === 1 ? getSelectedShapes(data)[0].rotation : 0
)
const isAllLocked = useSelector((s) => {
const page = getPage(s.data)
return Array.from(s.data.selectedIds.values()).every(
(id) => page.shapes[id].isLocked
)
})
if (!bounds) return null
if (!isSelecting) return null
@ -31,16 +39,28 @@ export default function Bounds() {
${(bounds.minY + bounds.maxY) / 2})
translate(${bounds.minX},${bounds.minY})`}
>
<CenterHandle bounds={bounds} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomRight} />
<CornerHandle size={size} bounds={bounds} corner={Corner.BottomLeft} />
<RotateHandle size={size} bounds={bounds} />
<CenterHandle bounds={bounds} isLocked={isAllLocked} />
{!isAllLocked && (
<>
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Bottom} />
<EdgeHandle size={size} bounds={bounds} edge={Edge.Left} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopLeft} />
<CornerHandle size={size} bounds={bounds} corner={Corner.TopRight} />
<CornerHandle
size={size}
bounds={bounds}
corner={Corner.BottomRight}
/>
<CornerHandle
size={size}
bounds={bounds}
corner={Corner.BottomLeft}
/>
<RotateHandle size={size} bounds={bounds} />
</>
)}
</g>
)
}

View file

@ -1,18 +1,34 @@
import styled from "styles"
import { Bounds } from "types"
import styled from 'styles'
import { Bounds } from 'types'
export default function CenterHandle({ bounds }: { bounds: Bounds }) {
export default function CenterHandle({
bounds,
isLocked,
}: {
bounds: Bounds
isLocked: boolean
}) {
return (
<StyledBounds
width={bounds.width}
height={bounds.height}
pointerEvents="none"
isLocked={isLocked}
/>
)
}
const StyledBounds = styled("rect", {
fill: "none",
stroke: "$bounds",
const StyledBounds = styled('rect', {
fill: 'none',
stroke: '$bounds',
zStrokeWidth: 2,
variants: {
isLocked: {
true: {
zStrokeWidth: 1,
zDash: 2,
},
},
},
})

View file

@ -43,6 +43,7 @@ export function ShapeOutline({ id }: { id: string }) {
as="use"
href={'#' + id}
transform={transform}
isLocked={shape.isLocked}
{...events}
/>
)
@ -55,4 +56,13 @@ const Indicator = styled('path', {
stroke: '$selected',
fill: 'transparent',
pointerEvents: 'all',
variants: {
isLocked: {
true: {
zDash: 2,
},
false: {},
},
},
})

View file

@ -1,59 +1,57 @@
import Command from "./command"
import history from "../history"
import { Data, Corner, Edge } from "types"
import { TransformSnapshot } from "state/sessions/transform-session"
import { getShapeUtils } from "lib/shape-utils"
import { getPage } from "utils/utils"
import Command from './command'
import history from '../history'
import { Data } from 'types'
import { TransformSnapshot } from 'state/sessions/transform-session'
import { getShapeUtils } from 'lib/shape-utils'
import { getPage } from 'utils/utils'
export default function transformCommand(
data: Data,
before: TransformSnapshot,
after: TransformSnapshot,
scaleX: number,
scaleY: number
after: TransformSnapshot
) {
history.execute(
data,
new Command({
name: "translate_shapes",
category: "canvas",
name: 'translate_shapes',
category: 'canvas',
do(data) {
const { type, selectedIds } = after
const { type, shapeBounds } = after
const { shapes } = getPage(data)
selectedIds.forEach((id) => {
for (let id in shapeBounds) {
const { initialShape, initialShapeBounds, transformOrigin } =
after.shapeBounds[id]
shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX: 1,
scaleY: 1,
transformOrigin,
})
})
}
},
undo(data) {
const { type, selectedIds } = before
const { type, shapeBounds } = before
const { shapes } = getPage(data)
selectedIds.forEach((id) => {
for (let id in shapeBounds) {
const { initialShape, initialShapeBounds, transformOrigin } =
before.shapeBounds[id]
shapeBounds[id]
const shape = shapes[id]
getShapeUtils(shape).transform(shape, initialShapeBounds, {
type,
initialShape,
transformOrigin,
scaleX: 1,
scaleY: 1,
transformOrigin,
})
})
}
},
})
)

View file

@ -1,9 +1,9 @@
import Command from "./command"
import history from "../history"
import { TranslateSnapshot } from "state/sessions/translate-session"
import { Data } from "types"
import { getPage } from "utils/utils"
import { getShapeUtils } from "lib/shape-utils"
import Command from './command'
import history from '../history'
import { TranslateSnapshot } from 'state/sessions/translate-session'
import { Data } from 'types'
import { getPage } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils'
export default function translateCommand(
data: Data,
@ -14,8 +14,8 @@ export default function translateCommand(
history.execute(
data,
new Command({
name: isCloning ? "clone_shapes" : "translate_shapes",
category: "canvas",
name: isCloning ? 'clone_shapes' : 'translate_shapes',
category: 'canvas',
manualSelection: true,
do(data, initial) {
if (initial) return

View file

@ -1,18 +1,19 @@
import { Data, Edge, Corner } from "types"
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { getShapeUtils } from "lib/shape-utils"
import { Data, Edge, Corner } from 'types'
import * as vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { getShapeUtils } from 'lib/shape-utils'
import {
getBoundsCenter,
getBoundsFromPoints,
getCommonBounds,
getPage,
getRelativeTransformedBoundingBox,
getSelectedShapes,
getShapes,
getTransformedBoundingBox,
} from "utils/utils"
} from 'utils/utils'
export default class TransformSession extends BaseSession {
scaleX = 1
@ -31,7 +32,7 @@ export default class TransformSession extends BaseSession {
update(data: Data, point: number[], isAspectRatioLocked = false) {
const { transformType } = this
const { selectedIds, shapeBounds, initialBounds } = this.snapshot
const { shapeBounds, initialBounds } = this.snapshot
const { shapes } = getPage(data)
@ -48,7 +49,7 @@ export default class TransformSession extends BaseSession {
// Now work backward to calculate a new bounding box for each of the shapes.
selectedIds.forEach((id) => {
for (let id in shapeBounds) {
const { initialShape, initialShapeBounds, transformOrigin } =
shapeBounds[id]
@ -69,15 +70,15 @@ export default class TransformSession extends BaseSession {
scaleY: this.scaleY,
transformOrigin,
})
})
}
}
cancel(data: Data) {
const { currentPageId, selectedIds, shapeBounds } = this.snapshot
const { currentPageId, shapeBounds } = this.snapshot
const page = getPage(data, currentPageId)
selectedIds.forEach((id) => {
for (let id in shapeBounds) {
const shape = page.shapes[id]
const { initialShape, initialShapeBounds, transformOrigin } =
@ -90,35 +91,33 @@ export default class TransformSession extends BaseSession {
scaleY: 1,
transformOrigin,
})
})
}
}
complete(data: Data) {
commands.transform(
data,
this.snapshot,
getTransformSnapshot(data, this.transformType),
this.scaleX,
this.scaleY
getTransformSnapshot(data, this.transformType)
)
}
}
export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
const {
document: { pages },
selectedIds,
currentPageId,
} = current(data)
const cData = current(data)
const { currentPageId } = cData
const pageShapes = pages[currentPageId].shapes
const initialShapes = getSelectedShapes(cData).filter(
(shape) => !shape.isLocked
)
const hasShapes = initialShapes.length > 0
// A mapping of selected shapes and their bounds
const shapesBounds = Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {
const shape = pageShapes[id]
return [shape.id, getShapeUtils(shape).getBounds(shape)]
})
initialShapes.map((shape) => [
shape.id,
getShapeUtils(shape).getBounds(shape),
])
)
const boundsArr = Object.values(shapesBounds)
@ -132,20 +131,19 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
// positions of the shape's bounds within the common bounds shape.
return {
type: transformType,
hasShapes,
currentPageId,
selectedIds: new Set(selectedIds),
initialBounds: bounds,
shapeBounds: Object.fromEntries(
Array.from(selectedIds.values()).map((id) => {
const shape = pageShapes[id]
const initialShapeBounds = shapesBounds[id]
initialShapes.map((shape) => {
const initialShapeBounds = shapesBounds[shape.id]
const ic = getBoundsCenter(initialShapeBounds)
let ix = (ic[0] - initialInnerBounds.minX) / initialInnerBounds.width
let iy = (ic[1] - initialInnerBounds.minY) / initialInnerBounds.height
return [
id,
shape.id,
{
initialShape: shape,
initialShapeBounds,

View file

@ -1,11 +1,11 @@
import { Data } from "types"
import * as vec from "utils/vec"
import BaseSession from "./base-session"
import commands from "state/commands"
import { current } from "immer"
import { v4 as uuid } from "uuid"
import { getChildIndexAbove, getPage, getSelectedShapes } from "utils/utils"
import { getShapeUtils } from "lib/shape-utils"
import { Data } from 'types'
import * as vec from 'utils/vec'
import BaseSession from './base-session'
import commands from 'state/commands'
import { current } from 'immer'
import { v4 as uuid } from 'uuid'
import { getChildIndexAbove, getPage, getSelectedShapes } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils'
export default class TranslateSession extends BaseSession {
delta = [0, 0]
@ -89,6 +89,8 @@ export default class TranslateSession extends BaseSession {
}
complete(data: Data) {
if (!this.snapshot.hasShapes) return
commands.translate(
data,
this.snapshot,
@ -100,7 +102,8 @@ export default class TranslateSession extends BaseSession {
export function getTranslateSnapshot(data: Data) {
const cData = current(data)
const shapes = getSelectedShapes(cData)
const shapes = getSelectedShapes(cData).filter((shape) => !shape.isLocked)
const hasShapes = shapes.length > 0
return {
currentPageId: data.currentPageId,
@ -110,6 +113,7 @@ export function getTranslateSnapshot(data: Data) {
id: uuid(),
childIndex: getChildIndexAbove(cData, shape.id),
})),
hasShapes,
}
}

View file

@ -141,7 +141,6 @@ const state = createState({
UNDO: 'undo',
REDO: 'redo',
SAVED_CODE: 'saveCode',
CANCELLED: 'clearSelectedIds',
DELETED: 'deleteSelectedIds',
STARTED_PINCHING: { to: 'pinching' },
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
@ -159,6 +158,7 @@ const state = createState({
states: {
notPointing: {
on: {
CANCELLED: 'clearSelectedIds',
POINTED_CANVAS: { to: 'brushSelecting' },
POINTED_BOUNDS: { to: 'pointingBounds' },
POINTED_BOUNDS_HANDLE: {

View file

@ -43,6 +43,11 @@ const { styled, global, css, theme, getCssString } = createCss({
transitions: {},
},
utils: {
zDash: () => (value: number) => {
return {
strokeDasharray: `calc(${value}px / var(--camera-zoom)) calc(${value}px / var(--camera-zoom))`,
}
},
zStrokeWidth: () => (value: number | number[]) => {
if (Array.isArray(value)) {
// const [val, min, max] = value