diff --git a/src/assets/hand_pointer.svg b/src/assets/hand_pointer.svg new file mode 100644 index 0000000..6c3677c --- /dev/null +++ b/src/assets/hand_pointer.svg @@ -0,0 +1,2 @@ + + diff --git a/src/index.ts b/src/index.ts index 3b4d194..5f23ac9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,13 @@ // 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'; @@ -11,27 +15,155 @@ 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); + } } } @@ -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; @@ -90,6 +205,8 @@ class LeftBar { } } +// > right_bar.ts + class RightBar { private rightBar: HTMLElement; @@ -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 () => { @@ -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(() => { });