Adds tool lock
This commit is contained in:
parent
c95c54dae6
commit
fe3980c80c
28 changed files with 1136 additions and 738 deletions
|
@ -23,9 +23,9 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||||
// detects the change and pulls this component.
|
// detects the change and pulls this component.
|
||||||
if (!shape) return null
|
if (!shape) return null
|
||||||
|
|
||||||
|
const center = getShapeUtils(shape).getCenter(shape)
|
||||||
const transform = `
|
const transform = `
|
||||||
rotate(${shape.rotation * (180 / Math.PI)},
|
rotate(${shape.rotation * (180 / Math.PI)}, ${center})
|
||||||
${getShapeUtils(shape).getCenter(shape)})
|
|
||||||
translate(${shape.point})`
|
translate(${shape.point})`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,18 +38,37 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||||
>
|
>
|
||||||
{isSelecting && <HoverIndicator as="use" href={'#' + id} />}
|
{isSelecting && <HoverIndicator as="use" href={'#' + id} />}
|
||||||
<StyledShape id={id} style={shape.style} />
|
<StyledShape id={id} style={shape.style} />
|
||||||
|
{/*
|
||||||
|
<text
|
||||||
|
y={4}
|
||||||
|
x={4}
|
||||||
|
fontSize={18}
|
||||||
|
fill="black"
|
||||||
|
stroke="none"
|
||||||
|
alignmentBaseline="text-before-edge"
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
{center.toString()}
|
||||||
|
</text> */}
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledShape = memo(
|
const StyledShape = memo(
|
||||||
({ id, style }: { id: string; style: ShapeStyles }) => {
|
({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||||
return <MainShape as="use" href={'#' + id} {...style} />
|
return (
|
||||||
|
<MainShape
|
||||||
|
as="use"
|
||||||
|
href={'#' + id}
|
||||||
|
{...style}
|
||||||
|
// css={{ zStrokeWidth: Number(style.strokeWidth) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const MainShape = styled('use', {
|
const MainShape = styled('use', {
|
||||||
zStrokeWidth: 1,
|
// zStrokeWidth: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const HoverIndicator = styled('path', {
|
const HoverIndicator = styled('path', {
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
|
|
||||||
export const Root = styled("div", {
|
export const Root = styled('div', {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
backgroundColor: "$panel",
|
backgroundColor: '$panel',
|
||||||
borderRadius: "4px",
|
borderRadius: '4px',
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
border: "1px solid $border",
|
border: '1px solid $border',
|
||||||
pointerEvents: "all",
|
pointerEvents: 'all',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
|
boxShadow: '0px 2px 25px rgba(0,0,0,.16)',
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
true: {
|
true: {
|
||||||
width: "auto",
|
width: 'auto',
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
},
|
},
|
||||||
false: {
|
false: {
|
||||||
|
@ -25,63 +25,74 @@ export const Root = styled("div", {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Layout = styled("div", {
|
export const Layout = styled('div', {
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
gridTemplateColumns: "1fr",
|
gridTemplateColumns: '1fr',
|
||||||
gridTemplateRows: "auto 1fr",
|
gridTemplateRows: 'auto 1fr',
|
||||||
gridAutoRows: "28px",
|
gridAutoRows: '28px',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
width: "auto",
|
width: 'auto',
|
||||||
minWidth: "100%",
|
minWidth: '100%',
|
||||||
maxWidth: 560,
|
maxWidth: 560,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
pointerEvents: "all",
|
pointerEvents: 'all',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Header = styled("div", {
|
export const Header = styled('div', {
|
||||||
pointerEvents: "all",
|
pointerEvents: 'all',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
borderBottom: "1px solid $border",
|
borderBottom: '1px solid $border',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
|
|
||||||
"& h3": {
|
'& h3': {
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
fontSize: "13px",
|
fontSize: '13px',
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
side: {
|
||||||
|
left: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ButtonsGroup = styled("div", {
|
export const ButtonsGroup = styled('div', {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Content = styled("div", {
|
export const Content = styled('div', {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
pointerEvents: "all",
|
pointerEvents: 'all',
|
||||||
overflowY: "scroll",
|
overflowY: 'scroll',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Footer = styled("div", {
|
export const Footer = styled('div', {
|
||||||
overflowX: "scroll",
|
overflowX: 'scroll',
|
||||||
color: "$text",
|
color: '$text',
|
||||||
font: "$debug",
|
font: '$debug',
|
||||||
padding: "0 12px",
|
padding: '0 12px',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
borderTop: "1px solid $border",
|
borderTop: '1px solid $border',
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,50 +9,50 @@ import {
|
||||||
SpaceEvenlyVerticallyIcon,
|
SpaceEvenlyVerticallyIcon,
|
||||||
StretchHorizontallyIcon,
|
StretchHorizontallyIcon,
|
||||||
StretchVerticallyIcon,
|
StretchVerticallyIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from '@radix-ui/react-icons'
|
||||||
import { IconButton } from "components/shared"
|
import { IconButton } from 'components/shared'
|
||||||
import state from "state"
|
import state from 'state'
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import { AlignType, DistributeType, StretchType } from "types"
|
import { AlignType, DistributeType, StretchType } from 'types'
|
||||||
|
|
||||||
function alignTop() {
|
function alignTop() {
|
||||||
state.send("ALIGNED", { type: AlignType.Top })
|
state.send('ALIGNED', { type: AlignType.Top })
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignCenterVertical() {
|
function alignCenterVertical() {
|
||||||
state.send("ALIGNED", { type: AlignType.CenterVertical })
|
state.send('ALIGNED', { type: AlignType.CenterVertical })
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignBottom() {
|
function alignBottom() {
|
||||||
state.send("ALIGNED", { type: AlignType.Bottom })
|
state.send('ALIGNED', { type: AlignType.Bottom })
|
||||||
}
|
}
|
||||||
|
|
||||||
function stretchVertically() {
|
function stretchVertically() {
|
||||||
state.send("STRETCHED", { type: StretchType.Vertical })
|
state.send('STRETCHED', { type: StretchType.Vertical })
|
||||||
}
|
}
|
||||||
|
|
||||||
function distributeVertically() {
|
function distributeVertically() {
|
||||||
state.send("DISTRIBUTED", { type: DistributeType.Vertical })
|
state.send('DISTRIBUTED', { type: DistributeType.Vertical })
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignLeft() {
|
function alignLeft() {
|
||||||
state.send("ALIGNED", { type: AlignType.Left })
|
state.send('ALIGNED', { type: AlignType.Left })
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignCenterHorizontal() {
|
function alignCenterHorizontal() {
|
||||||
state.send("ALIGNED", { type: AlignType.CenterHorizontal })
|
state.send('ALIGNED', { type: AlignType.CenterHorizontal })
|
||||||
}
|
}
|
||||||
|
|
||||||
function alignRight() {
|
function alignRight() {
|
||||||
state.send("ALIGNED", { type: AlignType.Right })
|
state.send('ALIGNED', { type: AlignType.Right })
|
||||||
}
|
}
|
||||||
|
|
||||||
function stretchHorizontally() {
|
function stretchHorizontally() {
|
||||||
state.send("STRETCHED", { type: StretchType.Horizontal })
|
state.send('STRETCHED', { type: StretchType.Horizontal })
|
||||||
}
|
}
|
||||||
|
|
||||||
function distributeHorizontally() {
|
function distributeHorizontally() {
|
||||||
state.send("DISTRIBUTED", { type: DistributeType.Horizontal })
|
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AlignDistribute({
|
export default function AlignDistribute({
|
||||||
|
@ -98,15 +98,15 @@ export default function AlignDistribute({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled("div", {
|
const Container = styled('div', {
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
gridTemplateColumns: "repeat(5, auto)",
|
gridTemplateColumns: 'repeat(5, auto)',
|
||||||
[`& ${IconButton}`]: {
|
[`& ${IconButton}`]: {
|
||||||
color: "$text",
|
color: '$text',
|
||||||
},
|
},
|
||||||
[`& ${IconButton} > svg`]: {
|
[`& ${IconButton} > svg`]: {
|
||||||
fill: "red",
|
fill: 'red',
|
||||||
stroke: "transparent",
|
stroke: 'transparent',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { Square } from "react-feather"
|
import { Square } from 'react-feather'
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string
|
label: string
|
||||||
|
@ -13,7 +13,7 @@ export default function ColorPicker({ label, color, colors, onChange }: Props) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<CurrentColor>
|
<CurrentColor>
|
||||||
<h3>{label}</h3>
|
<label>{label}</label>
|
||||||
<ColorIcon color={color} />
|
<ColorIcon color={color} />
|
||||||
</CurrentColor>
|
</CurrentColor>
|
||||||
<Colors sideOffset={4}>
|
<Colors sideOffset={4}>
|
||||||
|
@ -31,96 +31,96 @@ function ColorIcon({ color }: { color: string }) {
|
||||||
return (
|
return (
|
||||||
<Square
|
<Square
|
||||||
fill={color}
|
fill={color}
|
||||||
strokeDasharray={color === "transparent" ? "2, 3" : "none"}
|
strokeDasharray={color === 'transparent' ? '2, 3' : 'none'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Colors = styled(DropdownMenu.Content, {
|
const Colors = styled(DropdownMenu.Content, {
|
||||||
display: "grid",
|
display: 'grid',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
gridTemplateColumns: "repeat(6, 1fr)",
|
gridTemplateColumns: 'repeat(6, 1fr)',
|
||||||
border: "1px solid $border",
|
border: '1px solid $border',
|
||||||
backgroundColor: "$panel",
|
backgroundColor: '$panel',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
boxShadow: "0px 5px 15px -5px hsla(206,22%,7%,.15)",
|
boxShadow: '0px 5px 15px -5px hsla(206,22%,7%,.15)',
|
||||||
})
|
})
|
||||||
|
|
||||||
const ColorButton = styled(DropdownMenu.Item, {
|
const ColorButton = styled(DropdownMenu.Item, {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
height: 32,
|
height: 32,
|
||||||
width: 32,
|
width: 32,
|
||||||
border: "none",
|
border: 'none',
|
||||||
padding: "none",
|
padding: 'none',
|
||||||
background: "none",
|
background: 'none',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
|
|
||||||
"&::before": {
|
'&::before': {
|
||||||
content: "''",
|
content: "''",
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 4,
|
top: 4,
|
||||||
left: 4,
|
left: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:hover::before": {
|
'&:hover::before': {
|
||||||
backgroundColor: "$hover",
|
backgroundColor: '$hover',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
"& svg": {
|
'& svg': {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
stroke: "rgba(0,0,0,.2)",
|
stroke: 'rgba(0,0,0,.2)',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const CurrentColor = styled(DropdownMenu.Trigger, {
|
const CurrentColor = styled(DropdownMenu.Trigger, {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
background: "none",
|
background: 'none',
|
||||||
border: "none",
|
border: 'none',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
outline: "none",
|
outline: 'none',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
padding: "4px 6px 4px 12px",
|
padding: '4px 6px 4px 12px',
|
||||||
|
|
||||||
"&::before": {
|
'&::before': {
|
||||||
content: "''",
|
content: "''",
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
pointerEvents: "none",
|
pointerEvents: 'none',
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:hover::before": {
|
'&:hover::before': {
|
||||||
backgroundColor: "$hover",
|
backgroundColor: '$hover',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
"& h3": {
|
'& label': {
|
||||||
fontFamily: "$ui",
|
fontFamily: '$ui',
|
||||||
fontSize: "$2",
|
fontSize: '$2',
|
||||||
fontWeight: "$1",
|
fontWeight: '$1',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
},
|
},
|
||||||
|
|
||||||
"& svg": {
|
'& svg': {
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
stroke: "rgba(0,0,0,.2)",
|
stroke: 'rgba(0,0,0,.2)',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from 'state'
|
||||||
import * as Panel from "components/panel"
|
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, Trash, X } from "react-feather"
|
import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather'
|
||||||
import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils"
|
import { deepCompare, deepCompareArrays, 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 { CopyIcon } from '@radix-ui/react-icons'
|
||||||
|
|
||||||
const fillColors = { ...shades, ...fills }
|
const fillColors = { ...shades, ...fills }
|
||||||
const strokeColors = { ...shades, ...strokes }
|
const strokeColors = { ...shades, ...strokes }
|
||||||
|
@ -23,7 +25,7 @@ export default function StylePanel() {
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<SelectedShapeStyles />
|
<SelectedShapeStyles />
|
||||||
) : (
|
) : (
|
||||||
<IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
|
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
|
||||||
<Circle />
|
<Circle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
@ -72,17 +74,9 @@ function SelectedShapeStyles({}: {}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Layout>
|
<Panel.Layout>
|
||||||
<Panel.Header>
|
<Panel.Header side="right">
|
||||||
<h3>Style</h3>
|
<h3>Style</h3>
|
||||||
<Panel.ButtonsGroup>
|
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
|
||||||
<IconButton
|
|
||||||
disabled={!hasSelection}
|
|
||||||
onClick={() => state.send("DELETED")}
|
|
||||||
>
|
|
||||||
<Trash />
|
|
||||||
</IconButton>
|
|
||||||
</Panel.ButtonsGroup>
|
|
||||||
<IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
|
|
||||||
<X />
|
<X />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Panel.Header>
|
</Panel.Header>
|
||||||
|
@ -91,18 +85,40 @@ function SelectedShapeStyles({}: {}) {
|
||||||
label="Fill"
|
label="Fill"
|
||||||
color={shapesStyle.fill}
|
color={shapesStyle.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={shapesStyle.stroke}
|
||||||
colors={strokeColors}
|
colors={strokeColors}
|
||||||
onChange={(color) => state.send("CHANGED_STYLE", { stroke: color })}
|
onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
|
||||||
/>
|
/>
|
||||||
|
<Row>
|
||||||
|
<label htmlFor="width">Width</label>
|
||||||
|
<WidthPicker strokeWidth={Number(shapesStyle.strokeWidth)} />
|
||||||
|
</Row>
|
||||||
<AlignDistribute
|
<AlignDistribute
|
||||||
hasTwoOrMore={selectedIds.length > 1}
|
hasTwoOrMore={selectedIds.length > 1}
|
||||||
hasThreeOrMore={selectedIds.length > 2}
|
hasThreeOrMore={selectedIds.length > 2}
|
||||||
/>
|
/>
|
||||||
|
<ButtonsRow>
|
||||||
|
<IconButton
|
||||||
|
disabled={!hasSelection}
|
||||||
|
onClick={() => state.send('DELETED')}
|
||||||
|
>
|
||||||
|
<Trash />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
disabled={!hasSelection}
|
||||||
|
onClick={() => state.send('DUPLICATED')}
|
||||||
|
>
|
||||||
|
<Copy />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<IconButton>
|
||||||
|
<Unlock />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonsRow>
|
||||||
</Content>
|
</Content>
|
||||||
</Panel.Layout>
|
</Panel.Layout>
|
||||||
)
|
)
|
||||||
|
@ -112,8 +128,8 @@ const StylePanelRoot = styled(Panel.Root, {
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
width: 184,
|
width: 184,
|
||||||
maxWidth: 184,
|
maxWidth: 184,
|
||||||
overflow: "hidden",
|
overflow: 'hidden',
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
|
@ -129,3 +145,41 @@ const StylePanelRoot = styled(Panel.Root, {
|
||||||
const Content = styled(Panel.Content, {
|
const Content = styled(Panel.Content, {
|
||||||
padding: 8,
|
padding: 8,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Row = styled('div', {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
outline: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '4px 2px 4px 12px',
|
||||||
|
|
||||||
|
'& label': {
|
||||||
|
fontFamily: '$ui',
|
||||||
|
fontSize: '$2',
|
||||||
|
fontWeight: '$1',
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
'& > svg': {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ButtonsRow = styled('div', {
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
outline: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
padding: 4,
|
||||||
|
})
|
||||||
|
|
82
components/style-panel/width-picker.tsx
Normal file
82
components/style-panel/width-picker.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
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'
|
||||||
|
import styled from 'styles'
|
||||||
|
|
||||||
|
function setWidth(e: ChangeEvent<HTMLInputElement>) {
|
||||||
|
state.send('CHANGED_STYLE', {
|
||||||
|
strokeWidth: Number(e.currentTarget.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WidthPicker({
|
||||||
|
strokeWidth = 2,
|
||||||
|
}: {
|
||||||
|
strokeWidth?: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group name="width" onValueChange={setWidth}>
|
||||||
|
<RadioItem value="2" isActive={strokeWidth === 2}>
|
||||||
|
<Circle size={6} />
|
||||||
|
</RadioItem>
|
||||||
|
<RadioItem value="4" isActive={strokeWidth === 4}>
|
||||||
|
<Circle size={12} />
|
||||||
|
</RadioItem>
|
||||||
|
<RadioItem value="8" isActive={strokeWidth === 8}>
|
||||||
|
<Circle size={18} />
|
||||||
|
</RadioItem>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Group = styled(RadioGroup.Root, {
|
||||||
|
display: 'flex',
|
||||||
|
})
|
||||||
|
|
||||||
|
const RadioItem = styled(RadioGroup.Item, {
|
||||||
|
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',
|
||||||
|
'& svg': {
|
||||||
|
fill: '$text',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:disabled': {
|
||||||
|
opacity: '0.5',
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isActive: {
|
||||||
|
true: {
|
||||||
|
'& svg': {
|
||||||
|
fill: '$text',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
'& svg': {
|
||||||
|
fill: '$inactive',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,134 +1,147 @@
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from 'state'
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import { Menu } from "react-feather"
|
import { Lock, Menu, Unlock } from 'react-feather'
|
||||||
|
import { IconButton } from './shared'
|
||||||
|
|
||||||
export default function Toolbar() {
|
export default function Toolbar() {
|
||||||
const activeTool = useSelector((state) =>
|
const activeTool = useSelector((state) =>
|
||||||
state.whenIn({
|
state.whenIn({
|
||||||
selecting: "select",
|
selecting: 'select',
|
||||||
dot: "dot",
|
dot: 'dot',
|
||||||
circle: "circle",
|
circle: 'circle',
|
||||||
ellipse: "ellipse",
|
ellipse: 'ellipse',
|
||||||
ray: "ray",
|
ray: 'ray',
|
||||||
line: "line",
|
line: 'line',
|
||||||
polyline: "polyline",
|
polyline: 'polyline',
|
||||||
rectangle: "rectangle",
|
rectangle: 'rectangle',
|
||||||
draw: "draw",
|
draw: 'draw',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarContainer>
|
<ToolbarContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<Button>
|
<Button>
|
||||||
<Menu />
|
<Menu />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => state.send('TOGGLED_TOOL_LOCK')}>
|
||||||
|
{isToolLocked ? <Lock /> : <Unlock />}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "select"}
|
isSelected={activeTool === 'select'}
|
||||||
onClick={() => state.send("SELECTED_SELECT_TOOL")}
|
onClick={() => state.send('SELECTED_SELECT_TOOL')}
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "draw"}
|
isSelected={activeTool === 'draw'}
|
||||||
onClick={() => state.send("SELECTED_DRAW_TOOL")}
|
onClick={() => state.send('SELECTED_DRAW_TOOL')}
|
||||||
>
|
>
|
||||||
Draw
|
Draw
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "dot"}
|
isSelected={activeTool === 'dot'}
|
||||||
onClick={() => state.send("SELECTED_DOT_TOOL")}
|
onClick={() => state.send('SELECTED_DOT_TOOL')}
|
||||||
>
|
>
|
||||||
Dot
|
Dot
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "circle"}
|
isSelected={activeTool === 'circle'}
|
||||||
onClick={() => state.send("SELECTED_CIRCLE_TOOL")}
|
onClick={() => state.send('SELECTED_CIRCLE_TOOL')}
|
||||||
>
|
>
|
||||||
Circle
|
Circle
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "ellipse"}
|
isSelected={activeTool === 'ellipse'}
|
||||||
onClick={() => state.send("SELECTED_ELLIPSE_TOOL")}
|
onClick={() => state.send('SELECTED_ELLIPSE_TOOL')}
|
||||||
>
|
>
|
||||||
Ellipse
|
Ellipse
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "ray"}
|
isSelected={activeTool === 'ray'}
|
||||||
onClick={() => state.send("SELECTED_RAY_TOOL")}
|
onClick={() => state.send('SELECTED_RAY_TOOL')}
|
||||||
>
|
>
|
||||||
Ray
|
Ray
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "line"}
|
isSelected={activeTool === 'line'}
|
||||||
onClick={() => state.send("SELECTED_LINE_TOOL")}
|
onClick={() => state.send('SELECTED_LINE_TOOL')}
|
||||||
>
|
>
|
||||||
Line
|
Line
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "polyline"}
|
isSelected={activeTool === 'polyline'}
|
||||||
onClick={() => state.send("SELECTED_POLYLINE_TOOL")}
|
onClick={() => state.send('SELECTED_POLYLINE_TOOL')}
|
||||||
>
|
>
|
||||||
Polyline
|
Polyline
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isSelected={activeTool === "rectangle"}
|
isSelected={activeTool === 'rectangle'}
|
||||||
onClick={() => state.send("SELECTED_RECTANGLE_TOOL")}
|
onClick={() => state.send('SELECTED_RECTANGLE_TOOL')}
|
||||||
>
|
>
|
||||||
Rectangle
|
Rectangle
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
|
<Button onClick={() => state.send('RESET_CAMERA')}>Reset Camera</Button>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<Button onClick={() => state.send("UNDO")}>Undo</Button>
|
<Button onClick={() => state.send('UNDO')}>Undo</Button>
|
||||||
<Button onClick={() => state.send("REDO")}>Redo</Button>
|
<Button onClick={() => state.send('REDO')}>Redo</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</ToolbarContainer>
|
</ToolbarContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolbarContainer = styled("div", {
|
const ToolbarContainer = styled('div', {
|
||||||
gridArea: "toolbar",
|
gridArea: 'toolbar',
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
borderBottom: "1px solid black",
|
borderBottom: '1px solid black',
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
backgroundColor: "$panel",
|
backgroundColor: '$panel',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
fontSize: "$1",
|
fontSize: '$1',
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
const Section = styled("div", {
|
const Section = styled('div', {
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: 'nowrap',
|
||||||
overflow: "hidden",
|
overflowY: 'hidden',
|
||||||
display: "flex",
|
overflowX: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Button = styled("button", {
|
const Button = styled('button', {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
cursor: "pointer",
|
cursor: 'pointer',
|
||||||
font: "$ui",
|
font: '$ui',
|
||||||
fontSize: "$ui",
|
fontSize: '$ui',
|
||||||
height: "40px",
|
height: '40px',
|
||||||
outline: "none",
|
outline: 'none',
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
border: "none",
|
border: 'none',
|
||||||
padding: "0 12px",
|
padding: '0 12px',
|
||||||
background: "none",
|
background: 'none',
|
||||||
"&:hover": {
|
'&:hover': {
|
||||||
backgroundColor: "$hint",
|
backgroundColor: '$hint',
|
||||||
},
|
},
|
||||||
"& svg": {
|
'& svg': {
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 16,
|
width: 16,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
isSelected: {
|
isSelected: {
|
||||||
true: {
|
true: {
|
||||||
color: "$selected",
|
color: '$selected',
|
||||||
},
|
},
|
||||||
false: {},
|
false: {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,197 +1,217 @@
|
||||||
import { useEffect } from "react"
|
import { useEffect } from 'react'
|
||||||
import state from "state"
|
import state from 'state'
|
||||||
import { MoveType } from "types"
|
import { MoveType } from 'types'
|
||||||
import { getKeyboardEventInfo, metaKey } from "utils/utils"
|
import { getKeyboardEventInfo, metaKey } from 'utils/utils'
|
||||||
|
|
||||||
export default function useKeyboardEvents() {
|
export default function useKeyboardEvents() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if (metaKey(e) && !["i", "r", "j"].includes(e.key)) {
|
if (metaKey(e) && !['i', 'r', 'j'].includes(e.key)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "!": {
|
case 'ArrowUp': {
|
||||||
|
state.send('NUDGED', { delta: [0, -1], ...getKeyboardEventInfo(e) })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ArrowRight': {
|
||||||
|
state.send('NUDGED', { delta: [1, 0], ...getKeyboardEventInfo(e) })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ArrowDown': {
|
||||||
|
state.send('NUDGED', { delta: [0, 1], ...getKeyboardEventInfo(e) })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ArrowLeft': {
|
||||||
|
state.send('NUDGED', { delta: [-1, 0], ...getKeyboardEventInfo(e) })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '!': {
|
||||||
// Shift + 1
|
// Shift + 1
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
state.send("ZOOMED_TO_FIT")
|
state.send('ZOOMED_TO_FIT')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "@": {
|
case '@': {
|
||||||
// Shift + 2
|
// Shift + 2
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
state.send("ZOOMED_TO_SELECTION")
|
state.send('ZOOMED_TO_SELECTION')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ")": {
|
case ')': {
|
||||||
// Shift + 0
|
// Shift + 0
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
state.send("ZOOMED_TO_ACTUAL")
|
state.send('ZOOMED_TO_ACTUAL')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "Escape": {
|
case 'Escape': {
|
||||||
state.send("CANCELLED")
|
state.send('CANCELLED')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "z": {
|
case 'z': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
state.send("REDO", getKeyboardEventInfo(e))
|
state.send('REDO', getKeyboardEventInfo(e))
|
||||||
} else {
|
} else {
|
||||||
state.send("UNDO", getKeyboardEventInfo(e))
|
state.send('UNDO', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "‘": {
|
case '‘': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("MOVED", {
|
state.send('MOVED', {
|
||||||
...getKeyboardEventInfo(e),
|
...getKeyboardEventInfo(e),
|
||||||
type: MoveType.ToFront,
|
type: MoveType.ToFront,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "“": {
|
case '“': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("MOVED", {
|
state.send('MOVED', {
|
||||||
...getKeyboardEventInfo(e),
|
...getKeyboardEventInfo(e),
|
||||||
type: MoveType.ToBack,
|
type: MoveType.ToBack,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "]": {
|
case ']': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("MOVED", {
|
state.send('MOVED', {
|
||||||
...getKeyboardEventInfo(e),
|
...getKeyboardEventInfo(e),
|
||||||
type: MoveType.Forward,
|
type: MoveType.Forward,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "[": {
|
case '[': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("MOVED", {
|
state.send('MOVED', {
|
||||||
...getKeyboardEventInfo(e),
|
...getKeyboardEventInfo(e),
|
||||||
type: MoveType.Backward,
|
type: MoveType.Backward,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "Shift": {
|
case 'Shift': {
|
||||||
state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e))
|
state.send('PRESSED_SHIFT_KEY', getKeyboardEventInfo(e))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "Alt": {
|
case 'Alt': {
|
||||||
state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e))
|
state.send('PRESSED_ALT_KEY', getKeyboardEventInfo(e))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "Backspace": {
|
case 'Backspace': {
|
||||||
state.send("DELETED", getKeyboardEventInfo(e))
|
state.send('DELETED', getKeyboardEventInfo(e))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "s": {
|
case 's': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("SAVED", getKeyboardEventInfo(e))
|
state.send('SAVED', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "a": {
|
case 'a': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("SELECTED_ALL", getKeyboardEventInfo(e))
|
state.send('SELECTED_ALL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "v": {
|
case 'v': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("PASTED", getKeyboardEventInfo(e))
|
state.send('PASTED', getKeyboardEventInfo(e))
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_SELECT_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "d": {
|
case 'd': {
|
||||||
state.send("SELECTED_DRAW_TOOL", getKeyboardEventInfo(e))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "t": {
|
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("DUPLICATED", getKeyboardEventInfo(e))
|
state.send('DUPLICATED', getKeyboardEventInfo(e))
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_DRAW_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "c": {
|
case 't': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
state.send("COPIED", getKeyboardEventInfo(e))
|
state.send('DUPLICATED', getKeyboardEventInfo(e))
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_DOT_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "i": {
|
case 'c': {
|
||||||
|
if (metaKey(e)) {
|
||||||
|
state.send('COPIED', getKeyboardEventInfo(e))
|
||||||
|
} else {
|
||||||
|
state.send('SELECTED_CIRCLE_TOOL', getKeyboardEventInfo(e))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'i': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "l": {
|
case 'l': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_LINE_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "y": {
|
case 'y': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_RAY_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "p": {
|
case 'p': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_POLYLINE_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "r": {
|
case 'r': {
|
||||||
if (metaKey(e)) {
|
if (metaKey(e)) {
|
||||||
} else {
|
} else {
|
||||||
state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e))
|
state.send('SELECTED_RECTANGLE_TOOL', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
state.send("PRESSED_KEY", getKeyboardEventInfo(e))
|
state.send('PRESSED_KEY', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(e: KeyboardEvent) {
|
function handleKeyUp(e: KeyboardEvent) {
|
||||||
if (e.key === "Shift") {
|
if (e.key === 'Shift') {
|
||||||
state.send("RELEASED_SHIFT_KEY", getKeyboardEventInfo(e))
|
state.send('RELEASED_SHIFT_KEY', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "Alt") {
|
if (e.key === 'Alt') {
|
||||||
state.send("RELEASED_ALT_KEY", getKeyboardEventInfo(e))
|
state.send('RELEASED_ALT_KEY', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
state.send("RELEASED_KEY", getKeyboardEventInfo(e))
|
state.send('RELEASED_KEY', getKeyboardEventInfo(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.addEventListener("keydown", handleKeyDown)
|
document.body.addEventListener('keydown', handleKeyDown)
|
||||||
document.body.addEventListener("keyup", handleKeyUp)
|
document.body.addEventListener('keyup', handleKeyUp)
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener("keydown", handleKeyDown)
|
document.body.removeEventListener('keydown', handleKeyDown)
|
||||||
document.body.removeEventListener("keyup", handleKeyUp)
|
document.body.removeEventListener('keyup', handleKeyUp)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
export const shades = {
|
export const shades = {
|
||||||
transparent: "transparent",
|
none: 'none',
|
||||||
white: "rgba(248, 249, 250, 1.000)",
|
white: 'rgba(248, 249, 250, 1.000)',
|
||||||
lightGray: "rgba(224, 226, 230, 1.000)",
|
lightGray: 'rgba(224, 226, 230, 1.000)',
|
||||||
gray: "rgba(172, 181, 189, 1.000)",
|
gray: 'rgba(172, 181, 189, 1.000)',
|
||||||
darkGray: "rgba(52, 58, 64, 1.000)",
|
darkGray: 'rgba(52, 58, 64, 1.000)',
|
||||||
black: "rgba(0,0,0, 1.000)",
|
black: 'rgba(0,0,0, 1.000)',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const strokes = {
|
export const strokes = {
|
||||||
lime: "rgba(115, 184, 23, 1.000)",
|
lime: 'rgba(115, 184, 23, 1.000)',
|
||||||
green: "rgba(54, 178, 77, 1.000)",
|
green: 'rgba(54, 178, 77, 1.000)',
|
||||||
teal: "rgba(9, 167, 120, 1.000)",
|
teal: 'rgba(9, 167, 120, 1.000)',
|
||||||
cyan: "rgba(14, 152, 173, 1.000)",
|
cyan: 'rgba(14, 152, 173, 1.000)',
|
||||||
blue: "rgba(28, 126, 214, 1.000)",
|
blue: 'rgba(28, 126, 214, 1.000)',
|
||||||
indigo: "rgba(66, 99, 235, 1.000)",
|
indigo: 'rgba(66, 99, 235, 1.000)',
|
||||||
violet: "rgba(112, 72, 232, 1.000)",
|
violet: 'rgba(112, 72, 232, 1.000)',
|
||||||
grape: "rgba(174, 62, 200, 1.000)",
|
grape: 'rgba(174, 62, 200, 1.000)',
|
||||||
pink: "rgba(214, 51, 108, 1.000)",
|
pink: 'rgba(214, 51, 108, 1.000)',
|
||||||
red: "rgba(240, 63, 63, 1.000)",
|
red: 'rgba(240, 63, 63, 1.000)',
|
||||||
orange: "rgba(247, 103, 6, 1.000)",
|
orange: 'rgba(247, 103, 6, 1.000)',
|
||||||
yellow: "rgba(245, 159, 0, 1.000)",
|
yellow: 'rgba(245, 159, 0, 1.000)',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fills = {
|
export const fills = {
|
||||||
lime: "rgba(217, 245, 162, 1.000)",
|
lime: 'rgba(217, 245, 162, 1.000)',
|
||||||
green: "rgba(177, 242, 188, 1.000)",
|
green: 'rgba(177, 242, 188, 1.000)',
|
||||||
teal: "rgba(149, 242, 215, 1.000)",
|
teal: 'rgba(149, 242, 215, 1.000)',
|
||||||
cyan: "rgba(153, 233, 242, 1.000)",
|
cyan: 'rgba(153, 233, 242, 1.000)',
|
||||||
blue: "rgba(166, 216, 255, 1.000)",
|
blue: 'rgba(166, 216, 255, 1.000)',
|
||||||
indigo: "rgba(186, 200, 255, 1.000)",
|
indigo: 'rgba(186, 200, 255, 1.000)',
|
||||||
violet: "rgba(208, 191, 255, 1.000)",
|
violet: 'rgba(208, 191, 255, 1.000)',
|
||||||
grape: "rgba(237, 190, 250, 1.000)",
|
grape: 'rgba(237, 190, 250, 1.000)',
|
||||||
pink: "rgba(252, 194, 215, 1.000)",
|
pink: 'rgba(252, 194, 215, 1.000)',
|
||||||
red: "rgba(255, 201, 201, 1.000)",
|
red: 'rgba(255, 201, 201, 1.000)',
|
||||||
orange: "rgba(255, 216, 168, 1.000)",
|
orange: 'rgba(255, 216, 168, 1.000)',
|
||||||
yellow: "rgba(255, 236, 153, 1.000)",
|
yellow: 'rgba(255, 236, 153, 1.000)',
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { CircleShape, ShapeType } from "types"
|
import { CircleShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from 'utils/bounds'
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { pointInCircle } from "utils/hitTests"
|
import { pointInCircle } from 'utils/hitTests'
|
||||||
import { translateBounds } from "utils/utils"
|
import { translateBounds } from 'utils/utils'
|
||||||
|
|
||||||
const circle = registerShapeUtils<CircleShape>({
|
const circle = registerShapeUtils<CircleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -15,22 +15,29 @@ const circle = registerShapeUtils<CircleShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Circle,
|
type: ShapeType.Circle,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Circle",
|
name: 'Circle',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
radius: 1,
|
radius: 1,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
stroke: "#000",
|
stroke: '#000',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id, radius }) {
|
render({ id, radius, style }) {
|
||||||
return <circle id={id} cx={radius} cy={radius} r={radius} />
|
return (
|
||||||
|
<circle
|
||||||
|
id={id}
|
||||||
|
cx={radius}
|
||||||
|
cy={radius}
|
||||||
|
r={Math.max(0, radius - Number(style.strokeWidth) / 2)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
applyStyles(shape, style) {
|
applyStyles(shape, style) {
|
||||||
|
@ -92,7 +99,7 @@ const circle = registerShapeUtils<CircleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { DotShape, ShapeType } from "types"
|
import { DotShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from 'utils/bounds'
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from 'components/canvas/misc'
|
||||||
import { translateBounds } from "utils/utils"
|
import { translateBounds } from 'utils/utils'
|
||||||
|
|
||||||
const dot = registerShapeUtils<DotShape>({
|
const dot = registerShapeUtils<DotShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -15,14 +15,14 @@ const dot = registerShapeUtils<DotShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Dot,
|
type: ShapeType.Dot,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Dot",
|
name: 'Dot',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
strokeWidth: "0",
|
strokeWidth: '0',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ const dot = registerShapeUtils<DotShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -34,13 +34,13 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
...props.style,
|
...props.style,
|
||||||
stroke: 'transparent',
|
fill: props.style.stroke,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render(shape) {
|
render(shape) {
|
||||||
const { id, point, points } = shape
|
const { id, point, points, style } = shape
|
||||||
|
|
||||||
if (!pathCache.has(points)) {
|
if (!pathCache.has(points)) {
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
|
@ -51,7 +51,12 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
}
|
}
|
||||||
pathCache.set(points, getSvgPathFromStroke(d))
|
pathCache.set(points, getSvgPathFromStroke(d))
|
||||||
} else {
|
} else {
|
||||||
pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
|
pathCache.set(
|
||||||
|
points,
|
||||||
|
getSvgPathFromStroke(
|
||||||
|
getStroke(points, { size: Number(style.strokeWidth) * 2 })
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +65,7 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
|
|
||||||
applyStyles(shape, style) {
|
applyStyles(shape, style) {
|
||||||
Object.assign(shape.style, style)
|
Object.assign(shape.style, style)
|
||||||
|
shape.style.fill = shape.style.stroke
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -128,7 +134,7 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { EllipseShape, ShapeType } from "types"
|
import { EllipseShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsContained, getRotatedEllipseBounds } from "utils/bounds"
|
import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
|
||||||
import { intersectEllipseBounds } from "utils/intersections"
|
import { intersectEllipseBounds } from 'utils/intersections'
|
||||||
import { pointInEllipse } from "utils/hitTests"
|
import { pointInEllipse } from 'utils/hitTests'
|
||||||
import {
|
import {
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getRotatedCorners,
|
getRotatedCorners,
|
||||||
rotateBounds,
|
rotateBounds,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
} from "utils/utils"
|
} from 'utils/utils'
|
||||||
|
|
||||||
const ellipse = registerShapeUtils<EllipseShape>({
|
const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -20,24 +20,30 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Ellipse,
|
type: ShapeType.Ellipse,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Ellipse",
|
name: 'Ellipse',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
radiusX: 1,
|
radiusX: 1,
|
||||||
radiusY: 1,
|
radiusY: 1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
stroke: "#000",
|
stroke: '#000',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id, radiusX, radiusY }) {
|
render({ id, radiusX, radiusY, style }) {
|
||||||
return (
|
return (
|
||||||
<ellipse id={id} cx={radiusX} cy={radiusY} rx={radiusX} ry={radiusY} />
|
<ellipse
|
||||||
|
id={id}
|
||||||
|
cx={radiusX}
|
||||||
|
cy={radiusY}
|
||||||
|
rx={Math.max(0, radiusX - Number(style.strokeWidth) / 2)}
|
||||||
|
ry={Math.max(0, radiusY - Number(style.strokeWidth) / 2)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -110,7 +116,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { LineShape, ShapeType } from "types"
|
import { LineShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from 'utils/bounds'
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from 'components/canvas/misc'
|
||||||
import { translateBounds } from "utils/utils"
|
import { translateBounds } from 'utils/utils'
|
||||||
|
|
||||||
const line = registerShapeUtils<LineShape>({
|
const line = registerShapeUtils<LineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -15,15 +15,15 @@ const line = registerShapeUtils<LineShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Line,
|
type: ShapeType.Line,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Line",
|
name: 'Line',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
direction: [0, 0],
|
direction: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
stroke: "#000",
|
stroke: '#000',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ const line = registerShapeUtils<LineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { PolylineShape, ShapeType } from "types"
|
import { PolylineShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { intersectPolylineBounds } from "utils/intersections"
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
import { boundsContainPolygon } from "utils/bounds"
|
import { boundsContainPolygon } from 'utils/bounds'
|
||||||
import { getBoundsFromPoints, translateBounds } from "utils/utils"
|
import { getBoundsFromPoints, translateBounds } from 'utils/utils'
|
||||||
|
|
||||||
const polyline = registerShapeUtils<PolylineShape>({
|
const polyline = registerShapeUtils<PolylineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -14,16 +14,16 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Polyline,
|
type: ShapeType.Polyline,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
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,
|
||||||
style: {
|
style: {
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
strokeLinecap: "round",
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: "round",
|
strokeLinejoin: 'round',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { RayShape, ShapeType } from "types"
|
import { RayShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from 'utils/bounds'
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from 'components/canvas/misc'
|
||||||
import { translateBounds } from "utils/utils"
|
import { translateBounds } from 'utils/utils'
|
||||||
|
|
||||||
const ray = registerShapeUtils<RayShape>({
|
const ray = registerShapeUtils<RayShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -15,15 +15,15 @@ const ray = registerShapeUtils<RayShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Ray,
|
type: ShapeType.Ray,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
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,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
stroke: "#000",
|
stroke: '#000',
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
|
@ -88,7 +88,7 @@ const ray = registerShapeUtils<RayShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from 'uuid'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import { RectangleShape, ShapeType } from "types"
|
import { RectangleShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
|
import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds'
|
||||||
import {
|
import {
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getRotatedCorners,
|
getRotatedCorners,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
} from "utils/utils"
|
} from 'utils/utils'
|
||||||
|
|
||||||
const rectangle = registerShapeUtils<RectangleShape>({
|
const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -17,42 +17,31 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Rectangle,
|
type: ShapeType.Rectangle,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Rectangle",
|
name: 'Rectangle',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
size: [1, 1],
|
size: [1, 1],
|
||||||
radius: 2,
|
radius: 2,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: {
|
style: {
|
||||||
fill: "#c6cacb",
|
fill: '#c6cacb',
|
||||||
stroke: "#000",
|
stroke: '#000',
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id, size, radius, childIndex }) {
|
render({ id, size, radius, style }) {
|
||||||
return (
|
return (
|
||||||
<g id={id}>
|
<g id={id}>
|
||||||
<rect
|
<rect
|
||||||
id={id}
|
id={id}
|
||||||
width={size[0]}
|
|
||||||
height={size[1]}
|
|
||||||
rx={radius}
|
rx={radius}
|
||||||
ry={radius}
|
ry={radius}
|
||||||
|
width={Math.max(0, size[0] - Number(style.strokeWidth) / 2)}
|
||||||
|
height={Math.max(0, size[1] - Number(style.strokeWidth) / 2)}
|
||||||
/>
|
/>
|
||||||
<text
|
|
||||||
y={4}
|
|
||||||
x={4}
|
|
||||||
fontSize={18}
|
|
||||||
fill="black"
|
|
||||||
stroke="none"
|
|
||||||
alignmentBaseline="text-before-edge"
|
|
||||||
pointerEvents="none"
|
|
||||||
>
|
|
||||||
{childIndex}
|
|
||||||
</text>
|
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -113,7 +102,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
translateTo(shape, point) {
|
translateTo(shape, point) {
|
||||||
shape.point = point
|
shape.point = vec.toPrecision(point)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@monaco-editor/react": "^4.1.3",
|
"@monaco-editor/react": "^4.1.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
||||||
"@radix-ui/react-icons": "^1.0.3",
|
"@radix-ui/react-icons": "^1.0.3",
|
||||||
|
"@radix-ui/react-radio-group": "^0.0.16",
|
||||||
"@state-designer/react": "^1.7.1",
|
"@state-designer/react": "^1.7.1",
|
||||||
"@stitches/react": "^0.1.9",
|
"@stitches/react": "^0.1.9",
|
||||||
"framer-motion": "^4.1.16",
|
"framer-motion": "^4.1.16",
|
||||||
|
|
48
state/commands/duplicate.ts
Normal file
48
state/commands/duplicate.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data } from 'types'
|
||||||
|
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { current } from 'immer'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
|
||||||
|
export default function duplicateCommand(data: Data) {
|
||||||
|
const { currentPageId } = data
|
||||||
|
const selectedShapes = getSelectedShapes(current(data))
|
||||||
|
const duplicates = selectedShapes.map((shape) => ({
|
||||||
|
...shape,
|
||||||
|
id: uuid(),
|
||||||
|
point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)),
|
||||||
|
}))
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'duplicate_shapes',
|
||||||
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
|
do(data) {
|
||||||
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
|
data.selectedIds.clear()
|
||||||
|
|
||||||
|
for (const duplicate of duplicates) {
|
||||||
|
shapes[duplicate.id] = duplicate
|
||||||
|
data.selectedIds.add(duplicate.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
data.selectedIds.clear()
|
||||||
|
|
||||||
|
for (const duplicate of duplicates) {
|
||||||
|
delete shapes[duplicate.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let id in selectedShapes) {
|
||||||
|
data.selectedIds.add(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,22 +1,25 @@
|
||||||
import align from "./align"
|
import align from './align'
|
||||||
import deleteSelected from "./delete-selected"
|
import deleteSelected from './delete-selected'
|
||||||
import direct from "./direct"
|
import direct from './direct'
|
||||||
import distribute from "./distribute"
|
import distribute from './distribute'
|
||||||
import generate from "./generate"
|
import duplicate from './duplicate'
|
||||||
import move from "./move"
|
import generate from './generate'
|
||||||
import draw from "./draw"
|
import move from './move'
|
||||||
import rotate from "./rotate"
|
import draw from './draw'
|
||||||
import stretch from "./stretch"
|
import rotate from './rotate'
|
||||||
import style from "./style"
|
import stretch from './stretch'
|
||||||
import transform from "./transform"
|
import style from './style'
|
||||||
import transformSingle from "./transform-single"
|
import transform from './transform'
|
||||||
import translate from "./translate"
|
import transformSingle from './transform-single'
|
||||||
|
import translate from './translate'
|
||||||
|
import nudge from './nudge'
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
align,
|
align,
|
||||||
deleteSelected,
|
deleteSelected,
|
||||||
direct,
|
direct,
|
||||||
distribute,
|
distribute,
|
||||||
|
duplicate,
|
||||||
generate,
|
generate,
|
||||||
move,
|
move,
|
||||||
draw,
|
draw,
|
||||||
|
@ -26,6 +29,7 @@ const commands = {
|
||||||
transform,
|
transform,
|
||||||
transformSingle,
|
transformSingle,
|
||||||
translate,
|
translate,
|
||||||
|
nudge,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default commands
|
export default commands
|
||||||
|
|
40
state/commands/nudge.ts
Normal file
40
state/commands/nudge.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data } from 'types'
|
||||||
|
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
|
||||||
|
export default function nudgeCommand(data: Data, delta: number[]) {
|
||||||
|
const { currentPageId } = data
|
||||||
|
const selectedShapes = getSelectedShapes(data)
|
||||||
|
const shapeBounds = Object.fromEntries(
|
||||||
|
selectedShapes.map(
|
||||||
|
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'set_direction',
|
||||||
|
category: 'canvas',
|
||||||
|
do(data) {
|
||||||
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
|
for (let id in shapeBounds) {
|
||||||
|
const shape = shapes[id]
|
||||||
|
getShapeUtils(shape).translateTo(shape, vec.add(shape.point, delta))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { shapes } = getPage(data, currentPageId)
|
||||||
|
|
||||||
|
for (let id in shapeBounds) {
|
||||||
|
const shape = shapes[id]
|
||||||
|
getShapeUtils(shape).translateTo(shape, vec.sub(shape.point, delta))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import Command from "./command"
|
import Command from './command'
|
||||||
import history from "../history"
|
import history from '../history'
|
||||||
import { Data, Corner, Edge } from "types"
|
import { Data, Corner, Edge } from 'types'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { TransformSingleSnapshot } from "state/sessions/transform-single-session"
|
import { TransformSingleSnapshot } from 'state/sessions/transform-single-session'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage } from 'utils/utils'
|
||||||
|
|
||||||
export default function transformSingleCommand(
|
export default function transformSingleCommand(
|
||||||
data: Data,
|
data: Data,
|
||||||
|
@ -14,13 +14,13 @@ export default function transformSingleCommand(
|
||||||
scaleY: number,
|
scaleY: number,
|
||||||
isCreating: boolean
|
isCreating: boolean
|
||||||
) {
|
) {
|
||||||
const shape = getPage(data, after.currentPageId).shapes[after.id]
|
const shape = current(getPage(data, after.currentPageId).shapes[after.id])
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
new Command({
|
new Command({
|
||||||
name: "transform_single_shape",
|
name: 'transform_single_shape',
|
||||||
category: "canvas",
|
category: 'canvas',
|
||||||
manualSelection: true,
|
manualSelection: true,
|
||||||
do(data) {
|
do(data) {
|
||||||
const { id, type, initialShape, initialShapeBounds } = after
|
const { id, type, initialShape, initialShapeBounds } = after
|
||||||
|
|
492
state/state.ts
492
state/state.ts
|
@ -41,10 +41,15 @@ const initialData: Data = {
|
||||||
isDarkMode: false,
|
isDarkMode: false,
|
||||||
isCodeOpen: false,
|
isCodeOpen: false,
|
||||||
isStyleOpen: false,
|
isStyleOpen: false,
|
||||||
|
isToolLocked: false,
|
||||||
|
isPenLocked: false,
|
||||||
|
nudgeDistanceLarge: 10,
|
||||||
|
nudgeDistanceSmall: 1,
|
||||||
},
|
},
|
||||||
currentStyle: {
|
currentStyle: {
|
||||||
fill: shades.lightGray,
|
fill: shades.lightGray,
|
||||||
stroke: shades.darkGray,
|
stroke: shades.darkGray,
|
||||||
|
strokeWidth: 2,
|
||||||
},
|
},
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
@ -94,6 +99,9 @@ const state = createState({
|
||||||
else: 'zoomCameraToActual',
|
else: 'zoomCameraToActual',
|
||||||
},
|
},
|
||||||
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
|
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
|
||||||
|
NUDGED: { do: 'nudgeSelection' },
|
||||||
|
USED_PEN_DEVICE: 'enablePenLock',
|
||||||
|
DISABLED_PEN_LOCK: 'disablePenLock',
|
||||||
},
|
},
|
||||||
initial: 'loading',
|
initial: 'loading',
|
||||||
states: {
|
states: {
|
||||||
|
@ -124,20 +132,22 @@ const state = createState({
|
||||||
selecting: {
|
selecting: {
|
||||||
on: {
|
on: {
|
||||||
SAVED: 'forceSave',
|
SAVED: 'forceSave',
|
||||||
UNDO: { do: 'undo' },
|
UNDO: 'undo',
|
||||||
REDO: { do: 'redo' },
|
REDO: 'redo',
|
||||||
CANCELLED: { do: 'clearSelectedIds' },
|
|
||||||
DELETED: { do: 'deleteSelectedIds' },
|
|
||||||
SAVED_CODE: 'saveCode',
|
SAVED_CODE: 'saveCode',
|
||||||
GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
|
CANCELLED: 'clearSelectedIds',
|
||||||
|
DELETED: 'deleteSelectedIds',
|
||||||
|
STARTED_PINCHING: { to: 'pinching' },
|
||||||
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
|
INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize',
|
||||||
DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
|
DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize',
|
||||||
CHANGED_CODE_CONTROL: 'updateControls',
|
CHANGED_CODE_CONTROL: 'updateControls',
|
||||||
ALIGNED: 'alignSelection',
|
GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
|
||||||
STRETCHED: 'stretchSelection',
|
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
||||||
DISTRIBUTED: 'distributeSelection',
|
MOVED: { if: 'hasSelection', do: 'moveSelection' },
|
||||||
MOVED: 'moveSelection',
|
ALIGNED: { if: 'hasSelection', do: 'alignSelection' },
|
||||||
STARTED_PINCHING: { to: 'pinching' },
|
STRETCHED: { if: 'hasSelection', do: 'stretchSelection' },
|
||||||
|
DISTRIBUTED: { if: 'hasSelection', do: 'distributeSelection' },
|
||||||
|
DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
|
||||||
},
|
},
|
||||||
initial: 'notPointing',
|
initial: 'notPointing',
|
||||||
states: {
|
states: {
|
||||||
|
@ -262,239 +272,257 @@ const state = createState({
|
||||||
PINCHED: { do: 'pinchCamera' },
|
PINCHED: { do: 'pinchCamera' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
draw: {
|
usingTool: {
|
||||||
initial: 'creating',
|
initial: 'draw',
|
||||||
states: {
|
states: {
|
||||||
creating: {
|
draw: {
|
||||||
on: {
|
initial: 'creating',
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
get: 'newDraw',
|
|
||||||
do: 'createShape',
|
|
||||||
to: 'draw.editing',
|
|
||||||
},
|
|
||||||
UNDO: { do: 'undo' },
|
|
||||||
REDO: { do: 'redo' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
onEnter: 'startDrawSession',
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: {
|
|
||||||
do: 'completeSession',
|
|
||||||
to: 'draw.creating',
|
|
||||||
},
|
|
||||||
CANCELLED: {
|
|
||||||
do: ['cancelSession', 'deleteSelectedIds'],
|
|
||||||
to: 'selecting',
|
|
||||||
},
|
|
||||||
MOVED_POINTER: 'updateDrawSession',
|
|
||||||
PANNED_CAMERA: 'updateDrawSession',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dot: {
|
|
||||||
initial: 'creating',
|
|
||||||
states: {
|
|
||||||
creating: {
|
|
||||||
on: {
|
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
get: 'newDot',
|
|
||||||
do: 'createShape',
|
|
||||||
to: 'dot.editing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { do: 'completeSession', to: 'selecting' },
|
|
||||||
CANCELLED: {
|
|
||||||
do: ['cancelSession', 'deleteSelectedIds'],
|
|
||||||
to: 'selecting',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initial: 'inactive',
|
|
||||||
states: {
|
states: {
|
||||||
inactive: {
|
creating: {
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: {
|
CANCELLED: { to: 'selecting' },
|
||||||
if: 'distanceImpliesDrag',
|
POINTED_CANVAS: {
|
||||||
to: 'dot.editing.active',
|
get: 'newDraw',
|
||||||
|
do: 'createShape',
|
||||||
|
to: 'draw.editing',
|
||||||
|
},
|
||||||
|
UNDO: { do: 'undo' },
|
||||||
|
REDO: { do: 'redo' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
onEnter: 'startDrawSession',
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: {
|
||||||
|
do: 'completeSession',
|
||||||
|
to: 'draw.creating',
|
||||||
|
},
|
||||||
|
CANCELLED: {
|
||||||
|
do: ['cancelSession', 'deleteSelectedIds'],
|
||||||
|
to: 'selecting',
|
||||||
|
},
|
||||||
|
MOVED_POINTER: 'updateDrawSession',
|
||||||
|
PANNED_CAMERA: 'updateDrawSession',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dot: {
|
||||||
|
initial: 'creating',
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
get: 'newDot',
|
||||||
|
do: 'createShape',
|
||||||
|
to: 'dot.editing',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
active: {
|
editing: {
|
||||||
onEnter: 'startTranslateSession',
|
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: 'updateTranslateSession',
|
STOPPED_POINTING: [
|
||||||
PANNED_CAMERA: 'updateTranslateSession',
|
'completeSession',
|
||||||
|
{
|
||||||
|
if: 'isToolLocked',
|
||||||
|
to: 'dot.creating',
|
||||||
|
else: {
|
||||||
|
to: 'selecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
CANCELLED: {
|
||||||
|
do: ['cancelSession', 'deleteSelectedIds'],
|
||||||
|
to: 'selecting',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
initial: 'inactive',
|
||||||
|
states: {
|
||||||
|
inactive: {
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: 'distanceImpliesDrag',
|
||||||
|
to: 'dot.editing.active',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
onEnter: 'startTranslateSession',
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: 'updateTranslateSession',
|
||||||
|
PANNED_CAMERA: 'updateTranslateSession',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
circle: {
|
||||||
},
|
initial: 'creating',
|
||||||
circle: {
|
states: {
|
||||||
initial: 'creating',
|
creating: {
|
||||||
states: {
|
on: {
|
||||||
creating: {
|
CANCELLED: { to: 'selecting' },
|
||||||
on: {
|
POINTED_CANVAS: {
|
||||||
CANCELLED: { to: 'selecting' },
|
to: 'circle.editing',
|
||||||
POINTED_CANVAS: {
|
},
|
||||||
to: 'circle.editing',
|
},
|
||||||
},
|
},
|
||||||
},
|
editing: {
|
||||||
},
|
on: {
|
||||||
editing: {
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
on: {
|
CANCELLED: { to: 'selecting' },
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
MOVED_POINTER: {
|
||||||
CANCELLED: { to: 'selecting' },
|
if: 'distanceImpliesDrag',
|
||||||
MOVED_POINTER: {
|
then: {
|
||||||
if: 'distanceImpliesDrag',
|
get: 'newCircle',
|
||||||
then: {
|
do: 'createShape',
|
||||||
get: 'newCircle',
|
to: 'drawingShape.bounds',
|
||||||
do: 'createShape',
|
},
|
||||||
to: 'drawingShape.bounds',
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
ellipse: {
|
||||||
},
|
initial: 'creating',
|
||||||
ellipse: {
|
states: {
|
||||||
initial: 'creating',
|
creating: {
|
||||||
states: {
|
on: {
|
||||||
creating: {
|
CANCELLED: { to: 'selecting' },
|
||||||
on: {
|
POINTED_CANVAS: {
|
||||||
CANCELLED: { to: 'selecting' },
|
to: 'ellipse.editing',
|
||||||
POINTED_CANVAS: {
|
},
|
||||||
to: 'ellipse.editing',
|
},
|
||||||
},
|
},
|
||||||
},
|
editing: {
|
||||||
},
|
on: {
|
||||||
editing: {
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
on: {
|
CANCELLED: { to: 'selecting' },
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
MOVED_POINTER: {
|
||||||
CANCELLED: { to: 'selecting' },
|
if: 'distanceImpliesDrag',
|
||||||
MOVED_POINTER: {
|
then: {
|
||||||
if: 'distanceImpliesDrag',
|
get: 'newEllipse',
|
||||||
then: {
|
do: 'createShape',
|
||||||
get: 'newEllipse',
|
to: 'drawingShape.bounds',
|
||||||
do: 'createShape',
|
},
|
||||||
to: 'drawingShape.bounds',
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
rectangle: {
|
||||||
},
|
initial: 'creating',
|
||||||
rectangle: {
|
states: {
|
||||||
initial: 'creating',
|
creating: {
|
||||||
states: {
|
on: {
|
||||||
creating: {
|
CANCELLED: { to: 'selecting' },
|
||||||
on: {
|
POINTED_CANVAS: {
|
||||||
CANCELLED: { to: 'selecting' },
|
to: 'rectangle.editing',
|
||||||
POINTED_CANVAS: {
|
},
|
||||||
to: 'rectangle.editing',
|
},
|
||||||
},
|
},
|
||||||
},
|
editing: {
|
||||||
},
|
on: {
|
||||||
editing: {
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
on: {
|
CANCELLED: { to: 'selecting' },
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
MOVED_POINTER: {
|
||||||
CANCELLED: { to: 'selecting' },
|
if: 'distanceImpliesDrag',
|
||||||
MOVED_POINTER: {
|
then: {
|
||||||
if: 'distanceImpliesDrag',
|
get: 'newRectangle',
|
||||||
then: {
|
do: 'createShape',
|
||||||
get: 'newRectangle',
|
to: 'drawingShape.bounds',
|
||||||
do: 'createShape',
|
},
|
||||||
to: 'drawingShape.bounds',
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ray: {
|
||||||
|
initial: 'creating',
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
get: 'newRay',
|
||||||
|
do: 'createShape',
|
||||||
|
to: 'ray.editing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: 'distanceImpliesDrag',
|
||||||
|
to: 'drawingShape.direction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
line: {
|
||||||
|
initial: 'creating',
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
get: 'newLine',
|
||||||
|
do: 'createShape',
|
||||||
|
to: 'line.editing',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
on: {
|
||||||
|
STOPPED_POINTING: { to: 'selecting' },
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
MOVED_POINTER: {
|
||||||
|
if: 'distanceImpliesDrag',
|
||||||
|
to: 'drawingShape.direction',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
polyline: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ray: {
|
drawingShape: {
|
||||||
initial: 'creating',
|
|
||||||
states: {
|
|
||||||
creating: {
|
|
||||||
on: {
|
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
get: 'newRay',
|
|
||||||
do: 'createShape',
|
|
||||||
to: 'ray.editing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
MOVED_POINTER: {
|
|
||||||
if: 'distanceImpliesDrag',
|
|
||||||
to: 'drawingShape.direction',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
line: {
|
|
||||||
initial: 'creating',
|
|
||||||
states: {
|
|
||||||
creating: {
|
|
||||||
on: {
|
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
POINTED_CANVAS: {
|
|
||||||
get: 'newLine',
|
|
||||||
do: 'createShape',
|
|
||||||
to: 'line.editing',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editing: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: { to: 'selecting' },
|
|
||||||
CANCELLED: { to: 'selecting' },
|
|
||||||
MOVED_POINTER: {
|
|
||||||
if: 'distanceImpliesDrag',
|
|
||||||
to: 'drawingShape.direction',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
polyline: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
drawingShape: {
|
|
||||||
on: {
|
|
||||||
STOPPED_POINTING: {
|
|
||||||
do: 'completeSession',
|
|
||||||
to: 'selecting',
|
|
||||||
},
|
|
||||||
CANCELLED: {
|
|
||||||
do: ['cancelSession', 'deleteSelectedIds'],
|
|
||||||
to: 'selecting',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
initial: 'drawingShapeBounds',
|
|
||||||
states: {
|
|
||||||
bounds: {
|
|
||||||
onEnter: 'startDrawTransformSession',
|
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: 'updateTransformSession',
|
STOPPED_POINTING: [
|
||||||
PANNED_CAMERA: 'updateTransformSession',
|
'completeSession',
|
||||||
|
{
|
||||||
|
if: 'isToolLocked',
|
||||||
|
to: 'usingTool.previous',
|
||||||
|
else: { to: 'selecting' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
CANCELLED: {
|
||||||
|
do: ['cancelSession', 'deleteSelectedIds'],
|
||||||
|
to: 'selecting',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
initial: 'drawingShapeBounds',
|
||||||
direction: {
|
states: {
|
||||||
onEnter: 'startDirectionSession',
|
bounds: {
|
||||||
on: {
|
onEnter: 'startDrawTransformSession',
|
||||||
MOVED_POINTER: 'updateDirectionSession',
|
on: {
|
||||||
PANNED_CAMERA: 'updateDirectionSession',
|
MOVED_POINTER: 'updateTransformSession',
|
||||||
|
PANNED_CAMERA: 'updateTransformSession',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
direction: {
|
||||||
|
onEnter: 'startDirectionSession',
|
||||||
|
on: {
|
||||||
|
MOVED_POINTER: 'updateDirectionSession',
|
||||||
|
PANNED_CAMERA: 'updateDirectionSession',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -562,6 +590,12 @@ const state = createState({
|
||||||
hasSelection(data) {
|
hasSelection(data) {
|
||||||
return data.selectedIds.size > 0
|
return data.selectedIds.size > 0
|
||||||
},
|
},
|
||||||
|
isToolLocked(data) {
|
||||||
|
return data.settings.isToolLocked
|
||||||
|
},
|
||||||
|
isPenLocked(data) {
|
||||||
|
return data.settings.isPenLocked
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
/* --------------------- Shapes --------------------- */
|
/* --------------------- Shapes --------------------- */
|
||||||
|
@ -712,6 +746,19 @@ const state = createState({
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Nudges
|
||||||
|
nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) {
|
||||||
|
commands.nudge(
|
||||||
|
data,
|
||||||
|
vec.mul(
|
||||||
|
payload.delta,
|
||||||
|
payload.shiftKey
|
||||||
|
? data.settings.nudgeDistanceLarge
|
||||||
|
: data.settings.nudgeDistanceSmall
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
/* -------------------- Selection ------------------- */
|
/* -------------------- Selection ------------------- */
|
||||||
|
|
||||||
selectAll(data) {
|
selectAll(data) {
|
||||||
|
@ -756,6 +803,9 @@ const state = createState({
|
||||||
distributeSelection(data, payload: { type: DistributeType }) {
|
distributeSelection(data, payload: { type: DistributeType }) {
|
||||||
commands.distribute(data, payload.type)
|
commands.distribute(data, payload.type)
|
||||||
},
|
},
|
||||||
|
duplicateSelection(data) {
|
||||||
|
commands.duplicate(data)
|
||||||
|
},
|
||||||
|
|
||||||
/* --------------------- Camera --------------------- */
|
/* --------------------- Camera --------------------- */
|
||||||
|
|
||||||
|
@ -913,6 +963,7 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ---------------------- Code ---------------------- */
|
/* ---------------------- Code ---------------------- */
|
||||||
|
|
||||||
closeCodePanel(data) {
|
closeCodePanel(data) {
|
||||||
data.settings.isCodeOpen = false
|
data.settings.isCodeOpen = false
|
||||||
},
|
},
|
||||||
|
@ -962,7 +1013,20 @@ const state = createState({
|
||||||
history.enable()
|
history.enable()
|
||||||
},
|
},
|
||||||
|
|
||||||
// Data
|
/* -------------------- Settings -------------------- */
|
||||||
|
|
||||||
|
enablePenLock(data) {
|
||||||
|
data.settings.isPenLocked = true
|
||||||
|
},
|
||||||
|
disablePenLock(data) {
|
||||||
|
data.settings.isPenLocked = false
|
||||||
|
},
|
||||||
|
toggleToolLock(data) {
|
||||||
|
data.settings.isToolLocked = !data.settings.isToolLocked
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------------------- Data ---------------------- */
|
||||||
|
|
||||||
saveCode(data, payload: { code: string }) {
|
saveCode(data, payload: { code: string }) {
|
||||||
data.document.code[data.currentCodeFileId].code = payload.code
|
data.document.code[data.currentCodeFileId].code = payload.code
|
||||||
history.save(data)
|
history.save(data)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createCss, defaultThemeMap } from "@stitches/react"
|
import { createCss, defaultThemeMap } from '@stitches/react'
|
||||||
|
|
||||||
const { styled, global, css, theme, getCssString } = createCss({
|
const { styled, global, css, theme, getCssString } = createCss({
|
||||||
themeMap: {
|
themeMap: {
|
||||||
|
@ -6,26 +6,27 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
brushFill: "rgba(0,0,0,.1)",
|
brushFill: 'rgba(0,0,0,.1)',
|
||||||
brushStroke: "rgba(0,0,0,.5)",
|
brushStroke: 'rgba(0,0,0,.5)',
|
||||||
hint: "rgba(66, 133, 244, 0.200)",
|
hint: 'rgba(66, 133, 244, 0.200)',
|
||||||
selected: "rgba(66, 133, 244, 1.000)",
|
selected: 'rgba(66, 133, 244, 1.000)',
|
||||||
bounds: "rgba(65, 132, 244, 1.000)",
|
bounds: 'rgba(65, 132, 244, 1.000)',
|
||||||
boundsBg: "rgba(65, 132, 244, 0.100)",
|
boundsBg: 'rgba(65, 132, 244, 0.100)',
|
||||||
border: "#aaa",
|
border: '#aaa',
|
||||||
panel: "#fefefe",
|
panel: '#fefefe',
|
||||||
hover: "#efefef",
|
inactive: '#cccccf',
|
||||||
text: "#333",
|
hover: '#efefef',
|
||||||
input: "#f3f3f3",
|
text: '#333',
|
||||||
inputBorder: "#ddd",
|
input: '#f3f3f3',
|
||||||
|
inputBorder: '#ddd',
|
||||||
},
|
},
|
||||||
space: {},
|
space: {},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
0: "10px",
|
0: '10px',
|
||||||
1: "12px",
|
1: '12px',
|
||||||
2: "13px",
|
2: '13px',
|
||||||
3: "16px",
|
3: '16px',
|
||||||
4: "18px",
|
4: '18px',
|
||||||
},
|
},
|
||||||
fonts: {
|
fonts: {
|
||||||
ui: '"Recursive", system-ui, sans-serif',
|
ui: '"Recursive", system-ui, sans-serif',
|
||||||
|
@ -72,17 +73,17 @@ const light = theme({})
|
||||||
const dark = theme({})
|
const dark = theme({})
|
||||||
|
|
||||||
const globalStyles = global({
|
const globalStyles = global({
|
||||||
"*": { boxSizing: "border-box" },
|
'*': { boxSizing: 'border-box' },
|
||||||
":root": {
|
':root': {
|
||||||
"--camera-zoom": 1,
|
'--camera-zoom': 1,
|
||||||
"--scale": "calc(1 / var(--camera-zoom))",
|
'--scale': 'calc(1 / var(--camera-zoom))',
|
||||||
},
|
},
|
||||||
"html, body": {
|
'html, body': {
|
||||||
padding: "0px",
|
padding: '0px',
|
||||||
margin: "0px",
|
margin: '0px',
|
||||||
overscrollBehavior: "none",
|
overscrollBehavior: 'none',
|
||||||
fontFamily: "$ui",
|
fontFamily: '$ui',
|
||||||
fontSize: "$2",
|
fontSize: '$2',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
52
types.ts
52
types.ts
|
@ -1,6 +1,6 @@
|
||||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
|
||||||
|
|
||||||
import React from "react"
|
import React from 'react'
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Client State */
|
/* Client State */
|
||||||
|
@ -13,6 +13,10 @@ export interface Data {
|
||||||
isDarkMode: boolean
|
isDarkMode: boolean
|
||||||
isCodeOpen: boolean
|
isCodeOpen: boolean
|
||||||
isStyleOpen: boolean
|
isStyleOpen: boolean
|
||||||
|
nudgeDistanceSmall: number
|
||||||
|
nudgeDistanceLarge: number
|
||||||
|
isToolLocked: boolean
|
||||||
|
isPenLocked: boolean
|
||||||
}
|
}
|
||||||
currentStyle: ShapeStyles
|
currentStyle: ShapeStyles
|
||||||
camera: {
|
camera: {
|
||||||
|
@ -39,21 +43,21 @@ export interface Data {
|
||||||
|
|
||||||
export interface Page {
|
export interface Page {
|
||||||
id: string
|
id: string
|
||||||
type: "page"
|
type: 'page'
|
||||||
childIndex: number
|
childIndex: number
|
||||||
name: string
|
name: string
|
||||||
shapes: Record<string, Shape>
|
shapes: Record<string, Shape>
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ShapeType {
|
export enum ShapeType {
|
||||||
Dot = "dot",
|
Dot = 'dot',
|
||||||
Circle = "circle",
|
Circle = 'circle',
|
||||||
Ellipse = "ellipse",
|
Ellipse = 'ellipse',
|
||||||
Line = "line",
|
Line = 'line',
|
||||||
Ray = "ray",
|
Ray = 'ray',
|
||||||
Polyline = "polyline",
|
Polyline = 'polyline',
|
||||||
Rectangle = "rectangle",
|
Rectangle = 'rectangle',
|
||||||
Draw = "draw",
|
Draw = 'draw',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consider:
|
// Consider:
|
||||||
|
@ -164,17 +168,17 @@ export interface PointerInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Edge {
|
export enum Edge {
|
||||||
Top = "top_edge",
|
Top = 'top_edge',
|
||||||
Right = "right_edge",
|
Right = 'right_edge',
|
||||||
Bottom = "bottom_edge",
|
Bottom = 'bottom_edge',
|
||||||
Left = "left_edge",
|
Left = 'left_edge',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Corner {
|
export enum Corner {
|
||||||
TopLeft = "top_left_corner",
|
TopLeft = 'top_left_corner',
|
||||||
TopRight = "top_right_corner",
|
TopRight = 'top_right_corner',
|
||||||
BottomRight = "bottom_right_corner",
|
BottomRight = 'bottom_right_corner',
|
||||||
BottomLeft = "bottom_left_corner",
|
BottomLeft = 'bottom_left_corner',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Bounds {
|
export interface Bounds {
|
||||||
|
@ -262,10 +266,10 @@ export type IMonaco = typeof monaco
|
||||||
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
|
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
|
||||||
|
|
||||||
export enum ControlType {
|
export enum ControlType {
|
||||||
Number = "number",
|
Number = 'number',
|
||||||
Vector = "vector",
|
Vector = 'vector',
|
||||||
Text = "text",
|
Text = 'text',
|
||||||
Select = "select",
|
Select = 'select',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseCodeControl {
|
export interface BaseCodeControl {
|
||||||
|
@ -296,7 +300,7 @@ export interface TextCodeControl extends BaseCodeControl {
|
||||||
format?: (value: string) => string
|
format?: (value: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectCodeControl<T extends string = "">
|
export interface SelectCodeControl<T extends string = ''>
|
||||||
extends BaseCodeControl {
|
extends BaseCodeControl {
|
||||||
type: ControlType.Select
|
type: ControlType.Select
|
||||||
value: T
|
value: T
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Vector from "lib/code/vector"
|
import Vector from 'lib/code/vector'
|
||||||
import React from "react"
|
import React from 'react'
|
||||||
import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from "types"
|
import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from 'types'
|
||||||
import * as vec from "./vec"
|
import * as vec from './vec'
|
||||||
import _isMobile from "ismobilejs"
|
import _isMobile from 'ismobilejs'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
export function screenToWorld(point: number[], data: Data) {
|
export function screenToWorld(point: number[], data: Data) {
|
||||||
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
|
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
|
||||||
|
@ -132,7 +132,7 @@ export function getBezierCurveSegments(points: number[][], tension = 0.4) {
|
||||||
cpoints: number[][] = [...points]
|
cpoints: number[][] = [...points]
|
||||||
|
|
||||||
if (len < 2) {
|
if (len < 2) {
|
||||||
throw Error("Curve must have at least two points.")
|
throw Error('Curve must have at least two points.')
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 1; i < len - 1; i++) {
|
for (let i = 1; i < len - 1; i++) {
|
||||||
|
@ -260,12 +260,12 @@ export function copyToClipboard(string: string) {
|
||||||
navigator.clipboard.writeText(string)
|
navigator.clipboard.writeText(string)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
textarea = document.createElement("textarea")
|
textarea = document.createElement('textarea')
|
||||||
textarea.setAttribute("position", "fixed")
|
textarea.setAttribute('position', 'fixed')
|
||||||
textarea.setAttribute("top", "0")
|
textarea.setAttribute('top', '0')
|
||||||
textarea.setAttribute("readonly", "true")
|
textarea.setAttribute('readonly', 'true')
|
||||||
textarea.setAttribute("contenteditable", "true")
|
textarea.setAttribute('contenteditable', 'true')
|
||||||
textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set.
|
textarea.style.position = 'fixed' // prevent scroll from jumping to the bottom when focus is set.
|
||||||
textarea.value = string
|
textarea.value = string
|
||||||
|
|
||||||
document.body.appendChild(textarea)
|
document.body.appendChild(textarea)
|
||||||
|
@ -281,7 +281,7 @@ export function copyToClipboard(string: string) {
|
||||||
sel.addRange(range)
|
sel.addRange(range)
|
||||||
|
|
||||||
textarea.setSelectionRange(0, textarea.value.length)
|
textarea.setSelectionRange(0, textarea.value.length)
|
||||||
result = document.execCommand("copy")
|
result = document.execCommand('copy')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
result = null
|
result = null
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -549,7 +549,7 @@ export function arrsIntersect<T>(
|
||||||
|
|
||||||
export function getTouchDisplay() {
|
export function getTouchDisplay() {
|
||||||
return (
|
return (
|
||||||
"ontouchstart" in window ||
|
'ontouchstart' in window ||
|
||||||
navigator.maxTouchPoints > 0 ||
|
navigator.maxTouchPoints > 0 ||
|
||||||
navigator.msMaxTouchPoints > 0
|
navigator.msMaxTouchPoints > 0
|
||||||
)
|
)
|
||||||
|
@ -604,7 +604,7 @@ export function modulate(
|
||||||
export function clamp(n: number, min: number): number
|
export function clamp(n: number, min: number): number
|
||||||
export function clamp(n: number, min: number, max: number): number
|
export function clamp(n: number, min: number, max: number): number
|
||||||
export function clamp(n: number, min: number, max?: number): number {
|
export function clamp(n: number, min: number, max?: number): number {
|
||||||
return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
|
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CURVES
|
// CURVES
|
||||||
|
@ -871,8 +871,8 @@ export async function postJsonToEndpoint(
|
||||||
const d = await fetch(
|
const d = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
|
`${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -962,7 +962,7 @@ export function getTransformAnchor(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function vectorToPoint(point: number[] | Vector | undefined) {
|
export function vectorToPoint(point: number[] | Vector | undefined) {
|
||||||
if (typeof point === "undefined") {
|
if (typeof point === 'undefined') {
|
||||||
return [0, 0]
|
return [0, 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1062,7 +1062,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) {
|
||||||
|
|
||||||
export function getTransformedBoundingBox(
|
export function getTransformedBoundingBox(
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
handle: Corner | Edge | "center",
|
handle: Corner | Edge | 'center',
|
||||||
delta: number[],
|
delta: number[],
|
||||||
rotation = 0,
|
rotation = 0,
|
||||||
isAspectRatioLocked = false
|
isAspectRatioLocked = false
|
||||||
|
@ -1076,7 +1076,7 @@ export function getTransformedBoundingBox(
|
||||||
let [bx1, by1] = [bounds.maxX, bounds.maxY]
|
let [bx1, by1] = [bounds.maxX, bounds.maxY]
|
||||||
|
|
||||||
// If the drag is on the center, just translate the bounds.
|
// If the drag is on the center, just translate the bounds.
|
||||||
if (handle === "center") {
|
if (handle === 'center') {
|
||||||
return {
|
return {
|
||||||
minX: bx0 + delta[0],
|
minX: bx0 + delta[0],
|
||||||
minY: by0 + delta[1],
|
minY: by0 + delta[1],
|
||||||
|
@ -1491,7 +1491,7 @@ export function forceIntegerChildIndices(shapes: Shape[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function setZoomCSS(zoom: number) {
|
export function setZoomCSS(zoom: number) {
|
||||||
document.documentElement.style.setProperty("--camera-zoom", zoom.toString())
|
document.documentElement.style.setProperty('--camera-zoom', zoom.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrent<T extends object>(source: T): T {
|
export function getCurrent<T extends object>(source: T): T {
|
||||||
|
@ -1539,7 +1539,7 @@ export function simplify(points: number[][], tolerance = 1) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSvgPathFromStroke(stroke: number[][]) {
|
export function getSvgPathFromStroke(stroke: number[][]) {
|
||||||
if (!stroke.length) return ""
|
if (!stroke.length) return ''
|
||||||
|
|
||||||
const d = stroke.reduce(
|
const d = stroke.reduce(
|
||||||
(acc, [x0, y0], i, arr) => {
|
(acc, [x0, y0], i, arr) => {
|
||||||
|
@ -1547,9 +1547,9 @@ export function getSvgPathFromStroke(stroke: number[][]) {
|
||||||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
|
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
["M", ...stroke[0], "Q"]
|
['M', ...stroke[0], 'Q']
|
||||||
)
|
)
|
||||||
|
|
||||||
d.push("Z")
|
d.push('Z')
|
||||||
return d.join(" ")
|
return d.join(' ')
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,6 +483,6 @@ export function nudge(A: number[], B: number[], d: number) {
|
||||||
* @param a
|
* @param a
|
||||||
* @param n
|
* @param n
|
||||||
*/
|
*/
|
||||||
export function toPrecision(a: number[], n = 3) {
|
export function toPrecision(a: number[], n = 4) {
|
||||||
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
||||||
}
|
}
|
||||||
|
|
29
yarn.lock
29
yarn.lock
|
@ -1358,6 +1358,17 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-label@0.0.13":
|
||||||
|
version "0.0.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.0.13.tgz#b71930fa16a2cf859296317436cb88e31efb8ecf"
|
||||||
|
integrity sha512-csNElm8qA38pOHr772CXIvBXd/eCGaoAMImuLdawUxQNzwxQ4npd8lr/f9fi/4OLkgeNOVOqjsaVamiNmF/lIw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-compose-refs" "0.0.5"
|
||||||
|
"@radix-ui/react-id" "0.0.6"
|
||||||
|
"@radix-ui/react-polymorphic" "0.0.11"
|
||||||
|
"@radix-ui/react-primitive" "0.0.13"
|
||||||
|
|
||||||
"@radix-ui/react-menu@0.0.18":
|
"@radix-ui/react-menu@0.0.18":
|
||||||
version "0.0.18"
|
version "0.0.18"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.18.tgz#b36f7657eb6757c623ffc688c48a4781ffd82351"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.18.tgz#b36f7657eb6757c623ffc688c48a4781ffd82351"
|
||||||
|
@ -1431,6 +1442,24 @@
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-polymorphic" "0.0.11"
|
"@radix-ui/react-polymorphic" "0.0.11"
|
||||||
|
|
||||||
|
"@radix-ui/react-radio-group@^0.0.16":
|
||||||
|
version "0.0.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.0.16.tgz#10fc6e5c3102599cf422e9f6f8d2766088e602a1"
|
||||||
|
integrity sha512-vOtgflNWcauSul+EvnPCxATdmPw7fb1cuqBJX07yJdjbrw1Iv5v/+d79fNyIwPR+KrkhP+uCMIBfF0gvo6K7ZQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.0.5"
|
||||||
|
"@radix-ui/react-compose-refs" "0.0.5"
|
||||||
|
"@radix-ui/react-context" "0.0.5"
|
||||||
|
"@radix-ui/react-label" "0.0.13"
|
||||||
|
"@radix-ui/react-polymorphic" "0.0.11"
|
||||||
|
"@radix-ui/react-presence" "0.0.14"
|
||||||
|
"@radix-ui/react-primitive" "0.0.13"
|
||||||
|
"@radix-ui/react-roving-focus" "0.0.13"
|
||||||
|
"@radix-ui/react-slot" "0.0.10"
|
||||||
|
"@radix-ui/react-use-callback-ref" "0.0.5"
|
||||||
|
"@radix-ui/react-use-controllable-state" "0.0.6"
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus@0.0.13":
|
"@radix-ui/react-roving-focus@0.0.13":
|
||||||
version "0.0.13"
|
version "0.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.13.tgz#c72f503832577979c4caa9efcfd59140730c2f80"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.13.tgz#c72f503832577979c4caa9efcfd59140730c2f80"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue