From f35a2b8f94e9ebd807d2c0771e8e21ab42b28ae2 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Wed, 16 Jul 2025 19:11:15 +0900 Subject: [PATCH 1/7] fix relations trigger --- src/display/elements/Relations.js | 37 ++++++++++--------------------- src/display/mixins/Base.js | 18 +++++++++++++++ src/display/mixins/linksable.js | 18 ++++++++++++++- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 774a1a12..d1f4faa2 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,11 @@ 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(); + this.onRender = this._onUpdate; } update(changes, options) { @@ -33,30 +29,19 @@ 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; - } - + _onUpdate() { if (this._renderDirty) { - this._renderOnNextTick = true; - this._renderDirty = false; + this.renderLink(); } } - 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 +71,13 @@ 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(); + + this._renderDirty = false; } } diff --git a/src/display/mixins/Base.js b/src/display/mixins/Base.js index 8a53af26..26e62112 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'; @@ -16,12 +17,29 @@ export const Base = (superClass) => { super(rest); this.#context = context; this.props = {}; + + this._lastGroupTransform = new Matrix(); + this.onRender = this._onObjectUpdate; } get context() { return this.#context; } + _onObjectUpdate() { + if (!this.groupTransform || !this.visible) return; + + if (!this.groupTransform.equals(this._lastGroupTransform)) { + this.emit('transform_updated', this); + this._lastGroupTransform.copyFrom(this.groupTransform); + } + } + + 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..93d99a16 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -5,10 +5,26 @@ const KEYS = ['links']; export const Linksable = (superClass) => { const MixedClass = class extends superClass { + _onLinkedObjectUpdate = () => { + this._renderDirty = true; + }; + _applyLinks(relevantChanges) { const { links } = relevantChanges; + if (this.linkedObjects) { + Object.values(this.linkedObjects).forEach((obj) => { + if (obj) { + obj.off('transform_updated', this._onLinkedObjectUpdate); + } + }); + } + this.linkedObjects = uniqueLinked(this.context.viewport, links); - this._renderDirty = true; + Object.values(this.linkedObjects).forEach((obj) => { + if (obj) { + obj.on('transform_updated', this._onLinkedObjectUpdate, this); + } + }); } }; MixedClass.registerHandler( From 787d7c1c4afcadf685bd60ee5c9e6e21623da7c3 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 12:28:08 +0900 Subject: [PATCH 2/7] fix --- src/display/elements/Relations.js | 5 +++++ src/display/mixins/Base.js | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index d1f4faa2..8abb4e03 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -24,6 +24,11 @@ export class Relations extends ComposedRelations { super.update(changes, relationsSchema, options); } + destroy(options) { + this.onRender = null; + super.destroy(options); + } + initPath() { const path = new Graphics(); path.setStrokeStyle({ color: 'black' }); diff --git a/src/display/mixins/Base.js b/src/display/mixins/Base.js index 26e62112..cce7e7ba 100644 --- a/src/display/mixins/Base.js +++ b/src/display/mixins/Base.js @@ -6,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(); @@ -18,7 +20,7 @@ export const Base = (superClass) => { this.#context = context; this.props = {}; - this._lastGroupTransform = new Matrix(); + this._lastGroupTransform = tempMatrix.clone(); this.onRender = this._onObjectUpdate; } From 3b5c23eab4cbeded515eb3d619702e678e1f135f Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 12:58:04 +0900 Subject: [PATCH 3/7] refactor _applyLinks --- src/display/mixins/linksable.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index 93d99a16..73aa9e17 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -11,20 +11,34 @@ export const Linksable = (superClass) => { _applyLinks(relevantChanges) { const { links } = relevantChanges; - if (this.linkedObjects) { - Object.values(this.linkedObjects).forEach((obj) => { + + const oldLinkedObjects = this.linkedObjects || {}; + const oldIds = new Set(Object.keys(oldLinkedObjects)); + const newLinkedObjects = uniqueLinked(this.context.viewport, links); + const newIds = new Set(Object.keys(newLinkedObjects)); + + oldIds.forEach((id) => { + if (!newIds.has(id)) { + const obj = oldLinkedObjects[id]; if (obj) { obj.off('transform_updated', this._onLinkedObjectUpdate); } - }); - } + } + }); - this.linkedObjects = uniqueLinked(this.context.viewport, links); - Object.values(this.linkedObjects).forEach((obj) => { - if (obj) { - obj.on('transform_updated', this._onLinkedObjectUpdate, this); + newIds.forEach((id) => { + if (!oldIds.has(id)) { + const obj = newLinkedObjects[id]; + if (obj) { + obj.on('transform_updated', this._onLinkedObjectUpdate, this); + } } }); + + this.linkedObjects = newLinkedObjects; + if (newIds.size === 0) { + this._onLinkedObjectUpdate(); + } } }; MixedClass.registerHandler( From 6a70cc60e77a8bafc22989b135823691c81295bf Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 14:06:25 +0900 Subject: [PATCH 4/7] fix --- src/display/mixins/linksable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index 73aa9e17..2f04a5bb 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -21,7 +21,7 @@ export const Linksable = (superClass) => { if (!newIds.has(id)) { const obj = oldLinkedObjects[id]; if (obj) { - obj.off('transform_updated', this._onLinkedObjectUpdate); + obj.off('transform_updated', this._onLinkedObjectUpdate, this); } } }); From 44d59ebe130880ed3aa0d6defbd6907577e332ff Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 14:14:52 +0900 Subject: [PATCH 5/7] fix --- src/display/elements/Relations.js | 8 +++++--- src/display/mixins/linksable.js | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 8abb4e03..d613ec30 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -39,7 +39,11 @@ export class Relations extends ComposedRelations { _onUpdate() { if (this._renderDirty) { - this.renderLink(); + try { + this.renderLink(); + } finally { + this._renderDirty = false; + } } } @@ -82,7 +86,5 @@ export class Relations extends ComposedRelations { lastPoint = targetPoint; } this.path.stroke(); - - this._renderDirty = false; } } diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index 2f04a5bb..b2cafe44 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -36,9 +36,7 @@ export const Linksable = (superClass) => { }); this.linkedObjects = newLinkedObjects; - if (newIds.size === 0) { - this._onLinkedObjectUpdate(); - } + this._onLinkedObjectUpdate(); } }; MixedClass.registerHandler( From c34e321a102d48e0e4f5dd44a526bd37987124ae Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 16:18:21 +0900 Subject: [PATCH 6/7] fix event push --- src/display/elements/Relations.js | 11 ++--- src/display/mixins/Base.js | 10 ++-- src/display/mixins/linksable.js | 82 ++++++++++++++++++++----------- 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index d613ec30..2d8d3e96 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -17,18 +17,17 @@ export class Relations extends ComposedRelations { constructor(context) { super({ type: 'relations', context }); this.path = this.initPath(); - this.onRender = this._onUpdate; + const baseOnRender = this.onRender.bind(this); + this.onRender = () => { + baseOnRender(); + this._onUpdate(); + }; } update(changes, options) { super.update(changes, relationsSchema, options); } - destroy(options) { - this.onRender = null; - super.destroy(options); - } - initPath() { const path = new Graphics(); path.setStrokeStyle({ color: 'black' }); diff --git a/src/display/mixins/Base.js b/src/display/mixins/Base.js index cce7e7ba..6e041787 100644 --- a/src/display/mixins/Base.js +++ b/src/display/mixins/Base.js @@ -20,7 +20,7 @@ export const Base = (superClass) => { this.#context = context; this.props = {}; - this._lastGroupTransform = tempMatrix.clone(); + this._lastLocalTransform = tempMatrix.clone(); this.onRender = this._onObjectUpdate; } @@ -29,11 +29,11 @@ export const Base = (superClass) => { } _onObjectUpdate() { - if (!this.groupTransform || !this.visible) return; + if (!this.localTransform || !this.visible) return; - if (!this.groupTransform.equals(this._lastGroupTransform)) { - this.emit('transform_updated', this); - this._lastGroupTransform.copyFrom(this.groupTransform); + if (!this.localTransform.equals(this._lastLocalTransform)) { + this.context.viewport.emit('object_transformed', this); + this._lastLocalTransform.copyFrom(this.localTransform); } } diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index b2cafe44..9fbe4462 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -5,38 +5,48 @@ const KEYS = ['links']; export const Linksable = (superClass) => { const MixedClass = class extends superClass { - _onLinkedObjectUpdate = () => { - this._renderDirty = true; - }; + constructor(options = {}) { + super(options); - _applyLinks(relevantChanges) { - const { links } = relevantChanges; + this._boundOnObjectTransformed = this._onObjectTransformed.bind(this); + this.context.viewport.on( + 'object_transformed', + this._boundOnObjectTransformed, + ); + } - const oldLinkedObjects = this.linkedObjects || {}; - const oldIds = new Set(Object.keys(oldLinkedObjects)); - const newLinkedObjects = uniqueLinked(this.context.viewport, links); - const newIds = new Set(Object.keys(newLinkedObjects)); - - oldIds.forEach((id) => { - if (!newIds.has(id)) { - const obj = oldLinkedObjects[id]; - if (obj) { - obj.off('transform_updated', this._onLinkedObjectUpdate, this); - } - } - }); - - newIds.forEach((id) => { - if (!oldIds.has(id)) { - const obj = newLinkedObjects[id]; - if (obj) { - obj.on('transform_updated', this._onLinkedObjectUpdate, this); - } + 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 key in this.linkedObjects) { + const linkedObj = this.linkedObjects[key]; + if (!linkedObj || linkedObj.destroyed) continue; + + if ( + linkedObj === changedObject || + isAncestor(changedObject, linkedObj) + ) { + this._renderDirty = true; + return; } - }); + } + } - this.linkedObjects = newLinkedObjects; - this._onLinkedObjectUpdate(); + _applyLinks(relevantChanges) { + const { links } = relevantChanges; + this.linkedObjects = uniqueLinked(this.context.viewport, links); + this._renderDirty = true; } }; MixedClass.registerHandler( @@ -47,6 +57,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) => From c83eaaa9f61eb2360ae38286f246049b64601f50 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Thu, 17 Jul 2025 16:59:53 +0900 Subject: [PATCH 7/7] fix --- src/display/elements/Relations.js | 10 +++++----- src/display/mixins/Base.js | 7 ++++++- src/display/mixins/linksable.js | 9 ++++----- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 2d8d3e96..9014188e 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -17,11 +17,6 @@ export class Relations extends ComposedRelations { constructor(context) { super({ type: 'relations', context }); this.path = this.initPath(); - const baseOnRender = this.onRender.bind(this); - this.onRender = () => { - baseOnRender(); - this._onUpdate(); - }; } update(changes, options) { @@ -36,6 +31,11 @@ export class Relations extends ComposedRelations { return path; } + _afterRender() { + super._afterRender(); + this._onUpdate(); + } + _onUpdate() { if (this._renderDirty) { try { diff --git a/src/display/mixins/Base.js b/src/display/mixins/Base.js index 6e041787..18b8a731 100644 --- a/src/display/mixins/Base.js +++ b/src/display/mixins/Base.js @@ -21,13 +21,18 @@ export const Base = (superClass) => { this.props = {}; this._lastLocalTransform = tempMatrix.clone(); - this.onRender = this._onObjectUpdate; + this.onRender = () => { + this._onObjectUpdate(); + this._afterRender(); + }; } get context() { return this.#context; } + _afterRender() {} + _onObjectUpdate() { if (!this.localTransform || !this.visible) return; diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index 9fbe4462..c0f4d8a3 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -9,15 +9,15 @@ export const Linksable = (superClass) => { super(options); this._boundOnObjectTransformed = this._onObjectTransformed.bind(this); - this.context.viewport.on( + this.context?.viewport?.on( 'object_transformed', this._boundOnObjectTransformed, ); } destroy(options) { - if (this?.context?.viewport) { - this.context.viewport.off( + if (this.context?.viewport) { + this.context?.viewport?.off( 'object_transformed', this._boundOnObjectTransformed, ); @@ -29,8 +29,7 @@ export const Linksable = (superClass) => { if (this._renderDirty) return; if (!this.linkedObjects) return; - for (const key in this.linkedObjects) { - const linkedObj = this.linkedObjects[key]; + for (const linkedObj of Object.values(this.linkedObjects)) { if (!linkedObj || linkedObj.destroyed) continue; if (