diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 774a1a12..9014188e 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -1,6 +1,5 @@ import { Graphics } from 'pixi.js'; import { calcOrientedBounds } from '../../utils/bounds'; -import { selector } from '../../utils/selector/selector'; import { relationsSchema } from '../data-schema/element-schema'; import { Relationstyleable } from '../mixins/Relationstyleable'; import { Linksable } from '../mixins/linksable'; @@ -14,14 +13,10 @@ export class Relations extends ComposedRelations { static hitScope = 'children'; _renderDirty = true; - _renderOnNextTick = false; constructor(context) { super({ type: 'relations', context }); - this.initPath(); - - this._updateTransform = this._updateTransform.bind(this); - this.context.viewport.app.ticker.add(this._updateTransform); + this.path = this.initPath(); } update(changes, options) { @@ -33,30 +28,28 @@ export class Relations extends ComposedRelations { path.setStrokeStyle({ color: 'black' }); Object.assign(path, { type: 'path', links: [] }); this.addChild(path); + return path; } - _updateTransform() { - if (this._renderOnNextTick) { - this.renderLink(); - this._renderOnNextTick = false; - } + _afterRender() { + super._afterRender(); + this._onUpdate(); + } + _onUpdate() { if (this._renderDirty) { - this._renderOnNextTick = true; - this._renderDirty = false; + try { + this.renderLink(); + } finally { + this._renderDirty = false; + } } } - destroy(options) { - this.context.viewport.app.ticker.remove(this._updateTransform); - super.destroy(options); - } - renderLink() { const { links } = this.props; - const path = selector(this, '$.children[?(@.type==="path")]')[0]; - if (!path) return; - path.clear(); + if (!this.path) return; + this.path.clear(); let lastPoint = null; for (const link of links) { @@ -86,11 +79,11 @@ export class Relations extends ComposedRelations { lastPoint[0] !== sourcePoint[0] || lastPoint[1] !== sourcePoint[1] ) { - path.moveTo(...sourcePoint); + this.path.moveTo(...sourcePoint); } - path.lineTo(...targetPoint); + this.path.lineTo(...targetPoint); lastPoint = targetPoint; } - path.stroke(); + this.path.stroke(); } } diff --git a/src/display/mixins/Base.js b/src/display/mixins/Base.js index 8a53af26..18b8a731 100644 --- a/src/display/mixins/Base.js +++ b/src/display/mixins/Base.js @@ -1,3 +1,4 @@ +import { Matrix } from 'pixi.js'; import { isValidationError } from 'zod-validation-error'; import { deepMerge } from '../../utils/deepmerge/deepmerge'; import { diffJson } from '../../utils/diff/diff-json'; @@ -5,6 +6,8 @@ import { validate } from '../../utils/validator'; import { deepPartial } from '../../utils/zod-deep-strict-partial'; import { Type } from './Type'; +const tempMatrix = new Matrix(); + export const Base = (superClass) => { return class extends Type(superClass) { static _handlerMap = new Map(); @@ -16,12 +19,34 @@ export const Base = (superClass) => { super(rest); this.#context = context; this.props = {}; + + this._lastLocalTransform = tempMatrix.clone(); + this.onRender = () => { + this._onObjectUpdate(); + this._afterRender(); + }; } get context() { return this.#context; } + _afterRender() {} + + _onObjectUpdate() { + if (!this.localTransform || !this.visible) return; + + if (!this.localTransform.equals(this._lastLocalTransform)) { + this.context.viewport.emit('object_transformed', this); + this._lastLocalTransform.copyFrom(this.localTransform); + } + } + + destroy(options) { + this.onRender = null; + super.destroy(options); + } + static registerHandler(keys, handler, stage) { if (!Object.prototype.hasOwnProperty.call(this, '_handlerRegistry')) { this._handlerRegistry = new Map(this._handlerRegistry); diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index dafe8fe7..c0f4d8a3 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -5,6 +5,43 @@ const KEYS = ['links']; export const Linksable = (superClass) => { const MixedClass = class extends superClass { + constructor(options = {}) { + super(options); + + this._boundOnObjectTransformed = this._onObjectTransformed.bind(this); + this.context?.viewport?.on( + 'object_transformed', + this._boundOnObjectTransformed, + ); + } + + destroy(options) { + if (this.context?.viewport) { + this.context?.viewport?.off( + 'object_transformed', + this._boundOnObjectTransformed, + ); + } + super.destroy(options); + } + + _onObjectTransformed(changedObject) { + if (this._renderDirty) return; + if (!this.linkedObjects) return; + + for (const linkedObj of Object.values(this.linkedObjects)) { + if (!linkedObj || linkedObj.destroyed) continue; + + if ( + linkedObj === changedObject || + isAncestor(changedObject, linkedObj) + ) { + this._renderDirty = true; + return; + } + } + } + _applyLinks(relevantChanges) { const { links } = relevantChanges; this.linkedObjects = uniqueLinked(this.context.viewport, links); @@ -19,6 +56,22 @@ export const Linksable = (superClass) => { return MixedClass; }; +const isAncestor = (parent, target) => { + if (!target || !parent) return false; + + let current = target.parent; + while (current) { + if (current === parent) { + return true; + } + if (current.type === 'canvas' || !current.parent) { + return false; + } + current = current.parent; + } + return false; +}; + const uniqueLinked = (viewport, links) => { const uniqueIds = new Set(links.flatMap((link) => Object.values(link))); const objects = collectCandidates(viewport, (child) =>