diff --git a/components/canvas/bounds-bg.tsx b/components/canvas/bounds-bg.tsx
index 93fc56721..807583caf 100644
--- a/components/canvas/bounds-bg.tsx
+++ b/components/canvas/bounds-bg.tsx
@@ -16,8 +16,8 @@ export default function BoundsBg() {
ref={rBounds}
x={minX}
y={minY}
- width={width}
- height={height}
+ width={Math.max(1, width)}
+ height={Math.max(1, height)}
onPointerDown={(e) => {
if (e.buttons !== 1) return
e.stopPropagation()
diff --git a/components/canvas/bounds.tsx b/components/canvas/bounds.tsx
index 6032ef276..57469e4f0 100644
--- a/components/canvas/bounds.tsx
+++ b/components/canvas/bounds.tsx
@@ -16,8 +16,6 @@ export default function Bounds() {
const p = 4 / zoom
const cp = p * 2
- if (width < p || height < p) return null
-
return (
- {width * zoom > 8 && height * zoom > 8 && (
- <>
-
-
-
-
-
-
-
-
- >
- )}
+
+
+
+
+
+
+
+
)
}
@@ -142,6 +136,7 @@ function Corner({
rCorner.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS_CORNER", inputs.pointerDown(e, corner))
}}
+ onPointerCancelCapture={() => console.log("oops")}
onPointerUp={(e) => {
e.stopPropagation()
rCorner.current.releasePointerCapture(e.pointerId)
diff --git a/components/canvas/shape.tsx b/components/canvas/shape.tsx
index c1322d605..cae8aea65 100644
--- a/components/canvas/shape.tsx
+++ b/components/canvas/shape.tsx
@@ -7,6 +7,7 @@ import styled from "styles"
function Shape({ id }: { id: string }) {
const rGroup = useRef(null)
+ const isHovered = useSelector((state) => state.data.hoveredId === id)
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const shape = useSelector(
@@ -32,23 +33,35 @@ function Shape({ id }: { id: string }) {
)
const handlePointerEnter = useCallback(
- () => state.send("HOVERED_SHAPE", { id }),
- [id]
+ (e: React.PointerEvent) => {
+ 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(
- () => state.send("UNHOVERED_SHAPE", { id }),
+ () => state.send("UNHOVERED_SHAPE", { target: id }),
[id]
)
+
return (
{getShapeUtils(shape).render(shape)}
@@ -86,13 +99,12 @@ const StyledGroup = styled("g", {
[`& ${Indicator}`]: {
stroke: "$selected",
},
- [`&:hover ${HoverIndicator}`]: {
- opacity: "1",
- stroke: "$hint",
- },
},
- false: {
- [`&:hover ${HoverIndicator}`]: {
+ false: {},
+ },
+ isHovered: {
+ true: {
+ [`& ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
diff --git a/components/code-panel/code-as-string.ts b/components/code-panel/code-as-string.ts
new file mode 100644
index 000000000..bbd31b4e4
--- /dev/null
+++ b/components/code-panel/code-as-string.ts
@@ -0,0 +1,9 @@
+// This is the code library.
+
+export default `
+
+// Hello world
+const name = "steve"
+const age = 93
+
+`
diff --git a/components/code-panel/code-docs.tsx b/components/code-panel/code-docs.tsx
new file mode 100644
index 000000000..9fcb9eae1
--- /dev/null
+++ b/components/code-panel/code-docs.tsx
@@ -0,0 +1,101 @@
+import styled from "styles"
+
+export default function CodeDocs({ isHidden }: { isHidden: boolean }) {
+ return (
+
+ Docs
+
+ )
+}
+
+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,
+ },
+})
diff --git a/components/code-panel/code-editor.tsx b/components/code-panel/code-editor.tsx
new file mode 100644
index 000000000..48621d085
--- /dev/null
+++ b/components/code-panel/code-editor.tsx
@@ -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
+ editorRef?: React.MutableRefObject
+ 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(null)
+ const rMonaco = useRef(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) => {
+ 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) => e.stopPropagation(),
+ []
+ )
+
+ const rDecorations = useRef([])
+
+ 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 (
+
+
+
+ )
+}
+
+const EditorContainer = styled("div", {
+ height: "100%",
+ pointerEvents: "all",
+ userSelect: "all",
+
+ ".editorLineError": {
+ backgroundColor: "$lineError",
+ },
+})
diff --git a/components/code-panel/code-panel.tsx b/components/code-panel/code-panel.tsx
new file mode 100644
index 000000000..0125eeaf4
--- /dev/null
+++ b/components/code-panel/code-panel.tsx
@@ -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(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 (
+
+ {isOpen ? (
+
+
+ state.send("CLOSED_CODE_PANEL")}>
+
+
+ Code
+
+
+ state.send("INCREASED_CODE_FONT_SIZE")}
+ >
+
+
+ state.send("DECREASED_CODE_FONT_SIZE")}
+ >
+
+
+
+ local.send("TOGGLED_DOCS")}>
+
+
+ local.send("SAVED_CODE")}
+ >
+
+
+
+
+
+ local.send("CHANGED_CODE", { code })}
+ onSave={() => local.send("SAVED_CODE")}
+ onKey={() => local.send("CLEARED_ERROR")}
+ />
+
+
+
+ {error &&
+ (error.line
+ ? `(${Number(error.line) - 2}:${error.column}) ${error.message}`
+ : error.message)}
+
+
+ ) : (
+ state.send("OPENED_CODE_PANEL")}>
+
+
+ )}
+
+ )
+}
+
+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,
+ },
+ },
+})
diff --git a/components/code-panel/example-code.ts b/components/code-panel/example-code.ts
new file mode 100644
index 000000000..e49765e90
--- /dev/null
+++ b/components/code-panel/example-code.ts
@@ -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;
+}
+`
diff --git a/components/editor.tsx b/components/editor.tsx
index 6fc587bf6..0d58d7ea3 100644
--- a/components/editor.tsx
+++ b/components/editor.tsx
@@ -1,6 +1,7 @@
import useKeyboardEvents from "hooks/useKeyboardEvents"
import Canvas from "./canvas/canvas"
import StatusBar from "./status-bar"
+import CodePanel from "./code-panel/code-panel"
export default function Editor() {
useKeyboardEvents()
@@ -9,6 +10,7 @@ export default function Editor() {
<>
+ {/* */}
>
)
}
diff --git a/hooks/useTheme.ts b/hooks/useTheme.ts
new file mode 100644
index 000000000..5dcf044ee
--- /dev/null
+++ b/hooks/useTheme.ts
@@ -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 }
+}
diff --git a/lib/code/circle.ts b/lib/code/circle.ts
new file mode 100644
index 000000000..7f010073c
--- /dev/null
+++ b/lib/code/circle.ts
@@ -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 {
+ constructor(props = {} as Partial) {
+ 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
+ }
+}
diff --git a/lib/code/index.ts b/lib/code/index.ts
new file mode 100644
index 000000000..05be00788
--- /dev/null
+++ b/lib/code/index.ts
@@ -0,0 +1,54 @@
+import { Shape } from "types"
+import * as vec from "utils/vec"
+import { getShapeUtils } from "lib/shapes"
+
+export default class CodeShape {
+ 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>([])
diff --git a/lib/code/rectangle.ts b/lib/code/rectangle.ts
new file mode 100644
index 000000000..7f85d9d7d
--- /dev/null
+++ b/lib/code/rectangle.ts
@@ -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 {
+ constructor(props = {} as Partial) {
+ 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
+ }
+}
diff --git a/lib/shapes/circle.tsx b/lib/shapes/circle.tsx
index 1956409a3..171a5d19d 100644
--- a/lib/shapes/circle.tsx
+++ b/lib/shapes/circle.tsx
@@ -4,6 +4,7 @@ import { CircleShape, ShapeType } from "types"
import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
+import { pointInCircle } from "utils/hitTests"
const circle = createShape({
boundsCache: new WeakMap([]),
@@ -16,8 +17,8 @@ const circle = createShape({
parentId: "page0",
childIndex: 0,
point: [0, 0],
- radius: 20,
rotation: 0,
+ radius: 20,
style: {},
...props,
}
@@ -51,9 +52,11 @@ const circle = createShape({
return bounds
},
- hitTest(shape, test) {
- return (
- vec.dist(vec.addScalar(shape.point, shape.radius), test) < shape.radius
+ hitTest(shape, point) {
+ return pointInCircle(
+ point,
+ vec.addScalar(shape.point, shape.radius),
+ shape.radius
)
},
diff --git a/lib/shapes/dot.tsx b/lib/shapes/dot.tsx
index 27d0e362d..a8eccfedc 100644
--- a/lib/shapes/dot.tsx
+++ b/lib/shapes/dot.tsx
@@ -50,7 +50,7 @@ const dot = createShape({
},
hitTest(shape, test) {
- return vec.dist(shape.point, test) < 4
+ return true
},
hitTestBounds(this, shape, brushBounds) {
diff --git a/lib/shapes/ellipse.tsx b/lib/shapes/ellipse.tsx
index bb00a9da6..6963616d0 100644
--- a/lib/shapes/ellipse.tsx
+++ b/lib/shapes/ellipse.tsx
@@ -57,7 +57,12 @@ const ellipse = createShape({
},
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) {
diff --git a/lib/shapes/line.tsx b/lib/shapes/line.tsx
index e3ebcea4f..12b7bbb75 100644
--- a/lib/shapes/line.tsx
+++ b/lib/shapes/line.tsx
@@ -59,7 +59,7 @@ const line = createShape({
},
hitTest(shape, test) {
- return vec.dist(shape.point, test) < 4
+ return true
},
hitTestBounds(this, shape, brushBounds) {
diff --git a/lib/shapes/polyline.tsx b/lib/shapes/polyline.tsx
index efb5cbab9..745fa323f 100644
--- a/lib/shapes/polyline.tsx
+++ b/lib/shapes/polyline.tsx
@@ -57,8 +57,19 @@ const polyline = createShape({
return bounds
},
- hitTest(shape) {
- return true
+ hitTest(shape, point) {
+ 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) {
diff --git a/package.json b/package.json
index 6a4f1a93f..51143f3c1 100644
--- a/package.json
+++ b/package.json
@@ -8,20 +8,24 @@
"start": "next start"
},
"dependencies": {
+ "@monaco-editor/react": "^4.1.3",
"@state-designer/react": "^1.7.1",
"@stitches/react": "^0.1.9",
"@types/uuid": "^8.3.0",
"framer-motion": "^4.1.16",
"next": "10.2.0",
"perfect-freehand": "^0.4.7",
+ "prettier": "^2.3.0",
"react": "17.0.2",
"react-dom": "17.0.2",
+ "react-feather": "^2.0.9",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/next": "^9.0.0",
"@types/react": "^17.0.5",
"@types/react-dom": "^17.0.3",
+ "monaco-editor": "^0.24.0",
"typescript": "^4.2.4"
}
}
diff --git a/state/data.ts b/state/data.ts
index 98356c1b2..af3b6453f 100644
--- a/state/data.ts
+++ b/state/data.ts
@@ -90,4 +90,11 @@ export const defaultDocument: Data["document"] = {
},
},
},
+ code: {
+ file0: {
+ id: "file0",
+ name: "index.ts",
+ code: "// Hello world",
+ },
+ },
}
diff --git a/state/inputs.tsx b/state/inputs.tsx
index edf77ebc9..18332a84a 100644
--- a/state/inputs.tsx
+++ b/state/inputs.tsx
@@ -23,6 +23,23 @@ class Inputs {
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) {
const { shiftKey, ctrlKey, metaKey, altKey } = e
diff --git a/state/state.ts b/state/state.ts
index 694556837..5ea881a2b 100644
--- a/state/state.ts
+++ b/state/state.ts
@@ -9,12 +9,17 @@ import * as Sessions from "./sessions"
const initialData: Data = {
isReadOnly: false,
+ settings: {
+ fontSize: 13,
+ darkMode: false,
+ },
camera: {
point: [0, 0],
zoom: 1,
},
brush: undefined,
pointedId: null,
+ hoveredId: null,
selectedIds: new Set([]),
currentPageId: "page0",
document: defaultDocument,
@@ -45,6 +50,15 @@ const state = createState({
POINTED_BOUNDS: { to: "pointingBounds" },
POINTED_BOUNDS_EDGE: { 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: [
"setPointedId",
{
@@ -135,6 +149,22 @@ const state = createState({
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
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: {
// History
@@ -199,6 +229,12 @@ const state = createState({
},
// Selection
+ setHoveredId(data, payload: PointerInfo) {
+ data.hoveredId = payload.target
+ },
+ clearHoveredId(data) {
+ data.hoveredId = undefined
+ },
setPointedId(data, payload: PointerInfo) {
data.pointedId = payload.target
},
diff --git a/styles/stitches.config.ts b/styles/stitches.config.ts
index 26f3bf10f..0f570db2a 100644
--- a/styles/stitches.config.ts
+++ b/styles/stitches.config.ts
@@ -12,6 +12,9 @@ const { styled, global, css, theme, getCssString } = createCss({
selected: "rgba(66, 133, 244, 1.000)",
bounds: "rgba(65, 132, 244, 1.000)",
boundsBg: "rgba(65, 132, 244, 0.100)",
+ border: "#aaa",
+ panel: "#fefefe",
+ text: "#000",
},
space: {},
fontSizes: {
@@ -22,7 +25,7 @@ const { styled, global, css, theme, getCssString } = createCss({
4: "18px",
},
fonts: {
- ui: `"Recursive", system-ui, sans-serif`,
+ ui: '"Recursive", system-ui, sans-serif',
},
fontWeights: {},
lineHeights: {},
diff --git a/types.ts b/types.ts
index 7cd7849c1..96207086c 100644
--- a/types.ts
+++ b/types.ts
@@ -1,7 +1,13 @@
+import * as monaco from "monaco-editor/esm/vs/editor/editor.api"
+
import React from "react"
export interface Data {
isReadOnly: boolean
+ settings: {
+ fontSize: number
+ darkMode: boolean
+ }
camera: {
point: number[]
zoom: number
@@ -10,11 +16,19 @@ export interface Data {
currentPageId: string
selectedIds: Set
pointedId?: string
+ hoveredId?: string
document: {
pages: Record
+ code: Record
}
}
+export interface CodeFile {
+ id: string
+ name: string
+ code: string
+}
+
export interface Page {
id: string
type: "page"
@@ -172,3 +186,7 @@ export enum TransformCorner {
BottomRight = "bottom_right_corner",
BottomLeft = "bottom_left_corner",
}
+
+export type IMonaco = typeof monaco
+
+export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor
diff --git a/yarn.lock b/yarn.lock
index 686bd8ba7..245de2e7c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1155,6 +1155,22 @@
"@types/yargs" "^15.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":
version "10.2.0"
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:
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:
version "1.1.6"
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"
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:
version "25.5.0"
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"
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:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -6591,6 +6624,11 @@ stacktrace-parser@0.1.10:
dependencies:
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:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"