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

View file

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

View file

@ -1,32 +1,60 @@
import styled from "styles"
import styled from 'styles'
export const IconButton = styled("button", {
height: "32px",
width: "32px",
backgroundColor: "$panel",
borderRadius: "4px",
padding: "0",
margin: "0",
display: "flex",
alignItems: "center",
justifyContent: "center",
outline: "none",
border: "none",
pointerEvents: "all",
cursor: "pointer",
export const IconButton = styled('button', {
height: '32px',
width: '32px',
backgroundColor: '$panel',
borderRadius: '4px',
padding: '0',
margin: '0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
outline: 'none',
border: 'none',
pointerEvents: 'all',
cursor: 'pointer',
"&:hover:not(:disabled)": {
backgroundColor: "$hover",
'&:hover:not(:disabled)': {
backgroundColor: '$hover',
},
"&:disabled": {
opacity: "0.5",
'&:disabled': {
opacity: '0.5',
},
svg: {
height: "16px",
width: "16px",
strokeWidth: "2px",
stroke: "$text",
'& > svg': {
height: '16px',
width: '16px',
// strokeWidth: '2px',
// 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 { IconButton } from 'components/shared'
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 ColorPicker from './color-picker'
import AlignDistribute from './align-distribute'
import { ShapeStyles } from 'types'
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 strokeColors = { ...shades, ...strokes }
@ -43,31 +58,47 @@ function SelectedShapeStyles({}: {}) {
deepCompareArrays
)
const shapesStyle = useSelector((s) => {
const { currentStyle } = s.data
const shapes = getSelectedShapes(s.data)
const isAllLocked = useSelector((s) => {
const page = getPage(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
}
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>([])
for (const shape of shapes) {
for (const shapeStyle of shapeStyles) {
for (let key in currentStyle) {
if (overrides.has(key)) continue
if (style[key] === undefined) {
style[key] = shape.style[key]
if (commonStyle[key] === undefined) {
commonStyle[key] = shapeStyle[key]
} else {
if (style[key] === shape.style[key]) continue
style[key] = currentStyle[key]
if (commonStyle[key] === shapeStyle[key]) continue
commonStyle[key] = currentStyle[key]
overrides.add(key)
}
}
}
return style
return commonStyle
}, deepCompare)
const hasSelection = selectedIds.length > 0
@ -83,19 +114,19 @@ function SelectedShapeStyles({}: {}) {
<Content>
<ColorPicker
label="Fill"
color={shapesStyle.fill}
color={commonStyle.fill}
colors={fillColors}
onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
/>
<ColorPicker
label="Stroke"
color={shapesStyle.stroke}
color={commonStyle.stroke}
colors={strokeColors}
onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
/>
<Row>
<label htmlFor="width">Width</label>
<WidthPicker strokeWidth={Number(shapesStyle.strokeWidth)} />
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
</Row>
<AlignDistribute
hasTwoOrMore={selectedIds.length > 1}
@ -104,19 +135,39 @@ function SelectedShapeStyles({}: {}) {
<ButtonsRow>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('DELETED')}
onClick={() => state.send('DUPLICATED')}
>
<Trash />
<CopyIcon />
</IconButton>
<IconButton
disabled={!hasSelection}
onClick={() => state.send('DUPLICATED')}
onClick={() => state.send('ROTATED_CCW')}
>
<Copy />
<RotateCounterClockwiseIcon />
</IconButton>
<IconButton>
<Unlock />
<IconButton
disabled={!hasSelection}
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>
</ButtonsRow>
</Content>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,9 @@ const draw = registerShapeUtils<DrawShape>({
point: [0, 0],
points: [[0, 0]],
rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
...props,
style: {
strokeWidth: 2,
@ -169,25 +172,8 @@ const draw = registerShapeUtils<DrawShape>({
return this
},
setParent(shape, parentId) {
shape.parentId = parentId
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
setProperty(shape, prop, value) {
shape[prop] = value
return this
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import transform from './transform'
import transformSingle from './transform-single'
import translate from './translate'
import nudge from './nudge'
import toggle from './toggle'
const commands = {
align,
@ -30,6 +31,7 @@ const commands = {
transformSingle,
translate,
nudge,
toggle,
}
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: {
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_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
@ -638,7 +644,7 @@ const state = createState({
? siblings[siblings.length - 1].childIndex + 1
: 1
getShapeUtils(shape).setChildIndex(shape, childIndex)
getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
getPage(data).shapes[shape.id] = shape
@ -834,6 +840,15 @@ const state = createState({
duplicateSelection(data) {
commands.duplicate(data)
},
lockSelection(data) {
commands.toggle(data, 'isLocked')
},
hideSelection(data) {
commands.toggle(data, 'isHidden')
},
aspectLockSelection(data) {
commands.toggle(data, 'isAspectRatioLocked')
},
/* --------------------- Camera --------------------- */

View file

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