Utilizing PIXI js in conjunction with Vue 3 (see code snippet below) Due to the consistent pattern of most graphics with varying behaviors and properties, we opted for an OOP approach with TypeScript to prevent code duplication. However, following this approach often results in errors such as
cannot find propagation path to disconnected target
More information is provided in comments within the code
Before diving in, here's a brief overview
When the DisplayObject is instantiated as a class property or overridden from a parent class, event listeners do not function correctly, resulting in the
cannot find propagation path to disconnected target
error
Additionally, modifying the properties of the display object does not trigger a rerender
<script setup lang="ts">
const main = ref<HTMLElement | null>(null)
let visualisationEngine: VisualisationEngine
onMounted(() => {
if (main.value) {
visualisationEngine = new VisualisationEngine(main.value!!, window)
visualisationEngine.init()
}
})
</script>
VisualisationEngine.ts
import { Application, Graphics } from 'pixi.js'
import '@pixi/unsafe-eval' // used for an electron app
import { onEscKeyDown, onPointerDown, onPointerMove, onPointerUp } from '@/visualisation_engine/eventHandlers'
import { Viewport } from 'pixi-viewport'
interface GlobalThis {
__PIXI_APP__: Application<HTMLCanvasElement>
}
export class VisualisationEngine {
app: Application<HTMLCanvasElement>
elem: HTMLElement
window: Window
viewport: Viewport
constructor(elem: HTMLElement, window: Window) {
this.window = window
this.app = new Application<HTMLCanvasElement>({
antialias: true,
autoDensity: true,
resizeTo: elem,
hello: true
})
this.viewport = new Viewport({
...
})
this.elem = elem
;(globalThis as any as GlobalThis).__PIXI_APP__ = this.app //for debugging w/ pixi devtools
}
init() {
this.render()
const background = this.drawBackground()
this.startEventHandlers(background)
}
render() {
this.elem.appendChild(this.app.view)
this.app.stage.addChild(this.viewport)
}
drawBackground() {
const background = new Graphics()
...
this.viewport.addChild(background)
return background
}
startEventHandlers(graphic: Graphics) {
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
onEscKeyDown()
}
})
graphic
.on('pointerdown', (event) => {
onPointerDown(event, (elem) => this.viewport.addChild(elem))
})
.on('pointermove', (event) => {
onPointerMove(event, (elem) => this.viewport.addChild(elem))
})
.on('pointerup', (event) => onPointerUp(event))
}
}
eventListeners.ts
export const onPointerDown = (event: FederatedPointerEvent, callback: (elem: Graphics) => void) => {
const mainStore = useMainStore()
...
}
export const onPointerUp = (event: FederatedPointerEvent) => {
const mainStore = useMainStore()
...
}
export const onPointerMove = (event: FederatedPointerEvent, callback: (elem: Graphics) => void) => {
...
}
BaseElement.ts
import { defineStore } from 'pinia'
import { Container, Graphics } from 'pixi.js'
import { useMainStore } from '@/store/main'
import { ref } from 'vue'
export abstract class BaseElement extends Container {
useStore
nodes: number[]
abstract readonly type: string
...
protected constructor(x: number = 0, y: number = 0, name: string, nodes: number[]) {
...
}
abstract draw(): Graphics
initializeEventListeners(graphic: Graphics) {
...
}
move(x: number, y: number) {
...
}
}
Sample element
import { BaseElement } from './BaseElement'
import { Graphics, Point } from 'pixi.js'
export class Resistor extends BaseElement {
override type = 'Resistor'
constructor(x: number, y: number, nodes: [number, number], name: string = 'R', resistance: number) {
super(x, y, name, nodes)
}
override draw() {
...
}
}
How are the elements added to the canvas
The user selects from various elements, and upon selection, a new object is created with the chosen element and added to the global pinia store instance like
mainStore.setElementToAdd(new Resistor(...))
setElementToAdd(element: BaseElement) {
...
}