Skip to content

Commit

Permalink
feat: disable movement when using tools (#4)
Browse files Browse the repository at this point in the history
This PR refactors tools to use the `Strategy` pattern, and restricts
movement to a single tool (the hand pointer).
  • Loading branch information
MegaRedHand authored Oct 18, 2024
1 parent 40c5864 commit f9210db
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 77 deletions.
2 changes: 2 additions & 0 deletions src/assets/hand_pointer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
234 changes: 157 additions & 77 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,169 @@
// Doing this includes the file in the build
import './style.css';

import { Application, Graphics, GraphicsContext, FederatedPointerEvent, EventSystem, PointData } from 'pixi.js';
// Assets
import RouterSvg from './assets/router.svg';
import ConnectionSvg from './assets/connection.svg';
import HandPointer from './assets/hand_pointer.svg';

import { Application, Graphics, GraphicsContext, FederatedPointerEvent, EventSystem, PointData } from 'pixi.js';

import * as pixi_viewport from 'pixi-viewport';


const WORLD_WIDTH = 10000;
const WORLD_HEIGHT = 10000;


enum CursorMode {
Router,
Connection,
// > context.ts

class GlobalContext {
private mode: ModeStrategy = null;
private viewport: Viewport = null;

initialize(viewport: Viewport, mode: ModeStrategy) {
this.viewport = viewport;
this.setMode(mode);
}

getViewport() { return this.viewport; }

getMode() { return this.mode; }

setMode(mode: ModeStrategy) {
this.mode = mode;
this.mode.initialize(this);
}
}

interface ModeStrategy {
initialize(ctx: GlobalContext): void;
clickViewport(ctx: GlobalContext, e: FederatedPointerEvent): void;
clickCircle(ctx: GlobalContext, e: FederatedPointerEvent, circle: Circle): void;
}

class GlobalContext {
// TODO: merge mode and selected fields into strategy class
mode: CursorMode = CursorMode.Router;
selected: Circle = null;
class MoveMode implements ModeStrategy {
initialize(ctx: GlobalContext) {
ctx.getViewport().enableMovement();
}

viewport: Viewport = null;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
clickViewport(ctx: GlobalContext, e: FederatedPointerEvent) {
// do nothing
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
clickCircle(ctx: GlobalContext, e: FederatedPointerEvent, circle: Circle) {
// do nothing
}
}

class RouterMode {
initialize(ctx: GlobalContext) {
ctx.getViewport().disableMovement();
}

clickViewport(ctx: GlobalContext, e: FederatedPointerEvent) {
const position = ctx.getViewport().toWorld(e.global);
const circle = new Circle(ctx, position);
ctx.getViewport().addChild(circle);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
clickCircle(ctx: GlobalContext, e: FederatedPointerEvent, circle: Circle) {
// To avoid overlapping circles
e.stopPropagation();
}
}

popSelected() {
class ConnectionMode {
private selected: Circle = null;

initialize(ctx: GlobalContext) {
ctx.getViewport().disableMovement();
}

clickViewport(ctx: GlobalContext, e: FederatedPointerEvent) {
e.stopPropagation();
}

clickCircle(ctx: GlobalContext, e: FederatedPointerEvent, circle: Circle) {
e.stopPropagation();
const selected = this.popSelected();

if (selected === null) {
this.selected = circle;
return;
}
// TODO: this could be moved to a separate function/class
const line = new Graphics()
.moveTo(selected.x, selected.y)
.lineTo(circle.x, circle.y)
.stroke({ width: 2, color: 0 });
line.zIndex = 1;
ctx.getViewport().addChild(line);
}

// Private
private popSelected() {
const selected = this.selected;
this.selected = null;
return selected;
}
}

setViewport(viewport: Viewport) {
this.viewport = viewport;

// > graphics.ts

class Background extends Graphics {
constructor() {
super();
this.rect(0, 0, WORLD_WIDTH, WORLD_HEIGHT).fill(0xe3e2e1);
this.zIndex = 0;
}

resize(width: number, height: number) {
this.width = width;
this.height = height;
}
}

class Viewport extends pixi_viewport.Viewport {
static usedPlugins = ['drag', 'pinch'];

constructor(ctx: GlobalContext, events: EventSystem) {
super({
worldWidth: WORLD_WIDTH,
worldHeight: WORLD_HEIGHT,
events: events,
});
this.moveCenter(WORLD_WIDTH / 2, WORLD_HEIGHT / 2);
this.sortableChildren = true;
this.initializeMovement();

this.addChild(new Background());

// Circle and lines logic
this.on('click', (e) => { ctx.getMode().clickViewport(ctx, e) });
}

private initializeMovement() {
this.drag().pinch().wheel()
.clamp({ direction: 'all' })
// TODO: revisit when all icons are finalized
.clampZoom({ minHeight: 200, minWidth: 200, maxWidth: WORLD_WIDTH / 5, maxHeight: WORLD_HEIGHT / 5 });
}

enableMovement() {
for (const plugin of Viewport.usedPlugins) {
this.plugins.resume(plugin);
}
}

disableMovement() {
for (const plugin of Viewport.usedPlugins) {
this.plugins.pause(plugin);
}
}
}

Expand All @@ -42,30 +174,13 @@ class Circle extends Graphics {
super(Circle.graphicsContext);
this.position = position;
this.zIndex = 2;
this.on('click', (e) => this.onClick(ctx, e));
this.on('click', (e) => ctx.getMode().clickCircle(ctx, e, this));
this.eventMode = 'static';
}
}

onClick(ctx: GlobalContext, e: FederatedPointerEvent) {
if (ctx.mode != CursorMode.Connection) {
return;
}
e.stopPropagation();
const selected = ctx.popSelected();

if (selected === null) {
ctx.selected = this;
} else {
// TODO: this could be moved to a separate function/class
const line = new Graphics()
.moveTo(selected.x, selected.y)
.lineTo(this.x, this.y)
.stroke({ width: 2, color: 0 });
line.zIndex = 1;
ctx.viewport.addChild(line);
}
}
}
// > left_bar.ts

class LeftBar {
private leftBar: HTMLElement;
Expand All @@ -90,6 +205,8 @@ class LeftBar {
}
}

// > right_bar.ts

class RightBar {
private rightBar: HTMLElement;

Expand All @@ -101,46 +218,8 @@ class RightBar {
}
}

class Background extends Graphics {
constructor() {
super();
this.rect(0, 0, WORLD_WIDTH, WORLD_HEIGHT).fill(0xe3e2e1);
this.zIndex = 0;
}

resize(width: number, height: number) {
this.width = width;
this.height = height;
}
}

class Viewport extends pixi_viewport.Viewport {
constructor(ctx: GlobalContext, events: EventSystem) {
super({
worldWidth: WORLD_WIDTH,
worldHeight: WORLD_HEIGHT,
events: events,
});
this.moveCenter(WORLD_WIDTH / 2, WORLD_HEIGHT / 2);
this.sortableChildren = true;
ctx.setViewport(this);
this.drag().pinch().wheel()
.clamp({ direction: 'all' })
// TODO: revisit when all icons are finalized
.clampZoom({ minHeight: 200, minWidth: 200, maxWidth: WORLD_WIDTH / 5, maxHeight: WORLD_HEIGHT / 5 });

this.addChild(new Background());

// Circle and lines logic
this.on('click', (e) => {
if (ctx.mode == CursorMode.Router) {
const position = this.toWorld(e.global);
const circle = new Circle(ctx, position);
this.addChild(circle);
}
});
}
}
// > index.ts

// IIFE to avoid errors
(async () => {
Expand All @@ -161,20 +240,21 @@ class Viewport extends pixi_viewport.Viewport {
// Left bar logic
const leftBar = LeftBar.getFrom(document);

// Add move button
leftBar.addButton(HandPointer, () => { ctx.setMode(new MoveMode()) });

// Add router button
leftBar.addButton(RouterSvg, () => {
ctx.mode = CursorMode.Router;
});
leftBar.addButton(RouterSvg, () => { ctx.setMode(new RouterMode()) });

// Add connection button
leftBar.addButton(ConnectionSvg, () => {
ctx.mode = CursorMode.Connection;
});
leftBar.addButton(ConnectionSvg, () => { ctx.setMode(new ConnectionMode()) });

// Get right bar
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const rightBar = RightBar.getFrom(document);

ctx.initialize(viewport, new MoveMode());

// Ticker logic
// app.ticker.add(() => { });

Expand Down

0 comments on commit f9210db

Please sign in to comment.