Adds boolean props for lock, hide, aspect lock

This commit is contained in:
Steve Ruiz 2021-05-29 11:12:28 +01:00
parent b369aef7fc
commit 3329c16e57
29 changed files with 601 additions and 366 deletions

View file

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from "styles" import styled from 'styles'
import { useStateDesigner } from "@state-designer/react" import { useStateDesigner } from '@state-designer/react'
import React, { useEffect, useRef } from "react" import React, { useEffect, useRef } from 'react'
import { motion } from "framer-motion" import { motion } from 'framer-motion'
import state, { useSelector } from "state" import state, { useSelector } from 'state'
import { CodeFile } from "types" import { CodeFile } from 'types'
import CodeDocs from "./code-docs" import CodeDocs from './code-docs'
import CodeEditor from "./code-editor" import CodeEditor from './code-editor'
import { generateFromCode } from "lib/code/generate" import { generateFromCode } from 'lib/code/generate'
import * as Panel from "../panel" import * as Panel from '../panel'
import { IconButton } from "../shared" import { IconButton } from '../shared'
import { import {
X, X,
Code, Code,
@ -17,10 +17,10 @@ import {
PlayCircle, PlayCircle,
ChevronUp, ChevronUp,
ChevronDown, ChevronDown,
} from "react-feather" } from 'react-feather'
const getErrorLineAndColumn = (e: any) => { const getErrorLineAndColumn = (e: any) => {
if ("line" in e) { if ('line' in e) {
return { line: Number(e.line), column: e.column } return { line: Number(e.line), column: e.column }
} }
@ -46,23 +46,23 @@ export default function CodePanel() {
error: null as { message: string; line: number; column: number } | null, error: null as { message: string; line: number; column: number } | null,
}, },
on: { on: {
MOUNTED: "setCode", MOUNTED: 'setCode',
CHANGED_FILE: "loadFile", CHANGED_FILE: 'loadFile',
}, },
initial: "editingCode", initial: 'editingCode',
states: { states: {
editingCode: { editingCode: {
on: { on: {
RAN_CODE: ["saveCode", "runCode"], RAN_CODE: ['saveCode', 'runCode'],
SAVED_CODE: ["saveCode", "runCode"], SAVED_CODE: ['saveCode', 'runCode'],
CHANGED_CODE: { secretlyDo: "setCode" }, CHANGED_CODE: { secretlyDo: 'setCode' },
CLEARED_ERROR: { if: "hasError", do: "clearError" }, CLEARED_ERROR: { if: 'hasError', do: 'clearError' },
TOGGLED_DOCS: { to: "viewingDocs" }, TOGGLED_DOCS: { to: 'viewingDocs' },
}, },
}, },
viewingDocs: { viewingDocs: {
on: { on: {
TOGGLED_DOCS: { to: "editingCode" }, TOGGLED_DOCS: { to: 'editingCode' },
}, },
}, },
}, },
@ -83,7 +83,7 @@ export default function CodePanel() {
try { try {
const { shapes, controls } = generateFromCode(data.code) const { shapes, controls } = generateFromCode(data.code)
state.send("GENERATED_FROM_CODE", { shapes, controls }) state.send('GENERATED_FROM_CODE', { shapes, controls })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
error = { message: e.message, ...getErrorLineAndColumn(e) } error = { message: e.message, ...getErrorLineAndColumn(e) }
@ -93,7 +93,7 @@ export default function CodePanel() {
}, },
saveCode(data) { saveCode(data) {
const { code } = data const { code } = data
state.send("SAVED_CODE", { code }) state.send('SAVED_CODE', { code })
}, },
clearError(data) { clearError(data) {
data.error = null data.error = null
@ -102,13 +102,13 @@ export default function CodePanel() {
}) })
useEffect(() => { useEffect(() => {
local.send("CHANGED_FILE", { file }) local.send('CHANGED_FILE', { file })
}, [file]) }, [file])
useEffect(() => { useEffect(() => {
local.send("MOUNTED", { code: state.data.document.code[fileId].code }) local.send('MOUNTED', { code: state.data.document.code[fileId].code })
return () => { return () => {
state.send("CHANGED_CODE", { fileId, code: local.data.code }) state.send('CHANGED_CODE', { fileId, code: local.data.code })
} }
}, []) }, [])
@ -118,32 +118,32 @@ export default function CodePanel() {
<Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}> <Panel.Root data-bp-desktop ref={rContainer} isOpen={isOpen}>
{isOpen ? ( {isOpen ? (
<Panel.Layout> <Panel.Layout>
<Panel.Header> <Panel.Header side="left">
<IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}> <IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}>
<X /> <X />
</IconButton> </IconButton>
<h3>Code</h3> <h3>Code</h3>
<ButtonsGroup> <ButtonsGroup>
<FontSizeButtons> <FontSizeButtons>
<IconButton <IconButton
disabled={!local.isIn("editingCode")} disabled={!local.isIn('editingCode')}
onClick={() => state.send("INCREASED_CODE_FONT_SIZE")} onClick={() => state.send('INCREASED_CODE_FONT_SIZE')}
> >
<ChevronUp /> <ChevronUp />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!local.isIn("editingCode")} disabled={!local.isIn('editingCode')}
onClick={() => state.send("DECREASED_CODE_FONT_SIZE")} onClick={() => state.send('DECREASED_CODE_FONT_SIZE')}
> >
<ChevronDown /> <ChevronDown />
</IconButton> </IconButton>
</FontSizeButtons> </FontSizeButtons>
<IconButton onClick={() => local.send("TOGGLED_DOCS")}> <IconButton onClick={() => local.send('TOGGLED_DOCS')}>
<Info /> <Info />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!local.isIn("editingCode")} disabled={!local.isIn('editingCode')}
onClick={() => local.send("SAVED_CODE")} onClick={() => local.send('SAVED_CODE')}
> >
<PlayCircle /> <PlayCircle />
</IconButton> </IconButton>
@ -155,11 +155,11 @@ export default function CodePanel() {
readOnly={isReadOnly} readOnly={isReadOnly}
value={file.code} value={file.code}
error={error} error={error}
onChange={(code) => local.send("CHANGED_CODE", { code })} onChange={(code) => local.send('CHANGED_CODE', { code })}
onSave={() => local.send("SAVED_CODE")} onSave={() => local.send('SAVED_CODE')}
onKey={() => local.send("CLEARED_ERROR")} onKey={() => local.send('CLEARED_ERROR')}
/> />
<CodeDocs isHidden={!local.isIn("viewingDocs")} /> <CodeDocs isHidden={!local.isIn('viewingDocs')} />
</Panel.Content> </Panel.Content>
<Panel.Footer> <Panel.Footer>
{error && {error &&
@ -169,7 +169,7 @@ export default function CodePanel() {
</Panel.Footer> </Panel.Footer>
</Panel.Layout> </Panel.Layout>
) : ( ) : (
<IconButton onClick={() => state.send("TOGGLED_CODE_PANEL_OPEN")}> <IconButton onClick={() => state.send('TOGGLED_CODE_PANEL_OPEN')}>
<Code /> <Code />
</IconButton> </IconButton>
)} )}
@ -177,28 +177,28 @@ export default function CodePanel() {
) )
} }
const ButtonsGroup = styled("div", { const ButtonsGroup = styled('div', {
gridRow: "1", gridRow: '1',
gridColumn: "3", gridColumn: '3',
display: "flex", display: 'flex',
}) })
const FontSizeButtons = styled("div", { const FontSizeButtons = styled('div', {
paddingRight: 4, paddingRight: 4,
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
"& > button": { '& > button': {
height: "50%", height: '50%',
"&:nth-of-type(1)": { '&:nth-of-type(1)': {
alignItems: "flex-end", alignItems: 'flex-end',
}, },
"&:nth-of-type(2)": { '&:nth-of-type(2)': {
alignItems: "flex-start", alignItems: 'flex-start',
}, },
"& svg": { '& svg': {
height: 12, height: 12,
}, },
}, },

View file

@ -1,13 +1,14 @@
import useKeyboardEvents from "hooks/useKeyboardEvents" import useKeyboardEvents from 'hooks/useKeyboardEvents'
import useLoadOnMount from "hooks/useLoadOnMount" import useLoadOnMount from 'hooks/useLoadOnMount'
import Canvas from "./canvas/canvas" import Canvas from './canvas/canvas'
import StatusBar from "./status-bar" import StatusBar from './status-bar'
import Toolbar from "./toolbar" import Toolbar from './toolbar'
import CodePanel from "./code-panel/code-panel" import CodePanel from './code-panel/code-panel'
import ControlsPanel from "./controls-panel/controls-panel" import ControlsPanel from './controls-panel/controls-panel'
import styled from "styles" import ToolsPanel from './tools-panel/tools-panel'
import StylePanel from "./style-panel/style-panel" import StylePanel from './style-panel/style-panel'
import { useSelector } from "state" import { useSelector } from 'state'
import styled from 'styles'
export default function Editor() { export default function Editor() {
useKeyboardEvents() useKeyboardEvents()
@ -19,9 +20,8 @@ export default function Editor() {
return ( return (
<Layout> <Layout>
<Canvas />
<StatusBar />
<Toolbar /> <Toolbar />
<Canvas />
<LeftPanels> <LeftPanels>
<CodePanel /> <CodePanel />
{hasControls && <ControlsPanel />} {hasControls && <ControlsPanel />}
@ -29,40 +29,43 @@ export default function Editor() {
<RightPanels> <RightPanels>
<StylePanel /> <StylePanel />
</RightPanels> </RightPanels>
<ToolsPanel />
<StatusBar />
</Layout> </Layout>
) )
} }
const Layout = styled("div", { const Layout = styled('div', {
position: "fixed", position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
bottom: 0, bottom: 0,
right: 0, right: 0,
display: "grid", display: 'grid',
gridTemplateRows: "40px 1fr 40px", gridTemplateRows: '40px 1fr auto 40px',
gridTemplateColumns: "minmax(50%, 400px) 1fr auto", gridTemplateColumns: 'minmax(50%, 400px) 1fr auto',
gridTemplateAreas: ` gridTemplateAreas: `
"toolbar toolbar toolbar" "toolbar toolbar toolbar"
"leftPanels main rightPanels" "leftPanels main rightPanels"
"tools tools tools"
"statusbar statusbar statusbar" "statusbar statusbar statusbar"
`, `,
}) })
const LeftPanels = styled("main", { const LeftPanels = styled('main', {
display: "grid", display: 'grid',
gridArea: "leftPanels", gridArea: 'leftPanels',
gridTemplateRows: "1fr auto", gridTemplateRows: '1fr auto',
padding: 8, padding: 8,
gap: 8, gap: 8,
}) })
const RightPanels = styled("main", { const RightPanels = styled('main', {
display: "grid", display: 'grid',
gridArea: "rightPanels", gridArea: 'rightPanels',
gridTemplateRows: "auto", gridTemplateRows: 'auto',
height: "fit-content", height: 'fit-content',
justifyContent: "flex-end", justifyContent: 'flex-end',
padding: 8, padding: 8,
gap: 8, gap: 8,
}) })

View file

@ -1,32 +1,60 @@
import styled from "styles" import styled from 'styles'
export const IconButton = styled("button", { export const IconButton = styled('button', {
height: "32px", height: '32px',
width: "32px", width: '32px',
backgroundColor: "$panel", backgroundColor: '$panel',
borderRadius: "4px", borderRadius: '4px',
padding: "0", padding: '0',
margin: "0", margin: '0',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
outline: "none", outline: 'none',
border: "none", border: 'none',
pointerEvents: "all", pointerEvents: 'all',
cursor: "pointer", cursor: 'pointer',
"&:hover:not(:disabled)": { '&:hover:not(:disabled)': {
backgroundColor: "$hover", backgroundColor: '$hover',
}, },
"&:disabled": { '&:disabled': {
opacity: "0.5", opacity: '0.5',
}, },
svg: { '& > svg': {
height: "16px", height: '16px',
width: "16px", width: '16px',
strokeWidth: "2px", // strokeWidth: '2px',
stroke: "$text", // stroke: '$text',
},
variants: {
size: {
medium: {
height: 44,
width: 44,
'& svg': {
height: 16,
width: 16,
strokeWidth: 0,
},
},
large: {
height: 44,
width: 44,
'& svg': {
height: 24,
width: 24,
strokeWidth: 0,
},
},
},
isActive: {
true: {
color: '$selected',
},
},
}, },
}) })

View file

@ -4,14 +4,29 @@ import * as Panel from 'components/panel'
import { useRef } from 'react' import { useRef } from 'react'
import { IconButton } from 'components/shared' import { IconButton } from 'components/shared'
import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather' import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getSelectedShapes } from 'utils/utils' import {
deepCompare,
deepCompareArrays,
getPage,
getSelectedShapes,
} from 'utils/utils'
import { shades, fills, strokes } from 'lib/colors' import { shades, fills, strokes } from 'lib/colors'
import ColorPicker from './color-picker' import ColorPicker from './color-picker'
import AlignDistribute from './align-distribute' import AlignDistribute from './align-distribute'
import { ShapeStyles } from 'types' import { ShapeStyles } from 'types'
import WidthPicker from './width-picker' import WidthPicker from './width-picker'
import { CopyIcon } from '@radix-ui/react-icons' import {
AspectRatioIcon,
BoxIcon,
CopyIcon,
EyeClosedIcon,
EyeOpenIcon,
LockClosedIcon,
LockOpen1Icon,
RotateCounterClockwiseIcon,
TrashIcon,
} from '@radix-ui/react-icons'
const fillColors = { ...shades, ...fills } const fillColors = { ...shades, ...fills }
const strokeColors = { ...shades, ...strokes } const strokeColors = { ...shades, ...strokes }
@ -43,31 +58,47 @@ function SelectedShapeStyles({}: {}) {
deepCompareArrays deepCompareArrays
) )
const shapesStyle = useSelector((s) => { const isAllLocked = useSelector((s) => {
const { currentStyle } = s.data const page = getPage(s.data)
const shapes = getSelectedShapes(s.data) return selectedIds.every((id) => page.shapes[id].isLocked)
})
if (shapes.length === 0) { const isAllAspectLocked = useSelector((s) => {
const page = getPage(s.data)
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
})
const isAllHidden = useSelector((s) => {
const page = getPage(s.data)
return selectedIds.every((id) => page.shapes[id].isHidden)
})
const commonStyle = useSelector((s) => {
const { currentStyle } = s.data
if (selectedIds.length === 0) {
return currentStyle return currentStyle
} }
const page = getPage(s.data)
const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
const style: Partial<ShapeStyles> = {} const commonStyle: Partial<ShapeStyles> = {}
const overrides = new Set<string>([]) const overrides = new Set<string>([])
for (const shape of shapes) { for (const shapeStyle of shapeStyles) {
for (let key in currentStyle) { for (let key in currentStyle) {
if (overrides.has(key)) continue if (overrides.has(key)) continue
if (style[key] === undefined) { if (commonStyle[key] === undefined) {
style[key] = shape.style[key] commonStyle[key] = shapeStyle[key]
} else { } else {
if (style[key] === shape.style[key]) continue if (commonStyle[key] === shapeStyle[key]) continue
style[key] = currentStyle[key] commonStyle[key] = currentStyle[key]
overrides.add(key) overrides.add(key)
} }
} }
} }
return style return commonStyle
}, deepCompare) }, deepCompare)
const hasSelection = selectedIds.length > 0 const hasSelection = selectedIds.length > 0
@ -83,19 +114,19 @@ function SelectedShapeStyles({}: {}) {
<Content> <Content>
<ColorPicker <ColorPicker
label="Fill" label="Fill"
color={shapesStyle.fill} color={commonStyle.fill}
colors={fillColors} colors={fillColors}
onChange={(color) => state.send('CHANGED_STYLE', { fill: color })} onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
/> />
<ColorPicker <ColorPicker
label="Stroke" label="Stroke"
color={shapesStyle.stroke} color={commonStyle.stroke}
colors={strokeColors} colors={strokeColors}
onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })} onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
/> />
<Row> <Row>
<label htmlFor="width">Width</label> <label htmlFor="width">Width</label>
<WidthPicker strokeWidth={Number(shapesStyle.strokeWidth)} /> <WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
</Row> </Row>
<AlignDistribute <AlignDistribute
hasTwoOrMore={selectedIds.length > 1} hasTwoOrMore={selectedIds.length > 1}
@ -104,19 +135,39 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow> <ButtonsRow>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
onClick={() => state.send('DELETED')} onClick={() => state.send('DUPLICATED')}
> >
<Trash /> <CopyIcon />
</IconButton> </IconButton>
<IconButton <IconButton
disabled={!hasSelection} disabled={!hasSelection}
onClick={() => state.send('DUPLICATED')} onClick={() => state.send('ROTATED_CCW')}
> >
<Copy /> <RotateCounterClockwiseIcon />
</IconButton> </IconButton>
<IconButton
<IconButton> disabled={!hasSelection}
<Unlock /> onClick={() => state.send('DELETED')}
>
<TrashIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
>
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
>
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
>
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
</IconButton> </IconButton>
</ButtonsRow> </ButtonsRow>
</Content> </Content>

View file

@ -1,6 +1,4 @@
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import * as RadioGroup from '@radix-ui/react-radio-group' import * as RadioGroup from '@radix-ui/react-radio-group'
import { IconButton } from 'components/shared'
import { ChangeEvent } from 'react' import { ChangeEvent } from 'react'
import { Circle } from 'react-feather' import { Circle } from 'react-feather'
import state from 'state' import state from 'state'
@ -26,7 +24,7 @@ export default function WidthPicker({
<Circle size={12} /> <Circle size={12} />
</RadioItem> </RadioItem>
<RadioItem value="8" isActive={strokeWidth === 8}> <RadioItem value="8" isActive={strokeWidth === 8}>
<Circle size={18} /> <Circle size={22} />
</RadioItem> </RadioItem>
</Group> </Group>
) )

View file

@ -26,63 +26,6 @@ export default function Toolbar() {
<Button> <Button>
<Menu /> <Menu />
</Button> </Button>
<Button onClick={() => state.send('TOGGLED_TOOL_LOCK')}>
{isToolLocked ? <Lock /> : <Unlock />}
</Button>
<Button
isSelected={activeTool === 'select'}
onClick={() => state.send('SELECTED_SELECT_TOOL')}
>
Select
</Button>
<Button
isSelected={activeTool === 'draw'}
onClick={() => state.send('SELECTED_DRAW_TOOL')}
>
Draw
</Button>
<Button
isSelected={activeTool === 'dot'}
onClick={() => state.send('SELECTED_DOT_TOOL')}
>
Dot
</Button>
<Button
isSelected={activeTool === 'circle'}
onClick={() => state.send('SELECTED_CIRCLE_TOOL')}
>
Circle
</Button>
<Button
isSelected={activeTool === 'ellipse'}
onClick={() => state.send('SELECTED_ELLIPSE_TOOL')}
>
Ellipse
</Button>
<Button
isSelected={activeTool === 'ray'}
onClick={() => state.send('SELECTED_RAY_TOOL')}
>
Ray
</Button>
<Button
isSelected={activeTool === 'line'}
onClick={() => state.send('SELECTED_LINE_TOOL')}
>
Line
</Button>
<Button
isSelected={activeTool === 'polyline'}
onClick={() => state.send('SELECTED_POLYLINE_TOOL')}
>
Polyline
</Button>
<Button
isSelected={activeTool === 'rectangle'}
onClick={() => state.send('SELECTED_RECTANGLE_TOOL')}
>
Rectangle
</Button>
<Button onClick={() => state.send('RESET_CAMERA')}>Reset Camera</Button> <Button onClick={() => state.send('RESET_CAMERA')}>Reset Camera</Button>
</Section> </Section>
<Section> <Section>

View file

@ -0,0 +1,162 @@
import {
CircleIcon,
CursorArrowIcon,
DividerHorizontalIcon,
DotIcon,
LineHeightIcon,
LockClosedIcon,
LockOpen1Icon,
Pencil1Icon,
Pencil2Icon,
SewingPinIcon,
SquareIcon,
} from '@radix-ui/react-icons'
import { IconButton } from 'components/shared'
import React from 'react'
import state, { useSelector } from 'state'
import styled from 'styles'
import { ShapeType } from 'types'
const selectSelectTool = () => state.send('SELECTED_SELECT_TOOL')
const selectDrawTool = () => state.send('SELECTED_DRAW_TOOL')
const selectDotTool = () => state.send('SELECTED_DOT_TOOL')
const selectCircleTool = () => state.send('SELECTED_CIRCLE_TOOL')
const selectEllipseTool = () => state.send('SELECTED_ELLIPSE_TOOL')
const selectRayTool = () => state.send('SELECTED_RAY_TOOL')
const selectLineTool = () => state.send('SELECTED_LINE_TOOL')
const selectPolylineTool = () => state.send('SELECTED_POLYLINE_TOOL')
const selectRectangleTool = () => state.send('SELECTED_RECTANGLE_TOOL')
const selectToolLock = () => state.send('TOGGLED_TOOL_LOCK')
export default function ToolsPanel() {
const activeTool = useSelector((state) =>
state.whenIn({
selecting: 'select',
dot: ShapeType.Dot,
circle: ShapeType.Circle,
ellipse: ShapeType.Ellipse,
ray: ShapeType.Ray,
line: ShapeType.Line,
polyline: ShapeType.Polyline,
rectangle: ShapeType.Rectangle,
draw: ShapeType.Draw,
})
)
const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
const isPenLocked = useSelector((s) => s.data.settings.isPenLocked)
return (
<OuterContainer>
<Container>
<IconButton
name="select"
size="large"
onClick={selectSelectTool}
isActive={activeTool === 'select'}
>
<CursorArrowIcon />
</IconButton>
</Container>
<Container>
<IconButton
name={ShapeType.Draw}
size="large"
onClick={selectDrawTool}
isActive={activeTool === ShapeType.Draw}
>
<Pencil1Icon />
</IconButton>
<IconButton
name={ShapeType.Rectangle}
size="large"
onClick={selectRectangleTool}
isActive={activeTool === ShapeType.Rectangle}
>
<SquareIcon />
</IconButton>
<IconButton
name={ShapeType.Circle}
size="large"
onClick={selectCircleTool}
isActive={activeTool === ShapeType.Circle}
>
<CircleIcon />
</IconButton>
<IconButton
name={ShapeType.Ellipse}
size="large"
onClick={selectEllipseTool}
isActive={activeTool === ShapeType.Ellipse}
>
<CircleIcon transform="rotate(-45) scale(1, .8)" />
</IconButton>
<IconButton
name={ShapeType.Line}
size="large"
onClick={selectLineTool}
isActive={activeTool === ShapeType.Line}
>
<DividerHorizontalIcon transform="rotate(-45)" />
</IconButton>
<IconButton
name={ShapeType.Ray}
size="large"
onClick={selectRayTool}
isActive={activeTool === ShapeType.Ray}
>
<SewingPinIcon transform="rotate(-135)" />
</IconButton>
<IconButton
name={ShapeType.Dot}
size="large"
onClick={selectDotTool}
isActive={activeTool === ShapeType.Dot}
>
<DotIcon />
</IconButton>
</Container>
<Container>
<IconButton size="medium" onClick={selectToolLock}>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
{isPenLocked && (
<IconButton size="medium" onClick={selectToolLock}>
<Pencil2Icon />
</IconButton>
)}
</Container>
</OuterContainer>
)
}
const OuterContainer = styled('div', {
gridArea: 'tools',
padding: '0 8px 12px 8px',
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
})
const Container = styled('div', {
position: 'relative',
backgroundColor: '$panel',
borderRadius: '4px',
overflow: 'hidden',
border: '1px solid $border',
pointerEvents: 'all',
userSelect: 'none',
zIndex: 200,
boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
height: '100%',
display: 'flex',
padding: 4,
'& svg': {
strokeWidth: 0,
},
})

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { CircleShape, ShapeType } from "types" import { CircleShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Circle extends CodeShape<CircleShape> { export default class Circle extends CodeShape<CircleShape> {
constructor(props = {} as Partial<CircleShape>) { constructor(props = {} as Partial<CircleShape>) {
@ -11,15 +11,18 @@ export default class Circle extends CodeShape<CircleShape> {
id: uuid(), id: uuid(),
type: ShapeType.Circle, type: ShapeType.Circle,
isGenerated: true, isGenerated: true,
name: "Circle", name: 'Circle',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
radius: 20, radius: 20,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { DotShape, ShapeType } from "types" import { DotShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Dot extends CodeShape<DotShape> { export default class Dot extends CodeShape<DotShape> {
constructor(props = {} as Partial<DotShape>) { constructor(props = {} as Partial<DotShape>) {
@ -11,14 +11,17 @@ export default class Dot extends CodeShape<DotShape> {
id: uuid(), id: uuid(),
type: ShapeType.Dot, type: ShapeType.Dot,
isGenerated: true, isGenerated: true,
name: "Dot", name: 'Dot',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { EllipseShape, ShapeType } from "types" import { EllipseShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Ellipse extends CodeShape<EllipseShape> { export default class Ellipse extends CodeShape<EllipseShape> {
constructor(props = {} as Partial<EllipseShape>) { constructor(props = {} as Partial<EllipseShape>) {
@ -11,16 +11,19 @@ export default class Ellipse extends CodeShape<EllipseShape> {
id: uuid(), id: uuid(),
type: ShapeType.Ellipse, type: ShapeType.Ellipse,
isGenerated: true, isGenerated: true,
name: "Ellipse", name: 'Ellipse',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
radiusX: 20, radiusX: 20,
radiusY: 20, radiusY: 20,
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { LineShape, ShapeType } from "types" import { LineShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Line extends CodeShape<LineShape> { export default class Line extends CodeShape<LineShape> {
constructor(props = {} as Partial<LineShape>) { constructor(props = {} as Partial<LineShape>) {
@ -12,15 +12,18 @@ export default class Line extends CodeShape<LineShape> {
id: uuid(), id: uuid(),
type: ShapeType.Line, type: ShapeType.Line,
isGenerated: true, isGenerated: true,
name: "Line", name: 'Line',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
direction: [-0.5, 0.5], direction: [-0.5, 0.5],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { PolylineShape, ShapeType } from "types" import { PolylineShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Polyline extends CodeShape<PolylineShape> { export default class Polyline extends CodeShape<PolylineShape> {
constructor(props = {} as Partial<PolylineShape>) { constructor(props = {} as Partial<PolylineShape>) {
@ -12,15 +12,18 @@ export default class Polyline extends CodeShape<PolylineShape> {
id: uuid(), id: uuid(),
type: ShapeType.Polyline, type: ShapeType.Polyline,
isGenerated: true, isGenerated: true,
name: "Polyline", name: 'Polyline',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
points: [[0, 0]], points: [[0, 0]],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "none", fill: 'none',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { RayShape, ShapeType } from "types" import { RayShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Ray extends CodeShape<RayShape> { export default class Ray extends CodeShape<RayShape> {
constructor(props = {} as Partial<RayShape>) { constructor(props = {} as Partial<RayShape>) {
@ -12,15 +12,18 @@ export default class Ray extends CodeShape<RayShape> {
id: uuid(), id: uuid(),
type: ShapeType.Ray, type: ShapeType.Ray,
isGenerated: true, isGenerated: true,
name: "Ray", name: 'Ray',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
direction: [0, 1], direction: [0, 1],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -1,7 +1,7 @@
import CodeShape from "./index" import CodeShape from './index'
import { v4 as uuid } from "uuid" import { v4 as uuid } from 'uuid'
import { RectangleShape, ShapeType } from "types" import { RectangleShape, ShapeType } from 'types'
import { vectorToPoint } from "utils/utils" import { vectorToPoint } from 'utils/utils'
export default class Rectangle extends CodeShape<RectangleShape> { export default class Rectangle extends CodeShape<RectangleShape> {
constructor(props = {} as Partial<RectangleShape>) { constructor(props = {} as Partial<RectangleShape>) {
@ -12,16 +12,19 @@ export default class Rectangle extends CodeShape<RectangleShape> {
id: uuid(), id: uuid(),
type: ShapeType.Rectangle, type: ShapeType.Rectangle,
isGenerated: true, isGenerated: true,
name: "Rectangle", name: 'Rectangle',
parentId: "page0", parentId: 'page0',
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
size: [100, 100], size: [100, 100],
rotation: 0, rotation: 0,
radius: 2, radius: 2,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: "#c6cacb", fill: '#c6cacb',
stroke: "#000", stroke: '#000',
strokeWidth: 1, strokeWidth: 1,
}, },
...props, ...props,

View file

@ -21,6 +21,9 @@ const circle = registerShapeUtils<CircleShape>({
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
radius: 1, radius: 1,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
stroke: '#000', stroke: '#000',
@ -125,13 +128,8 @@ const circle = registerShapeUtils<CircleShape>({
return this return this
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -20,6 +20,9 @@ const dot = registerShapeUtils<DotShape>({
childIndex: 0, childIndex: 0,
point: [0, 0], point: [0, 0],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
strokeWidth: '0', strokeWidth: '0',
@ -94,13 +97,8 @@ const dot = registerShapeUtils<DotShape>({
return this return this
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -28,6 +28,9 @@ const draw = registerShapeUtils<DrawShape>({
point: [0, 0], point: [0, 0],
points: [[0, 0]], points: [[0, 0]],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
...props, ...props,
style: { style: {
strokeWidth: 2, strokeWidth: 2,
@ -169,25 +172,8 @@ const draw = registerShapeUtils<DrawShape>({
return this return this
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this
},
setPoints(shape, points) {
// const bounds = getBoundsFromPoints(points)
// const corner = [bounds.minX, bounds.minY]
// const nudged = points.map((point) => vec.sub(point, corner))
// this.boundsCache.set(shape, translategetBoundsFromPoints(nudged))
// shape.point = vec.add(shape.point, corner)
shape.points = points
return this return this
}, },

View file

@ -27,6 +27,9 @@ const ellipse = registerShapeUtils<EllipseShape>({
radiusX: 1, radiusX: 1,
radiusY: 1, radiusY: 1,
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
stroke: '#000', stroke: '#000',
@ -137,13 +140,8 @@ const ellipse = registerShapeUtils<EllipseShape>({
return this.transform(shape, bounds, info) return this.transform(shape, bounds, info)
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -8,15 +8,16 @@ import {
Edge, Edge,
ShapeByType, ShapeByType,
ShapeStyles, ShapeStyles,
} from "types" PropsOfType,
import circle from "./circle" } from 'types'
import dot from "./dot" import circle from './circle'
import polyline from "./polyline" import dot from './dot'
import rectangle from "./rectangle" import polyline from './polyline'
import ellipse from "./ellipse" import rectangle from './rectangle'
import line from "./line" import ellipse from './ellipse'
import ray from "./ray" import line from './line'
import draw from "./draw" import ray from './ray'
import draw from './draw'
/* /*
Shape Utiliies Shape Utiliies
@ -82,21 +83,11 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
} }
): ShapeUtility<K> ): ShapeUtility<K>
// Move a shape to a new parent. setProperty<P extends keyof K>(
setParent(this: ShapeUtility<K>, shape: K, parentId: string): ShapeUtility<K>
// Change the child index of a shape
setChildIndex(
this: ShapeUtility<K>, this: ShapeUtility<K>,
shape: K, shape: K,
childIndex: number prop: P,
): ShapeUtility<K> value: K[P]
// Add a point
setPoints?(
this: ShapeUtility<K>,
shape: K,
points: number[][]
): ShapeUtility<K> ): ShapeUtility<K>
// Render a shape to JSX. // Render a shape to JSX.
@ -151,7 +142,7 @@ export function registerShapeUtils<T extends Shape>(
} }
export function createShape<T extends Shape>( export function createShape<T extends Shape>(
type: T["type"], type: T['type'],
props: Partial<T> props: Partial<T>
) { ) {
return shapeUtilityMap[type].create(props) as T return shapeUtilityMap[type].create(props) as T

View file

@ -21,6 +21,9 @@ const line = registerShapeUtils<LineShape>({
point: [0, 0], point: [0, 0],
direction: [0, 0], direction: [0, 0],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
stroke: '#000', stroke: '#000',
@ -102,13 +105,8 @@ const line = registerShapeUtils<LineShape>({
return this.transform(shape, bounds, info) return this.transform(shape, bounds, info)
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -20,6 +20,9 @@ const polyline = registerShapeUtils<PolylineShape>({
point: [0, 0], point: [0, 0],
points: [[0, 0]], points: [[0, 0]],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
strokeWidth: 2, strokeWidth: 2,
strokeLinecap: 'round', strokeLinecap: 'round',
@ -127,13 +130,8 @@ const polyline = registerShapeUtils<PolylineShape>({
return this return this
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -21,6 +21,9 @@ const ray = registerShapeUtils<RayShape>({
point: [0, 0], point: [0, 0],
direction: [0, 1], direction: [0, 1],
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
stroke: '#000', stroke: '#000',
@ -102,13 +105,8 @@ const ray = registerShapeUtils<RayShape>({
return this.transform(shape, bounds, info) return this.transform(shape, bounds, info)
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -24,6 +24,9 @@ const rectangle = registerShapeUtils<RectangleShape>({
size: [1, 1], size: [1, 1],
radius: 2, radius: 2,
rotation: 0, rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
style: { style: {
fill: '#c6cacb', fill: '#c6cacb',
stroke: '#000', stroke: '#000',
@ -140,13 +143,8 @@ const rectangle = registerShapeUtils<RectangleShape>({
return this return this
}, },
setParent(shape, parentId) { setProperty(shape, prop, value) {
shape.parentId = parentId shape[prop] = value
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this return this
}, },

View file

@ -9,16 +9,16 @@ interface Core {
interface Instance extends Props, Core {} interface Instance extends Props, Core {}
const defaults: Props = { const defaults: Props = {
name: "Spot", name: 'Spot',
} }
const core: Core = { const core: Core = {
id: "0", id: '0',
} }
class ClassInstance<T extends object = {}> implements Instance { class ClassInstance<T extends object = {}> implements Instance {
id = "0" id = '0'
name = "Spot" name = 'Spot'
constructor( constructor(
props: Partial<Props> & props: Partial<Props> &
@ -51,7 +51,7 @@ function getInstance<T extends object = {}>(
} }
const instance = getInstance({ const instance = getInstance({
name: "Steve", name: 'Steve',
age: 93, age: 93,
wag(this: Instance) { wag(this: Instance) {
return this.name return this.name
@ -76,29 +76,29 @@ const getAnimal = <T extends object>(
): Animal & T => { ): Animal & T => {
return { return {
// Defaults // Defaults
name: "Animal", name: 'Animal',
greet(name) { greet(name) {
return "Hey " + name return 'Hey ' + name
}, },
// Overrides // Overrides
...props, ...props,
// Core // Core
id: "hi", id: 'hi',
sleep() {}, sleep() {},
} }
} }
const dog = getAnimal({ const dog = getAnimal({
name: "doggo", name: 'doggo',
greet(name) { greet(name) {
return "Woof " + this.name return 'Woof ' + this.name
}, },
wag() { wag() {
return "wagging..." return 'wagging...'
}, },
}) })
dog.greet("steve") dog.greet('steve')
dog.wag() dog.wag()
dog.sleep() dog.sleep()
@ -111,5 +111,5 @@ export default shapeTest
type Greet = (name: string) => string type Greet = (name: string) => string
const greet: Greet = (name: string | number) => { const greet: Greet = (name: string | number) => {
return "hello " + name return 'hello ' + name
} }

View file

@ -1,24 +1,20 @@
import Command from "./command" import Command from './command'
import history from "../history" import history from '../history'
import { Data } from "types" import { Data, DrawShape } from 'types'
import { getPage } from "utils/utils" import { getPage } from 'utils/utils'
import { getShapeUtils } from "lib/shape-utils" import { getShapeUtils } from 'lib/shape-utils'
import { current } from "immer" import { current } from 'immer'
export default function drawCommand( export default function drawCommand(data: Data, id: string, after: number[][]) {
data: Data, const restoreShape = current(getPage(data)).shapes[id] as DrawShape
id: string,
before: number[][], getShapeUtils(restoreShape).setProperty!(restoreShape, 'points', after)
after: number[][]
) {
const restoreShape = current(getPage(data).shapes[id])
getShapeUtils(restoreShape).setPoints!(restoreShape, after)
history.execute( history.execute(
data, data,
new Command({ new Command({
name: "set_points", name: 'set_points',
category: "canvas", category: 'canvas',
manualSelection: true, manualSelection: true,
do(data, initial) { do(data, initial) {
if (!initial) { if (!initial) {

View file

@ -13,6 +13,7 @@ import transform from './transform'
import transformSingle from './transform-single' import transformSingle from './transform-single'
import translate from './translate' import translate from './translate'
import nudge from './nudge' import nudge from './nudge'
import toggle from './toggle'
const commands = { const commands = {
align, align,
@ -30,6 +31,7 @@ const commands = {
transformSingle, transformSingle,
translate, translate,
nudge, nudge,
toggle,
} }
export default commands export default commands

46
state/commands/toggle.ts Normal file
View file

@ -0,0 +1,46 @@
import Command from './command'
import history from '../history'
import { Data, Shape } from 'types'
import { getPage, getSelectedShapes } from 'utils/utils'
import { getShapeUtils } from 'lib/shape-utils'
import { PropsOfType } from 'types'
export default function toggleCommand(
data: Data,
prop: PropsOfType<Shape, boolean>
) {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data)
const isAllToggled = selectedShapes.every((shape) => shape[prop])
const initialShapes = Object.fromEntries(
selectedShapes.map((shape) => [shape.id, shape[prop]])
)
history.execute(
data,
new Command({
name: 'hide_shapes',
category: 'canvas',
do(data) {
const { shapes } = getPage(data, currentPageId)
for (const id in initialShapes) {
const shape = shapes[id]
getShapeUtils(shape).setProperty(
shape,
prop,
isAllToggled ? false : true
)
}
},
undo(data) {
const { shapes } = getPage(data, currentPageId)
for (const id in initialShapes) {
const shape = shapes[id]
getShapeUtils(shape).setProperty(shape, prop, initialShapes[id])
}
},
})
)
}

View file

@ -75,6 +75,12 @@ const state = createState({
PANNED_CAMERA: { PANNED_CAMERA: {
do: 'panCamera', do: 'panCamera',
}, },
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
TOGGLED_SHAPE_ASPECT_LOCK: {
if: 'hasSelection',
do: 'aspectLockSelection',
},
SELECTED_SELECT_TOOL: { to: 'selecting' }, SELECTED_SELECT_TOOL: { to: 'selecting' },
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' }, SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' }, SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
@ -638,7 +644,7 @@ const state = createState({
? siblings[siblings.length - 1].childIndex + 1 ? siblings[siblings.length - 1].childIndex + 1
: 1 : 1
getShapeUtils(shape).setChildIndex(shape, childIndex) getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
getPage(data).shapes[shape.id] = shape getPage(data).shapes[shape.id] = shape
@ -834,6 +840,15 @@ const state = createState({
duplicateSelection(data) { duplicateSelection(data) {
commands.duplicate(data) commands.duplicate(data)
}, },
lockSelection(data) {
commands.toggle(data, 'isLocked')
},
hideSelection(data) {
commands.toggle(data, 'isHidden')
},
aspectLockSelection(data) {
commands.toggle(data, 'isAspectRatioLocked')
},
/* --------------------- Camera --------------------- */ /* --------------------- Camera --------------------- */

View file

@ -78,6 +78,9 @@ export interface BaseShape {
point: number[] point: number[]
rotation: number rotation: number
style: ShapeStyles style: ShapeStyles
isLocked: boolean
isHidden: boolean
isAspectRatioLocked: boolean
} }
export interface DotShape extends BaseShape { export interface DotShape extends BaseShape {
@ -313,3 +316,7 @@ export type CodeControl =
| VectorCodeControl | VectorCodeControl
| TextCodeControl | TextCodeControl
| SelectCodeControl | SelectCodeControl
export type PropsOfType<T extends object, K> = {
[K in keyof T]: T[K] extends boolean ? K : never
}[keyof T]