From 0b33be33f2e9728c6623a98ba2b4568752a3a253 Mon Sep 17 00:00:00 2001 From: chelproc Date: Sat, 28 Dec 2024 19:25:04 +0900 Subject: [PATCH] add editor background grid --- src/common/theme.ts | 6 +- src/common/types.ts | 4 +- src/common/vector2.ts | 25 ++++++++ src/pages/edit/Editor/components/Grid.tsx | 59 +++++++++++++++++ src/pages/edit/Editor/index.tsx | 11 +++- src/pages/edit/Editor/renderer/Background.tsx | 63 ++++++++----------- src/pages/edit/Editor/renderer/Node.tsx | 43 +++++-------- src/pages/edit/Editor/renderer/NodePin.tsx | 25 +++----- src/pages/edit/Editor/renderer/index.tsx | 10 +-- .../Editor/store/slices/contextMenu/types.ts | 4 +- .../edit/Editor/store/slices/core/types.ts | 4 +- .../Editor/store/slices/perspective/index.tsx | 57 ++++++++--------- .../Editor/store/slices/perspective/types.ts | 15 ++--- src/store/node.ts | 4 +- 14 files changed, 196 insertions(+), 134 deletions(-) create mode 100644 src/common/vector2.ts create mode 100644 src/pages/edit/Editor/components/Grid.tsx diff --git a/src/common/theme.ts b/src/common/theme.ts index b1f1464..e8210d3 100644 --- a/src/common/theme.ts +++ b/src/common/theme.ts @@ -3,14 +3,10 @@ import { createTheme } from "@mui/material"; // See https://www.figma.com/file/M3dC0Gk98IGSGlxY901rBh/ export const blackColor = "#000000"; export const whiteColor = "#ffffff"; -export const grayColor = { - main: "#9e9e9e", - darken2: "#616161", -}; export const primaryColor = "#00d372"; export const activeColor = "#00aaff"; export const errorColor = "#ff0000"; -export const editorBackgroundColor = "#f3f3f3"; +export const editorBackgroundColor = "#fafafa"; export const editorGridColor = "#dddddd"; export const theme = createTheme({ diff --git a/src/common/types.ts b/src/common/types.ts index 5816552..6f0cfc1 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1 +1,3 @@ -export type Point = { x: number; y: number }; +import type { Vector2 } from "./vector2"; + +export type Perspective = { center: Vector2; scale: number }; diff --git a/src/common/vector2.ts b/src/common/vector2.ts new file mode 100644 index 0000000..4ae63e4 --- /dev/null +++ b/src/common/vector2.ts @@ -0,0 +1,25 @@ +export type Vector2 = { x: number; y: number }; + +export const vector2 = { + zero: { x: 0, y: 0 }, + add: (a: Vector2, b: Vector2): Vector2 => ({ + x: a.x + b.x, + y: a.y + b.y, + }), + sub: (a: Vector2, b: Vector2): Vector2 => ({ + x: a.x - b.x, + y: a.y - b.y, + }), + mul: (a: Vector2, b: number): Vector2 => ({ + x: a.x * b, + y: a.y * b, + }), + div: (a: Vector2, b: number): Vector2 => ({ + x: a.x / b, + y: a.y / b, + }), + fromDomEvent: (e: { offsetX: number; offsetY: number }): Vector2 => ({ + x: e.offsetX, + y: e.offsetY, + }), +}; diff --git a/src/pages/edit/Editor/components/Grid.tsx b/src/pages/edit/Editor/components/Grid.tsx new file mode 100644 index 0000000..2653d5a --- /dev/null +++ b/src/pages/edit/Editor/components/Grid.tsx @@ -0,0 +1,59 @@ +import type { ReactElement } from "react"; +import { useComponentEditorStore } from "../store"; +import { vector2 } from "../../../../common/vector2"; + +export default function CCComponentEditorGrid() { + const componentEditorState = useComponentEditorStore()(); + const logScale = Math.log2(componentEditorState.perspective.scale); + const canvasOriginPosition = componentEditorState.fromStageToCanvas( + vector2.zero + ); + const canvasGridSize = 100 * 2 ** (Math.ceil(logScale) - logScale); + + const elements: ReactElement[] = []; + let i = 0; + for ( + let x = (canvasOriginPosition.x % canvasGridSize) - canvasGridSize; + x <= componentEditorState.rendererSize.x; + x += canvasGridSize + ) { + elements.push( +
+ ); + i += 1; + } + for ( + let y = (canvasOriginPosition.y % canvasGridSize) - canvasGridSize; + y <= componentEditorState.rendererSize.y; + y += canvasGridSize + ) { + elements.push( +
+ ); + i += 1; + } + + return
{elements}
; +} diff --git a/src/pages/edit/Editor/index.tsx b/src/pages/edit/Editor/index.tsx index 26973c1..a614ecd 100644 --- a/src/pages/edit/Editor/index.tsx +++ b/src/pages/edit/Editor/index.tsx @@ -9,6 +9,8 @@ import CCComponentEditorViewModeSwitcher from "./components/ViewModeSwitcher"; import CCComponentEditorContextMenu from "./components/ContextMenu"; import type { CCComponentId } from "../../../store/component"; import CCComponentEditorRenderer from "./renderer"; +import CCComponentEditorGrid from "./components/Grid"; +import { editorBackgroundColor } from "../../../common/theme"; export type CCComponentEditorProps = { componentId: CCComponentId; @@ -27,7 +29,14 @@ function CCComponentEditorContent({ useState(false); return ( - + + diff --git a/src/pages/edit/Editor/renderer/Background.tsx b/src/pages/edit/Editor/renderer/Background.tsx index aee2ce4..a63595b 100644 --- a/src/pages/edit/Editor/renderer/Background.tsx +++ b/src/pages/edit/Editor/renderer/Background.tsx @@ -1,6 +1,5 @@ -import * as matrix from "transformation-matrix"; import { useComponentEditorStore } from "../store"; -import { whiteColor } from "../../../../common/theme"; +import { vector2 } from "../../../../common/vector2"; export default function CCComponentEditorRendererBackground() { const componentEditorState = useComponentEditorStore()(); @@ -14,28 +13,20 @@ export default function CCComponentEditorRendererBackground() { }} onPointerDown={(pointerDownEvent) => { const { currentTarget } = pointerDownEvent; - const startUserTransformation = - componentEditorState.userPerspectiveTransformation; - const startInverseViewTransformation = - componentEditorState.getInverseViewTransformation(); - const startPoint = matrix.applyToPoint(startInverseViewTransformation, { - x: pointerDownEvent.nativeEvent.offsetX, - y: pointerDownEvent.nativeEvent.offsetY, - }); + const startPerspective = componentEditorState.perspective; + const startPoint = vector2.fromDomEvent(pointerDownEvent.nativeEvent); const onPointerMove = (pointerMoveEvent: PointerEvent) => { - const endPoint = matrix.applyToPoint(startInverseViewTransformation, { - x: pointerMoveEvent.offsetX, - y: pointerMoveEvent.offsetY, - }); - componentEditorState.setUserPerspectiveTransformation( - matrix.compose( - startUserTransformation, - matrix.translate( - endPoint.x - startPoint.x, - endPoint.y - startPoint.y + const endPoint = vector2.fromDomEvent(pointerMoveEvent); + componentEditorState.setPerspective({ + ...startPerspective, + center: vector2.sub( + startPerspective.center, + vector2.mul( + vector2.sub(endPoint, startPoint), + startPerspective.scale ) - ) - ); + ), + }); }; currentTarget.addEventListener("pointermove", onPointerMove); const onPointerUp = () => { @@ -45,22 +36,22 @@ export default function CCComponentEditorRendererBackground() { currentTarget.addEventListener("pointerup", onPointerUp); }} onWheel={(wheelEvent) => { - const scale = Math.exp(-wheelEvent.deltaY / 256); - const center = matrix.applyToPoint( - componentEditorState.getInverseViewTransformation(), - { - x: wheelEvent.nativeEvent.offsetX, - y: wheelEvent.nativeEvent.offsetY, - } - ); - componentEditorState.setUserPerspectiveTransformation( - matrix.compose( - componentEditorState.userPerspectiveTransformation, - matrix.scale(scale, scale, center.x, center.y) - ) + const scaleDelta = Math.exp(wheelEvent.deltaY / 256); + const scaleCenter = componentEditorState.fromCanvasToStage( + vector2.fromDomEvent(wheelEvent.nativeEvent) ); + componentEditorState.setPerspective({ + scale: componentEditorState.perspective.scale * scaleDelta, + center: vector2.add( + scaleCenter, + vector2.mul( + vector2.sub(componentEditorState.perspective.center, scaleCenter), + scaleDelta + ) + ), + }); }} - fill={whiteColor} + fill="transparent" /> ); } diff --git a/src/pages/edit/Editor/renderer/Node.tsx b/src/pages/edit/Editor/renderer/Node.tsx index ee279e0..9819e3a 100644 --- a/src/pages/edit/Editor/renderer/Node.tsx +++ b/src/pages/edit/Editor/renderer/Node.tsx @@ -1,6 +1,5 @@ import nullthrows from "nullthrows"; import { useState } from "react"; -import * as matrix from "transformation-matrix"; import type { CCNodeId } from "../../../../store/node"; import { useNode } from "../../../../store/react/selectors"; import { useStore } from "../../../../store/react"; @@ -9,6 +8,7 @@ import CCComponentEditorRendererNodePin from "./NodePin"; import getCCComponentEditorRendererNodeGeometry from "./Node.geometry"; import ensureStoreItem from "../../../../store/react/error"; import { blackColor, primaryColor, whiteColor } from "../../../../common/theme"; +import { vector2 } from "../../../../common/vector2"; export type CCComponentEditorRendererNodeProps = { nodeId: CCNodeId; @@ -22,40 +22,31 @@ const CCComponentEditorRendererNode = ensureStoreItem( const geometry = getCCComponentEditorRendererNodeGeometry(store, nodeId); const componentEditorState = useComponentEditorStore()(); const [dragging, setDragging] = useState(false); - const [dragStartPosition, setDragStartPosition] = useState({ x: 0, y: 0 }); - const [previousNodePosition, setPreviousNodePosition] = useState({ - x: 0, - y: 0, - }); + const [dragStartPosition, setDragStartPosition] = useState(vector2.zero); + const [previousNodePosition, setPreviousNodePosition] = useState( + vector2.zero + ); const handleDragStart = (e: React.PointerEvent) => { - setDragStartPosition({ - x: e.nativeEvent.offsetX, - y: e.nativeEvent.offsetY, - }); - setPreviousNodePosition({ - x: node.position.x, - y: node.position.y, - }); + setDragStartPosition(vector2.fromDomEvent(e.nativeEvent)); + setPreviousNodePosition(node.position); setDragging(true); e.currentTarget.setPointerCapture(e.pointerId); }; const handleDragging = (e: React.PointerEvent) => { if (dragging) { - const { sx, sy } = matrix.decomposeTSR( - componentEditorState.getInverseViewTransformation() - ).scale; - const transformation = matrix.scale(sx, sy); - const diff = matrix.applyToPoint(transformation, { - x: e.nativeEvent.offsetX - dragStartPosition.x, - y: e.nativeEvent.offsetY - dragStartPosition.y, - }); store.nodes.update(nodeId, { - position: { - x: previousNodePosition.x + diff.x, - y: previousNodePosition.y + diff.y, - }, + position: vector2.add( + previousNodePosition, + vector2.mul( + vector2.sub( + vector2.fromDomEvent(e.nativeEvent), + dragStartPosition + ), + componentEditorState.perspective.scale + ) + ), }); } }; diff --git a/src/pages/edit/Editor/renderer/NodePin.tsx b/src/pages/edit/Editor/renderer/NodePin.tsx index ec7a30d..6d75a19 100644 --- a/src/pages/edit/Editor/renderer/NodePin.tsx +++ b/src/pages/edit/Editor/renderer/NodePin.tsx @@ -1,8 +1,6 @@ import { useState, type PointerEvent, type ReactNode } from "react"; -import * as matrix from "transformation-matrix"; import { KDTree } from "mnemonist"; import nullthrows from "nullthrows"; -import type { Point } from "../../../../common/types"; import type { CCNodePinId } from "../../../../store/nodePin"; import { CCComponentEditorRendererConnectionCore } from "./Connection"; import { useComponentEditorStore } from "../store"; @@ -10,29 +8,28 @@ import { useStore } from "../../../../store/react"; import getCCComponentEditorRendererNodeGeometry from "./Node.geometry"; import { CCConnectionStore } from "../../../../store/connection"; import type { SimulationValue } from "../store/slices/core"; +import { vector2, type Vector2 } from "../../../../common/vector2"; const NODE_PIN_POSITION_SENSITIVITY = 10; export type CCComponentEditorRendererNodeProps = { nodePinId: CCNodePinId; - position: Point; + position: Vector2; }; export default function CCComponentEditorRendererNodePin({ nodePinId, position, }: CCComponentEditorRendererNodeProps) { const { store } = useStore(); + const componentEditorState = useComponentEditorStore()(); const nodePin = nullthrows(store.nodePins.get(nodePinId)); const node = nullthrows(store.nodes.get(nodePin.nodeId)); const componentPin = nullthrows( store.componentPins.get(nodePin.componentPinId) ); - const inverseViewTransformation = useComponentEditorStore()((s) => - s.getInverseViewTransformation() - ); const [draggingState, setDraggingState] = useState<{ - cursorPosition: Point; + cursorPosition: Vector2; nodePinPositionKDTree: KDTree; } | null>(null); const onDrag = (e: PointerEvent) => { @@ -62,10 +59,9 @@ export default function CCComponentEditorRendererNodePin({ ); } setDraggingState({ - cursorPosition: matrix.applyToPoint(inverseViewTransformation, { - x: e.nativeEvent.offsetX, - y: e.nativeEvent.offsetY, - }), + cursorPosition: componentEditorState.fromCanvasToStage( + vector2.fromDomEvent(e.nativeEvent) + ), nodePinPositionKDTree, }); }; @@ -110,7 +106,6 @@ export default function CCComponentEditorRendererNodePin({ const hasNoConnection = store.connections.getConnectionsByNodePinId(nodePinId).length === 0; - const componentEditorStore = useComponentEditorStore()(); const pinType = componentPin.type; const simulationValueToString = (simulationValue: SimulationValue) => { return simulationValue.reduce( @@ -123,18 +118,18 @@ export default function CCComponentEditorRendererNodePin({ let nodePinValueInit = null; if (isSimulationMode && hasNoConnection) { if (pinType === "input") { - nodePinValueInit = componentEditorStore.getInputValue( + nodePinValueInit = componentEditorState.getInputValue( implementedComponentPin!.id )!; } else { - nodePinValueInit = componentEditorStore.getNodePinValue(nodePinId)!; + nodePinValueInit = componentEditorState.getNodePinValue(nodePinId)!; } } const nodePinValue = nodePinValueInit; const updateInputValue = () => { const updatedPinValue = [...nodePinValue!]; updatedPinValue[0] = !updatedPinValue[0]; - componentEditorStore.setInputValue( + componentEditorState.setInputValue( implementedComponentPin!.id, updatedPinValue ); diff --git a/src/pages/edit/Editor/renderer/index.tsx b/src/pages/edit/Editor/renderer/index.tsx index 0224a9f..7004a9f 100644 --- a/src/pages/edit/Editor/renderer/index.tsx +++ b/src/pages/edit/Editor/renderer/index.tsx @@ -1,4 +1,3 @@ -import * as matrix from "transformation-matrix"; import { parseDataTransferAsComponent } from "../../../../common/serialization"; import { useConnectionIds, @@ -10,6 +9,7 @@ import CCComponentEditorRendererConnection from "./Connection"; import CCComponentEditorRendererNode from "./Node"; import { useStore } from "../../../../store/react"; import { CCNodeStore } from "../../../../store/node"; +import { vector2 } from "../../../../common/vector2"; export default function CCComponentEditorRenderer() { const componentEditorState = useComponentEditorStore()(); @@ -40,12 +40,8 @@ export default function CCComponentEditorRenderer() { CCNodeStore.create({ componentId: droppedComponentId, parentComponentId: componentEditorState.componentId, - position: matrix.applyToPoint( - componentEditorState.getInverseViewTransformation(), - { - x: e.nativeEvent.offsetX, - y: e.nativeEvent.offsetY, - } + position: componentEditorState.fromCanvasToStage( + vector2.fromDomEvent(e.nativeEvent) ), }) ); diff --git a/src/pages/edit/Editor/store/slices/contextMenu/types.ts b/src/pages/edit/Editor/store/slices/contextMenu/types.ts index 537c035..35978d8 100644 --- a/src/pages/edit/Editor/store/slices/contextMenu/types.ts +++ b/src/pages/edit/Editor/store/slices/contextMenu/types.ts @@ -1,8 +1,8 @@ import type { MouseEvent } from "react"; -import type { Point } from "../../../../../../common/types"; +import type { Vector2 } from "../../../../../../common/vector2"; export type ContextMenuState = { - position: Point; + position: Vector2; }; export type ContextMenuStoreSlice = { diff --git a/src/pages/edit/Editor/store/slices/core/types.ts b/src/pages/edit/Editor/store/slices/core/types.ts index e9c1238..710ba7a 100644 --- a/src/pages/edit/Editor/store/slices/core/types.ts +++ b/src/pages/edit/Editor/store/slices/core/types.ts @@ -3,13 +3,13 @@ import type { CCNodeId } from "../../../../../../store/node"; import type { CCConnectionId } from "../../../../../../store/connection"; import type { SimulationValue } from "."; import type { CCNodePinId } from "../../../../../../store/nodePin"; -import type { Point } from "../../../../../../common/types"; +import type { Vector2 } from "../../../../../../common/vector2"; export type EditorMode = EditorModeEdit | EditorModePlay; export type EditorModeEdit = "edit"; export type EditorModePlay = "play"; -export type RangeSelect = { start: Point; end: Point } | null; +export type RangeSelect = { start: Vector2; end: Vector2 } | null; export type InputValueKey = CCComponentPinId; diff --git a/src/pages/edit/Editor/store/slices/perspective/index.tsx b/src/pages/edit/Editor/store/slices/perspective/index.tsx index ffb5a98..a934104 100644 --- a/src/pages/edit/Editor/store/slices/perspective/index.tsx +++ b/src/pages/edit/Editor/store/slices/perspective/index.tsx @@ -1,6 +1,7 @@ import * as matrix from "transformation-matrix"; import { type ComponentEditorSliceCreator } from "../../types"; import type { PerspectiveStoreSlice } from "./types"; +import { vector2 } from "../../../../../../common/vector2"; const createComponentEditorStorePerspectiveSlice: ComponentEditorSliceCreator< PerspectiveStoreSlice @@ -16,39 +17,30 @@ const createComponentEditorStorePerspectiveSlice: ComponentEditorSliceCreator< }; return { define: (set, get) => ({ - rendererSize: { width: 0, height: 0 }, + perspective: { center: vector2.zero, scale: 1 }, + rendererSize: vector2.zero, userPerspectiveTransformation: matrix.identity(), + setPerspective: (perspective) => set((s) => ({ ...s, perspective })), registerRendererElement, - setUserPerspectiveTransformation: (transformation) => { - set((state) => ({ - ...state, - userPerspectiveTransformation: transformation, - })); - }, - getViewTransformation: () => { - return matrix.compose( - matrix.translate( - get().rendererSize.width / 2, - get().rendererSize.height / 2 + fromCanvasToStage: (point) => + vector2.add( + vector2.mul( + vector2.sub(point, vector2.div(get().rendererSize, 2)), + get().perspective.scale ), - get().userPerspectiveTransformation - ); - }, - getInverseViewTransformation: () => - matrix.inverse(get().getViewTransformation()), + get().perspective.center + ), + fromStageToCanvas: (point) => + vector2.add( + vector2.div( + vector2.sub(point, get().perspective.center), + get().perspective.scale + ), + vector2.div(get().rendererSize, 2) + ), getViewBox: () => { - const inverseViewTransformation = get().getInverseViewTransformation(); - const viewBoxTopLeft = matrix.applyToPoint(inverseViewTransformation, { - x: 0, - y: 0, - }); - const viewBoxBottomRight = matrix.applyToPoint( - inverseViewTransformation, - { - x: get().rendererSize.width, - y: get().rendererSize.height, - } - ); + const viewBoxTopLeft = get().fromCanvasToStage(vector2.zero); + const viewBoxBottomRight = get().fromCanvasToStage(get().rendererSize); return { x: viewBoxTopLeft.x, y: viewBoxTopLeft.y, @@ -60,7 +52,12 @@ const createComponentEditorStorePerspectiveSlice: ComponentEditorSliceCreator< postCreate(editorStore) { resizeObserver = new ResizeObserver((entries) => { if (!entries[0]) return; - editorStore.setState({ rendererSize: entries[0].contentRect }); + editorStore.setState({ + rendererSize: { + x: entries[0].contentRect.width, + y: entries[0].contentRect.height, + }, + }); }); }, }; diff --git a/src/pages/edit/Editor/store/slices/perspective/types.ts b/src/pages/edit/Editor/store/slices/perspective/types.ts index e29b4de..b4203fc 100644 --- a/src/pages/edit/Editor/store/slices/perspective/types.ts +++ b/src/pages/edit/Editor/store/slices/perspective/types.ts @@ -1,11 +1,12 @@ -import type * as matrix from "transformation-matrix"; +import type { Perspective } from "../../../../../../common/types"; +import type { Vector2 } from "../../../../../../common/vector2"; export type PerspectiveStoreSlice = { - rendererSize: { width: number; height: number }; - userPerspectiveTransformation: matrix.Matrix; + perspective: Perspective; + rendererSize: Vector2; + setPerspective: (perspective: Perspective) => void; registerRendererElement: (element: SVGSVGElement | null) => void; - setUserPerspectiveTransformation: (transformation: matrix.Matrix) => void; - getViewTransformation(): matrix.Matrix; - getInverseViewTransformation(): matrix.Matrix; - getViewBox(): { x: number; y: number; width: number; height: number }; + fromCanvasToStage: (point: Vector2) => Vector2; + fromStageToCanvas: (point: Vector2) => Vector2; + getViewBox: () => { x: number; y: number; width: number; height: number }; }; diff --git a/src/store/node.ts b/src/store/node.ts index 05fe91f..81a52ab 100644 --- a/src/store/node.ts +++ b/src/store/node.ts @@ -4,7 +4,7 @@ import invariant from "tiny-invariant"; import nullthrows from "nullthrows"; import type CCStore from "."; import type { CCComponentId } from "./component"; -import type { Point } from "../common/types"; +import type { Vector2 } from "../common/vector2"; export type CCNodeId = Opaque; @@ -12,7 +12,7 @@ export type CCNode = { readonly id: CCNodeId; readonly parentComponentId: CCComponentId; readonly componentId: CCComponentId; - position: Point; + position: Vector2; }; export type CCNodeStoreEvents = {