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 = {