mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2026-01-02 11:29:05 -05:00
feat: implements multiselect
This commit is contained in:
@@ -11,10 +11,7 @@ interface Props {
|
||||
|
||||
export const NodeContextMenu = ({ node }: Props) => {
|
||||
const renderer = useGlobalState((state) => state.renderer);
|
||||
const position = renderer.getTileScreenPosition(
|
||||
node.position.x,
|
||||
node.position.y
|
||||
);
|
||||
const position = renderer.getTileScreenPosition(node.position);
|
||||
|
||||
return (
|
||||
<ContextMenu position={position}>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
export const TileContextMenu = ({ tile }: Props) => {
|
||||
const renderer = useGlobalState((state) => state.renderer);
|
||||
const icons = useGlobalState((state) => state.initialScene.icons);
|
||||
const position = renderer.getTileScreenPosition(tile.x, tile.y);
|
||||
const position = renderer.getTileScreenPosition(tile);
|
||||
|
||||
return (
|
||||
<ContextMenu position={position}>
|
||||
|
||||
@@ -13,11 +13,16 @@ export const ContextMenu = () => {
|
||||
}
|
||||
|
||||
if (targetElement instanceof Node) {
|
||||
return <NodeContextMenu node={targetElement} />;
|
||||
return <NodeContextMenu node={targetElement} key={targetElement.id} />;
|
||||
}
|
||||
|
||||
if (targetElement instanceof Coords) {
|
||||
return <TileContextMenu tile={targetElement} />;
|
||||
return (
|
||||
<TileContextMenu
|
||||
tile={targetElement}
|
||||
key={JSON.stringify(targetElement)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -12,10 +12,11 @@ interface GlobalState {
|
||||
initialScene: SceneI;
|
||||
setInitialScene: (scene: SceneI) => void;
|
||||
selectedSideNavItem: number | null;
|
||||
setSelectedElements: (elements: Node[]) => void;
|
||||
setSelectedSideNavItem: (index: number) => void;
|
||||
closeSideNav: () => void;
|
||||
renderer: Renderer;
|
||||
selectedElements: string[];
|
||||
selectedElements: Node[];
|
||||
setRenderer: (containerEl: HTMLDivElement) => Renderer;
|
||||
onRendererEvent: (event: SceneEventI) => void;
|
||||
}
|
||||
@@ -35,6 +36,15 @@ export const useGlobalState = create<GlobalState>((set, get) => ({
|
||||
setInitialScene: (scene) => {
|
||||
set({ initialScene: scene });
|
||||
},
|
||||
setSelectedElements: (elements: Node[]) => {
|
||||
const { renderer } = get();
|
||||
|
||||
renderer.unfocusAll();
|
||||
elements.forEach((element) => {
|
||||
element.setFocus(true);
|
||||
});
|
||||
set({ selectedElements: elements });
|
||||
},
|
||||
setSelectedSideNavItem: (val) => {
|
||||
set({ selectedSideNavItem: val });
|
||||
},
|
||||
@@ -43,34 +53,37 @@ export const useGlobalState = create<GlobalState>((set, get) => ({
|
||||
},
|
||||
renderer: new Renderer(document.createElement("div")),
|
||||
onRendererEvent: (event) => {
|
||||
const { renderer } = get();
|
||||
const { setSelectedElements } = get();
|
||||
|
||||
switch (event.type) {
|
||||
case "TILE_SELECTED":
|
||||
set({ showContextMenuFor: event.data.tile, selectedElements: [] });
|
||||
setSelectedElements([]);
|
||||
set({ showContextMenuFor: event.data.tile });
|
||||
break;
|
||||
case "NODES_SELECTED":
|
||||
set({ selectedElements: event.data.nodes });
|
||||
setSelectedElements(event.data.nodes);
|
||||
|
||||
if (event.data.nodes.length === 1) {
|
||||
const node = renderer.sceneElements.nodes.getNodeById(
|
||||
event.data.nodes[0]
|
||||
);
|
||||
set({ showContextMenuFor: node });
|
||||
set({ showContextMenuFor: event.data.nodes[0] });
|
||||
}
|
||||
break;
|
||||
case "NODE_REMOVED":
|
||||
setSelectedElements([]);
|
||||
set({
|
||||
showContextMenuFor: null,
|
||||
selectedElements: [],
|
||||
selectedSideNavItem: null,
|
||||
});
|
||||
break;
|
||||
case "NODE_MOVED":
|
||||
set({ showContextMenuFor: null, selectedElements: [] });
|
||||
setSelectedElements([]);
|
||||
set({ showContextMenuFor: null });
|
||||
break;
|
||||
case "ZOOM_CHANGED":
|
||||
set({ showContextMenuFor: null, selectedElements: [] });
|
||||
setSelectedElements([]);
|
||||
set({ showContextMenuFor: null });
|
||||
break;
|
||||
case "MULTISELECT_UPDATED":
|
||||
setSelectedElements(event.data.itemsSelected);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
85
src/modes/CreateLasso.ts
Normal file
85
src/modes/CreateLasso.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Select } from "./Select";
|
||||
import { getGridSubset, isWithinBounds } from "../renderer/utils/gridHelpers";
|
||||
import { Mouse } from "../types";
|
||||
import { Coords } from "../renderer/elements/Coords";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
import { ManipulateLasso } from "./ManipulateLasso";
|
||||
|
||||
export class CreateLasso extends ModeBase {
|
||||
startTile: Coords | null = null;
|
||||
nodesSelected: Node[] = [];
|
||||
selectionGrid: Coords[] = [];
|
||||
|
||||
entry(mouse: Mouse) {
|
||||
if (!this.startTile) {
|
||||
this.startTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
}
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(this.startTile, {
|
||||
skipAnimation: true,
|
||||
});
|
||||
this.ctx.renderer.sceneElements.cursor.setVisible(true);
|
||||
}
|
||||
|
||||
setStartTile(tile: Coords) {
|
||||
this.startTile = tile;
|
||||
}
|
||||
|
||||
exit() {}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
const currentTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
if (mouse.delta) {
|
||||
const prevTile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.subtract(mouse.delta)
|
||||
);
|
||||
|
||||
if (currentTile.isEqual(prevTile)) return;
|
||||
}
|
||||
|
||||
if (!this.startTile) return;
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.createSelection(
|
||||
this.startTile,
|
||||
currentTile
|
||||
);
|
||||
|
||||
this.selectionGrid = getGridSubset([this.startTile, currentTile]);
|
||||
this.nodesSelected = this.selectionGrid.reduce<Node[]>((acc, tile) => {
|
||||
const tileItems = this.ctx.renderer.getItemsByTile(tile);
|
||||
const filtered = tileItems.filter((i) => i?.type === "NODE") as Node[];
|
||||
return [...acc, ...filtered];
|
||||
}, []);
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "MULTISELECT_UPDATED",
|
||||
data: {
|
||||
itemsSelected: this.nodesSelected,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
MOUSE_DOWN(mouse: Mouse) {
|
||||
if (this.nodesSelected.length > 0) {
|
||||
this.ctx.activateMode(ManipulateLasso, (mode) => {
|
||||
mode.setSelectedItems(this.nodesSelected);
|
||||
mode.MOUSE_DOWN(mouse);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MOUSE_UP(mouse: Mouse) {
|
||||
this.startTile = null;
|
||||
|
||||
const currentTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
if (
|
||||
this.nodesSelected.length === 0 ||
|
||||
!isWithinBounds(currentTile, this.selectionGrid)
|
||||
) {
|
||||
this.ctx.activateMode(Select);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/modes/ManipulateLasso.ts
Normal file
98
src/modes/ManipulateLasso.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Select } from "./Select";
|
||||
import { getBoundingBox, isWithinBounds } from "../renderer/utils/gridHelpers";
|
||||
import { Mouse } from "../types";
|
||||
import { Coords } from "../renderer/elements/Coords";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
import { CreateLasso } from "./CreateLasso";
|
||||
|
||||
export class ManipulateLasso extends ModeBase {
|
||||
selectedItems: Node[] = [];
|
||||
isMouseDownWithinLassoBounds = false;
|
||||
dragOffset = new Coords(0, 0);
|
||||
isDragging = false;
|
||||
|
||||
entry(mouse: Mouse) {}
|
||||
|
||||
setSelectedItems(items: Node[]) {
|
||||
this.selectedItems = items;
|
||||
}
|
||||
|
||||
exit() {}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const currentTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
if (mouse.delta) {
|
||||
const prevTile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.subtract(mouse.delta)
|
||||
);
|
||||
|
||||
if (currentTile.isEqual(prevTile)) return;
|
||||
}
|
||||
|
||||
const { renderer } = this.ctx;
|
||||
const { cursor, grid } = renderer.sceneElements;
|
||||
|
||||
if (this.isMouseDownWithinLassoBounds) {
|
||||
const validTile = grid.getAreaWithinGrid(
|
||||
currentTile,
|
||||
cursor.size,
|
||||
this.dragOffset
|
||||
);
|
||||
|
||||
const oldCursorPosition = cursor.position.clone();
|
||||
const newCursorPosition = validTile.subtract(this.dragOffset);
|
||||
|
||||
cursor.displayAt(newCursorPosition, {
|
||||
skipAnimation: true,
|
||||
});
|
||||
|
||||
const translateBy = new Coords(
|
||||
-(oldCursorPosition.x - newCursorPosition.x),
|
||||
-(oldCursorPosition.y - newCursorPosition.y)
|
||||
);
|
||||
|
||||
renderer.sceneElements.nodes.translateNodes(
|
||||
this.selectedItems.filter((i) => i.type === "NODE"),
|
||||
translateBy
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MOUSE_DOWN(mouse: Mouse) {
|
||||
const currentTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
const { renderer } = this.ctx;
|
||||
const { cursor } = renderer.sceneElements;
|
||||
const boundingBox = getBoundingBox([
|
||||
renderer.sceneElements.cursor.position,
|
||||
new Coords(
|
||||
cursor.position.x + cursor.size.x,
|
||||
cursor.position.y - cursor.size.y
|
||||
),
|
||||
]);
|
||||
|
||||
this.isMouseDownWithinLassoBounds = isWithinBounds(
|
||||
currentTile,
|
||||
boundingBox
|
||||
);
|
||||
|
||||
if (this.isMouseDownWithinLassoBounds) {
|
||||
this.isDragging = true;
|
||||
this.dragOffset.set(
|
||||
currentTile.x - cursor.position.x,
|
||||
currentTile.y - cursor.position.y
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.activateMode(Select, (mode) => mode.MOUSE_DOWN(mouse));
|
||||
}
|
||||
|
||||
MOUSE_UP(mouse: Mouse) {
|
||||
this.isDragging = false;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,91 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "../types";
|
||||
import { getTargetFromSelection, isMouseOverNewTile } from "./utils";
|
||||
import { getTargetFromSelection } from "./utils";
|
||||
import { SelectNode } from "./SelectNode";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
import { CreateLasso } from "./CreateLasso";
|
||||
import { CURSOR_TYPES } from "../renderer/elements/Cursor";
|
||||
import { Coords } from "../renderer/elements/Coords";
|
||||
|
||||
export class Select extends ModeBase {
|
||||
entry(mouse: Mouse) {
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
dragStartTile: Coords | null = null;
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.enable();
|
||||
entry(mouse: Mouse) {
|
||||
this.ctx.renderer.unfocusAll();
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.setCursorType(CURSOR_TYPES.TILE);
|
||||
|
||||
const tile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile, {
|
||||
skipAnimation: true,
|
||||
});
|
||||
this.ctx.renderer.sceneElements.cursor.setVisible(true);
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.ctx.renderer.sceneElements.cursor.disable();
|
||||
this.ctx.renderer.sceneElements.cursor.setVisible(false);
|
||||
}
|
||||
|
||||
MOUSE_ENTER(mouse: Mouse) {
|
||||
MOUSE_UP(mouse: Mouse) {
|
||||
const { renderer } = this.ctx;
|
||||
const tile = renderer.getTileFromMouse(mouse.position);
|
||||
const items = renderer.getItemsByTile(tile);
|
||||
const target = getTargetFromSelection(items);
|
||||
|
||||
renderer.sceneElements.cursor.enable();
|
||||
}
|
||||
if (!target?.type) {
|
||||
this.ctx.emitEvent({
|
||||
type: "TILE_SELECTED",
|
||||
data: { tile },
|
||||
});
|
||||
}
|
||||
|
||||
MOUSE_LEAVE() {
|
||||
this.ctx.renderer.sceneElements.cursor.disable();
|
||||
this.dragStartTile = null;
|
||||
}
|
||||
|
||||
MOUSE_DOWN(mouse: Mouse) {
|
||||
this.dragStartTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
const { renderer } = this.ctx;
|
||||
const { x, y } = renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
const items = renderer.getItemsByTile(x, y);
|
||||
const tile = renderer.getTileFromMouse(mouse.position);
|
||||
const items = renderer.getItemsByTile(tile);
|
||||
const target = getTargetFromSelection(items);
|
||||
|
||||
if (target instanceof Node) {
|
||||
if (target?.type === "NODE") {
|
||||
this.ctx.activateMode(SelectNode, (instance) => (instance.node = target));
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "TILE_SELECTED",
|
||||
data: { tile: new Coords(x, y) },
|
||||
});
|
||||
}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
const newTile = isMouseOverNewTile(
|
||||
mouse,
|
||||
this.ctx.renderer.getTileFromMouse
|
||||
);
|
||||
const currentTile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
if (newTile) {
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(newTile.x, newTile.y);
|
||||
if (mouse.delta) {
|
||||
const prevTile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.subtract(mouse.delta)
|
||||
);
|
||||
|
||||
const items = this.ctx.renderer.getItemsByTile(newTile.x, newTile.y);
|
||||
const target = getTargetFromSelection(items);
|
||||
if (currentTile.isEqual(prevTile)) return;
|
||||
}
|
||||
|
||||
this.ctx.renderer.unfocusAll();
|
||||
if (this.dragStartTile && !currentTile.isEqual(this.dragStartTile)) {
|
||||
this.ctx.activateMode(CreateLasso, (mode) => {
|
||||
console.log(this.dragStartTile);
|
||||
this.dragStartTile && mode.setStartTile(this.dragStartTile);
|
||||
mode.MOUSE_MOVE(mouse);
|
||||
});
|
||||
|
||||
if (target instanceof Node) {
|
||||
target.setFocus(true);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(currentTile);
|
||||
this.ctx.renderer.unfocusAll();
|
||||
|
||||
const items = this.ctx.renderer.getItemsByTile(currentTile);
|
||||
const target = getTargetFromSelection(items);
|
||||
|
||||
if (target?.type === "NODE") {
|
||||
target.setFocus(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Select } from "../modes/Select";
|
||||
import { Mouse, ModeContext } from "../types";
|
||||
import { Mouse } from "../types";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
|
||||
export class SelectNode extends ModeBase {
|
||||
@@ -8,30 +8,24 @@ export class SelectNode extends ModeBase {
|
||||
hasMoved = false;
|
||||
|
||||
entry(mouse: Mouse) {
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
const tile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.enable();
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile);
|
||||
this.ctx.renderer.sceneElements.cursor.setVisible(true);
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.ctx.renderer.sceneElements.cursor.disable();
|
||||
this.ctx.renderer.sceneElements.cursor.setVisible(false);
|
||||
}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
if (!this.node) return;
|
||||
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
const tile = this.ctx.renderer.getTileFromMouse(mouse.position);
|
||||
|
||||
if (this.node.position.x !== tile.x || this.node.position.y !== tile.y) {
|
||||
this.node.moveTo(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
this.node.moveTo(tile);
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile);
|
||||
this.hasMoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
113
src/modes/tests/SelectMode.test.ts
Normal file
113
src/modes/tests/SelectMode.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { ModeManager } from "../ModeManager";
|
||||
import { Renderer } from "../../renderer/Renderer";
|
||||
import { Select } from "../Select";
|
||||
import { CreateLasso } from "../CreateLasso";
|
||||
import { SelectNode } from "../SelectNode";
|
||||
import { Coords } from "../../renderer/elements/Coords";
|
||||
import { Node } from "../../renderer/elements/Node";
|
||||
import * as utils from "../utils";
|
||||
|
||||
jest.mock("../utils", () => ({
|
||||
getTargetFromSelection: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../renderer/elements/Cursor", () => ({
|
||||
CURSOR_TYPES: {
|
||||
TILE: "TILE",
|
||||
},
|
||||
}));
|
||||
jest.mock("../../renderer/Renderer", () => ({
|
||||
Renderer: jest.fn().mockImplementation(() => ({
|
||||
getTileFromMouse: (coords: Coords) => coords,
|
||||
getItemsByTile: jest.fn(() => []),
|
||||
unfocusAll: jest.fn(),
|
||||
sceneElements: {
|
||||
cursor: {
|
||||
displayAt: jest.fn(),
|
||||
setVisible: jest.fn(),
|
||||
setCursorType: jest.fn(),
|
||||
createSelection: jest.fn(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
}));
|
||||
jest.mock("../../renderer/elements/Node", () => ({
|
||||
Node: jest.fn().mockImplementation(() => ({
|
||||
type: "NODE",
|
||||
isFocussed: false,
|
||||
setFocus: jest.fn(),
|
||||
moveTo: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const MockNode = Node as jest.Mock<Node>;
|
||||
|
||||
const createRenderer = () => {
|
||||
const renderer = new Renderer({} as unknown as HTMLDivElement);
|
||||
const modeManager = new ModeManager();
|
||||
|
||||
modeManager.setRenderer(renderer);
|
||||
modeManager.activateMode(Select);
|
||||
|
||||
return { renderer, modeManager };
|
||||
};
|
||||
|
||||
describe("Select mode functions correctly", () => {
|
||||
it("Cursor repositions when tile is hovered", () => {
|
||||
const { renderer, modeManager } = createRenderer();
|
||||
const displayAtSpy = jest.spyOn(renderer.sceneElements.cursor, "displayAt");
|
||||
modeManager.onMouseEvent("MOUSE_MOVE", {
|
||||
position: new Coords(2, 2),
|
||||
delta: null,
|
||||
});
|
||||
expect(displayAtSpy).toHaveBeenCalled();
|
||||
expect(displayAtSpy.mock.calls[1][0]).toStrictEqual(new Coords(2, 2));
|
||||
});
|
||||
|
||||
it("Node gains focus when mouse hovers over it", () => {
|
||||
const { modeManager } = createRenderer();
|
||||
const mockNode = new MockNode();
|
||||
jest.spyOn(utils, "getTargetFromSelection").mockReturnValueOnce(mockNode);
|
||||
modeManager.onMouseEvent("MOUSE_MOVE", {
|
||||
position: new Coords(1, 1),
|
||||
delta: null,
|
||||
});
|
||||
expect(mockNode.setFocus).toHaveBeenCalled();
|
||||
});
|
||||
it("All focussed elements reset when user hovers over a different tile", () => {
|
||||
const { renderer, modeManager } = createRenderer();
|
||||
jest.spyOn(utils, "getTargetFromSelection").mockReturnValueOnce(null);
|
||||
const unfocusAllSpy = jest.spyOn(renderer, "unfocusAll");
|
||||
modeManager.onMouseEvent("MOUSE_MOVE", {
|
||||
position: new Coords(1, 1),
|
||||
delta: null,
|
||||
});
|
||||
expect(unfocusAllSpy).toHaveBeenCalled();
|
||||
});
|
||||
it("Activates multiselect mode when mouse is dragged (dragging must start from an empty tile)", () => {
|
||||
const activateModeSpy = jest.spyOn(ModeManager.prototype, "activateMode");
|
||||
const { modeManager } = createRenderer();
|
||||
jest.spyOn(utils, "getTargetFromSelection").mockReturnValue(null);
|
||||
modeManager.onMouseEvent("MOUSE_DOWN", {
|
||||
position: new Coords(0, 0),
|
||||
delta: null,
|
||||
});
|
||||
modeManager.onMouseEvent("MOUSE_MOVE", {
|
||||
position: new Coords(2, 2),
|
||||
delta: new Coords(2, 2),
|
||||
});
|
||||
expect(activateModeSpy).toHaveBeenCalledTimes(2);
|
||||
expect(activateModeSpy.mock.calls[1][0]).toStrictEqual(CreateLasso);
|
||||
});
|
||||
it("Activates Node selection mode when user clicks on a node", () => {
|
||||
const activateModeSpy = jest.spyOn(ModeManager.prototype, "activateMode");
|
||||
const { modeManager } = createRenderer();
|
||||
const mockNode = new MockNode();
|
||||
jest.spyOn(utils, "getTargetFromSelection").mockReturnValue(mockNode);
|
||||
modeManager.onMouseEvent("MOUSE_DOWN", {
|
||||
position: new Coords(0, 0),
|
||||
delta: null,
|
||||
});
|
||||
expect(activateModeSpy).toHaveBeenCalledTimes(2);
|
||||
expect(activateModeSpy.mock.calls[1][0]).toStrictEqual(SelectNode);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
import { Mouse } from "../types";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { Coords } from "../renderer/elements/Coords";
|
||||
|
||||
export const getTargetFromSelection = (items: (Node | undefined)[]) => {
|
||||
const node = items.find((item) => item instanceof Node);
|
||||
@@ -21,11 +22,13 @@ export const isMouseOverNewTile = (
|
||||
}
|
||||
|
||||
const prevTile = getTileFromMouse(
|
||||
mouse.position.x - mouse.delta.x,
|
||||
mouse.position.y - mouse.delta.y
|
||||
new Coords(
|
||||
mouse.position.x - mouse.delta.x,
|
||||
mouse.position.y - mouse.delta.y
|
||||
)
|
||||
);
|
||||
|
||||
const currentTile = getTileFromMouse(mouse.position.x, mouse.position.y);
|
||||
const currentTile = getTileFromMouse(mouse.position);
|
||||
|
||||
if (prevTile.x !== currentTile.x || prevTile.y !== currentTile.y) {
|
||||
return currentTile;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { makeAutoObservable, toJS } from "mobx";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import Paper, { Group } from "paper";
|
||||
import gsap from "gsap";
|
||||
import autobind from "auto-bind";
|
||||
import { Grid } from "./elements/Grid";
|
||||
import { Cursor } from "./elements/Cursor";
|
||||
import { PROJECTED_TILE_WIDTH, PROJECTED_TILE_HEIGHT } from "./constants";
|
||||
import { clamp } from "../utils";
|
||||
import { Nodes } from "./elements/Nodes";
|
||||
import { SceneI, IconI } from "../validation/SceneSchema";
|
||||
import { Coords } from "./elements/Coords";
|
||||
import { OnSceneChange, SceneEventI } from "../types";
|
||||
|
||||
interface Config {
|
||||
grid: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
icons: IconI[];
|
||||
}
|
||||
|
||||
@@ -23,10 +19,6 @@ export class Renderer {
|
||||
zoom = 1;
|
||||
|
||||
config: Config = {
|
||||
grid: {
|
||||
width: 51,
|
||||
height: 51,
|
||||
},
|
||||
icons: [],
|
||||
};
|
||||
callbacks: {
|
||||
@@ -53,7 +45,6 @@ export class Renderer {
|
||||
|
||||
constructor(containerEl: HTMLDivElement) {
|
||||
makeAutoObservable(this);
|
||||
autobind(this);
|
||||
|
||||
Paper.settings = {
|
||||
insertelements: false,
|
||||
@@ -72,7 +63,7 @@ export class Renderer {
|
||||
Paper.setup(this.domElements.canvas);
|
||||
|
||||
this.sceneElements = {
|
||||
grid: new Grid(this),
|
||||
grid: new Grid(new Coords(51, 51), this),
|
||||
cursor: new Cursor(this),
|
||||
nodes: new Nodes(this),
|
||||
};
|
||||
@@ -136,78 +127,74 @@ export class Renderer {
|
||||
return { canvas };
|
||||
}
|
||||
|
||||
getTileFromMouse(_mouseX: number, _mouseY: number) {
|
||||
getTileFromMouse(mouse: Coords) {
|
||||
const halfW = PROJECTED_TILE_WIDTH / 2;
|
||||
const halfH = PROJECTED_TILE_HEIGHT / 2;
|
||||
|
||||
const mouseX =
|
||||
(_mouseX - this.groups.elements.position.x) * (1 / this.zoom);
|
||||
(mouse.x - this.groups.elements.position.x) * (1 / this.zoom);
|
||||
const mouseY =
|
||||
(_mouseY - this.groups.elements.position.y) * (1 / this.zoom) + halfH;
|
||||
(mouse.y - this.groups.elements.position.y) * (1 / this.zoom) + halfH;
|
||||
|
||||
const row = Math.floor((mouseX / halfW + mouseY / halfH) / 2);
|
||||
const col = Math.floor((mouseY / halfH - mouseX / halfW) / 2);
|
||||
|
||||
const halfRowNum = Math.floor(this.config.grid.width * 0.5);
|
||||
const halfColNum = Math.floor(this.config.grid.height * 0.5);
|
||||
const halfRowNum = Math.floor(this.sceneElements.grid.size.x * 0.5);
|
||||
const halfColNum = Math.floor(this.sceneElements.grid.size.y * 0.5);
|
||||
|
||||
return {
|
||||
x: clamp(row, -halfRowNum, halfRowNum),
|
||||
y: clamp(col, -halfColNum, halfColNum),
|
||||
};
|
||||
return new Coords(
|
||||
clamp(row, -halfRowNum, halfRowNum),
|
||||
clamp(col, -halfColNum, halfColNum)
|
||||
);
|
||||
}
|
||||
|
||||
getTilePosition(x: number, y: number) {
|
||||
getTilePosition({ x, y }: Coords) {
|
||||
const halfW = PROJECTED_TILE_WIDTH * 0.5;
|
||||
const halfH = PROJECTED_TILE_HEIGHT * 0.5;
|
||||
|
||||
return {
|
||||
x: x * halfW - y * halfW,
|
||||
y: x * halfH + y * halfH,
|
||||
};
|
||||
return new Coords(x * halfW - y * halfW, x * halfH + y * halfH);
|
||||
}
|
||||
|
||||
getTileBounds(x: number, y: number) {
|
||||
const position = this.getTilePosition(x, y);
|
||||
getTileBounds(coords: Coords) {
|
||||
const position = this.getTilePosition(coords);
|
||||
|
||||
return {
|
||||
left: {
|
||||
x: position.x - PROJECTED_TILE_WIDTH * 0.5,
|
||||
y: position.y - PROJECTED_TILE_HEIGHT * 0.5,
|
||||
y: position.y,
|
||||
},
|
||||
right: {
|
||||
x: position.x + PROJECTED_TILE_WIDTH * 0.5,
|
||||
y: position.y - PROJECTED_TILE_HEIGHT * 0.5,
|
||||
y: position.y,
|
||||
},
|
||||
top: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT },
|
||||
bottom: { x: position.x, y: position.y },
|
||||
center: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
top: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
bottom: { x: position.x, y: position.y + PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
center: { x: position.x, y: position.y },
|
||||
};
|
||||
}
|
||||
|
||||
getTileScreenPosition(x: number, y: number) {
|
||||
getTileScreenPosition(position: Coords) {
|
||||
const { width: viewW, height: viewH } = Paper.view.bounds;
|
||||
const { offsetLeft: offsetX, offsetTop: offsetY } = this.domElements.canvas;
|
||||
const tilePosition = this.getTileBounds(x, y).center;
|
||||
const tilePosition = this.getTileBounds(position).center;
|
||||
const globalItemsGroupPosition = this.groups.elements.globalToLocal([0, 0]);
|
||||
const screenPosition = {
|
||||
x:
|
||||
(tilePosition.x +
|
||||
this.scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
this.groups.elements.position.x +
|
||||
viewW * 0.5) *
|
||||
this.zoom +
|
||||
const screenPosition = new Coords(
|
||||
(tilePosition.x +
|
||||
this.scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
this.groups.elements.position.x +
|
||||
viewW * 0.5) *
|
||||
this.zoom +
|
||||
offsetX,
|
||||
y:
|
||||
(tilePosition.y +
|
||||
this.scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
this.groups.elements.position.y +
|
||||
viewH * 0.5) *
|
||||
this.zoom +
|
||||
offsetY,
|
||||
};
|
||||
|
||||
(tilePosition.y +
|
||||
this.scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
this.groups.elements.position.y +
|
||||
viewH * 0.5) *
|
||||
this.zoom +
|
||||
offsetY
|
||||
);
|
||||
|
||||
return screenPosition;
|
||||
}
|
||||
@@ -291,8 +278,8 @@ export class Renderer {
|
||||
this.callbacks.emitEvent(event);
|
||||
}
|
||||
|
||||
getItemsByTile(x: number, y: number) {
|
||||
const node = this.sceneElements.nodes.getNodeByTile(x, y);
|
||||
getItemsByTile(coords: Coords) {
|
||||
const node = this.sceneElements.nodes.getNodeByTile(coords);
|
||||
|
||||
return [node].filter((i) => Boolean(i));
|
||||
}
|
||||
|
||||
@@ -11,4 +11,44 @@ export class Coords {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
setX(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
setY(y: number) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
isEqual(comparator: Coords) {
|
||||
return this.x === comparator.x && this.y === comparator.y;
|
||||
}
|
||||
|
||||
subtract(operand: Coords) {
|
||||
return new Coords(this.x - operand.x, this.y - operand.y);
|
||||
}
|
||||
|
||||
subtractX(operand: number) {
|
||||
return new Coords(this.x - operand, this.y);
|
||||
}
|
||||
|
||||
subtractY(operand: number) {
|
||||
return new Coords(this.x, this.y - operand);
|
||||
}
|
||||
|
||||
add(operand: Coords) {
|
||||
return new Coords(this.x + operand.x, this.y + operand.y);
|
||||
}
|
||||
|
||||
addX(operand: number) {
|
||||
return new Coords(this.x + operand, this.y);
|
||||
}
|
||||
|
||||
addY(operand: number) {
|
||||
return new Coords(this.x, this.y + operand);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Coords(this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Shape, Point } from "paper";
|
||||
import { Shape, Point, Group } from "paper";
|
||||
import { gsap } from "gsap";
|
||||
import { applyProjectionMatrix } from "../utils/projection";
|
||||
import { TILE_SIZE, PIXEL_UNIT } from "../constants";
|
||||
@@ -21,6 +21,8 @@ export enum CURSOR_TYPES {
|
||||
}
|
||||
|
||||
export class Cursor extends SceneElement {
|
||||
container = new Group();
|
||||
|
||||
renderElements = {
|
||||
rectangle: new Shape.Rectangle([0, 0]),
|
||||
};
|
||||
@@ -29,20 +31,15 @@ export class Cursor extends SceneElement {
|
||||
highlight: gsap.core.Tween;
|
||||
};
|
||||
|
||||
position = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
position = new Coords(0, 0);
|
||||
|
||||
size = {
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
size: Coords = new Coords(1, 1);
|
||||
|
||||
currentType?: CURSOR_TYPES;
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx);
|
||||
|
||||
this.displayAt = this.displayAt.bind(this);
|
||||
|
||||
this.renderElements.rectangle = new Shape.Rectangle({});
|
||||
@@ -62,8 +59,8 @@ export class Cursor extends SceneElement {
|
||||
applyProjectionMatrix(this.container);
|
||||
|
||||
this.setCursorType(CURSOR_TYPES.TILE);
|
||||
this.displayAt(0, 0);
|
||||
this.enable();
|
||||
this.displayAt(new Coords(0, 0));
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
setCursorType(type: CURSOR_TYPES) {
|
||||
@@ -71,10 +68,7 @@ export class Cursor extends SceneElement {
|
||||
|
||||
this.currentType = type;
|
||||
this.container.set({ pivot: [0, 0] });
|
||||
this.size = {
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
this.size = new Coords(1, 1);
|
||||
|
||||
switch (type) {
|
||||
case CURSOR_TYPES.OUTLINE:
|
||||
@@ -134,12 +128,8 @@ export class Cursor extends SceneElement {
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.container.visible = true;
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.container.visible = false;
|
||||
setVisible(state: boolean) {
|
||||
this.container.visible = state;
|
||||
}
|
||||
|
||||
createSelection(from: Coords, to: Coords) {
|
||||
@@ -151,21 +141,19 @@ export class Cursor extends SceneElement {
|
||||
this.setCursorType(CURSOR_TYPES.LASSO);
|
||||
|
||||
const sorted = sortByPosition(boundingBox);
|
||||
const position = new Coords(sorted.lowX, sorted.highY);
|
||||
|
||||
this.position = {
|
||||
x: sorted.lowX,
|
||||
y: sorted.lowY,
|
||||
};
|
||||
this.position.set(position.x, position.y);
|
||||
|
||||
this.size = {
|
||||
width: sorted.highX - sorted.lowX,
|
||||
height: sorted.highY - sorted.lowY,
|
||||
};
|
||||
this.size = new Coords(
|
||||
sorted.highX - sorted.lowX,
|
||||
sorted.highY - sorted.lowY
|
||||
);
|
||||
|
||||
this.renderElements.rectangle.set({
|
||||
size: [
|
||||
(this.size.width + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
(this.size.height + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
(this.size.x + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
(this.size.y + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -175,38 +163,29 @@ export class Cursor extends SceneElement {
|
||||
|
||||
const targetTile = boundingBox[3];
|
||||
|
||||
this.container.position = new Point(
|
||||
getTileBounds(targetTile.x, targetTile.y).left
|
||||
);
|
||||
this.container.position = new Point(getTileBounds(targetTile).left);
|
||||
}
|
||||
|
||||
predictBoundsAt(tile: Coords) {
|
||||
const bounds = [
|
||||
{ x: tile.x, y: tile.y },
|
||||
{ x: tile.x, y: tile.y - this.size.height },
|
||||
{ x: tile.x + this.size.width, y: tile.y - this.size.height },
|
||||
{ x: tile.x + this.size.width, y: tile.y },
|
||||
{ x: tile.x, y: tile.y - this.size.y },
|
||||
{ x: tile.x + this.size.x, y: tile.y - this.size.y },
|
||||
{ x: tile.x + this.size.x, y: tile.y },
|
||||
];
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return { ...this.position, ...this.size };
|
||||
}
|
||||
displayAt(position: Coords, opts?: { skipAnimation: boolean }) {
|
||||
if (this.position.isEqual(position)) return;
|
||||
|
||||
displayAt(x: number, y: number, opts?: { skipAnimation: boolean }) {
|
||||
if (x === this.position.x && y === this.position.y) return;
|
||||
|
||||
this.position = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
this.position.set(position.x, position.y);
|
||||
|
||||
const tileBoundsPosition =
|
||||
this.currentType === CURSOR_TYPES.LASSO ? "left" : "center";
|
||||
|
||||
const tile = getTileBounds(x, y)[tileBoundsPosition];
|
||||
const tile = getTileBounds(position)[tileBoundsPosition];
|
||||
|
||||
tweenPosition(this.container, {
|
||||
...tile,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { applyProjectionMatrix } from "../utils/projection";
|
||||
import type { Context } from "../../types";
|
||||
import { TILE_SIZE, PIXEL_UNIT, SCALING_CONST } from "../constants";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
import { Coords } from "./Coords";
|
||||
import { sortByPosition, getBoundingBox } from "../utils/gridHelpers";
|
||||
|
||||
export class Grid extends SceneElement {
|
||||
container = new Group();
|
||||
@@ -10,13 +12,16 @@ export class Grid extends SceneElement {
|
||||
grid: new Group({ applyMatrix: true }),
|
||||
};
|
||||
|
||||
constructor(ctx: Context) {
|
||||
size: Coords;
|
||||
|
||||
constructor(size: Coords, ctx: Context) {
|
||||
super(ctx);
|
||||
|
||||
this.size = size;
|
||||
this.container.addChild(this.renderElements.grid);
|
||||
|
||||
for (let x = 0; x <= this.ctx.config.grid.width; x++) {
|
||||
const lineLength = this.ctx.config.grid.height * TILE_SIZE;
|
||||
for (let x = 0; x <= this.size.x; x++) {
|
||||
const lineLength = this.size.y * TILE_SIZE;
|
||||
const start = x * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
@@ -30,8 +35,8 @@ export class Grid extends SceneElement {
|
||||
this.renderElements.grid.addChild(line);
|
||||
}
|
||||
|
||||
for (let y = 0; y <= this.ctx.config.grid.height; y++) {
|
||||
const lineLength = this.ctx.config.grid.width * TILE_SIZE;
|
||||
for (let y = 0; y <= this.size.y; y++) {
|
||||
const lineLength = this.size.x * TILE_SIZE;
|
||||
const start = y * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
@@ -48,4 +53,52 @@ export class Grid extends SceneElement {
|
||||
this.renderElements.grid.scaling = new Point(SCALING_CONST, SCALING_CONST);
|
||||
applyProjectionMatrix(this.renderElements.grid);
|
||||
}
|
||||
|
||||
getGridBounds() {
|
||||
const halfW = Math.floor(this.size.x * 0.5);
|
||||
const halfH = Math.floor(this.size.y * 0.5);
|
||||
|
||||
return getBoundingBox([
|
||||
new Coords(-halfW, -halfH),
|
||||
new Coords(-halfW, halfH),
|
||||
new Coords(halfW, halfH),
|
||||
new Coords(halfW, -halfH),
|
||||
]);
|
||||
}
|
||||
|
||||
getAreaWithinGrid(
|
||||
tile: Coords,
|
||||
size: Coords,
|
||||
offset: Coords = new Coords(0, 0)
|
||||
) {
|
||||
const position = tile.subtract(offset);
|
||||
|
||||
const areaBounds = sortByPosition([
|
||||
position,
|
||||
position.subtractY(size.y),
|
||||
new Coords(position.x + size.x, position.y - size.y),
|
||||
position.addX(size.x),
|
||||
]);
|
||||
const gridBounds = sortByPosition(this.getGridBounds());
|
||||
|
||||
const delta = new Coords(0, 0);
|
||||
|
||||
if (areaBounds.highX > gridBounds.highX) {
|
||||
delta.setX(-(areaBounds.highX - gridBounds.highX));
|
||||
}
|
||||
|
||||
if (areaBounds.lowX < gridBounds.lowX) {
|
||||
delta.setX(gridBounds.lowX - areaBounds.lowX);
|
||||
}
|
||||
|
||||
if (areaBounds.highY > gridBounds.highY) {
|
||||
delta.setY(-(areaBounds.highY - gridBounds.highY));
|
||||
}
|
||||
|
||||
if (areaBounds.lowY < gridBounds.lowY) {
|
||||
delta.setY(gridBounds.lowY - areaBounds.lowY);
|
||||
}
|
||||
|
||||
return new Coords(tile.x + delta.x, tile.y + delta.y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,16 +14,20 @@ export interface NodeOptions {
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
onMove: (x: number, y: number, node: Node) => void;
|
||||
onMove: (
|
||||
coords: Coords,
|
||||
node: Node,
|
||||
opts?: { skipAnimation: boolean }
|
||||
) => void;
|
||||
onDestroy: (node: Node) => void;
|
||||
}
|
||||
|
||||
export class Node {
|
||||
ctx: Context;
|
||||
container = new Group();
|
||||
type = "NODE";
|
||||
|
||||
id;
|
||||
selected = false;
|
||||
callbacks: Callbacks;
|
||||
position;
|
||||
color: string = theme.customVars.diagramPalette.purple;
|
||||
@@ -46,7 +50,7 @@ export class Node {
|
||||
|
||||
this.container.addChild(this.tile.container);
|
||||
this.container.addChild(this.icon.container);
|
||||
this.moveTo(this.position.x, this.position.y);
|
||||
this.moveTo(this.position);
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
}
|
||||
@@ -68,8 +72,8 @@ export class Node {
|
||||
this.tile.setFocus(state);
|
||||
}
|
||||
|
||||
moveTo(x: number, y: number) {
|
||||
this.callbacks.onMove(x, y, this);
|
||||
moveTo(coords: Coords, opts?: { skipAnimation: boolean }) {
|
||||
this.callbacks.onMove(coords, this, opts);
|
||||
}
|
||||
|
||||
export() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Group } from "paper";
|
||||
import autobind from "auto-bind";
|
||||
import { makeAutoObservable, toJS } from "mobx";
|
||||
import { Context } from "../../types";
|
||||
import { Node, NodeOptions } from "./Node";
|
||||
@@ -15,7 +14,6 @@ export class Nodes {
|
||||
|
||||
constructor(ctx: Context) {
|
||||
makeAutoObservable(this);
|
||||
autobind(this);
|
||||
|
||||
this.ctx = ctx;
|
||||
}
|
||||
@@ -50,15 +48,15 @@ export class Nodes {
|
||||
});
|
||||
}
|
||||
|
||||
onMove(x: number, y: number, node: Node, opts?: { skipAnimation: boolean }) {
|
||||
const from = new Coords(node.position.x, node.position.y);
|
||||
const to = new Coords(x, y);
|
||||
onMove(coords: Coords, node: Node, opts?: { skipAnimation: boolean }) {
|
||||
const from = node.position;
|
||||
const to = coords;
|
||||
|
||||
const tile = this.ctx.getTileBounds(x, y);
|
||||
node.position = new Coords(x, y);
|
||||
const tile = this.ctx.getTileBounds(coords);
|
||||
node.position = coords;
|
||||
|
||||
tweenPosition(node.container, {
|
||||
...tile.bottom,
|
||||
...tile.center,
|
||||
duration: opts?.skipAnimation ? 0 : 0.05,
|
||||
});
|
||||
|
||||
@@ -86,9 +84,9 @@ export class Nodes {
|
||||
return this.nodes.find((node) => node.id === id);
|
||||
}
|
||||
|
||||
getNodeByTile(x: number, y: number) {
|
||||
getNodeByTile(coords: Coords) {
|
||||
return this.nodes.find(
|
||||
(node) => node.position.x === x && node.position.y === y
|
||||
(node) => node.position.x === coords.x && node.position.y === coords.y
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,16 +102,47 @@ export class Nodes {
|
||||
}
|
||||
|
||||
setSelectedNodes(ids: string[]) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.selected = ids.includes(node.id);
|
||||
});
|
||||
const nodes = ids
|
||||
.map((id) => {
|
||||
const node = this.getNodeById(id);
|
||||
node?.setSelected(true);
|
||||
|
||||
return node;
|
||||
})
|
||||
.filter((node) => node !== undefined) as Node[];
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "NODES_SELECTED",
|
||||
data: { nodes: ids },
|
||||
data: { nodes },
|
||||
});
|
||||
}
|
||||
|
||||
translateNodes(nodes: Node[], translation: Coords) {
|
||||
// const updatedConnectors = [];
|
||||
|
||||
nodes.forEach((node) => {
|
||||
// const connectors = this.connectors.getConnectorsByNode(node.id);
|
||||
|
||||
// connectors.forEach((con) => {
|
||||
// if (updatedConnectors.includes(con.id)) return;
|
||||
|
||||
// const connectedNode = con.from.id === node.id ? con.to : con.from;
|
||||
|
||||
// if (!nodes.find(({ id }) => id === connectedNode.id)) {
|
||||
// con.removeAllAnchors();
|
||||
// } else {
|
||||
// con.translateAnchors(translate);
|
||||
// }
|
||||
|
||||
// updatedConnectors.push(con.id);
|
||||
// });
|
||||
|
||||
node.moveTo(node.position.add(translation), { skipAnimation: true });
|
||||
});
|
||||
|
||||
// this.connectors.updateAllPaths();
|
||||
}
|
||||
|
||||
export() {
|
||||
const exported = this.nodes.map((node) => node.export());
|
||||
return exported;
|
||||
|
||||
@@ -57,47 +57,44 @@ export const getBoundingBox = (
|
||||
];
|
||||
};
|
||||
|
||||
export const getTilePosition = (x: number, y: number) => {
|
||||
export const getTilePosition = ({ x, y }: Coords) => {
|
||||
const halfW = PROJECTED_TILE_WIDTH * 0.5;
|
||||
const halfH = PROJECTED_TILE_HEIGHT * 0.5;
|
||||
|
||||
return new Coords(x * halfW - y * halfW, x * halfH + y * halfH);
|
||||
};
|
||||
|
||||
export const getTileBounds = (coords: Coords) => {
|
||||
const position = getTilePosition(coords);
|
||||
|
||||
return {
|
||||
x: x * halfW - y * halfW,
|
||||
y: x * halfH + y * halfH,
|
||||
left: new Coords(position.x - PROJECTED_TILE_WIDTH * 0.5, position.y),
|
||||
right: new Coords(position.x + PROJECTED_TILE_WIDTH * 0.5, position.y),
|
||||
top: new Coords(position.x, position.y - PROJECTED_TILE_HEIGHT * 0.5),
|
||||
bottom: new Coords(position.x, position.y + PROJECTED_TILE_HEIGHT * 0.5),
|
||||
center: new Coords(position.x, position.y),
|
||||
};
|
||||
};
|
||||
|
||||
export const getTileBounds = (x: number, y: number) => {
|
||||
const position = getTilePosition(x, y);
|
||||
export const getGridSubset = (tiles: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
return {
|
||||
left: { x: position.x - PROJECTED_TILE_WIDTH * 0.5, y: position.y },
|
||||
right: { x: position.x + PROJECTED_TILE_WIDTH * 0.5, y: position.y },
|
||||
top: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
bottom: { x: position.x, y: position.y + PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
center: { x: position.x, y: position.y },
|
||||
};
|
||||
const subset = [];
|
||||
|
||||
for (let x = lowX; x < highX + 1; x += 1) {
|
||||
for (let y = lowY; y < highY + 1; y += 1) {
|
||||
subset.push(new Coords(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
return subset;
|
||||
};
|
||||
|
||||
// function getGridSubset(tiles) {
|
||||
// const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
export const isWithinBounds = (tile: Coords, bounds: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(bounds);
|
||||
|
||||
// const subset = [];
|
||||
|
||||
// for (let x = lowX; x < highX + 1; x += 1) {
|
||||
// for (let y = lowY; y < highY + 1; y += 1) {
|
||||
// subset.push({ x, y });
|
||||
// }
|
||||
// }
|
||||
|
||||
// return subset;
|
||||
// }
|
||||
|
||||
// function isWithinBounds(tile, bounds) {
|
||||
// const { lowX, lowY, highX, highY } = sortByPosition(bounds);
|
||||
|
||||
// return tile.x >= lowX && tile.x <= highX && tile.y >= lowY && tile.y <= highY;
|
||||
// }
|
||||
return tile.x >= lowX && tile.x <= highX && tile.y >= lowY && tile.y <= highY;
|
||||
};
|
||||
|
||||
// function getTranslation(start, end) {
|
||||
// return { x: start.x - end.x, y: start.y - end.y };
|
||||
@@ -137,12 +134,3 @@ export const getTileBounds = (x: number, y: number) => {
|
||||
|
||||
// return changes;
|
||||
// }
|
||||
|
||||
// module.exports = {
|
||||
// tileIterator,
|
||||
// sortByPosition,
|
||||
// getBoundingBox,
|
||||
// getGridSubset,
|
||||
// isWithinBounds,
|
||||
// diffItems,
|
||||
// };
|
||||
|
||||
37
src/renderer/utils/tests/gridHelpers.test.ts
Normal file
37
src/renderer/utils/tests/gridHelpers.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getGridSubset, isWithinBounds } from "../gridHelpers";
|
||||
import { Coords } from "../../elements/Coords";
|
||||
|
||||
describe("Tests gridhelper functions", () => {
|
||||
test("Gets grid subset correctly", () => {
|
||||
const gridSubset = getGridSubset([new Coords(5, 5), new Coords(7, 7)]);
|
||||
|
||||
expect(gridSubset).toEqual([
|
||||
new Coords(5, 5),
|
||||
new Coords(5, 6),
|
||||
new Coords(5, 7),
|
||||
new Coords(6, 5),
|
||||
new Coords(6, 6),
|
||||
new Coords(6, 7),
|
||||
new Coords(7, 5),
|
||||
new Coords(7, 6),
|
||||
new Coords(7, 7),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Calculates within bounds correctly", () => {
|
||||
const BOUNDS: Coords[] = [
|
||||
new Coords(4, 4),
|
||||
new Coords(6, 4),
|
||||
new Coords(6, 6),
|
||||
new Coords(4, 6),
|
||||
];
|
||||
|
||||
const withinBounds = isWithinBounds(new Coords(5, 5), BOUNDS);
|
||||
const onBorder = isWithinBounds(new Coords(4, 4), BOUNDS);
|
||||
const outsideBounds = isWithinBounds(new Coords(3, 3), BOUNDS);
|
||||
|
||||
expect(withinBounds).toBe(true);
|
||||
expect(onBorder).toBe(true);
|
||||
expect(outsideBounds).toBe(false);
|
||||
});
|
||||
});
|
||||
36
src/types.ts
36
src/types.ts
@@ -1,6 +1,7 @@
|
||||
import { Renderer } from "./renderer/Renderer";
|
||||
import type { ModeManager } from "./modes/ModeManager";
|
||||
import { Coords } from "./renderer/elements/Coords";
|
||||
import { Node } from "./renderer/elements/Node";
|
||||
|
||||
export interface Mode {
|
||||
initial: string;
|
||||
@@ -19,19 +20,31 @@ export interface ModeContext {
|
||||
emitEvent: OnSceneChange;
|
||||
}
|
||||
|
||||
export type GeneralEventI = {
|
||||
type: "SCENE_LOAD";
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type NodeEventI =
|
||||
// Grid Events
|
||||
export type GeneralEventI =
|
||||
| {
|
||||
type: "SCENE_LOAD";
|
||||
data: {};
|
||||
}
|
||||
| {
|
||||
type: "TILE_SELECTED";
|
||||
data: {
|
||||
tile: Coords;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "MULTISELECT_UPDATED";
|
||||
data: {
|
||||
itemsSelected: Node[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "ZOOM_CHANGED";
|
||||
data: {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type NodeEventI =
|
||||
// Node Events
|
||||
| {
|
||||
type: "NODE_CREATED";
|
||||
@@ -48,7 +61,7 @@ export type NodeEventI =
|
||||
| {
|
||||
type: "NODES_SELECTED";
|
||||
data: {
|
||||
nodes: string[];
|
||||
nodes: Node[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -58,13 +71,6 @@ export type NodeEventI =
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
};
|
||||
}
|
||||
// Utility Events
|
||||
| {
|
||||
type: "ZOOM_CHANGED";
|
||||
data: {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SceneEventI = NodeEventI | GeneralEventI;
|
||||
|
||||
Reference in New Issue
Block a user