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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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