adds hit testing for hovers
This commit is contained in:
parent
b8d3b35b07
commit
afa8f53dff
25 changed files with 1034 additions and 83 deletions
|
@ -16,8 +16,8 @@ export default function BoundsBg() {
|
||||||
ref={rBounds}
|
ref={rBounds}
|
||||||
x={minX}
|
x={minX}
|
||||||
y={minY}
|
y={minY}
|
||||||
width={width}
|
width={Math.max(1, width)}
|
||||||
height={height}
|
height={Math.max(1, height)}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (e.buttons !== 1) return
|
if (e.buttons !== 1) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
|
@ -16,8 +16,6 @@ export default function Bounds() {
|
||||||
const p = 4 / zoom
|
const p = 4 / zoom
|
||||||
const cp = p * 2
|
const cp = p * 2
|
||||||
|
|
||||||
if (width < p || height < p) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g pointerEvents={isBrushing ? "none" : "all"}>
|
<g pointerEvents={isBrushing ? "none" : "all"}>
|
||||||
<StyledBounds
|
<StyledBounds
|
||||||
|
@ -27,66 +25,62 @@ export default function Bounds() {
|
||||||
height={height}
|
height={height}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
{width * zoom > 8 && height * zoom > 8 && (
|
<EdgeHorizontal
|
||||||
<>
|
x={minX + p}
|
||||||
<EdgeHorizontal
|
y={minY}
|
||||||
x={minX + p}
|
width={Math.max(0, width - p * 2)}
|
||||||
y={minY}
|
height={p}
|
||||||
width={Math.max(0, width - p * 2)}
|
edge={TransformEdge.Top}
|
||||||
height={p}
|
/>
|
||||||
edge={TransformEdge.Top}
|
<EdgeVertical
|
||||||
/>
|
x={maxX}
|
||||||
<EdgeVertical
|
y={minY + p}
|
||||||
x={maxX}
|
width={p}
|
||||||
y={minY + p}
|
height={Math.max(0, height - p * 2)}
|
||||||
width={p}
|
edge={TransformEdge.Right}
|
||||||
height={Math.max(0, height - p * 2)}
|
/>
|
||||||
edge={TransformEdge.Right}
|
<EdgeHorizontal
|
||||||
/>
|
x={minX + p}
|
||||||
<EdgeHorizontal
|
y={maxY}
|
||||||
x={minX + p}
|
width={Math.max(0, width - p * 2)}
|
||||||
y={maxY}
|
height={p}
|
||||||
width={Math.max(0, width - p * 2)}
|
edge={TransformEdge.Bottom}
|
||||||
height={p}
|
/>
|
||||||
edge={TransformEdge.Bottom}
|
<EdgeVertical
|
||||||
/>
|
x={minX}
|
||||||
<EdgeVertical
|
y={minY + p}
|
||||||
x={minX}
|
width={p}
|
||||||
y={minY + p}
|
height={Math.max(0, height - p * 2)}
|
||||||
width={p}
|
edge={TransformEdge.Left}
|
||||||
height={Math.max(0, height - p * 2)}
|
/>
|
||||||
edge={TransformEdge.Left}
|
<Corner
|
||||||
/>
|
x={minX}
|
||||||
<Corner
|
y={minY}
|
||||||
x={minX}
|
width={cp}
|
||||||
y={minY}
|
height={cp}
|
||||||
width={cp}
|
corner={TransformCorner.TopLeft}
|
||||||
height={cp}
|
/>
|
||||||
corner={TransformCorner.TopLeft}
|
<Corner
|
||||||
/>
|
x={maxX}
|
||||||
<Corner
|
y={minY}
|
||||||
x={maxX}
|
width={cp}
|
||||||
y={minY}
|
height={cp}
|
||||||
width={cp}
|
corner={TransformCorner.TopRight}
|
||||||
height={cp}
|
/>
|
||||||
corner={TransformCorner.TopRight}
|
<Corner
|
||||||
/>
|
x={maxX}
|
||||||
<Corner
|
y={maxY}
|
||||||
x={maxX}
|
width={cp}
|
||||||
y={maxY}
|
height={cp}
|
||||||
width={cp}
|
corner={TransformCorner.BottomRight}
|
||||||
height={cp}
|
/>
|
||||||
corner={TransformCorner.BottomRight}
|
<Corner
|
||||||
/>
|
x={minX}
|
||||||
<Corner
|
y={maxY}
|
||||||
x={minX}
|
width={cp}
|
||||||
y={maxY}
|
height={cp}
|
||||||
width={cp}
|
corner={TransformCorner.BottomLeft}
|
||||||
height={cp}
|
/>
|
||||||
corner={TransformCorner.BottomLeft}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -142,6 +136,7 @@ function Corner({
|
||||||
rCorner.current.setPointerCapture(e.pointerId)
|
rCorner.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
|
state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
|
||||||
}}
|
}}
|
||||||
|
onPointerCancelCapture={() => console.log("oops")}
|
||||||
onPointerUp={(e) => {
|
onPointerUp={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rCorner.current.releasePointerCapture(e.pointerId)
|
rCorner.current.releasePointerCapture(e.pointerId)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import styled from "styles"
|
||||||
function Shape({ id }: { id: string }) {
|
function Shape({ id }: { id: string }) {
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
|
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||||
|
|
||||||
const shape = useSelector(
|
const shape = useSelector(
|
||||||
|
@ -32,23 +33,35 @@ function Shape({ id }: { id: string }) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
() => state.send("HOVERED_SHAPE", { id }),
|
(e: React.PointerEvent) => {
|
||||||
[id]
|
state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
|
||||||
|
},
|
||||||
|
[id, shape]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
|
||||||
|
},
|
||||||
|
[id, shape]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
() => state.send("UNHOVERED_SHAPE", { id }),
|
() => state.send("UNHOVERED_SHAPE", { target: id }),
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGroup
|
<StyledGroup
|
||||||
ref={rGroup}
|
ref={rGroup}
|
||||||
|
isHovered={isHovered}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
transform={`translate(${shape.point})`}
|
transform={`translate(${shape.point})`}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
onPointerEnter={handlePointerEnter}
|
onPointerEnter={handlePointerEnter}
|
||||||
onPointerLeave={handlePointerLeave}
|
onPointerLeave={handlePointerLeave}
|
||||||
|
onPointerMove={handlePointerMove}
|
||||||
>
|
>
|
||||||
<defs>{getShapeUtils(shape).render(shape)}</defs>
|
<defs>{getShapeUtils(shape).render(shape)}</defs>
|
||||||
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
||||||
|
@ -86,13 +99,12 @@ const StyledGroup = styled("g", {
|
||||||
[`& ${Indicator}`]: {
|
[`& ${Indicator}`]: {
|
||||||
stroke: "$selected",
|
stroke: "$selected",
|
||||||
},
|
},
|
||||||
[`&:hover ${HoverIndicator}`]: {
|
|
||||||
opacity: "1",
|
|
||||||
stroke: "$hint",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
false: {
|
false: {},
|
||||||
[`&:hover ${HoverIndicator}`]: {
|
},
|
||||||
|
isHovered: {
|
||||||
|
true: {
|
||||||
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: "1",
|
opacity: "1",
|
||||||
stroke: "$hint",
|
stroke: "$hint",
|
||||||
},
|
},
|
||||||
|
|
9
components/code-panel/code-as-string.ts
Normal file
9
components/code-panel/code-as-string.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// This is the code library.
|
||||||
|
|
||||||
|
export default `
|
||||||
|
|
||||||
|
// Hello world
|
||||||
|
const name = "steve"
|
||||||
|
const age = 93
|
||||||
|
|
||||||
|
`
|
101
components/code-panel/code-docs.tsx
Normal file
101
components/code-panel/code-docs.tsx
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import styled from "styles"
|
||||||
|
|
||||||
|
export default function CodeDocs({ isHidden }: { isHidden: boolean }) {
|
||||||
|
return (
|
||||||
|
<StyledDocs isHidden={isHidden}>
|
||||||
|
<h2>Docs</h2>
|
||||||
|
</StyledDocs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledDocs = styled("div", {
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: "$panel",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
padding: 16,
|
||||||
|
font: "$docs",
|
||||||
|
overflowY: "scroll",
|
||||||
|
userSelect: "none",
|
||||||
|
paddingBottom: 64,
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isHidden: {
|
||||||
|
true: {
|
||||||
|
visibility: "hidden",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
visibility: "visible",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"& ol": {},
|
||||||
|
|
||||||
|
"& li": {
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
"& code": {
|
||||||
|
font: "$mono",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& hr": {
|
||||||
|
margin: "32px 0",
|
||||||
|
borderColor: "$muted",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h2": {
|
||||||
|
margin: "24px 0px",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h3": {
|
||||||
|
fontSize: 20,
|
||||||
|
margin: "48px 0px 32px 0px",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h3 > code": {
|
||||||
|
fontWeight: 600,
|
||||||
|
font: "$monoheading",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h4": {
|
||||||
|
margin: "32px 0px 0px 0px",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h4 > code": {
|
||||||
|
font: "$monoheading",
|
||||||
|
fontSize: 16,
|
||||||
|
userSelect: "all",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& h4 > code > i": {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "$muted",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& pre": {
|
||||||
|
backgroundColor: "$bounds_bg",
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 4,
|
||||||
|
userSelect: "all",
|
||||||
|
margin: "24px 0",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& p > code, blockquote > code": {
|
||||||
|
backgroundColor: "$bounds_bg",
|
||||||
|
padding: "2px 4px",
|
||||||
|
borderRadius: 2,
|
||||||
|
color: "$code",
|
||||||
|
},
|
||||||
|
|
||||||
|
"& blockquote": {
|
||||||
|
backgroundColor: "rgba(144, 144, 144, .05)",
|
||||||
|
padding: 12,
|
||||||
|
margin: "20px 0",
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
})
|
214
components/code-panel/code-editor.tsx
Normal file
214
components/code-panel/code-editor.tsx
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
import Editor, { Monaco } from "@monaco-editor/react"
|
||||||
|
import useTheme from "hooks/useTheme"
|
||||||
|
import prettier from "prettier/standalone"
|
||||||
|
import parserTypeScript from "prettier/parser-typescript"
|
||||||
|
import codeAsString from "./code-as-string"
|
||||||
|
import React, { useCallback, useEffect, useRef } from "react"
|
||||||
|
import styled from "styles"
|
||||||
|
import { IMonaco, IMonacoEditor } from "types"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string
|
||||||
|
error: { line: number }
|
||||||
|
fontSize: number
|
||||||
|
monacoRef?: React.MutableRefObject<IMonaco>
|
||||||
|
editorRef?: React.MutableRefObject<IMonacoEditor>
|
||||||
|
readOnly?: boolean
|
||||||
|
onMount?: (value: string, editor: IMonacoEditor) => void
|
||||||
|
onUnmount?: (editor: IMonacoEditor) => void
|
||||||
|
onChange?: (value: string, editor: IMonacoEditor) => void
|
||||||
|
onSave?: (value: string, editor: IMonacoEditor) => void
|
||||||
|
onError?: (error: Error, line: number, col: number) => void
|
||||||
|
onKey?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeEditor({
|
||||||
|
editorRef,
|
||||||
|
monacoRef,
|
||||||
|
fontSize,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
readOnly,
|
||||||
|
onChange,
|
||||||
|
onSave,
|
||||||
|
onKey,
|
||||||
|
}: Props) {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const rEditor = useRef<IMonacoEditor>(null)
|
||||||
|
const rMonaco = useRef<IMonaco>(null)
|
||||||
|
|
||||||
|
const handleBeforeMount = useCallback((monaco: Monaco) => {
|
||||||
|
if (monacoRef) {
|
||||||
|
monacoRef.current = monaco
|
||||||
|
}
|
||||||
|
rMonaco.current = monaco
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||||
|
allowJs: true,
|
||||||
|
checkJs: false,
|
||||||
|
strict: false,
|
||||||
|
noLib: true,
|
||||||
|
lib: ["es6"],
|
||||||
|
target: monaco.languages.typescript.ScriptTarget.ES2015,
|
||||||
|
allowNonTsExtensions: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true)
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true)
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||||
|
noSemanticValidation: true,
|
||||||
|
noSyntaxValidation: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||||
|
noSemanticValidation: true,
|
||||||
|
noSyntaxValidation: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
monaco.languages.typescript.javascriptDefaults.addExtraLib(codeAsString)
|
||||||
|
|
||||||
|
monaco.languages.registerDocumentFormattingEditProvider("javascript", {
|
||||||
|
async provideDocumentFormattingEdits(model) {
|
||||||
|
const text = prettier.format(model.getValue(), {
|
||||||
|
parser: "typescript",
|
||||||
|
plugins: [parserTypeScript],
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: "es5",
|
||||||
|
semi: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
range: model.getFullModelRange(),
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleMount = useCallback((editor: IMonacoEditor) => {
|
||||||
|
if (editorRef) {
|
||||||
|
editorRef.current = editor
|
||||||
|
}
|
||||||
|
rEditor.current = editor
|
||||||
|
|
||||||
|
editor.updateOptions({
|
||||||
|
fontSize,
|
||||||
|
wordBasedSuggestions: false,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
lightbulb: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
readOnly,
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleChange = useCallback((code: string | undefined) => {
|
||||||
|
onChange(code, rEditor.current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleKeydown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
onKey && onKey()
|
||||||
|
e.stopPropagation()
|
||||||
|
const metaKey = navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey
|
||||||
|
if (e.key === "s" && metaKey) {
|
||||||
|
const editor = rEditor.current
|
||||||
|
if (!editor) return
|
||||||
|
editor
|
||||||
|
.getAction("editor.action.formatDocument")
|
||||||
|
.run()
|
||||||
|
.then(() =>
|
||||||
|
onSave(rEditor.current?.getModel().getValue(), rEditor.current)
|
||||||
|
)
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (e.key === "p" && metaKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
if (e.key === "d" && metaKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleKeyUp = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => e.stopPropagation(),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rDecorations = useRef<any>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const monaco = rMonaco.current
|
||||||
|
if (!monaco) return
|
||||||
|
const editor = rEditor.current
|
||||||
|
if (!editor) return
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
rDecorations.current = editor.deltaDecorations(rDecorations.current, [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error.line) return
|
||||||
|
|
||||||
|
rDecorations.current = editor.deltaDecorations(rDecorations.current, [
|
||||||
|
{
|
||||||
|
range: new monaco.Range(
|
||||||
|
Number(error.line) - 1,
|
||||||
|
0,
|
||||||
|
Number(error.line) - 1,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: "editorLineError",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const monaco = rMonaco.current
|
||||||
|
if (!monaco) return
|
||||||
|
monaco.editor.setTheme(theme === "dark" ? "vs-dark" : "light")
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const editor = rEditor.current
|
||||||
|
if (!editor) return
|
||||||
|
|
||||||
|
editor.updateOptions({
|
||||||
|
fontSize,
|
||||||
|
})
|
||||||
|
}, [fontSize])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorContainer onKeyDown={handleKeydown} onKeyUp={handleKeyUp}>
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
language="javascript"
|
||||||
|
value={value}
|
||||||
|
theme={theme === "dark" ? "vs-dark" : "light"}
|
||||||
|
beforeMount={handleBeforeMount}
|
||||||
|
onMount={handleMount}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</EditorContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditorContainer = styled("div", {
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "all",
|
||||||
|
userSelect: "all",
|
||||||
|
|
||||||
|
".editorLineError": {
|
||||||
|
backgroundColor: "$lineError",
|
||||||
|
},
|
||||||
|
})
|
315
components/code-panel/code-panel.tsx
Normal file
315
components/code-panel/code-panel.tsx
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
|
import React, { useEffect, useRef } from "react"
|
||||||
|
import state, { useSelector } from "state"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { CodeFile } from "types"
|
||||||
|
import { useStateDesigner } from "@state-designer/react"
|
||||||
|
import CodeDocs from "./code-docs"
|
||||||
|
import CodeEditor from "./code-editor"
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Code,
|
||||||
|
Info,
|
||||||
|
PlayCircle,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
} from "react-feather"
|
||||||
|
import styled from "styles"
|
||||||
|
|
||||||
|
// import evalCode from "lib/code"
|
||||||
|
|
||||||
|
const getErrorLineAndColumn = (e: any) => {
|
||||||
|
if ("line" in e) {
|
||||||
|
return { line: Number(e.line), column: e.column }
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = e.stack.match(/:([0-9]+):([0-9]+)/)
|
||||||
|
if (result) {
|
||||||
|
return { line: Number(result[1]) - 1, column: result[2] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodePanel() {
|
||||||
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const fileId = "file0"
|
||||||
|
const isReadOnly = useSelector((s) => s.data.isReadOnly)
|
||||||
|
const file = useSelector((s) => s.data.document.code[fileId])
|
||||||
|
const isOpen = true
|
||||||
|
const fontSize = useSelector((s) => s.data.settings.fontSize)
|
||||||
|
|
||||||
|
const local = useStateDesigner({
|
||||||
|
data: {
|
||||||
|
code: file.code,
|
||||||
|
error: null as { message: string; line: number; column: number } | null,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
MOUNTED: "setCode",
|
||||||
|
CHANGED_FILE: "loadFile",
|
||||||
|
},
|
||||||
|
initial: "editingCode",
|
||||||
|
states: {
|
||||||
|
editingCode: {
|
||||||
|
on: {
|
||||||
|
RAN_CODE: "runCode",
|
||||||
|
SAVED_CODE: ["runCode", "saveCode"],
|
||||||
|
CHANGED_CODE: [{ secretlyDo: "setCode" }],
|
||||||
|
CLEARED_ERROR: { if: "hasError", do: "clearError" },
|
||||||
|
TOGGLED_DOCS: { to: "viewingDocs" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewingDocs: {
|
||||||
|
on: {
|
||||||
|
TOGGLED_DOCS: { to: "editingCode" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
hasError(data) {
|
||||||
|
return !!data.error
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
loadFile(data, payload: { file: CodeFile }) {
|
||||||
|
data.code = payload.file.code
|
||||||
|
},
|
||||||
|
setCode(data, payload: { code: string }) {
|
||||||
|
data.code = payload.code
|
||||||
|
},
|
||||||
|
runCode(data) {
|
||||||
|
let error = null
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const { nodes, globs } = evalCode(data.code)
|
||||||
|
// state.send("GENERATED_ITEMS", { nodes, globs })
|
||||||
|
// } catch (e) {
|
||||||
|
// error = { message: e.message, ...getErrorLineAndColumn(e) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
data.error = error
|
||||||
|
},
|
||||||
|
saveCode(data) {
|
||||||
|
state.send("CHANGED_CODE", { fileId, code: data.code })
|
||||||
|
},
|
||||||
|
clearError(data) {
|
||||||
|
data.error = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
local.send("CHANGED_FILE", { file })
|
||||||
|
}, [file])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
local.send("MOUNTED", { code: state.data.document.code[fileId].code })
|
||||||
|
return () => {
|
||||||
|
state.send("CHANGED_CODE", { fileId, code: local.data.code })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { error } = local.data
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelContainer
|
||||||
|
data-bp-desktop
|
||||||
|
ref={rContainer}
|
||||||
|
dragMomentum={false}
|
||||||
|
isCollapsed={!isOpen}
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<Content>
|
||||||
|
<Header>
|
||||||
|
<IconButton onClick={() => state.send("CLOSED_CODE_PANEL")}>
|
||||||
|
<X />
|
||||||
|
</IconButton>
|
||||||
|
<h3>Code</h3>
|
||||||
|
<ButtonsGroup>
|
||||||
|
<FontSizeButtons>
|
||||||
|
<IconButton
|
||||||
|
disabled={!local.isIn("editingCode")}
|
||||||
|
onClick={() => state.send("INCREASED_CODE_FONT_SIZE")}
|
||||||
|
>
|
||||||
|
<ChevronUp />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
disabled={!local.isIn("editingCode")}
|
||||||
|
onClick={() => state.send("DECREASED_CODE_FONT_SIZE")}
|
||||||
|
>
|
||||||
|
<ChevronDown />
|
||||||
|
</IconButton>
|
||||||
|
</FontSizeButtons>
|
||||||
|
<IconButton onClick={() => local.send("TOGGLED_DOCS")}>
|
||||||
|
<Info />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
disabled={!local.isIn("editingCode")}
|
||||||
|
onClick={() => local.send("SAVED_CODE")}
|
||||||
|
>
|
||||||
|
<PlayCircle />
|
||||||
|
</IconButton>
|
||||||
|
</ButtonsGroup>
|
||||||
|
</Header>
|
||||||
|
<EditorContainer>
|
||||||
|
<CodeEditor
|
||||||
|
fontSize={fontSize}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
value={file.code}
|
||||||
|
error={error}
|
||||||
|
onChange={(code) => local.send("CHANGED_CODE", { code })}
|
||||||
|
onSave={() => local.send("SAVED_CODE")}
|
||||||
|
onKey={() => local.send("CLEARED_ERROR")}
|
||||||
|
/>
|
||||||
|
<CodeDocs isHidden={!local.isIn("viewingDocs")} />
|
||||||
|
</EditorContainer>
|
||||||
|
<ErrorContainer>
|
||||||
|
{error &&
|
||||||
|
(error.line
|
||||||
|
? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
|
||||||
|
: error.message)}
|
||||||
|
</ErrorContainer>
|
||||||
|
</Content>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={() => state.send("OPENED_CODE_PANEL")}>
|
||||||
|
<Code />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</PanelContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelContainer = styled(motion.div, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "8px",
|
||||||
|
right: "8px",
|
||||||
|
bottom: "8px",
|
||||||
|
backgroundColor: "$panel",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid $border",
|
||||||
|
pointerEvents: "all",
|
||||||
|
userSelect: "none",
|
||||||
|
zIndex: 200,
|
||||||
|
|
||||||
|
button: {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isCollapsed: {
|
||||||
|
true: {},
|
||||||
|
false: {
|
||||||
|
height: "400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
minWidth: "100%",
|
||||||
|
width: 560,
|
||||||
|
maxWidth: 560,
|
||||||
|
overflow: "hidden",
|
||||||
|
height: "100%",
|
||||||
|
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,
|
||||||
|
|
||||||
|
"& > button": {
|
||||||
|
height: "50%",
|
||||||
|
width: "100%",
|
||||||
|
|
||||||
|
"&:nth-of-type(1)": {
|
||||||
|
paddingTop: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
"&:nth-of-type(2)": {
|
||||||
|
paddingBottom: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
"& svg": {
|
||||||
|
height: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
47
components/code-panel/example-code.ts
Normal file
47
components/code-panel/example-code.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
export default `// Basic nodes and globs
|
||||||
|
|
||||||
|
const nodeA = new Node({
|
||||||
|
x: -100,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeB = new Node({
|
||||||
|
x: 100,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const glob = new Glob({
|
||||||
|
start: nodeA,
|
||||||
|
end: nodeB,
|
||||||
|
D: { x: 0, y: 60 },
|
||||||
|
Dp: { x: 0, y: 90 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Something more interesting...
|
||||||
|
|
||||||
|
const PI2 = Math.PI * 2,
|
||||||
|
center = { x: 0, y: 0 },
|
||||||
|
radius = 400;
|
||||||
|
|
||||||
|
let prev;
|
||||||
|
|
||||||
|
for (let i = 0; i < 21; i++) {
|
||||||
|
const t = i * (PI2 / 20);
|
||||||
|
|
||||||
|
const node = new Node({
|
||||||
|
x: center.x + radius * Math.sin(t),
|
||||||
|
y: center.y + radius * Math.cos(t),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prev !== undefined) {
|
||||||
|
new Glob({
|
||||||
|
start: prev,
|
||||||
|
end: node,
|
||||||
|
D: center,
|
||||||
|
Dp: center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = node;
|
||||||
|
}
|
||||||
|
`
|
|
@ -1,6 +1,7 @@
|
||||||
import useKeyboardEvents from "hooks/useKeyboardEvents"
|
import useKeyboardEvents from "hooks/useKeyboardEvents"
|
||||||
import Canvas from "./canvas/canvas"
|
import Canvas from "./canvas/canvas"
|
||||||
import StatusBar from "./status-bar"
|
import StatusBar from "./status-bar"
|
||||||
|
import CodePanel from "./code-panel/code-panel"
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
useKeyboardEvents()
|
useKeyboardEvents()
|
||||||
|
@ -9,6 +10,7 @@ export default function Editor() {
|
||||||
<>
|
<>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
{/* <CodePanel /> */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
12
hooks/useTheme.ts
Normal file
12
hooks/useTheme.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useCallback } from "react"
|
||||||
|
import state, { useSelector } from "state"
|
||||||
|
|
||||||
|
export default function useTheme() {
|
||||||
|
const theme = useSelector((state) =>
|
||||||
|
state.data.settings.darkMode ? "dark" : "light"
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => state.send("TOGGLED_THEME"), [])
|
||||||
|
|
||||||
|
return { theme, toggleTheme }
|
||||||
|
}
|
24
lib/code/circle.ts
Normal file
24
lib/code/circle.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import CodeShape from "./index"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
import { CircleShape, ShapeType } from "types"
|
||||||
|
|
||||||
|
export default class Circle extends CodeShape<CircleShape> {
|
||||||
|
constructor(props = {} as Partial<CircleShape>) {
|
||||||
|
super({
|
||||||
|
id: uuid(),
|
||||||
|
type: ShapeType.Circle,
|
||||||
|
name: "Circle",
|
||||||
|
parentId: "page0",
|
||||||
|
childIndex: 0,
|
||||||
|
point: [0, 0],
|
||||||
|
rotation: 0,
|
||||||
|
radius: 20,
|
||||||
|
style: {},
|
||||||
|
...props,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get radius() {
|
||||||
|
return this.shape.radius
|
||||||
|
}
|
||||||
|
}
|
54
lib/code/index.ts
Normal file
54
lib/code/index.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { Shape } from "types"
|
||||||
|
import * as vec from "utils/vec"
|
||||||
|
import { getShapeUtils } from "lib/shapes"
|
||||||
|
|
||||||
|
export default class CodeShape<T extends Shape> {
|
||||||
|
private _shape: T
|
||||||
|
|
||||||
|
constructor(props: T) {
|
||||||
|
this._shape = props
|
||||||
|
shapeMap.add(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
shapeMap.delete(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTo(point: number[]) {
|
||||||
|
this.shape.point = point
|
||||||
|
}
|
||||||
|
|
||||||
|
translate(delta: number[]) {
|
||||||
|
this.shape.point = vec.add(this._shape.point, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(rotation: number) {
|
||||||
|
this.shape.rotation = rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
scale(scale: number) {
|
||||||
|
return getShapeUtils(this.shape).scale(this.shape, scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds() {
|
||||||
|
return getShapeUtils(this.shape).getBounds(this.shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
hitTest(point: number[]) {
|
||||||
|
return getShapeUtils(this.shape).hitTest(this.shape, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
get shape() {
|
||||||
|
return this._shape
|
||||||
|
}
|
||||||
|
|
||||||
|
get point() {
|
||||||
|
return [...this.shape.point]
|
||||||
|
}
|
||||||
|
|
||||||
|
get rotation() {
|
||||||
|
return this.shape.rotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shapeMap = new Set<CodeShape<Shape>>([])
|
24
lib/code/rectangle.ts
Normal file
24
lib/code/rectangle.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import CodeShape from "./index"
|
||||||
|
import { v4 as uuid } from "uuid"
|
||||||
|
import { RectangleShape, ShapeType } from "types"
|
||||||
|
|
||||||
|
export default class Rectangle extends CodeShape<RectangleShape> {
|
||||||
|
constructor(props = {} as Partial<RectangleShape>) {
|
||||||
|
super({
|
||||||
|
id: uuid(),
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
name: "Rectangle",
|
||||||
|
parentId: "page0",
|
||||||
|
childIndex: 0,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [1, 1],
|
||||||
|
rotation: 0,
|
||||||
|
style: {},
|
||||||
|
...props,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.shape.size
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { CircleShape, ShapeType } from "types"
|
||||||
import { createShape } from "./index"
|
import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
|
import { pointInCircle } from "utils/hitTests"
|
||||||
|
|
||||||
const circle = createShape<CircleShape>({
|
const circle = createShape<CircleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -16,8 +17,8 @@ const circle = createShape<CircleShape>({
|
||||||
parentId: "page0",
|
parentId: "page0",
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
radius: 20,
|
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
radius: 20,
|
||||||
style: {},
|
style: {},
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
|
@ -51,9 +52,11 @@ const circle = createShape<CircleShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, point) {
|
||||||
return (
|
return pointInCircle(
|
||||||
vec.dist(vec.addScalar(shape.point, shape.radius), test) < shape.radius
|
point,
|
||||||
|
vec.addScalar(shape.point, shape.radius),
|
||||||
|
shape.radius
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ const dot = createShape<DotShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
return vec.dist(shape.point, test) < 4
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, brushBounds) {
|
hitTestBounds(this, shape, brushBounds) {
|
||||||
|
|
|
@ -57,7 +57,12 @@ const ellipse = createShape<EllipseShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, point) {
|
hitTest(shape, point) {
|
||||||
return pointInEllipse(point, shape.point, shape.radiusX, shape.radiusY)
|
return pointInEllipse(
|
||||||
|
point,
|
||||||
|
vec.add(shape.point, [shape.radiusX, shape.radiusY]),
|
||||||
|
shape.radiusX,
|
||||||
|
shape.radiusY
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, brushBounds) {
|
hitTestBounds(this, shape, brushBounds) {
|
||||||
|
|
|
@ -59,7 +59,7 @@ const line = createShape<LineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
return vec.dist(shape.point, test) < 4
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, brushBounds) {
|
hitTestBounds(this, shape, brushBounds) {
|
||||||
|
|
|
@ -57,8 +57,19 @@ const polyline = createShape<PolylineShape>({
|
||||||
return bounds
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape) {
|
hitTest(shape, point) {
|
||||||
return true
|
let pt = vec.sub(point, shape.point)
|
||||||
|
let prev = shape.points[0]
|
||||||
|
|
||||||
|
for (let i = 1; i < shape.points.length; i++) {
|
||||||
|
let curr = shape.points[i]
|
||||||
|
if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
prev = curr
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, bounds) {
|
hitTestBounds(this, shape, bounds) {
|
||||||
|
|
|
@ -8,20 +8,24 @@
|
||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.1.3",
|
||||||
"@state-designer/react": "^1.7.1",
|
"@state-designer/react": "^1.7.1",
|
||||||
"@stitches/react": "^0.1.9",
|
"@stitches/react": "^0.1.9",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"framer-motion": "^4.1.16",
|
"framer-motion": "^4.1.16",
|
||||||
"next": "10.2.0",
|
"next": "10.2.0",
|
||||||
"perfect-freehand": "^0.4.7",
|
"perfect-freehand": "^0.4.7",
|
||||||
|
"prettier": "^2.3.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-feather": "^2.0.9",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/next": "^9.0.0",
|
"@types/next": "^9.0.0",
|
||||||
"@types/react": "^17.0.5",
|
"@types/react": "^17.0.5",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.3",
|
||||||
|
"monaco-editor": "^0.24.0",
|
||||||
"typescript": "^4.2.4"
|
"typescript": "^4.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,4 +90,11 @@ export const defaultDocument: Data["document"] = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
code: {
|
||||||
|
file0: {
|
||||||
|
id: "file0",
|
||||||
|
name: "index.ts",
|
||||||
|
code: "// Hello world",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,23 @@ class Inputs {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pointerEnter(e: PointerEvent | React.PointerEvent, target: string) {
|
||||||
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
target,
|
||||||
|
pointerId: e.pointerId,
|
||||||
|
origin: [e.clientX, e.clientY],
|
||||||
|
point: [e.clientX, e.clientY],
|
||||||
|
shiftKey,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey: isDarwin() ? metaKey : ctrlKey,
|
||||||
|
altKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
pointerMove(e: PointerEvent | React.PointerEvent) {
|
pointerMove(e: PointerEvent | React.PointerEvent) {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,17 @@ import * as Sessions from "./sessions"
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
settings: {
|
||||||
|
fontSize: 13,
|
||||||
|
darkMode: false,
|
||||||
|
},
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
},
|
},
|
||||||
brush: undefined,
|
brush: undefined,
|
||||||
pointedId: null,
|
pointedId: null,
|
||||||
|
hoveredId: null,
|
||||||
selectedIds: new Set([]),
|
selectedIds: new Set([]),
|
||||||
currentPageId: "page0",
|
currentPageId: "page0",
|
||||||
document: defaultDocument,
|
document: defaultDocument,
|
||||||
|
@ -45,6 +50,15 @@ const state = createState({
|
||||||
POINTED_BOUNDS: { to: "pointingBounds" },
|
POINTED_BOUNDS: { to: "pointingBounds" },
|
||||||
POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
|
POINTED_BOUNDS_EDGE: { to: "transformingSelection" },
|
||||||
POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
|
POINTED_BOUNDS_CORNER: { to: "transformingSelection" },
|
||||||
|
MOVED_OVER_SHAPE: {
|
||||||
|
if: "pointHitsShape",
|
||||||
|
then: {
|
||||||
|
unless: "shapeIsHovered",
|
||||||
|
do: "setHoveredId",
|
||||||
|
},
|
||||||
|
else: { if: "shapeIsHovered", do: "clearHoveredId" },
|
||||||
|
},
|
||||||
|
UNHOVERED_SHAPE: "clearHoveredId",
|
||||||
POINTED_SHAPE: [
|
POINTED_SHAPE: [
|
||||||
"setPointedId",
|
"setPointedId",
|
||||||
{
|
{
|
||||||
|
@ -135,6 +149,22 @@ const state = createState({
|
||||||
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
|
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
|
||||||
return payload.shiftKey
|
return payload.shiftKey
|
||||||
},
|
},
|
||||||
|
shapeIsHovered(data, payload: { target: string }) {
|
||||||
|
return data.hoveredId === payload.target
|
||||||
|
},
|
||||||
|
pointHitsShape(data, payload: { target: string; point: number[] }) {
|
||||||
|
const shape =
|
||||||
|
data.document.pages[data.currentPageId].shapes[payload.target]
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
getShapeUtils(shape).hitTest(shape, screenToWorld(payload.point, data))
|
||||||
|
)
|
||||||
|
|
||||||
|
return getShapeUtils(shape).hitTest(
|
||||||
|
shape,
|
||||||
|
screenToWorld(payload.point, data)
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// History
|
// History
|
||||||
|
@ -199,6 +229,12 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
|
setHoveredId(data, payload: PointerInfo) {
|
||||||
|
data.hoveredId = payload.target
|
||||||
|
},
|
||||||
|
clearHoveredId(data) {
|
||||||
|
data.hoveredId = undefined
|
||||||
|
},
|
||||||
setPointedId(data, payload: PointerInfo) {
|
setPointedId(data, payload: PointerInfo) {
|
||||||
data.pointedId = payload.target
|
data.pointedId = payload.target
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,6 +12,9 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
selected: "rgba(66, 133, 244, 1.000)",
|
selected: "rgba(66, 133, 244, 1.000)",
|
||||||
bounds: "rgba(65, 132, 244, 1.000)",
|
bounds: "rgba(65, 132, 244, 1.000)",
|
||||||
boundsBg: "rgba(65, 132, 244, 0.100)",
|
boundsBg: "rgba(65, 132, 244, 0.100)",
|
||||||
|
border: "#aaa",
|
||||||
|
panel: "#fefefe",
|
||||||
|
text: "#000",
|
||||||
},
|
},
|
||||||
space: {},
|
space: {},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
|
@ -22,7 +25,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
4: "18px",
|
4: "18px",
|
||||||
},
|
},
|
||||||
fonts: {
|
fonts: {
|
||||||
ui: `"Recursive", system-ui, sans-serif`,
|
ui: '"Recursive", system-ui, sans-serif',
|
||||||
},
|
},
|
||||||
fontWeights: {},
|
fontWeights: {},
|
||||||
lineHeights: {},
|
lineHeights: {},
|
||||||
|
|
18
types.ts
18
types.ts
|
@ -1,7 +1,13 @@
|
||||||
|
import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
|
||||||
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
isReadOnly: boolean
|
isReadOnly: boolean
|
||||||
|
settings: {
|
||||||
|
fontSize: number
|
||||||
|
darkMode: boolean
|
||||||
|
}
|
||||||
camera: {
|
camera: {
|
||||||
point: number[]
|
point: number[]
|
||||||
zoom: number
|
zoom: number
|
||||||
|
@ -10,11 +16,19 @@ export interface Data {
|
||||||
currentPageId: string
|
currentPageId: string
|
||||||
selectedIds: Set<string>
|
selectedIds: Set<string>
|
||||||
pointedId?: string
|
pointedId?: string
|
||||||
|
hoveredId?: string
|
||||||
document: {
|
document: {
|
||||||
pages: Record<string, Page>
|
pages: Record<string, Page>
|
||||||
|
code: Record<string, CodeFile>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeFile {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Page {
|
export interface Page {
|
||||||
id: string
|
id: string
|
||||||
type: "page"
|
type: "page"
|
||||||
|
@ -172,3 +186,7 @@ export enum TransformCorner {
|
||||||
BottomRight = "bottom_right_corner",
|
BottomRight = "bottom_right_corner",
|
||||||
BottomLeft = "bottom_left_corner",
|
BottomLeft = "bottom_left_corner",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IMonaco = typeof monaco
|
||||||
|
|
||||||
|
export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
|
||||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -1155,6 +1155,22 @@
|
||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^3.0.0"
|
chalk "^3.0.0"
|
||||||
|
|
||||||
|
"@monaco-editor/loader@^1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.0.1.tgz#7068c9b07bbc65387c0e7a4df6dac0a326155905"
|
||||||
|
integrity sha512-hycGOhLqLYjnD0A/FHs56covEQWnDFrSnm/qLKkB/yoeayQ7ju+Vaj4SdTojGrXeY6jhMDx59map0+Jqwquh1Q==
|
||||||
|
dependencies:
|
||||||
|
state-local "^1.0.6"
|
||||||
|
|
||||||
|
"@monaco-editor/react@^4.1.3":
|
||||||
|
version "4.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.1.3.tgz#7dcaa584f2a4e8bd8f5298604f0b5368f8ebca55"
|
||||||
|
integrity sha512-kqcjVuoy6btcgALAk4RV/SlasveM+WTw5lzzlyq5FhKXjF8wu5tSe/2oCQ1uhLpcdtxcHfx3L0HrcAPWnejFnQ==
|
||||||
|
dependencies:
|
||||||
|
"@monaco-editor/loader" "^1.0.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
state-local "^1.0.7"
|
||||||
|
|
||||||
"@next/env@10.2.0":
|
"@next/env@10.2.0":
|
||||||
version "10.2.0"
|
version "10.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-10.2.0.tgz#154dbce2efa3ad067ebd20b7d0aa9aed775e7c97"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-10.2.0.tgz#154dbce2efa3ad067ebd20b7d0aa9aed775e7c97"
|
||||||
|
@ -5041,6 +5057,11 @@ mkdirp@0.x, mkdirp@^0.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
monaco-editor@^0.24.0:
|
||||||
|
version "0.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.24.0.tgz#990b55096bcc95d08d8d28e55264c6eb17707269"
|
||||||
|
integrity sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A==
|
||||||
|
|
||||||
mri@^1.1.0:
|
mri@^1.1.0:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"
|
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.6.tgz#49952e1044db21dbf90f6cd92bc9c9a777d415a6"
|
||||||
|
@ -5700,6 +5721,11 @@ prettier@^1.19.1:
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||||
|
|
||||||
|
prettier@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
|
||||||
|
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
|
||||||
|
|
||||||
pretty-format@^25.2.1, pretty-format@^25.5.0:
|
pretty-format@^25.2.1, pretty-format@^25.5.0:
|
||||||
version "25.5.0"
|
version "25.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
|
||||||
|
@ -5846,6 +5872,13 @@ react-dom@17.0.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
scheduler "^0.20.2"
|
||||||
|
|
||||||
|
react-feather@^2.0.9:
|
||||||
|
version "2.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
|
||||||
|
integrity sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw==
|
||||||
|
dependencies:
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
react-is@16.13.1, react-is@^16.12.0, react-is@^16.8.1:
|
react-is@16.13.1, react-is@^16.12.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
@ -6591,6 +6624,11 @@ stacktrace-parser@0.1.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest "^0.7.1"
|
type-fest "^0.7.1"
|
||||||
|
|
||||||
|
state-local@^1.0.6, state-local@^1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
|
||||||
|
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
|
||||||
|
|
||||||
static-extend@^0.1.1:
|
static-extend@^0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
|
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
|
||||||
|
|
Loading…
Reference in a new issue