Jump to content

Vue 3 and Pixi.js: Event listeners not working on DisplayObjects created as properties of classes or overridden from parent classes


ayitinya
 Share

Recommended Posts

I'm using PIXI js with vue 3 (sample code below)
Because most of the graphics follow a similar pattern with different behaviours and props, we chose to go the OOP route with typescript to avoid code duplication
But doing it this way, or we can't tell what is wrong leads to errors like `cannot find propagation path to disconnected target`  

details are provided as comments in the code

 

before delving in, here is a summary

when the DisplayObject is created as a property of a class, or overriden from a parent class
event listeners do not work, the error cannot find propagation path to disconnected target  is encountered

changing the properties of the display object also do not cause it to rerender

App.vue (It's a single page application, no router shenanigans)

const main = ref<HTMLElement | null>(null)
  
let visualisationEngine: VisualisationEngine

onMounted(() => {
  if (main.value) {
    visualisationEngine = new VisualisationEngine(main.value!!, window)
    visualisationEngine.init()
  }
})

VisualisationEngine.ts

import { Application, Graphics } from 'pixi.js'
import '@pixi/unsafe-eval' // usage here because it's 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.viewport.drag().pinch().wheel().decelerate()
    this.elem = elem

    ;(globalThis as any as GlobalThis).__PIXI_APP__ = this.app //for debugging w/ pixi devtools
  }

  init() {
    this.render()
    const background = this.drawBackground() // event listeners on app.stage didn't run an so this was done as a hack
    this.startEventHandlers(background)
  }

  render() {
    this.elem.appendChild(this.app.view)
    this.app.stage.addChild(this.viewport)
  }

  drawBackground() {
    // using background as a property of the class caused eventListeners to not be fired properly, actually at all
    // error regarding propagation occurs
    const background = new Graphics()
    background.beginFill(0x000000)
    background.drawRect(0, 0, this.elem.clientWidth, this.elem.clientHeight)
    background.endFill()
    background.eventMode = 'static'

    // this.app.stage.addChild(background)
    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()

  if (mainStore.elementToAdd !== null) {
    mainStore.elementToAdd.position.set(Math.round(event.globalX), Math.round(event.globalY))

    const elem = mainStore.elementToAdd.draw() // 
   /*
   draw draws out the graphics and returns it, this was another hack as returning the whole container caused the events launched from the graphics object    to error
   */
    if (callback) {
      callback(elem)
    }
    mainStore.onScreenElements.push(mainStore.elementToAdd)
    mainStore.elementToAdd = null

    return
  }

  ...

  mainStore.currentlySelectedElement = null
}

export const onPointerUp = (event: FederatedPointerEvent) => {
  const mainStore = useMainStore()

  if (useMainStore().draggingElement !== null) {
    useMainStore().draggingElement = null
  }
}

export const onPointerMove = (event: FederatedPointerEvent, callback: (elem: Graphics) => void) => {
if (useMainStore().draggingElement !== null) {
    useMainStore().draggingElement!!.move(event.globalX, event.globalY) // nothing is updated on screen
    console.log(useMainStore().draggingElement!!.position) // but the x, y of the container changes

	/*
    trying to call element.parent yields null, and errors because it has lost reference to it's parent
    */
  }
}

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

  id: number = Math.floor(Math.random() * 1000000)
  readonly store = defineStore(`${this.constructor.name}-${this.id}`, () => {
    const name = ref(this.name)
    const value = ref<{ [key: string]: { [key: string]: string } }>({})

    return { name, value }
  })

  protected constructor(x: number = 0, y: number = 0, name: string, nodes: number[]) {
    super()
    this.nodes = nodes

    this.x = x
    this.y = y

    this.useStore = this.store()

    this.useStore.$patch({
      name: name
    })
  }

  abstract draw(): Graphics

  initializeEventListeners(graphic: Graphics) {
    // this function is called in the draw method and the graphic object passed as argument
    graphic.hitArea = graphic.getBounds()
    graphic.eventMode = 'static'

    graphic.on(
      'pointerdown',
      () => {
        if (useMainStore().currentAction === 'move') {
          useMainStore().draggingElement = this
        } else {
          useMainStore().currentlySelectedElement = this
        }
      },
      this
    )

    graphic.on('pointerup', () => {
      console.log('pointer up')
    })
  }

  move(x: number, y: number) {
    this.x = x
    this.y = y
  }
}

a 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() {
    
    // plan was to make all graphics properties of the parent class, but doing so just prevent all events from being triggerd
    const resistorGraphic = new Graphics()

    const body = new Graphics()

    resistorGraphic
      .lineStyle(DIMENSIONS.CONDUCTOR_WIDTH, COLORS.CONDUCTOR)
      .moveTo(this.x, this.y)
      ...

    body.addChild(resistorGraphic)

    this.addConductor().forEach((conductor) => {
      body.addChild(conductor)
    })

    this.initializeEventListeners(resistorGraphic)

    return body
  }
}

How are the elements added to the canvas

The user is required to select from several elements. On selecting, a new object is created with the selected element and added to the global pinia store instance like 
 

mainStore.setElementToAdd(new Resistor(...))

setElementToAdd(element: BaseElement) {
...
state.elementToAdd = element
...
}

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...