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 CodeDocs from "./code-docs"
|
||||
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 {
|
||||
X,
|
||||
Code,
|
||||
|
@ -51,8 +53,8 @@ export default function CodePanel() {
|
|||
states: {
|
||||
editingCode: {
|
||||
on: {
|
||||
RAN_CODE: "runCode",
|
||||
SAVED_CODE: ["runCode", "saveCode"],
|
||||
RAN_CODE: ["saveCode", "runCode"],
|
||||
SAVED_CODE: ["saveCode", "runCode"],
|
||||
CHANGED_CODE: { secretlyDo: "setCode" },
|
||||
CLEARED_ERROR: { if: "hasError", do: "clearError" },
|
||||
TOGGLED_DOCS: { to: "viewingDocs" },
|
||||
|
@ -80,8 +82,8 @@ export default function CodePanel() {
|
|||
let error = null
|
||||
|
||||
try {
|
||||
const shapes = getShapesFromCode(data.code)
|
||||
state.send("GENERATED_SHAPES_FROM_CODE", { shapes })
|
||||
const { shapes, controls } = generateFromCode(data.code)
|
||||
state.send("GENERATED_FROM_CODE", { shapes, controls })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||
|
@ -113,15 +115,10 @@ export default function CodePanel() {
|
|||
const { error } = local.data
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
data-bp-desktop
|
||||
ref={rContainer}
|
||||
dragMomentum={false}
|
||||
isCollapsed={!isOpen}
|
||||
>
|
||||
<Panel.Root data-bp-desktop ref={rContainer} isCollapsed={!isOpen}>
|
||||
{isOpen ? (
|
||||
<Content>
|
||||
<Header>
|
||||
<Panel.Layout>
|
||||
<Panel.Header>
|
||||
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
||||
<X />
|
||||
</IconButton>
|
||||
|
@ -151,8 +148,8 @@ export default function CodePanel() {
|
|||
<PlayCircle />
|
||||
</IconButton>
|
||||
</ButtonsGroup>
|
||||
</Header>
|
||||
<EditorContainer>
|
||||
</Panel.Header>
|
||||
<Panel.Content>
|
||||
<CodeEditor
|
||||
fontSize={fontSize}
|
||||
readOnly={isReadOnly}
|
||||
|
@ -163,149 +160,42 @@ export default function CodePanel() {
|
|||
onKey={() => local.send("CLEARED_ERROR")}
|
||||
/>
|
||||
<CodeDocs isHidden={!local.isIn("viewingDocs")} />
|
||||
</EditorContainer>
|
||||
<ErrorContainer>
|
||||
</Panel.Content>
|
||||
<Panel.Footer>
|
||||
{error &&
|
||||
(error.line
|
||||
? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
|
||||
: error.message)}
|
||||
</ErrorContainer>
|
||||
</Content>
|
||||
</Panel.Footer>
|
||||
</Panel.Layout>
|
||||
) : (
|
||||
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
||||
<Code />
|
||||
</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", {
|
||||
gridRow: "1",
|
||||
gridColumn: "3",
|
||||
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", {
|
||||
paddingRight: 4,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
"& > button": {
|
||||
height: "50%",
|
||||
width: "100%",
|
||||
|
||||
"&:nth-of-type(1)": {
|
||||
paddingTop: 4,
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
|
||||
"&:nth-of-type(2)": {
|
||||
paddingBottom: 4,
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
|
||||
"& 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 Toolbar from "./toolbar"
|
||||
import CodePanel from "./code-panel/code-panel"
|
||||
import ControlsPanel from "./controls-panel/controls-panel"
|
||||
import styled from "styles"
|
||||
|
||||
export default function Editor() {
|
||||
useKeyboardEvents()
|
||||
useLoadOnMount()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<Canvas />
|
||||
<StatusBar />
|
||||
<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
|
||||
</Button>
|
||||
<Button onClick={() => state.send("RESET_CAMERA")}>Reset Camera</Button>
|
||||
</Section>
|
||||
</ToolbarContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarContainer = styled("div", {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: 40,
|
||||
gridArea: "toolbar",
|
||||
userSelect: "none",
|
||||
borderBottom: "1px solid black",
|
||||
gridArea: "status",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "auto 1fr auto",
|
||||
alignItems: "center",
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { CircleShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Circle extends CodeShape<CircleShape> {
|
||||
constructor(props = {} as Partial<CircleShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
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() {
|
||||
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 { v4 as uuid } from "uuid"
|
||||
import { DotShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Dot extends CodeShape<DotShape> {
|
||||
constructor(props = {} as Partial<DotShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Dot,
|
||||
|
@ -21,4 +24,12 @@ export default class Dot extends CodeShape<DotShape> {
|
|||
...props,
|
||||
})
|
||||
}
|
||||
|
||||
export() {
|
||||
const shape = { ...this.shape }
|
||||
|
||||
shape.point = vectorToPoint(shape.point)
|
||||
|
||||
return shape
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { EllipseShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Ellipse extends CodeShape<EllipseShape> {
|
||||
constructor(props = {} as Partial<EllipseShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Ellipse,
|
||||
|
@ -24,7 +27,19 @@ export default class Ellipse extends CodeShape<EllipseShape> {
|
|||
})
|
||||
}
|
||||
|
||||
get radius() {
|
||||
return this.shape.radius
|
||||
export() {
|
||||
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 Vector from "./vector"
|
||||
import Utils from "./utils"
|
||||
import { NumberControl, VectorControl, codeControls, controls } from "./control"
|
||||
import { codeShapes } from "./index"
|
||||
import { CodeControl } from "types"
|
||||
|
||||
const scope = {
|
||||
const baseScope = {
|
||||
Dot,
|
||||
Circle,
|
||||
Ellipse,
|
||||
|
@ -19,6 +21,8 @@ const scope = {
|
|||
Rectangle,
|
||||
Vector,
|
||||
Utils,
|
||||
VectorControl,
|
||||
NumberControl,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,8 +30,12 @@ const scope = {
|
|||
* collected shapes as an array.
|
||||
* @param code
|
||||
*/
|
||||
export function getShapesFromCode(code: string) {
|
||||
export function generateFromCode(code: string) {
|
||||
codeControls.clear()
|
||||
codeShapes.clear()
|
||||
;(window as any).isUpdatingCode = false
|
||||
|
||||
const scope = { ...baseScope, controls }
|
||||
|
||||
new Function(...Object.keys(scope), `${code}`)(...Object.values(scope))
|
||||
|
||||
|
@ -36,5 +44,39 @@ export function getShapesFromCode(code: string) {
|
|||
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 * as vec from "utils/vec"
|
||||
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>>([])
|
||||
|
||||
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
|
||||
* 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)
|
||||
}
|
||||
|
||||
moveTo(point: number[]) {
|
||||
this.shape.point = point
|
||||
moveTo(point: Vector) {
|
||||
this.shape.point = vectorToPoint(point)
|
||||
}
|
||||
|
||||
translate(delta: number[]) {
|
||||
this.shape.point = vec.add(this._shape.point, delta)
|
||||
translate(delta: Vector) {
|
||||
this.shape.point = vec.add(this._shape.point, vectorToPoint(delta))
|
||||
}
|
||||
|
||||
rotate(rotation: number) {
|
||||
|
@ -40,8 +46,8 @@ export default class CodeShape<T extends Shape> {
|
|||
return getShapeUtils(this.shape).getBounds(this.shape)
|
||||
}
|
||||
|
||||
hitTest(point: number[]) {
|
||||
return getShapeUtils(this.shape).hitTest(this.shape, point)
|
||||
hitTest(point: Vector) {
|
||||
return getShapeUtils(this.shape).hitTest(this.shape, vectorToPoint(point))
|
||||
}
|
||||
|
||||
get shape() {
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import CodeShape from "./index"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { LineShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Line extends CodeShape<LineShape> {
|
||||
constructor(props = {} as Partial<LineShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
props.direction = vectorToPoint(props.direction)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Line,
|
||||
|
@ -22,4 +26,17 @@ export default class Line extends CodeShape<LineShape> {
|
|||
...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 { v4 as uuid } from "uuid"
|
||||
import { PolylineShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Polyline extends CodeShape<PolylineShape> {
|
||||
constructor(props = {} as Partial<PolylineShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
props.points = props.points.map(vectorToPoint)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Polyline,
|
||||
|
@ -22,4 +26,17 @@ export default class Polyline extends CodeShape<PolylineShape> {
|
|||
...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 { v4 as uuid } from "uuid"
|
||||
import { RayShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Ray extends CodeShape<RayShape> {
|
||||
constructor(props = {} as Partial<RayShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
props.direction = vectorToPoint(props.direction)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Ray,
|
||||
|
@ -22,4 +26,17 @@ export default class Ray extends CodeShape<RayShape> {
|
|||
...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 { v4 as uuid } from "uuid"
|
||||
import { RectangleShape, ShapeType } from "types"
|
||||
import { vectorToPoint } from "utils/utils"
|
||||
|
||||
export default class Rectangle extends CodeShape<RectangleShape> {
|
||||
constructor(props = {} as Partial<RectangleShape>) {
|
||||
props.point = vectorToPoint(props.point)
|
||||
props.size = vectorToPoint(props.size)
|
||||
|
||||
super({
|
||||
id: uuid(),
|
||||
type: ShapeType.Rectangle,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Command from "./command"
|
||||
import history from "../history"
|
||||
import { Data, Shape } from "types"
|
||||
import { CodeControl, Data, Shape } from "types"
|
||||
import { current } from "immer"
|
||||
|
||||
export default function setGeneratedShapes(
|
||||
|
@ -8,12 +8,24 @@ export default function setGeneratedShapes(
|
|||
currentPageId: string,
|
||||
generatedShapes: Shape[]
|
||||
) {
|
||||
const cData = current(data)
|
||||
|
||||
const prevGeneratedShapes = Object.values(
|
||||
current(data).document.pages[currentPageId].shapes
|
||||
cData.document.pages[currentPageId].shapes
|
||||
).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) {
|
||||
data.document.pages[currentPageId].shapes[shape.id] = shape
|
||||
currentShapes[shape.id] = shape
|
||||
}
|
||||
|
||||
history.execute(
|
|
@ -1,13 +1,13 @@
|
|||
import translate from "./translate"
|
||||
import transform from "./transform"
|
||||
import generateShapes from "./generate-shapes"
|
||||
import generate from "./generate"
|
||||
import createShape from "./create-shape"
|
||||
import direction from "./direction"
|
||||
|
||||
const commands = {
|
||||
translate,
|
||||
transform,
|
||||
generateShapes,
|
||||
generate,
|
||||
createShape,
|
||||
direction,
|
||||
}
|
||||
|
|
|
@ -121,32 +121,32 @@ export const defaultDocument: Data["document"] = {
|
|||
name: "index.ts",
|
||||
code: `
|
||||
new Dot({
|
||||
point: [0, 0],
|
||||
point: new Vector(0, 0),
|
||||
})
|
||||
|
||||
new Circle({
|
||||
point: [200, 0],
|
||||
point: new Vector(200, 0),
|
||||
radius: 50,
|
||||
})
|
||||
|
||||
new Ellipse({
|
||||
point: [400, 0],
|
||||
point: new Vector(400, 0),
|
||||
radiusX: 50,
|
||||
radiusY: 75
|
||||
})
|
||||
|
||||
new Rectangle({
|
||||
point: [0, 300],
|
||||
point: new Vector(0, 300),
|
||||
})
|
||||
|
||||
new Line({
|
||||
point: [200, 300],
|
||||
direction: [1,0.2]
|
||||
point: new Vector(200, 300),
|
||||
direction: new Vector(1,0.2)
|
||||
})
|
||||
|
||||
new Polyline({
|
||||
point: [400, 300],
|
||||
points: [[0, 200], [0,0], [200, 200], [200, 0]],
|
||||
point: new Vector(400, 300),
|
||||
points: [new Vector(0, 200), new Vector(0,0), new Vector(200, 200), new Vector(200, 0)],
|
||||
})
|
||||
`,
|
||||
},
|
||||
|
|
|
@ -9,12 +9,15 @@ import {
|
|||
Shapes,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
CodeControl,
|
||||
} from "types"
|
||||
import { defaultDocument } from "./data"
|
||||
import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
|
||||
import history from "state/history"
|
||||
import * as Sessions from "./sessions"
|
||||
import commands from "./commands"
|
||||
import { controls } from "lib/code/control"
|
||||
import { generateFromCode, updateFromCode } from "lib/code/generate"
|
||||
|
||||
const initialData: Data = {
|
||||
isReadOnly: false,
|
||||
|
@ -32,6 +35,7 @@ const initialData: Data = {
|
|||
selectedIds: new Set([]),
|
||||
currentPageId: "page0",
|
||||
currentCodeFileId: "file0",
|
||||
codeControls: {},
|
||||
document: defaultDocument,
|
||||
}
|
||||
|
||||
|
@ -52,6 +56,7 @@ const state = createState({
|
|||
SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "line" },
|
||||
SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "polyline" },
|
||||
SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "rectangle" },
|
||||
RESET_CAMERA: "resetCamera",
|
||||
},
|
||||
initial: "loading",
|
||||
states: {
|
||||
|
@ -70,9 +75,10 @@ const state = createState({
|
|||
CANCELLED: { do: "clearSelectedIds" },
|
||||
DELETED: { do: "deleteSelectedIds" },
|
||||
SAVED_CODE: "saveCode",
|
||||
GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
|
||||
GENERATED_FROM_CODE: ["setCodeControls", "setGeneratedShapes"],
|
||||
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
|
||||
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize",
|
||||
CHANGED_CODE_CONTROL: "updateControls",
|
||||
},
|
||||
initial: "notPointing",
|
||||
states: {
|
||||
|
@ -437,6 +443,12 @@ const state = createState({
|
|||
data.selectedIds.add(data.pointedId)
|
||||
},
|
||||
// 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[] }) {
|
||||
const { camera } = data
|
||||
const p0 = screenToWorld(payload.point, data)
|
||||
|
@ -493,8 +505,16 @@ const state = createState({
|
|||
},
|
||||
|
||||
// Code
|
||||
setGeneratedShapes(data, payload: { shapes: Shape[] }) {
|
||||
commands.generateShapes(data, data.currentPageId, payload.shapes)
|
||||
setGeneratedShapes(
|
||||
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) {
|
||||
data.settings.fontSize++
|
||||
|
@ -502,6 +522,28 @@ const state = createState({
|
|||
decreaseCodeFontSize(data) {
|
||||
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
|
||||
saveCode(data, payload: { code: string }) {
|
||||
|
@ -524,9 +566,9 @@ const state = createState({
|
|||
document: { pages },
|
||||
} = data
|
||||
|
||||
const shapes = Array.from(selectedIds.values()).map(
|
||||
(id) => pages[currentPageId].shapes[id]
|
||||
)
|
||||
const shapes = Array.from(selectedIds.values())
|
||||
.map((id) => pages[currentPageId].shapes[id])
|
||||
.filter(Boolean)
|
||||
|
||||
if (selectedIds.size === 0) return null
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
border: "#aaa",
|
||||
panel: "#fefefe",
|
||||
text: "#000",
|
||||
input: "#f3f3f3",
|
||||
inputBorder: "#ddd",
|
||||
},
|
||||
space: {},
|
||||
fontSizes: {
|
||||
|
@ -47,7 +49,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
min !== undefined && max !== undefined
|
||||
? `clamp(${min}, ${val} / var(--camera-zoom), ${max})`
|
||||
: min !== undefined
|
||||
? `max(${min}, ${val} / var(--camera-zoom))`
|
||||
? `min(${min}, ${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"
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Client State */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface Data {
|
||||
isReadOnly: boolean
|
||||
settings: {
|
||||
|
@ -18,17 +22,16 @@ export interface Data {
|
|||
hoveredId?: string
|
||||
currentPageId: string
|
||||
currentCodeFileId: string
|
||||
codeControls: Record<string, CodeControl>
|
||||
document: {
|
||||
pages: Record<string, Page>
|
||||
code: Record<string, CodeFile>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeFile {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
/* -------------------------------------------------- */
|
||||
/* Document */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface Page {
|
||||
id: string
|
||||
|
@ -46,12 +49,14 @@ export enum ShapeType {
|
|||
Ray = "ray",
|
||||
Polyline = "polyline",
|
||||
Rectangle = "rectangle",
|
||||
// Glob = "glob",
|
||||
// Spline = "spline",
|
||||
// Cubic = "cubic",
|
||||
// Conic = "conic",
|
||||
}
|
||||
|
||||
// Consider:
|
||||
// Glob = "glob",
|
||||
// Spline = "spline",
|
||||
// Cubic = "cubic",
|
||||
// Conic = "conic",
|
||||
|
||||
export interface BaseShape {
|
||||
id: string
|
||||
type: ShapeType
|
||||
|
@ -108,31 +113,6 @@ export type Shape =
|
|||
| PolylineShape
|
||||
| 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> {
|
||||
[ShapeType.Dot]: DotShape
|
||||
[ShapeType.Circle]: CircleShape
|
||||
|
@ -143,27 +123,16 @@ export interface Shapes extends Record<ShapeType, Shape> {
|
|||
[ShapeType.Rectangle]: RectangleShape
|
||||
}
|
||||
|
||||
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
|
||||
export interface CodeFile {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Editor UI */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface PointerInfo {
|
||||
target: string
|
||||
pointerId: number
|
||||
|
@ -189,6 +158,104 @@ export enum TransformCorner {
|
|||
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 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 { Data, Bounds, TransformEdge, TransformCorner } from "types"
|
||||
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
|
||||
let inThrottle: boolean, lastFn: any, lastTime: number
|
||||
return function(...args: P) {
|
||||
return function (...args: P) {
|
||||
if (preventDefault) args[0].preventDefault()
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const context = this
|
||||
|
@ -811,7 +812,7 @@ export function throttle<P extends any[], T extends (...args: P) => any>(
|
|||
inThrottle = true
|
||||
} else {
|
||||
clearTimeout(lastFn)
|
||||
lastFn = setTimeout(function() {
|
||||
lastFn = setTimeout(function () {
|
||||
if (Date.now() - lastTime >= wait) {
|
||||
fn.apply(context, args)
|
||||
lastTime = Date.now()
|
||||
|
@ -950,3 +951,14 @@ export function getTransformAnchor(
|
|||
|
||||
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…
Reference in a new issue