From a126d73c5681837dc6b8230f008ee1f8bf1deccb Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Fri, 12 Sep 2025 16:56:58 +0900 Subject: [PATCH 1/4] fix --- .gitignore | 3 ++- src/display/elements/Relations.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 388cb3b4..b239677f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode node_modules dist -*.tgz \ No newline at end of file +*.tgz +src/tests/*/__screenshots__ diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index c07a12f3..194a3d3a 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -49,6 +49,7 @@ export class Relations extends ComposedRelations { const { links } = this.props; if (!this.path) return; this.path.clear(); + this.path.links.length = 0; let lastPoint = null; for (const link of links) { @@ -81,6 +82,7 @@ export class Relations extends ComposedRelations { this.path.moveTo(...sourcePoint); } this.path.lineTo(...targetPoint); + this.path.links.push({ sourcePoint, targetPoint }); lastPoint = targetPoint; } this.path.stroke(); From 6b46d889b541b3ed80e5ab0b94710b5dbba8b95e Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Fri, 12 Sep 2025 16:57:23 +0900 Subject: [PATCH 2/4] separate point calculation logic --- src/display/elements/Relations.js | 35 ++++++++++++++++++------------- src/display/mixins/linksable.js | 2 ++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index 194a3d3a..e7baa87c 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -11,6 +11,7 @@ const ComposedRelations = mixins(Element, Linksable, Relationstyleable); export class Relations extends ComposedRelations { static isSelectable = true; static hitScope = 'children'; + linkPoints = []; _renderDirty = true; @@ -25,7 +26,7 @@ export class Relations extends ComposedRelations { initPath() { const path = new Graphics(); - Object.assign(path, { type: 'path', links: [], allowContainsPoint: true }); + Object.assign(path, { type: 'path', allowContainsPoint: true }); this.addChild(path); return path; } @@ -46,12 +47,28 @@ export class Relations extends ComposedRelations { } renderLink() { - const { links } = this.props; if (!this.path) return; this.path.clear(); - this.path.links.length = 0; let lastPoint = null; + for (const points of this.linkPoints) { + const { sourcePoint, targetPoint } = points; + if ( + !lastPoint || + lastPoint[0] !== sourcePoint[0] || + lastPoint[1] !== sourcePoint[1] + ) { + this.path.moveTo(...sourcePoint); + } + this.path.lineTo(...targetPoint); + lastPoint = targetPoint; + } + this.path.stroke(); + } + + _calculateLinkPoints() { + this.linkPoints.length = 0; + const { links } = this.props; for (const link of links) { const sourceObject = this.linkedObjects?.[link.source]; const targetObject = this.linkedObjects?.[link.target]; @@ -74,17 +91,7 @@ export class Relations extends ComposedRelations { const sourcePoint = [sourceBounds.x, sourceBounds.y]; const targetPoint = [targetBounds.x, targetBounds.y]; - if ( - !lastPoint || - lastPoint[0] !== sourcePoint[0] || - lastPoint[1] !== sourcePoint[1] - ) { - this.path.moveTo(...sourcePoint); - } - this.path.lineTo(...targetPoint); - this.path.links.push({ sourcePoint, targetPoint }); - lastPoint = targetPoint; + this.linkPoints.push({ sourcePoint, targetPoint }); } - this.path.stroke(); } } diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index c0f4d8a3..65e7c3fd 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -36,6 +36,7 @@ export const Linksable = (superClass) => { linkedObj === changedObject || isAncestor(changedObject, linkedObj) ) { + this._calculateLinkPoints(); this._renderDirty = true; return; } @@ -45,6 +46,7 @@ export const Linksable = (superClass) => { _applyLinks(relevantChanges) { const { links } = relevantChanges; this.linkedObjects = uniqueLinked(this.context.viewport, links); + this._calculateLinkPoints(); this._renderDirty = true; } }; From 817cb2f0dbd809f1b7b8ed843a139ec1c6c1e6c7 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Fri, 12 Sep 2025 17:14:09 +0900 Subject: [PATCH 3/4] add test --- src/tests/render/Relations.test.js | 124 +++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/tests/render/Relations.test.js diff --git a/src/tests/render/Relations.test.js b/src/tests/render/Relations.test.js new file mode 100644 index 00000000..b01c92b5 --- /dev/null +++ b/src/tests/render/Relations.test.js @@ -0,0 +1,124 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { setupPatchmapTests } from './patchmap.setup'; + +describe('Relations Component Rendering Tests', () => { + const { getPatchmap } = setupPatchmapTests(); + + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + const baseMapData = [ + { type: 'item', id: 'item-A', size: 50, attrs: { x: 100, y: 100 } }, + { type: 'item', id: 'item-B', size: 50, attrs: { x: 300, y: 100 } }, + { type: 'item', id: 'item-C', size: 50, attrs: { x: 200, y: 300 } }, + { + type: 'relations', + id: 'rel-1', + links: [{ source: 'item-A', target: 'item-B' }], + style: { width: 2, color: 'primary.default' }, // 0x0c73bf + }, + ]; + + const getRelations = (patchmap) => { + return patchmap.selector('$..[?(@.id=="rel-1")]')[0]; + }; + + const getPath = (patchmap) => { + return getRelations(patchmap).children[0]; + }; + + it('should render correctly with initial properties', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const path = getPath(patchmap); + + expect(relations).toBeDefined(); + expect(path).toBeDefined(); + expect(path.type).toBe('path'); + + expect(relations.props.links).toHaveLength(1); + expect(relations.props.style.width).toBe(2); + + expect(relations.linkPoints).toHaveLength(1); + const points = relations.linkPoints[0]; + const itemA = patchmap.selector('$..[?(@.id=="item-A")]')[0]; + const itemB = patchmap.selector('$..[?(@.id=="item-B")]')[0]; + + expect(points.sourcePoint).toEqual([itemA.x, itemA.y]); + expect(points.targetPoint).toEqual([itemB.x, itemB.y]); + }); + + it('should update path style when the "style" property changes', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + patchmap.update({ + path: '$..[?(@.id=="rel-1")]', + changes: { style: { width: 5, color: 'primary.accent' } }, // 0xef4444 + }); + + const path = getPath(patchmap); + expect(path.strokeStyle.width).toBe(5); + expect(path.strokeStyle.color).toBe(0xef4444); + }); + + it('should recalculate points and redraw when "links" property changes', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const originalPointsLength = relations.linkPoints.length; + expect(originalPointsLength).toBe(1); + + patchmap.update({ + path: '$..[?(@.id=="rel-1")]', + changes: { + links: [{ source: 'item-B', target: 'item-C' }], + }, + }); + await vi.advanceTimersByTimeAsync(100); + + const updatedRelations = getRelations(patchmap); + expect(updatedRelations.props.links).toHaveLength(2); + expect(updatedRelations.linkPoints).toHaveLength(2); + + const newPoints = updatedRelations.linkPoints[1]; + const itemB = patchmap.selector('$..[?(@.id=="item-B")]')[0]; + const itemC = patchmap.selector('$..[?(@.id=="item-C")]')[0]; + expect(newPoints.sourcePoint).toEqual([itemB.x, itemB.y]); + expect(newPoints.targetPoint).toEqual([itemC.x, itemC.y]); + }); + + it('should recalculate points and redraw when a linked item is moved', async () => { + const patchmap = getPatchmap(); + patchmap.draw(baseMapData); + await vi.advanceTimersByTimeAsync(100); + + const relations = getRelations(patchmap); + const originalPoint = [...relations.linkPoints[0].targetPoint]; + expect(originalPoint).toEqual([300, 100]); + + patchmap.update({ + path: '$..[?(@.id=="item-B")]', + changes: { attrs: { x: 400, y: 250 } }, + }); + + await vi.advanceTimersByTimeAsync(100); + + const updatedPoints = relations.linkPoints[0].targetPoint; + expect(updatedPoints).not.toEqual(originalPoint); + expect(updatedPoints).toEqual([400, 250]); + + const path = getPath(patchmap); + expect(path.getBounds().width).toBeGreaterThan(0); + }); +}); From 482c0783d47cfe28120cb918214521994d5da6f0 Mon Sep 17 00:00:00 2001 From: MinHo Lim Date: Fri, 12 Sep 2025 17:29:54 +0900 Subject: [PATCH 4/4] fix --- src/display/elements/Relations.js | 30 ++++++++++++------------------ src/display/mixins/linksable.js | 2 -- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/display/elements/Relations.js b/src/display/elements/Relations.js index e7baa87c..f890d136 100644 --- a/src/display/elements/Relations.js +++ b/src/display/elements/Relations.js @@ -47,28 +47,12 @@ export class Relations extends ComposedRelations { } renderLink() { + const { links } = this.props; if (!this.path) return; this.path.clear(); + this.linkPoints.length = 0; let lastPoint = null; - for (const points of this.linkPoints) { - const { sourcePoint, targetPoint } = points; - if ( - !lastPoint || - lastPoint[0] !== sourcePoint[0] || - lastPoint[1] !== sourcePoint[1] - ) { - this.path.moveTo(...sourcePoint); - } - this.path.lineTo(...targetPoint); - lastPoint = targetPoint; - } - this.path.stroke(); - } - - _calculateLinkPoints() { - this.linkPoints.length = 0; - const { links } = this.props; for (const link of links) { const sourceObject = this.linkedObjects?.[link.source]; const targetObject = this.linkedObjects?.[link.target]; @@ -91,7 +75,17 @@ export class Relations extends ComposedRelations { const sourcePoint = [sourceBounds.x, sourceBounds.y]; const targetPoint = [targetBounds.x, targetBounds.y]; + if ( + !lastPoint || + lastPoint[0] !== sourcePoint[0] || + lastPoint[1] !== sourcePoint[1] + ) { + this.path.moveTo(...sourcePoint); + } + this.path.lineTo(...targetPoint); + lastPoint = targetPoint; this.linkPoints.push({ sourcePoint, targetPoint }); } + this.path.stroke(); } } diff --git a/src/display/mixins/linksable.js b/src/display/mixins/linksable.js index 65e7c3fd..c0f4d8a3 100644 --- a/src/display/mixins/linksable.js +++ b/src/display/mixins/linksable.js @@ -36,7 +36,6 @@ export const Linksable = (superClass) => { linkedObj === changedObject || isAncestor(changedObject, linkedObj) ) { - this._calculateLinkPoints(); this._renderDirty = true; return; } @@ -46,7 +45,6 @@ export const Linksable = (superClass) => { _applyLinks(relevantChanges) { const { links } = relevantChanges; this.linkedObjects = uniqueLinked(this.context.viewport, links); - this._calculateLinkPoints(); this._renderDirty = true; } };