Updates code editor
This commit is contained in:
parent
e21748f7b7
commit
abd310aa2e
24 changed files with 792 additions and 227 deletions
|
@ -7,7 +7,9 @@ import state, { useSelector } from "state"
|
||||||
import { CodeFile } from "types"
|
import { CodeFile } from "types"
|
||||||
import CodeDocs from "./code-docs"
|
import CodeDocs from "./code-docs"
|
||||||
import CodeEditor from "./code-editor"
|
import CodeEditor from "./code-editor"
|
||||||
import { getShapesFromCode } from "lib/code/generate"
|
import { generateFromCode } from "lib/code/generate"
|
||||||
|
import * as Panel from "../panel"
|
||||||
|
import { IconButton } from "../shared"
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
Code,
|
Code,
|
||||||
|
@ -51,8 +53,8 @@ export default function CodePanel() {
|
||||||
states: {
|
states: {
|
||||||
editingCode: {
|
editingCode: {
|
||||||
on: {
|
on: {
|
||||||
RAN_CODE: "runCode",
|
RAN_CODE: ["saveCode", "runCode"],
|
||||||
SAVED_CODE: ["runCode", "saveCode"],
|
SAVED_CODE: ["saveCode", "runCode"],
|
||||||
CHANGED_CODE: { secretlyDo: "setCode" },
|
CHANGED_CODE: { secretlyDo: "setCode" },
|
||||||
CLEARED_ERROR: { if: "hasError", do: "clearError" },
|
CLEARED_ERROR: { if: "hasError", do: "clearError" },
|
||||||
TOGGLED_DOCS: { to: "viewingDocs" },
|
TOGGLED_DOCS: { to: "viewingDocs" },
|
||||||
|
@ -80,8 +82,8 @@ export default function CodePanel() {
|
||||||
let error = null
|
let error = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const shapes = getShapesFromCode(data.code)
|
const { shapes, controls } = generateFromCode(data.code)
|
||||||
state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
|
state.send("GENERATED_FROM_CODE", { shapes, controls })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||||
|
@ -113,15 +115,10 @@ export default function CodePanel() {
|
||||||
const { error } = local.data
|
const { error } = local.data
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelContainer
|
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}>
|
||||||
data-bp-desktop
|
|
||||||
ref={rContainer}
|
|
||||||
dragMomentum={false}
|
|
||||||
isCollapsed={!isOpen}
|
|
||||||
>
|
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<Content>
|
<Panel.Layout>
|
||||||
<Header>
|
<Panel.Header>
|
||||||
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
||||||
<X />
|
<X />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -151,8 +148,8 @@ export default function CodePanel() {
|
||||||
<PlayCircle />
|
<PlayCircle />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ButtonsGroup>
|
</ButtonsGroup>
|
||||||
</Header>
|
</Panel.Header>
|
||||||
<EditorContainer>
|
<Panel.Content>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
readOnly={isReadOnly}
|
readOnly={isReadOnly}
|
||||||
|
@ -163,149 +160,42 @@ export default function CodePanel() {
|
||||||
onKey={() => local.send("CLEARED_ERROR")}
|
onKey={() => local.send("CLEARED_ERROR")}
|
||||||
/>
|
/>
|
||||||
<CodeDocs isHidden={!local.isIn("viewingDocs")} />
|
<CodeDocs isHidden={!local.isIn("viewingDocs")} />
|
||||||
</EditorContainer>
|
</Panel.Content>
|
||||||
<ErrorContainer>
|
<Panel.Footer>
|
||||||
{error &&
|
{error &&
|
||||||
(error.line
|
(error.line
|
||||||
? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
|
? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
|
||||||
: error.message)}
|
: error.message)}
|
||||||
</ErrorContainer>
|
</Panel.Footer>
|
||||||
</Content>
|
</Panel.Layout>
|
||||||
) : (
|
) : (
|
||||||
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
||||||
<Code />
|
<Code />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</PanelContainer>
|
</Panel.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PanelContainer = styled(motion.div, {
|
|
||||||
position: "absolute",
|
|
||||||
top: "48px",
|
|
||||||
right: "8px",
|
|
||||||
bottom: "48px",
|
|
||||||
backgroundColor: "$panel",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "hidden",
|
|
||||||
border: "1px solid $border",
|
|
||||||
pointerEvents: "all",
|
|
||||||
userSelect: "none",
|
|
||||||
zIndex: 200,
|
|
||||||
|
|
||||||
button: {
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
|
|
||||||
variants: {
|
|
||||||
isCollapsed: {
|
|
||||||
true: {},
|
|
||||||
false: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const IconButton = styled("button", {
|
|
||||||
height: "40px",
|
|
||||||
width: "40px",
|
|
||||||
backgroundColor: "$panel",
|
|
||||||
borderRadius: "4px",
|
|
||||||
border: "1px solid $border",
|
|
||||||
padding: "0",
|
|
||||||
margin: "0",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
outline: "none",
|
|
||||||
pointerEvents: "all",
|
|
||||||
cursor: "pointer",
|
|
||||||
|
|
||||||
"&:hover:not(:disabled)": {
|
|
||||||
backgroundColor: "$panel",
|
|
||||||
},
|
|
||||||
|
|
||||||
"&:disabled": {
|
|
||||||
opacity: "0.5",
|
|
||||||
},
|
|
||||||
|
|
||||||
svg: {
|
|
||||||
height: "20px",
|
|
||||||
width: "20px",
|
|
||||||
strokeWidth: "2px",
|
|
||||||
stroke: "$text",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Content = styled("div", {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr",
|
|
||||||
gridTemplateRows: "auto 1fr 28px",
|
|
||||||
height: "100%",
|
|
||||||
width: 560,
|
|
||||||
minWidth: "100%",
|
|
||||||
maxWidth: 560,
|
|
||||||
overflow: "hidden",
|
|
||||||
userSelect: "none",
|
|
||||||
pointerEvents: "all",
|
|
||||||
})
|
|
||||||
|
|
||||||
const Header = styled("div", {
|
|
||||||
pointerEvents: "all",
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "auto 1fr",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderBottom: "1px solid $border",
|
|
||||||
|
|
||||||
"& button": {
|
|
||||||
gridColumn: "1",
|
|
||||||
gridRow: "1",
|
|
||||||
},
|
|
||||||
|
|
||||||
"& h3": {
|
|
||||||
gridColumn: "1 / span 3",
|
|
||||||
gridRow: "1",
|
|
||||||
textAlign: "center",
|
|
||||||
margin: "0",
|
|
||||||
padding: "0",
|
|
||||||
fontSize: "16px",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const ButtonsGroup = styled("div", {
|
const ButtonsGroup = styled("div", {
|
||||||
gridRow: "1",
|
gridRow: "1",
|
||||||
gridColumn: "3",
|
gridColumn: "3",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
})
|
})
|
||||||
|
|
||||||
const EditorContainer = styled("div", {
|
|
||||||
position: "relative",
|
|
||||||
pointerEvents: "all",
|
|
||||||
overflowY: "scroll",
|
|
||||||
})
|
|
||||||
|
|
||||||
const ErrorContainer = styled("div", {
|
|
||||||
overflowX: "scroll",
|
|
||||||
color: "$text",
|
|
||||||
font: "$debug",
|
|
||||||
padding: "0 12px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
})
|
|
||||||
|
|
||||||
const FontSizeButtons = styled("div", {
|
const FontSizeButtons = styled("div", {
|
||||||
paddingRight: 4,
|
paddingRight: 4,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
|
||||||
"& > button": {
|
"& > button": {
|
||||||
height: "50%",
|
height: "50%",
|
||||||
width: "100%",
|
|
||||||
|
|
||||||
"&:nth-of-type(1)": {
|
"&:nth-of-type(1)": {
|
||||||
paddingTop: 4,
|
alignItems: "flex-end",
|
||||||
},
|
},
|
||||||
|
|
||||||
"&:nth-of-type(2)": {
|
"&:nth-of-type(2)": {
|
||||||
paddingBottom: 4,
|
alignItems: "flex-start",
|
||||||
},
|
},
|
||||||
|
|
||||||
"& svg": {
|
"& svg": {
|
||||||
|
|
145
components/controls-panel/control.tsx
Normal file
145
components/controls-panel/control.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import state, { useSelector } from "state"
|
||||||
|
import styled from "styles"
|
||||||
|
import {
|
||||||
|
ControlType,
|
||||||
|
NumberCodeControl,
|
||||||
|
SelectCodeControl,
|
||||||
|
TextCodeControl,
|
||||||
|
VectorCodeControl,
|
||||||
|
} from "types"
|
||||||
|
|
||||||
|
export default function Control({ id }: { id: string }) {
|
||||||
|
const control = useSelector((s) => s.data.codeControls[id])
|
||||||
|
|
||||||
|
if (!control) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label>{control.label}</label>
|
||||||
|
{control.type === ControlType.Number ? (
|
||||||
|
<NumberControl {...control} />
|
||||||
|
) : control.type === ControlType.Vector ? (
|
||||||
|
<VectorControl {...control} />
|
||||||
|
) : control.type === ControlType.Text ? (
|
||||||
|
<TextControl {...control} />
|
||||||
|
) : control.type === ControlType.Select ? (
|
||||||
|
<SelectControl {...control} />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberControl({ id, min, max, step, value }: NumberCodeControl) {
|
||||||
|
return (
|
||||||
|
<Inputs>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: Number(e.currentTarget.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: Number(e.currentTarget.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Inputs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function VectorControl({ id, value, isNormalized }: VectorCodeControl) {
|
||||||
|
return (
|
||||||
|
<Inputs>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={isNormalized ? -1 : -Infinity}
|
||||||
|
max={isNormalized ? 1 : Infinity}
|
||||||
|
step={0.01}
|
||||||
|
value={value[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: [Number(e.currentTarget.value), value[1]],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={isNormalized ? -1 : -Infinity}
|
||||||
|
max={isNormalized ? 1 : Infinity}
|
||||||
|
step={0.01}
|
||||||
|
value={value[0]}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: [Number(e.currentTarget.value), value[1]],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={isNormalized ? -1 : -Infinity}
|
||||||
|
max={isNormalized ? 1 : Infinity}
|
||||||
|
step={0.01}
|
||||||
|
value={value[1]}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: [value[0], Number(e.currentTarget.value)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={isNormalized ? -1 : -Infinity}
|
||||||
|
max={isNormalized ? 1 : Infinity}
|
||||||
|
step={0.01}
|
||||||
|
value={value[1]}
|
||||||
|
onChange={(e) =>
|
||||||
|
state.send("CHANGED_CODE_CONTROL", {
|
||||||
|
[id]: [value[0], Number(e.currentTarget.value)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Inputs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextControl({}: TextCodeControl) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectControl({}: SelectCodeControl) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Inputs = styled("div", {
|
||||||
|
display: "flex",
|
||||||
|
gap: "8px",
|
||||||
|
height: "100%",
|
||||||
|
|
||||||
|
"& input": {
|
||||||
|
font: "$ui",
|
||||||
|
width: "64px",
|
||||||
|
fontSize: "$1",
|
||||||
|
border: "1px solid $inputBorder",
|
||||||
|
backgroundColor: "$input",
|
||||||
|
color: "$text",
|
||||||
|
height: "100%",
|
||||||
|
padding: "0px 6px",
|
||||||
|
},
|
||||||
|
"& input[type='range']": {
|
||||||
|
padding: 0,
|
||||||
|
flexGrow: 2,
|
||||||
|
},
|
||||||
|
})
|
62
components/controls-panel/controls-panel.tsx
Normal file
62
components/controls-panel/controls-panel.tsx
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
import styled from "styles"
|
||||||
|
import React, { useEffect, useRef } from "react"
|
||||||
|
import state, { useSelector } from "state"
|
||||||
|
import { X, Code, PlayCircle } from "react-feather"
|
||||||
|
import { IconButton } from "components/shared"
|
||||||
|
import * as Panel from "../panel"
|
||||||
|
import Control from "./control"
|
||||||
|
import { deepCompareArrays } from "utils/utils"
|
||||||
|
|
||||||
|
export default function ControlPanel() {
|
||||||
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
const codeControls = useSelector(
|
||||||
|
(state) => Object.keys(state.data.codeControls),
|
||||||
|
deepCompareArrays
|
||||||
|
)
|
||||||
|
const isOpen = true
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}>
|
||||||
|
{isOpen ? (
|
||||||
|
<Panel.Layout>
|
||||||
|
<Panel.Header>
|
||||||
|
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
||||||
|
<X />
|
||||||
|
</IconButton>
|
||||||
|
<h3>Controls</h3>
|
||||||
|
</Panel.Header>
|
||||||
|
<ControlsList>
|
||||||
|
{codeControls.map((id) => (
|
||||||
|
<Control key={id} id={id} />
|
||||||
|
))}
|
||||||
|
</ControlsList>
|
||||||
|
</Panel.Layout>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
||||||
|
<Code />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Panel.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ControlsList = styled(Panel.Content, {
|
||||||
|
padding: 12,
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 4fr",
|
||||||
|
gridAutoRows: "24px",
|
||||||
|
alignItems: "center",
|
||||||
|
gridColumnGap: "8px",
|
||||||
|
gridRowGap: "8px",
|
||||||
|
|
||||||
|
"& input": {
|
||||||
|
font: "$ui",
|
||||||
|
fontSize: "$1",
|
||||||
|
border: "1px solid $inputBorder",
|
||||||
|
backgroundColor: "$input",
|
||||||
|
color: "$text",
|
||||||
|
height: "100%",
|
||||||
|
padding: "0px 6px",
|
||||||
|
},
|
||||||
|
})
|
|
@ -4,17 +4,46 @@ import Canvas from "./canvas/canvas"
|
||||||
import StatusBar from "./status-bar"
|
import StatusBar from "./status-bar"
|
||||||
import Toolbar from "./toolbar"
|
import Toolbar from "./toolbar"
|
||||||
import CodePanel from "./code-panel/code-panel"
|
import CodePanel from "./code-panel/code-panel"
|
||||||
|
import ControlsPanel from "./controls-panel/controls-panel"
|
||||||
|
import styled from "styles"
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
useKeyboardEvents()
|
useKeyboardEvents()
|
||||||
useLoadOnMount()
|
useLoadOnMount()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Layout>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<CodePanel />
|
<LeftPanels>
|
||||||
</>
|
<CodePanel />
|
||||||
|
<ControlsPanel />
|
||||||
|
</LeftPanels>
|
||||||
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Layout = styled("div", {
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateRows: "40px 1fr 40px",
|
||||||
|
gridTemplateColumns: "auto 1fr",
|
||||||
|
gridTemplateAreas: `
|
||||||
|
"toolbar toolbar"
|
||||||
|
"leftPanels main"
|
||||||
|
"statusbar statusbar"
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const LeftPanels = styled("main", {
|
||||||
|
display: "grid",
|
||||||
|
gridArea: "leftPanels",
|
||||||
|
gridTemplateRows: "1fr auto",
|
||||||
|
padding: 8,
|
||||||
|
gap: 8,
|
||||||
|
})
|
||||||
|
|
79
components/panel.tsx
Normal file
79
components/panel.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import styled from "styles"
|
||||||
|
|
||||||
|
export const Root = styled("div", {
|
||||||
|
position: "relative",
|
||||||
|
backgroundColor: "$panel",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid $border",
|
||||||
|
pointerEvents: "all",
|
||||||
|
userSelect: "none",
|
||||||
|
zIndex: 200,
|
||||||
|
boxShadow: "0px 2px 25px rgba(0,0,0,.16)",
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isCollapsed: {
|
||||||
|
true: {},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Layout = styled("div", {
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr",
|
||||||
|
gridTemplateRows: "auto 1fr",
|
||||||
|
gridAutoRows: "28px",
|
||||||
|
height: "100%",
|
||||||
|
width: 560,
|
||||||
|
minWidth: "100%",
|
||||||
|
maxWidth: 560,
|
||||||
|
overflow: "hidden",
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "all",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Header = styled("div", {
|
||||||
|
pointerEvents: "all",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "auto 1fr auto",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderBottom: "1px solid $border",
|
||||||
|
|
||||||
|
"& button": {
|
||||||
|
gridColumn: "1",
|
||||||
|
gridRow: "1",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h3": {
|
||||||
|
gridColumn: "1 / span 3",
|
||||||
|
gridRow: "1",
|
||||||
|
textAlign: "center",
|
||||||
|
margin: "0",
|
||||||
|
padding: "0",
|
||||||
|
fontSize: "13px",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ButtonsGroup = styled("div", {
|
||||||
|
gridRow: "1",
|
||||||
|
gridColumn: "3",
|
||||||
|
display: "flex",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Content = styled("div", {
|
||||||
|
position: "relative",
|
||||||
|
pointerEvents: "all",
|
||||||
|
overflowY: "scroll",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Footer = styled("div", {
|
||||||
|
overflowX: "scroll",
|
||||||
|
color: "$text",
|
||||||
|
font: "$debug",
|
||||||
|
padding: "0 12px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderTop: "1px solid $border",
|
||||||
|
})
|
32
components/shared.tsx
Normal file
32
components/shared.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import styled from "styles"
|
||||||
|
|
||||||
|
export const IconButton = styled("button", {
|
||||||
|
height: "32px",
|
||||||
|
width: "32px",
|
||||||
|
backgroundColor: "$panel",
|
||||||
|
borderRadius: "4px",
|
||||||
|
padding: "0",
|
||||||
|
margin: "0",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
outline: "none",
|
||||||
|
border: "none",
|
||||||
|
pointerEvents: "all",
|
||||||
|
cursor: "pointer",
|
||||||
|
|
||||||
|
"&:hover:not(:disabled)": {
|
||||||
|
backgroundColor: "$panel",
|
||||||
|
},
|
||||||
|
|
||||||
|
"&:disabled": {
|
||||||
|
opacity: "0.5",
|
||||||
|
},
|
||||||
|
|
||||||
|
svg: {
|
||||||
|
height: "16px",
|
||||||
|
width: "16px",
|
||||||
|
strokeWidth: "2px",
|
||||||
|
stroke: "$text",
|
||||||
|
},
|
||||||
|
})
|
|
@ -70,20 +70,16 @@ export default function Toolbar() {
|
||||||
>
|
>
|
||||||
Rectangle
|
Rectangle
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
|
||||||
</Section>
|
</Section>
|
||||||
</ToolbarContainer>
|
</ToolbarContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolbarContainer = styled("div", {
|
const ToolbarContainer = styled("div", {
|
||||||
position: "absolute",
|
gridArea: "toolbar",
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: 40,
|
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
borderBottom: "1px solid black",
|
borderBottom: "1px solid black",
|
||||||
gridArea: "status",
|
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "auto 1fr auto",
|
gridTemplateColumns: "auto 1fr auto",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { CircleShape, ShapeType } from "types"
|
import { CircleShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Circle extends CodeShape<CircleShape> {
|
export default class Circle extends CodeShape<CircleShape> {
|
||||||
constructor(props = {} as Partial<CircleShape>) {
|
constructor(props = {} as Partial<CircleShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Circle,
|
type: ShapeType.Circle,
|
||||||
|
@ -23,6 +26,14 @@ export default class Circle extends CodeShape<CircleShape> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
get radius() {
|
get radius() {
|
||||||
return this.shape.radius
|
return this.shape.radius
|
||||||
}
|
}
|
||||||
|
|
57
lib/code/control.ts
Normal file
57
lib/code/control.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import {
|
||||||
|
CodeControl,
|
||||||
|
ControlType,
|
||||||
|
NumberCodeControl,
|
||||||
|
VectorCodeControl,
|
||||||
|
} from "types"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
import Vector from "./vector"
|
||||||
|
|
||||||
|
export const controls: Record<string, any> = {}
|
||||||
|
|
||||||
|
export const codeControls = new Set<CodeControl>([])
|
||||||
|
|
||||||
|
export class Control<T extends CodeControl> {
|
||||||
|
control: T
|
||||||
|
|
||||||
|
constructor(control: Omit<T, "id">) {
|
||||||
|
this.control = { ...control, id: uuid() } as T
|
||||||
|
codeControls.add(this.control)
|
||||||
|
|
||||||
|
// Could there be a better way to prevent this?
|
||||||
|
// When updating, constructor should just bind to
|
||||||
|
// the existing control rather than creating a new one?
|
||||||
|
if (!(window as any).isUpdatingCode) {
|
||||||
|
controls[this.control.label] = this.control.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
codeControls.delete(this.control)
|
||||||
|
delete controls[this.control.label]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberControl extends Control<NumberCodeControl> {
|
||||||
|
constructor(options: Omit<NumberCodeControl, "id" | "type">) {
|
||||||
|
const { value = 0, step = 1 } = options
|
||||||
|
super({
|
||||||
|
type: ControlType.Number,
|
||||||
|
...options,
|
||||||
|
value,
|
||||||
|
step,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VectorControl extends Control<VectorCodeControl> {
|
||||||
|
constructor(options: Omit<VectorCodeControl, "id" | "type">) {
|
||||||
|
const { value = [0, 0], isNormalized = false } = options
|
||||||
|
super({
|
||||||
|
type: ControlType.Vector,
|
||||||
|
...options,
|
||||||
|
value,
|
||||||
|
isNormalized,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { DotShape, ShapeType } from "types"
|
import { DotShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Dot extends CodeShape<DotShape> {
|
export default class Dot extends CodeShape<DotShape> {
|
||||||
constructor(props = {} as Partial<DotShape>) {
|
constructor(props = {} as Partial<DotShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Dot,
|
type: ShapeType.Dot,
|
||||||
|
@ -21,4 +24,12 @@ export default class Dot extends CodeShape<DotShape> {
|
||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { EllipseShape, ShapeType } from "types"
|
import { EllipseShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Ellipse extends CodeShape<EllipseShape> {
|
export default class Ellipse extends CodeShape<EllipseShape> {
|
||||||
constructor(props = {} as Partial<EllipseShape>) {
|
constructor(props = {} as Partial<EllipseShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Ellipse,
|
type: ShapeType.Ellipse,
|
||||||
|
@ -24,7 +27,19 @@ export default class Ellipse extends CodeShape<EllipseShape> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get radius() {
|
export() {
|
||||||
return this.shape.radius
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
|
get radiusX() {
|
||||||
|
return this.shape.radiusX
|
||||||
|
}
|
||||||
|
|
||||||
|
get radiusY() {
|
||||||
|
return this.shape.radiusY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,11 @@ import Ray from "./ray"
|
||||||
import Line from "./line"
|
import Line from "./line"
|
||||||
import Vector from "./vector"
|
import Vector from "./vector"
|
||||||
import Utils from "./utils"
|
import Utils from "./utils"
|
||||||
|
import { NumberControl, VectorControl, codeControls, controls } from "./control"
|
||||||
import { codeShapes } from "./index"
|
import { codeShapes } from "./index"
|
||||||
|
import { CodeControl } from "types"
|
||||||
|
|
||||||
const scope = {
|
const baseScope = {
|
||||||
Dot,
|
Dot,
|
||||||
Circle,
|
Circle,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
|
@ -19,6 +21,8 @@ const scope = {
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Vector,
|
Vector,
|
||||||
Utils,
|
Utils,
|
||||||
|
VectorControl,
|
||||||
|
NumberControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,8 +30,12 @@ const scope = {
|
||||||
* collected shapes as an array.
|
* collected shapes as an array.
|
||||||
* @param code
|
* @param code
|
||||||
*/
|
*/
|
||||||
export function getShapesFromCode(code: string) {
|
export function generateFromCode(code: string) {
|
||||||
|
codeControls.clear()
|
||||||
codeShapes.clear()
|
codeShapes.clear()
|
||||||
|
;(window as any).isUpdatingCode = false
|
||||||
|
|
||||||
|
const scope = { ...baseScope, controls }
|
||||||
|
|
||||||
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
||||||
|
|
||||||
|
@ -36,5 +44,39 @@ export function getShapesFromCode(code: string) {
|
||||||
return instance.shape
|
return instance.shape
|
||||||
})
|
})
|
||||||
|
|
||||||
return generatedShapes
|
const generatedControls = Array.from(codeControls.values())
|
||||||
|
|
||||||
|
return { shapes: generatedShapes, controls: generatedControls }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate code, collecting generated shapes in the shape set. Return the
|
||||||
|
* collected shapes as an array.
|
||||||
|
* @param code
|
||||||
|
*/
|
||||||
|
export function updateFromCode(
|
||||||
|
code: string,
|
||||||
|
controls: Record<string, CodeControl>
|
||||||
|
) {
|
||||||
|
codeShapes.clear()
|
||||||
|
;(window as any).isUpdatingCode = true
|
||||||
|
|
||||||
|
const scope = {
|
||||||
|
...baseScope,
|
||||||
|
controls: Object.fromEntries(
|
||||||
|
Object.entries(controls).map(([id, control]) => [
|
||||||
|
control.label,
|
||||||
|
control.value,
|
||||||
|
])
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
||||||
|
|
||||||
|
const generatedShapes = Array.from(codeShapes.values()).map((instance) => {
|
||||||
|
instance.shape.isGenerated = true
|
||||||
|
return instance.shape
|
||||||
|
})
|
||||||
|
|
||||||
|
return { shapes: generatedShapes }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { Shape } from "types"
|
import { Shape } from "types"
|
||||||
import * as vec from "utils/vec"
|
|
||||||
import { getShapeUtils } from "lib/shapes"
|
import { getShapeUtils } from "lib/shapes"
|
||||||
|
import * as vec from "utils/vec"
|
||||||
|
import Vector from "./vector"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export const codeShapes = new Set<CodeShape<Shape>>([])
|
export const codeShapes = new Set<CodeShape<Shape>>([])
|
||||||
|
|
||||||
|
type WithVectors<T extends Shape> = {
|
||||||
|
[key in keyof T]: number[] extends T[key] ? Vector : T[key]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for code shapes. Note that creating a shape adds it to the
|
* A base class for code shapes. Note that creating a shape adds it to the
|
||||||
* shape map, while deleting it removes it from the collected shapes set
|
* shape map, while deleting it removes it from the collected shapes set
|
||||||
|
@ -20,12 +26,12 @@ export default class CodeShape<T extends Shape> {
|
||||||
codeShapes.delete(this)
|
codeShapes.delete(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
moveTo(point: number[]) {
|
moveTo(point: Vector) {
|
||||||
this.shape.point = point
|
this.shape.point = vectorToPoint(point)
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(delta: number[]) {
|
translate(delta: Vector) {
|
||||||
this.shape.point = vec.add(this._shape.point, delta)
|
this.shape.point = vec.add(this._shape.point, vectorToPoint(delta))
|
||||||
}
|
}
|
||||||
|
|
||||||
rotate(rotation: number) {
|
rotate(rotation: number) {
|
||||||
|
@ -40,8 +46,8 @@ export default class CodeShape<T extends Shape> {
|
||||||
return getShapeUtils(this.shape).getBounds(this.shape)
|
return getShapeUtils(this.shape).getBounds(this.shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
hitTest(point: number[]) {
|
hitTest(point: Vector) {
|
||||||
return getShapeUtils(this.shape).hitTest(this.shape, point)
|
return getShapeUtils(this.shape).hitTest(this.shape, vectorToPoint(point))
|
||||||
}
|
}
|
||||||
|
|
||||||
get shape() {
|
get shape() {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { LineShape, ShapeType } from "types"
|
import { LineShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Line extends CodeShape<LineShape> {
|
export default class Line extends CodeShape<LineShape> {
|
||||||
constructor(props = {} as Partial<LineShape>) {
|
constructor(props = {} as Partial<LineShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
props.direction = vectorToPoint(props.direction)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Line,
|
type: ShapeType.Line,
|
||||||
|
@ -22,4 +26,17 @@ export default class Line extends CodeShape<LineShape> {
|
||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
shape.direction = vectorToPoint(shape.direction)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
|
get direction() {
|
||||||
|
return this.shape.direction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { PolylineShape, ShapeType } from "types"
|
import { PolylineShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Polyline extends CodeShape<PolylineShape> {
|
export default class Polyline extends CodeShape<PolylineShape> {
|
||||||
constructor(props = {} as Partial<PolylineShape>) {
|
constructor(props = {} as Partial<PolylineShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
props.points = props.points.map(vectorToPoint)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Polyline,
|
type: ShapeType.Polyline,
|
||||||
|
@ -22,4 +26,17 @@ export default class Polyline extends CodeShape<PolylineShape> {
|
||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
shape.points = shape.points.map(vectorToPoint)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
|
get points() {
|
||||||
|
return this.shape.points
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { RayShape, ShapeType } from "types"
|
import { RayShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Ray extends CodeShape<RayShape> {
|
export default class Ray extends CodeShape<RayShape> {
|
||||||
constructor(props = {} as Partial<RayShape>) {
|
constructor(props = {} as Partial<RayShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
props.direction = vectorToPoint(props.direction)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Ray,
|
type: ShapeType.Ray,
|
||||||
|
@ -22,4 +26,17 @@ export default class Ray extends CodeShape<RayShape> {
|
||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const shape = { ...this.shape }
|
||||||
|
|
||||||
|
shape.point = vectorToPoint(shape.point)
|
||||||
|
shape.direction = vectorToPoint(shape.direction)
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
|
|
||||||
|
get direction() {
|
||||||
|
return this.shape.direction
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import CodeShape from "./index"
|
import CodeShape from "./index"
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import { RectangleShape, ShapeType } from "types"
|
import { RectangleShape, ShapeType } from "types"
|
||||||
|
import { vectorToPoint } from "utils/utils"
|
||||||
|
|
||||||
export default class Rectangle extends CodeShape<RectangleShape> {
|
export default class Rectangle extends CodeShape<RectangleShape> {
|
||||||
constructor(props = {} as Partial<RectangleShape>) {
|
constructor(props = {} as Partial<RectangleShape>) {
|
||||||
|
props.point = vectorToPoint(props.point)
|
||||||
|
props.size = vectorToPoint(props.size)
|
||||||
|
|
||||||
super({
|
super({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Rectangle,
|
type: ShapeType.Rectangle,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Command from "./command"
|
import Command from "./command"
|
||||||
import history from "../history"
|
import history from "../history"
|
||||||
import { Data, Shape } from "types"
|
import { CodeControl, Data, Shape } from "types"
|
||||||
import { current } from "immer"
|
import { current } from "immer"
|
||||||
|
|
||||||
export default function setGeneratedShapes(
|
export default function setGeneratedShapes(
|
||||||
|
@ -8,12 +8,24 @@ export default function setGeneratedShapes(
|
||||||
currentPageId: string,
|
currentPageId: string,
|
||||||
generatedShapes: Shape[]
|
generatedShapes: Shape[]
|
||||||
) {
|
) {
|
||||||
|
const cData = current(data)
|
||||||
|
|
||||||
const prevGeneratedShapes = Object.values(
|
const prevGeneratedShapes = Object.values(
|
||||||
current(data).document.pages[currentPageId].shapes
|
cData.document.pages[currentPageId].shapes
|
||||||
).filter((shape) => shape.isGenerated)
|
).filter((shape) => shape.isGenerated)
|
||||||
|
|
||||||
|
const currentShapes = data.document.pages[currentPageId].shapes
|
||||||
|
|
||||||
|
// Remove previous generated shapes
|
||||||
|
for (let id in currentShapes) {
|
||||||
|
if (currentShapes[id].isGenerated) {
|
||||||
|
delete currentShapes[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new ones
|
||||||
for (let shape of generatedShapes) {
|
for (let shape of generatedShapes) {
|
||||||
data.document.pages[currentPageId].shapes[shape.id] = shape
|
currentShapes[shape.id] = shape
|
||||||
}
|
}
|
||||||
|
|
||||||
history.execute(
|
history.execute(
|
|
@ -1,13 +1,13 @@
|
||||||
import translate from "./translate"
|
import translate from "./translate"
|
||||||
import transform from "./transform"
|
import transform from "./transform"
|
||||||
import generateShapes from "./generate-shapes"
|
import generate from "./generate"
|
||||||
import createShape from "./create-shape"
|
import createShape from "./create-shape"
|
||||||
import direction from "./direction"
|
import direction from "./direction"
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
translate,
|
translate,
|
||||||
transform,
|
transform,
|
||||||
generateShapes,
|
generate,
|
||||||
createShape,
|
createShape,
|
||||||
direction,
|
direction,
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,32 +121,32 @@ export const defaultDocument: Data["document"] = {
|
||||||
name: "index.ts",
|
name: "index.ts",
|
||||||
code: `
|
code: `
|
||||||
new Dot({
|
new Dot({
|
||||||
point: [0, 0],
|
point: new Vector(0, 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
new Circle({
|
new Circle({
|
||||||
point: [200, 0],
|
point: new Vector(200, 0),
|
||||||
radius: 50,
|
radius: 50,
|
||||||
})
|
})
|
||||||
|
|
||||||
new Ellipse({
|
new Ellipse({
|
||||||
point: [400, 0],
|
point: new Vector(400, 0),
|
||||||
radiusX: 50,
|
radiusX: 50,
|
||||||
radiusY: 75
|
radiusY: 75
|
||||||
})
|
})
|
||||||
|
|
||||||
new Rectangle({
|
new Rectangle({
|
||||||
point: [0, 300],
|
point: new Vector(0, 300),
|
||||||
})
|
})
|
||||||
|
|
||||||
new Line({
|
new Line({
|
||||||
point: [200, 300],
|
point: new Vector(200, 300),
|
||||||
direction: [1,0.2]
|
direction: new Vector(1,0.2)
|
||||||
})
|
})
|
||||||
|
|
||||||
new Polyline({
|
new Polyline({
|
||||||
point: [400, 300],
|
point: new Vector(400, 300),
|
||||||
points: [[0, 200], [0,0], [200, 200], [200, 0]],
|
points: [new Vector(0, 200), new Vector(0,0), new Vector(200, 200), new Vector(200, 0)],
|
||||||
})
|
})
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,12 +9,15 @@ import {
|
||||||
Shapes,
|
Shapes,
|
||||||
TransformCorner,
|
TransformCorner,
|
||||||
TransformEdge,
|
TransformEdge,
|
||||||
|
CodeControl,
|
||||||
} from "types"
|
} from "types"
|
||||||
import { defaultDocument } from "./data"
|
import { defaultDocument } from "./data"
|
||||||
import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
|
import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
|
||||||
import history from "state/history"
|
import history from "state/history"
|
||||||
import * as Sessions from "./sessions"
|
import * as Sessions from "./sessions"
|
||||||
import commands from "./commands"
|
import commands from "./commands"
|
||||||
|
import { controls } from "lib/code/control"
|
||||||
|
import { generateFromCode, updateFromCode } from "lib/code/generate"
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
@ -32,6 +35,7 @@ const initialData: Data = {
|
||||||
selectedIds: new Set([]),
|
selectedIds: new Set([]),
|
||||||
currentPageId: "page0",
|
currentPageId: "page0",
|
||||||
currentCodeFileId: "file0",
|
currentCodeFileId: "file0",
|
||||||
|
codeControls: {},
|
||||||
document: defaultDocument,
|
document: defaultDocument,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +56,7 @@ const state = createState({
|
||||||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
||||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
||||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
||||||
|
RESET_CAMERA: "resetCamera",
|
||||||
},
|
},
|
||||||
initial: "loading",
|
initial: "loading",
|
||||||
states: {
|
states: {
|
||||||
|
@ -70,9 +75,10 @@ const state = createState({
|
||||||
CANCELLED: { do: "clearSelectedIds" },
|
CANCELLED: { do: "clearSelectedIds" },
|
||||||
DELETED: { do: "deleteSelectedIds" },
|
DELETED: { do: "deleteSelectedIds" },
|
||||||
SAVED_CODE: "saveCode",
|
SAVED_CODE: "saveCode",
|
||||||
GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
|
GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
|
||||||
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
||||||
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
||||||
|
CHANGED_CODE_CONTROL: "updateControls",
|
||||||
},
|
},
|
||||||
initial: "notPointing",
|
initial: "notPointing",
|
||||||
states: {
|
states: {
|
||||||
|
@ -437,6 +443,12 @@ const state = createState({
|
||||||
data.selectedIds.add(data.pointedId)
|
data.selectedIds.add(data.pointedId)
|
||||||
},
|
},
|
||||||
// Camera
|
// Camera
|
||||||
|
resetCamera(data) {
|
||||||
|
data.camera.zoom = 1
|
||||||
|
data.camera.point = [window.innerWidth / 2, window.innerHeight / 2]
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty("--camera-zoom", "1")
|
||||||
|
},
|
||||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||||
const { camera } = data
|
const { camera } = data
|
||||||
const p0 = screenToWorld(payload.point, data)
|
const p0 = screenToWorld(payload.point, data)
|
||||||
|
@ -493,8 +505,16 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
setGeneratedShapes(
|
||||||
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
data,
|
||||||
|
payload: { shapes: Shape[]; controls: CodeControl[] }
|
||||||
|
) {
|
||||||
|
commands.generate(data, data.currentPageId, payload.shapes)
|
||||||
|
},
|
||||||
|
setCodeControls(data, payload: { controls: CodeControl[] }) {
|
||||||
|
data.codeControls = Object.fromEntries(
|
||||||
|
payload.controls.map((control) => [control.id, control])
|
||||||
|
)
|
||||||
},
|
},
|
||||||
increaseCodeFontSize(data) {
|
increaseCodeFontSize(data) {
|
||||||
data.settings.fontSize++
|
data.settings.fontSize++
|
||||||
|
@ -502,6 +522,28 @@ const state = createState({
|
||||||
decreaseCodeFontSize(data) {
|
decreaseCodeFontSize(data) {
|
||||||
data.settings.fontSize--
|
data.settings.fontSize--
|
||||||
},
|
},
|
||||||
|
updateControls(data, payload: { [key: string]: any }) {
|
||||||
|
for (let key in payload) {
|
||||||
|
data.codeControls[key].value = payload[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
history.disable()
|
||||||
|
|
||||||
|
data.selectedIds.clear()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { shapes } = updateFromCode(
|
||||||
|
data.document.code[data.currentCodeFileId].code,
|
||||||
|
data.codeControls
|
||||||
|
)
|
||||||
|
|
||||||
|
commands.generate(data, data.currentPageId, shapes)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
history.enable()
|
||||||
|
},
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
saveCode(data, payload: { code: string }) {
|
saveCode(data, payload: { code: string }) {
|
||||||
|
@ -524,9 +566,9 @@ const state = createState({
|
||||||
document: { pages },
|
document: { pages },
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const shapes = Array.from(selectedIds.values()).map(
|
const shapes = Array.from(selectedIds.values())
|
||||||
(id) => pages[currentPageId].shapes[id]
|
.map((id) => pages[currentPageId].shapes[id])
|
||||||
)
|
.filter(Boolean)
|
||||||
|
|
||||||
if (selectedIds.size === 0) return null
|
if (selectedIds.size === 0) return null
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
border: "#aaa",
|
border: "#aaa",
|
||||||
panel: "#fefefe",
|
panel: "#fefefe",
|
||||||
text: "#000",
|
text: "#000",
|
||||||
|
input: "#f3f3f3",
|
||||||
|
inputBorder: "#ddd",
|
||||||
},
|
},
|
||||||
space: {},
|
space: {},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
|
@ -47,7 +49,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
min !== undefined && max !== undefined
|
min !== undefined && max !== undefined
|
||||||
? `clamp(${min}, ${val} / var(--camera-zoom), ${max})`
|
? `clamp(${min}, ${val} / var(--camera-zoom), ${max})`
|
||||||
: min !== undefined
|
: min !== undefined
|
||||||
? `max(${min}, ${val} / var(--camera-zoom))`
|
? `min(${min}, ${val} / var(--camera-zoom))`
|
||||||
: `calc(${val} / var(--camera-zoom))`,
|
: `calc(${val} / var(--camera-zoom))`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
173
types.ts
173
types.ts
|
@ -2,6 +2,10 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
|
||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* Client State */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
isReadOnly: boolean
|
isReadOnly: boolean
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -18,17 +22,16 @@ export interface Data {
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
currentPageId: string
|
currentPageId: string
|
||||||
currentCodeFileId: string
|
currentCodeFileId: string
|
||||||
|
codeControls: Record<string, CodeControl>
|
||||||
document: {
|
document: {
|
||||||
pages: Record<string, Page>
|
pages: Record<string, Page>
|
||||||
code: Record<string, CodeFile>
|
code: Record<string, CodeFile>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeFile {
|
/* -------------------------------------------------- */
|
||||||
id: string
|
/* Document */
|
||||||
name: string
|
/* -------------------------------------------------- */
|
||||||
code: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Page {
|
export interface Page {
|
||||||
id: string
|
id: string
|
||||||
|
@ -46,12 +49,14 @@ export enum ShapeType {
|
||||||
Ray = "ray",
|
Ray = "ray",
|
||||||
Polyline = "polyline",
|
Polyline = "polyline",
|
||||||
Rectangle = "rectangle",
|
Rectangle = "rectangle",
|
||||||
// Glob = "glob",
|
|
||||||
// Spline = "spline",
|
|
||||||
// Cubic = "cubic",
|
|
||||||
// Conic = "conic",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consider:
|
||||||
|
// Glob = "glob",
|
||||||
|
// Spline = "spline",
|
||||||
|
// Cubic = "cubic",
|
||||||
|
// Conic = "conic",
|
||||||
|
|
||||||
export interface BaseShape {
|
export interface BaseShape {
|
||||||
id: string
|
id: string
|
||||||
type: ShapeType
|
type: ShapeType
|
||||||
|
@ -108,31 +113,6 @@ export type Shape =
|
||||||
| PolylineShape
|
| PolylineShape
|
||||||
| RectangleShape
|
| RectangleShape
|
||||||
|
|
||||||
export interface Bounds {
|
|
||||||
minX: number
|
|
||||||
minY: number
|
|
||||||
maxX: number
|
|
||||||
maxY: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShapeBounds extends Bounds {
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PointSnapshot extends Bounds {
|
|
||||||
nx: number
|
|
||||||
nmx: number
|
|
||||||
ny: number
|
|
||||||
nmy: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BoundsSnapshot extends PointSnapshot {
|
|
||||||
nw: number
|
|
||||||
nh: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Shapes extends Record<ShapeType, Shape> {
|
export interface Shapes extends Record<ShapeType, Shape> {
|
||||||
[ShapeType.Dot]: DotShape
|
[ShapeType.Dot]: DotShape
|
||||||
[ShapeType.Circle]: CircleShape
|
[ShapeType.Circle]: CircleShape
|
||||||
|
@ -143,27 +123,16 @@ export interface Shapes extends Record<ShapeType, Shape> {
|
||||||
[ShapeType.Rectangle]: RectangleShape
|
[ShapeType.Rectangle]: RectangleShape
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Difference<A, B> = A extends B ? never : A
|
export interface CodeFile {
|
||||||
|
id: string
|
||||||
export type ShapeSpecificProps<T extends Shape> = Pick<
|
name: string
|
||||||
T,
|
code: string
|
||||||
Difference<keyof T, keyof BaseShape>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>
|
|
||||||
|
|
||||||
export type ShapeUtil<K extends Shape> = {
|
|
||||||
create(props: Partial<K>): K
|
|
||||||
getBounds(shape: K): Bounds
|
|
||||||
hitTest(shape: K, test: number[]): boolean
|
|
||||||
hitTestBounds(shape: K, bounds: Bounds): boolean
|
|
||||||
rotate(shape: K): K
|
|
||||||
translate(shape: K, delta: number[]): K
|
|
||||||
scale(shape: K, scale: number): K
|
|
||||||
stretch(shape: K, scaleX: number, scaleY: number): K
|
|
||||||
render(shape: K): JSX.Element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* Editor UI */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
export interface PointerInfo {
|
export interface PointerInfo {
|
||||||
target: string
|
target: string
|
||||||
pointerId: number
|
pointerId: number
|
||||||
|
@ -189,6 +158,104 @@ export enum TransformCorner {
|
||||||
BottomLeft = "bottom_left_corner",
|
BottomLeft = "bottom_left_corner",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Bounds {
|
||||||
|
minX: number
|
||||||
|
minY: number
|
||||||
|
maxX: number
|
||||||
|
maxY: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShapeBounds extends Bounds {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PointSnapshot extends Bounds {
|
||||||
|
nx: number
|
||||||
|
nmx: number
|
||||||
|
ny: number
|
||||||
|
nmy: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoundsSnapshot extends PointSnapshot {
|
||||||
|
nw: number
|
||||||
|
nh: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Difference<A, B> = A extends B ? never : A
|
||||||
|
|
||||||
|
export type ShapeSpecificProps<T extends Shape> = Pick<
|
||||||
|
T,
|
||||||
|
Difference<keyof T, keyof BaseShape>
|
||||||
|
>
|
||||||
|
|
||||||
|
export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>
|
||||||
|
|
||||||
|
export type ShapeUtil<K extends Shape> = {
|
||||||
|
create(props: Partial<K>): K
|
||||||
|
getBounds(shape: K): Bounds
|
||||||
|
hitTest(shape: K, test: number[]): boolean
|
||||||
|
hitTestBounds(shape: K, bounds: Bounds): boolean
|
||||||
|
rotate(shape: K): K
|
||||||
|
translate(shape: K, delta: number[]): K
|
||||||
|
scale(shape: K, scale: number): K
|
||||||
|
stretch(shape: K, scaleX: number, scaleY: number): K
|
||||||
|
render(shape: K): JSX.Element
|
||||||
|
}
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
/* Code Editor */
|
||||||
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
export type IMonaco = typeof monaco
|
export type IMonaco = typeof monaco
|
||||||
|
|
||||||
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
|
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
|
||||||
|
|
||||||
|
export enum ControlType {
|
||||||
|
Number = "number",
|
||||||
|
Vector = "vector",
|
||||||
|
Text = "text",
|
||||||
|
Select = "select",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseCodeControl {
|
||||||
|
id: string
|
||||||
|
type: ControlType
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumberCodeControl extends BaseCodeControl {
|
||||||
|
type: ControlType.Number
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
value: number
|
||||||
|
step: number
|
||||||
|
format?: (value: number) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorCodeControl extends BaseCodeControl {
|
||||||
|
type: ControlType.Vector
|
||||||
|
value: number[]
|
||||||
|
isNormalized: boolean
|
||||||
|
format?: (value: number[]) => number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextCodeControl extends BaseCodeControl {
|
||||||
|
type: ControlType.Text
|
||||||
|
value: string
|
||||||
|
format?: (value: string) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectCodeControl<T extends string = "">
|
||||||
|
extends BaseCodeControl {
|
||||||
|
type: ControlType.Select
|
||||||
|
value: T
|
||||||
|
options: T[]
|
||||||
|
format?: (string: T) => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CodeControl =
|
||||||
|
| NumberCodeControl
|
||||||
|
| VectorCodeControl
|
||||||
|
| TextCodeControl
|
||||||
|
| SelectCodeControl
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Vector from "lib/code/vector"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Data, Bounds, TransformEdge, TransformCorner } from "types"
|
import { Data, Bounds, TransformEdge, TransformCorner } from "types"
|
||||||
import * as svg from "./svg"
|
import * as svg from "./svg"
|
||||||
|
@ -801,7 +802,7 @@ export function throttle<P extends any[], T extends (...args: P) => any>(
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let inThrottle: boolean, lastFn: any, lastTime: number
|
let inThrottle: boolean, lastFn: any, lastTime: number
|
||||||
return function(...args: P) {
|
return function (...args: P) {
|
||||||
if (preventDefault) args[0].preventDefault()
|
if (preventDefault) args[0].preventDefault()
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const context = this
|
const context = this
|
||||||
|
@ -811,7 +812,7 @@ export function throttle<P extends any[], T extends (...args: P) => any>(
|
||||||
inThrottle = true
|
inThrottle = true
|
||||||
} else {
|
} else {
|
||||||
clearTimeout(lastFn)
|
clearTimeout(lastFn)
|
||||||
lastFn = setTimeout(function() {
|
lastFn = setTimeout(function () {
|
||||||
if (Date.now() - lastTime >= wait) {
|
if (Date.now() - lastTime >= wait) {
|
||||||
fn.apply(context, args)
|
fn.apply(context, args)
|
||||||
lastTime = Date.now()
|
lastTime = Date.now()
|
||||||
|
@ -950,3 +951,14 @@ export function getTransformAnchor(
|
||||||
|
|
||||||
return anchor
|
return anchor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function vectorToPoint(point: number[] | Vector | undefined) {
|
||||||
|
if (typeof point === "undefined") {
|
||||||
|
return [0, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point instanceof Vector) {
|
||||||
|
return [point.x, point.y]
|
||||||
|
}
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue