refactor: focus-element, lasso-select, brush-select behavior (#5707)

* refactor: focus-element behavior

* refactor: lasso select, brush select

* fix: remove unnecessary utils
This commit is contained in:
Yuxin 2024-05-08 17:04:47 +08:00 committed by GitHub
parent f0cbeba90a
commit 529a5ad246
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 389 additions and 708 deletions

View File

@ -43,6 +43,7 @@ export const behaviorBrushSelect: TestCase = async (context) => {
behaviors: [ behaviors: [
{ {
type: 'brush-select', type: 'brush-select',
key: 'brush-select',
trigger: 'drag', trigger: 'drag',
}, },
], ],
@ -50,5 +51,16 @@ export const behaviorBrushSelect: TestCase = async (context) => {
await graph.render(); await graph.render();
behaviorBrushSelect.form = (panel) => {
const config = {
mode: 'default',
};
const handleChange = () => {
graph.updateBehavior({ key: 'brush-select', ...config });
};
return [panel.add(config, 'mode', ['union', 'default', 'intersect', 'diff']).onChange(handleChange)];
};
return graph; return graph;
}; };

View File

@ -22,7 +22,7 @@ export const behaviorFocusElement: TestCase = async (context) => {
edge: { edge: {
style: { endArrow: true }, style: { endArrow: true },
}, },
behaviors: [{ type: 'focus-element' }], behaviors: ['focus-element'],
}); });
await graph.render(); await graph.render();

View File

@ -40,12 +40,7 @@ export const behaviorLassoSelect: TestCase = async (context) => {
}, },
}, },
animation: false, animation: false,
behaviors: [ behaviors: [{ type: 'lasso-select', key: 'lasso-select', trigger: 'drag' }],
{
type: 'lasso-select',
trigger: 'drag',
},
],
}); });
await graph.render(); await graph.render();

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(238,246,255,1)" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(238,246,255,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(238,246,255,1)" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 200,0 l 0,200 l-200 0 z" width="200" height="200"/> <path fill="rgba(238,246,255,1)" d="M 0,0 l 200,0 l 0,200 l-200 0 z" width="200" height="200" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -121,7 +121,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300"/> <path fill="rgba(0,128,0,1)" d="M 0,0 l 300,0 l 0,300 l-300 0 z" width="300" height="300" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -16,12 +16,8 @@
<g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)"> <g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)">
<g fill="none" marker-start="false" marker-end="false" stroke="transparent" stroke-width="3"/> <g fill="none" marker-start="false" marker-end="false" stroke="transparent" stroke-width="3"/>
<g transform="matrix(1,0,0,1,160.733124,205.366562)"> <g transform="matrix(1,0,0,1,160.733124,205.366562)">
<path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="12" stroke="rgba(153,173,209,1)" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/> <path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="1" stroke="rgba(153,173,209,1)"/>
<path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="14" stroke="transparent" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/> <path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="1" stroke="transparent"/>
</g>
<g transform="matrix(1,0,0,1,160.733124,205.366562)">
<path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="2" stroke="rgba(153,173,209,1)"/>
<path fill="none" d="M 0,39.266873708001015 L 78.53374741600203,0" stroke-width="2" stroke="transparent"/>
</g> </g>
</g> </g>
<g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)"> <g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)">
@ -68,14 +64,11 @@
</g> </g>
<g fill="none" transform="matrix(1,0,0,1,250,200)"> <g fill="none" transform="matrix(1,0,0,1,250,200)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<circle fill="none" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="12" stroke="rgba(23,131,255,1)" stroke-opacity="0.25" r="12" stroke-dasharray="0,0" pointer-events="none"/> <circle fill="rgba(23,131,255,1)" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="0" stroke="rgba(0,0,0,1)" r="12"/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<circle fill="rgba(23,131,255,1)" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="3" stroke="rgba(0,0,0,1)" r="12"/>
</g> </g>
<g fill="none" transform="matrix(1,0,0,1,0,12)"> <g fill="none" transform="matrix(1,0,0,1,0,12)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<text fill="rgba(0,0,0,0.8509803921568627)" dominant-baseline="central" paint-order="stroke" dx="0.5" dy="13.5px" font-size="14" text-anchor="middle" font-weight="700"> <text fill="rgba(0,0,0,0.8509803921568627)" dominant-baseline="central" paint-order="stroke" dx="0.5" dy="11.5px" font-size="12" text-anchor="middle" font-weight="400">
node2 node2
</text> </text>
</g> </g>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -3,12 +3,12 @@
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<g fill="none" transform="matrix(1,0,0,1,0,0)"> <g fill="none" transform="matrix(1,0,0,1,0,0)">
<g fill="none" transform="matrix(1,0,0,1,0,0)"> <g fill="none" transform="matrix(1,0,0,1,0,0)">
<g fill="none" transform="matrix(1,0,0,1,249.535004,263.500000)"> <g fill="none" transform="matrix(1,0,0,1,249.535004,261.500000)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<circle fill="none" transform="translate(-155.40014551151486,-155.40014551151486)" cx="155.40014551151486" cy="155.40014551151486" stroke-dasharray="0,0" stroke-width="12" stroke="rgba(153,173,209,1)" r="155.40014551151486" stroke-opacity="0.25" pointer-events="none"/> <circle fill="none" transform="translate(-154.30879827475812,-154.30879827475812)" cx="154.30879827475812" cy="154.30879827475812" stroke-dasharray="0,0" stroke-width="12" stroke="rgba(153,173,209,1)" r="154.30879827475812" stroke-opacity="0.25" pointer-events="none"/>
</g> </g>
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<circle fill="rgba(253,253,253,1)" transform="translate(-155.40014551151486,-155.40014551151486)" cx="155.40014551151486" cy="155.40014551151486" stroke-dasharray="0,0" stroke-width="4" stroke="rgba(153,173,209,1)" r="155.40014551151486"/> <circle fill="rgba(253,253,253,1)" transform="translate(-154.30879827475812,-154.30879827475812)" cx="154.30879827475812" cy="154.30879827475812" stroke-dasharray="0,0" stroke-width="4" stroke="rgba(153,173,209,1)" r="154.30879827475812"/>
</g> </g>
</g> </g>
</g> </g>
@ -41,12 +41,8 @@
<g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)"> <g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)">
<g fill="none" marker-start="false" marker-end="false" stroke="transparent" stroke-width="3"/> <g fill="none" marker-start="false" marker-end="false" stroke="transparent" stroke-width="3"/>
<g transform="matrix(1,0,0,1,160.733124,255.366562)"> <g transform="matrix(1,0,0,1,160.733124,255.366562)">
<path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="12" stroke="rgba(153,173,209,1)" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/> <path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="1" stroke="rgba(153,173,209,1)"/>
<path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="14" stroke="transparent" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/> <path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="1" stroke="transparent"/>
</g>
<g transform="matrix(1,0,0,1,160.733124,255.366562)">
<path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="2" stroke="rgba(153,173,209,1)"/>
<path fill="none" d="M 0,0 L 78.53374741600203,39.266873708000986" stroke-width="2" stroke="transparent"/>
</g> </g>
</g> </g>
</g> </g>
@ -95,14 +91,11 @@
</g> </g>
<g fill="none" transform="matrix(1,0,0,1,250,300)"> <g fill="none" transform="matrix(1,0,0,1,250,300)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<circle fill="none" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="12" stroke="rgba(23,131,255,1)" stroke-opacity="0.25" r="12" stroke-dasharray="0,0" pointer-events="none"/> <circle fill="rgba(23,131,255,1)" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="0" stroke="rgba(0,0,0,1)" r="12"/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<circle fill="rgba(23,131,255,1)" transform="translate(-12,-12)" cx="12" cy="12" stroke-width="3" stroke="rgba(0,0,0,1)" r="12"/>
</g> </g>
<g fill="none" transform="matrix(1,0,0,1,0,12)"> <g fill="none" transform="matrix(1,0,0,1,0,12)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<text fill="rgba(0,0,0,0.8509803921568627)" dominant-baseline="central" paint-order="stroke" dx="0.5" dy="13.5px" font-size="14" text-anchor="middle" font-weight="700"> <text fill="rgba(0,0,0,0.8509803921568627)" dominant-baseline="central" paint-order="stroke" dx="0.5" dy="11.5px" font-size="12" text-anchor="middle" font-weight="400">
node4 node4
</text> </text>
</g> </g>

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -5,10 +5,7 @@
<g fill="none" transform="matrix(1,0,0,1,0,0)"> <g fill="none" transform="matrix(1,0,0,1,0,0)">
<g fill="none" transform="matrix(1,0,0,1,250.979996,261.500000)"> <g fill="none" transform="matrix(1,0,0,1,250.979996,261.500000)">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<circle fill="none" transform="translate(-153.09563155100147,-153.09563155100147)" cx="153.09563155100147" cy="153.09563155100147" stroke-dasharray="0,0" stroke-width="12" stroke="rgba(153,173,209,1)" r="153.09563155100147" stroke-opacity="0.25" pointer-events="none"/> <circle fill="rgba(253,253,253,1)" transform="translate(-153.09563155100147,-153.09563155100147)" cx="153.09563155100147" cy="153.09563155100147" stroke-dasharray="0,0" stroke-width="1" stroke="rgba(153,173,209,1)" r="153.09563155100147"/>
</g>
<g transform="matrix(1,0,0,1,0,0)">
<circle fill="rgba(253,253,253,1)" transform="translate(-153.09563155100147,-153.09563155100147)" cx="153.09563155100147" cy="153.09563155100147" stroke-dasharray="0,0" stroke-width="4" stroke="rgba(153,173,209,1)" r="153.09563155100147"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(238,246,255,1)" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,300 L 300,300 L 300,0 Z"/> <path fill="rgba(238,246,255,1)" width="0" height="0" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,300 L 300,300 L 300,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(238,246,255,1)" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,300 L 300,300 Z"/> <path fill="rgba(238,246,255,1)" width="0" height="0" stroke-width="1" stroke="rgba(221,238,254,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,300 L 300,300 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,200 L 200,200 L 200,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,200 L 200,200 L 200,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,100 L 100,100 L 100,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,100 L 100,100 L 100,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -121,7 +121,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g> </g>
</g> </g>
<g transform="matrix(1,0,0,1,100,100)"> <g transform="matrix(1,0,0,1,100,100)">
<path fill="rgba(0,128,0,1)" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/> <path fill="rgba(0,128,0,1)" width="0" height="0" stroke-width="2" stroke="rgba(0,0,255,1)" fill-opacity="0.4" pointer-events="none" d="M 0,0 L 0,400 L 400,400 L 400,0 Z"/>
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -43,7 +43,9 @@ describe('behavior brush select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'brush-clear-2'); await expect(graph).toMatchSnapshot(__filename, 'brush-clear-2');
graph.setBehaviors([{ type: 'brush-select', style: { fill: 'green', lineWidth: 2, stroke: 'blue' } }]); graph.setBehaviors([
{ type: 'brush-select', trigger: 'drag', style: { fill: 'green', lineWidth: 2, stroke: 'blue' } },
]);
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } }); graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 400, y: 400 } });

View File

@ -46,7 +46,10 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-2'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-2');
graph.setBehaviors([{ type: 'lasso-select', style: { fill: 'green', lineWidth: 2, stroke: 'blue' } }]); graph.updateBehavior({
key: 'lasso-select',
style: { fill: 'green', lineWidth: 2, stroke: 'blue' },
});
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 300 } }); graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 300 } });
@ -62,7 +65,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-3'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-3');
graph.setBehaviors([{ type: 'lasso-select', trigger: 'shift' }]); graph.updateBehavior({ key: 'lasso-select', trigger: 'shift' });
graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' }); graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
@ -80,7 +83,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-4'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-4');
graph.setBehaviors([{ type: 'lasso-select', state: 'active', trigger: 'shift', immediately: true }]); graph.updateBehavior({ key: 'lasso-select', state: 'active', trigger: 'shift', immediately: true });
graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' }); graph.emit(CommonEvent.KEY_DOWN, { key: 'Shift' });
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
@ -98,7 +101,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-5'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-5');
graph.setBehaviors([{ type: 'lasso-select', mode: 'union', trigger: 'drag' }]); graph.updateBehavior({ key: 'lasso-select', mode: 'union', trigger: 'drag' });
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } }); graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });
@ -114,7 +117,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-union'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-union');
graph.setBehaviors([{ type: 'lasso-select', mode: 'diff' }]); graph.updateBehavior({ key: 'lasso-select', mode: 'diff' });
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } }); graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });
@ -130,7 +133,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`); graph.emit(`canvas:${CommonEvent.CLICK}`);
await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-diff'); await expect(graph).toMatchSnapshot(__filename, 'lasso-clear-mode-diff');
graph.setBehaviors([{ type: 'lasso-select', mode: 'intersect' }]); graph.updateBehavior({ key: 'lasso-select', mode: 'intersect' });
graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' }); graph.emit(CommonEvent.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } }); graph.emit(CommonEvent.POINTER_MOVE, { canvas: { x: 100, y: 500 } });

View File

@ -4,10 +4,10 @@ import { PortStyleProps } from '@/src/types';
import { import {
findPorts, findPorts,
getAllPorts, getAllPorts,
getBoundingPoints,
getHexagonPoints, getHexagonPoints,
getPortConnectionPoint, getPortConnectionPoint,
getPortXYByPlacement, getPortXYByPlacement,
getRectPoints,
getStarPoints, getStarPoints,
getStarPorts, getStarPorts,
getTextStyleByPlacement, getTextStyleByPlacement,
@ -258,9 +258,9 @@ describe('element', () => {
}); });
}); });
it('getRectPoints', () => { it('getBoundingPoints', () => {
expect(getRectPoints(100, 100).length).toBe(4); expect(getBoundingPoints(100, 100).length).toBe(4);
expect(getRectPoints(100, 100)).toEqual([ expect(getBoundingPoints(100, 100)).toEqual([
[50, -50], [50, -50],
[50, 50], [50, 50],
[-50, 50], [-50, 50],

View File

@ -198,6 +198,17 @@ describe('Point Functions', () => {
], ],
), ),
).toEqual(false); ).toEqual(false);
expect(
isPointInPolygon(
[20, 30],
[
[0, 0],
[20, 0],
[20, 20],
[0, 20],
],
),
).toEqual(false);
}); });
it('findNearestLine', () => { it('findNearestLine', () => {

View File

@ -1,21 +1,16 @@
import type { RectStyleProps } from '@antv/g';
import { Rect } from '@antv/g'; import { Rect } from '@antv/g';
import { isFunction } from '@antv/util'; import { deepMix, isFunction } from '@antv/util';
import { CommonEvent } from '../constants'; import { CommonEvent } from '../constants';
import { getRectPoints, isBBoxCenterInRect } from '../utils/behaviors/brush'; import { idOf } from '../exports';
import { getAllElementState, transformEdgeState } from '../utils/behaviors/utils';
import { Shortcut } from '../utils/shortcut';
import { BaseBehavior } from './base-behavior';
import type { Graph } from '../runtime/graph'; import type { Graph } from '../runtime/graph';
import type { RuntimeContext } from '../runtime/types'; import type { RuntimeContext } from '../runtime/types';
import type { NodeStyle } from '../spec/element/node'; import type { ElementDatum, ElementType, ID, IPointerEvent, Point, State } from '../types';
import type { ElementType, ID, IPointerEvent, Point, Points, State } from '../types'; import { getBoundingPoints, isPointInPolygon } from '../utils/point';
import type { ShortcutKey } from '../utils/shortcut'; import type { ShortcutKey } from '../utils/shortcut';
import { Shortcut } from '../utils/shortcut';
import type { BaseBehaviorOptions } from './base-behavior'; import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior';
const SHOW_RECT_ID = 'g6-brush-select-rect-id';
export type States = Record<ID, State | State[]>;
/** /**
* <zh/> * <zh/>
@ -23,6 +18,13 @@ export type States = Record<ID, State | State[]>;
* <en/> Brush select options * <en/> Brush select options
*/ */
export interface BrushSelectOptions extends BaseBehaviorOptions { export interface BrushSelectOptions extends BaseBehaviorOptions {
/**
* <zh/>
*
* <en/> Whether to enable animation.
* @defaultValue false
*/
animation?: boolean;
/** /**
* <zh/> * <zh/>
* *
@ -33,96 +35,87 @@ export interface BrushSelectOptions extends BaseBehaviorOptions {
/** /**
* <zh/> * <zh/>
* *
* <en/> Enable Elements type * <en/> Enable Elements type.
* @defaultValue ['node', 'combo', 'edge'] * @defaultValue ['node', 'combo', 'edge']
*/ */
enableElements?: ElementType[]; enableElements?: ElementType[];
/** /**
* <zh/> * <zh/>
* *
* <en/> Whether to enable animation. * <en/> Press this shortcut key to apply brush select with mouse click.
* @defaultValue false * @remarks
*/ * <zh/> `trigger` `['drag']` `drag-canvas`
animation?: boolean;
/**
* <zh/>
* *
* <en/> Trigger click or drag * <en/> Note that setting `trigger` to `['drag']` will cause the `drag-canvas` behavior to fail. The two cannot be configured at the same time.
* @defaultValue ['drag'] * @defaultValue ['shift']
*/ */
trigger?: ShortcutKey; trigger?: ShortcutKey;
/** /**
* <zh/> * <zh/>
* - union : 选中元素添加 state
* - intersect : 进一步筛选已经 state
* - diff : 反转选中元素的 state
* - default : state , state
* *
* <en/> Box Select Select the mode * <en/> The state to switch to when selected.
* - union : Select element add state
* - intersect : Further filter the elements that are already state enabled
* - diff : Inverts the state of the selected element
* - default : Check element state to turn on and other elements state to turn off
* @defaultValue 'default'
*/
mode?: 'union' | 'intersect' | 'diff' | 'default';
/**
* <zh/>
*
* <en/> Specify the state when selected
* @defaultValue 'selected' * @defaultValue 'selected'
*/ */
state?: State; state?: State;
/** /**
* <zh/> , default 使 * <zh/>
* - `'union'` state
* - `'intersect'` state
* - `'diff'` state
* - `'default'` state
* *
// * TODO fixme * <en/> Brush select mode
* <en/> Timely screening * - `'union'`: Keep the current state of the selected elements and add the specified state.
* - `'intersect'`: If the selected elements already have the specified state, keep it; otherwise, clearBrush it.
* - `'diff'`: Perform a negation operation on the specified state of the selected elements.
* - `'default'`: Clear the current state of the selected elements and add the specified state.
* @defaultValue 'default'
*/
mode?: 'union' | 'intersect' | 'diff' | 'default';
/**
* <zh/> , `default`
*
* <en/> Whether to brush select immediately, only valid when the brush select mode is `default`
* @defaultValue false * @defaultValue false
*/ */
immediately?: boolean; immediately?: boolean;
/** /**
* <zh/> * <zh/>
* *
* <en/> Brush select box style * <en/> Timely screening.
*/ */
style?: NodeStyle; style?: RectStyleProps;
/** /**
* <zh/> * <zh/>
* *
* <en/> Callback when selecting elements * <en/> Callback when brush select elements.
* @param states - <zh/> | <en/> Selected element state * @param states -
* @returns <zh/> | <en/> Element state * @returns
*/ */
onSelect?: (states: States) => States; onSelect?: (states: Record<ID, State | State[]>) => Record<ID, State | State[]>;
} }
export const DEFAULT_STYLE = {
lineWidth: 1,
fill: '#EEF6FF',
stroke: '#DDEEFE',
fillOpacity: 0.4,
zIndex: 2,
};
/** /**
* <zh/> * <zh/>
* *
* <en/> Brush select elements * <en/> Brush select elements
*/ */
export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> extends BaseBehavior<T> { export class BrushSelect extends BaseBehavior<BrushSelectOptions> {
static defaultOptions: Partial<BrushSelectOptions> = { static defaultOptions: Partial<BrushSelectOptions> = {
enable: true,
trigger: ['drag'],
immediately: false,
state: 'selected',
mode: 'default',
animation: false, animation: false,
enable: true,
enableElements: ['node', 'combo', 'edge'], enableElements: ['node', 'combo', 'edge'],
immediately: false,
mode: 'default',
state: 'selected',
trigger: ['shift'],
style: { style: {
size: 0, width: 0,
type: 'rect', height: 0,
...DEFAULT_STYLE, lineWidth: 1,
fill: '#EEF6FF',
stroke: '#DDEEFE',
fillOpacity: 0.4,
zIndex: 2,
pointerEvents: 'none', pointerEvents: 'none',
}, },
}; };
@ -130,55 +123,35 @@ export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> ext
private startPoint?: Point; private startPoint?: Point;
private endPoint?: Point; private endPoint?: Point;
private rectShape?: Rect; private rectShape?: Rect;
public shortcut?: Shortcut; private shortcut?: Shortcut;
public selectElementFn: (graph: Graph, id: ID, points: Points) => boolean = isBBoxCenterInRect; constructor(context: RuntimeContext, options: BrushSelectOptions) {
super(context, deepMix({}, BrushSelect.defaultOptions, options));
constructor(context: RuntimeContext, options: T) {
super(context, Object.assign({}, BrushSelect.defaultOptions, options));
this.shortcut = new Shortcut(context.graph); this.shortcut = new Shortcut(context.graph);
if (options.type === 'lasso-select') return;
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.clearStates = this.clearStates.bind(this);
this.bindEvents(); this.bindEvents();
} }
/** protected onPointerDown(event: IPointerEvent) {
* <zh/>
*
* <en/> pointer down
* @param event - <zh/> | <en/> pointer event
*/
protected pointerDown = async (event: IPointerEvent) => {
if (!this.validate(event) || !this.isKeydown() || this.startPoint) return; if (!this.validate(event) || !this.isKeydown() || this.startPoint) return;
const { style, trigger } = this.options;
const triggers = (Array.isArray(trigger) ? trigger : [trigger]) as string[];
if (event.targetType !== 'canvas' && triggers.includes('drag')) return;
const { canvas } = this.context; const { canvas } = this.context;
this.rectShape = new Rect({ this.rectShape = new Rect({ id: 'g6-brush-select', style: this.options.style });
id: SHOW_RECT_ID,
style: {
...BrushSelect.defaultOptions.style,
fill: style.fill || DEFAULT_STYLE.fill,
...style,
pointerEvents: 'none',
},
});
canvas.appendChild(this.rectShape); canvas.appendChild(this.rectShape);
this.startPoint = [event.canvas.x, event.canvas.y]; this.startPoint = [event.canvas.x, event.canvas.y];
}; }
/**
* <zh/> protected onPointerMove(event: IPointerEvent) {
*
* <en/> pointer move
* @param event - <zh/> | <en/> pointer event
*/
protected pointerMove = async (event: IPointerEvent) => {
if (!this.startPoint) return; if (!this.startPoint) return;
const { immediately, mode } = this.options; const { immediately, mode } = this.options;
this.endPoint = [event.canvas.x, event.canvas.y]; this.endPoint = getCursorPoint(event);
this.rectShape?.attr({ this.rectShape?.attr({
x: Math.min(this.endPoint[0], this.startPoint[0]), x: Math.min(this.endPoint[0], this.startPoint[0]),
@ -187,156 +160,170 @@ export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> ext
height: Math.abs(this.endPoint[1] - this.startPoint[1]), height: Math.abs(this.endPoint[1] - this.startPoint[1]),
}); });
if (immediately && mode === 'default') { if (immediately && mode === 'default') this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
this.updateElementState(getRectPoints(this.startPoint, this.endPoint)); }
}
}; protected onPointerUp(event: IPointerEvent) {
/**
* <zh/>
*
* <en/> pointer up
* @param event - <zh/> | <en/> pointer event
*/
protected pointerUp = async (event: IPointerEvent) => {
if (!this.startPoint) return; if (!this.startPoint) return;
if (!this.endPoint) { if (!this.endPoint) {
await this.clearBrush(); this.clearBrush();
return; return;
} }
const points = getRectPoints(this.startPoint, [event.canvas.x, event.canvas.y]); this.endPoint = getCursorPoint(event);
this.updateElementState(points); this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
await this.clearBrush(); this.clearBrush();
}; }
/** /**
* <zh/> * <zh/>
* *
* <en/> clear brush * <en/> Clear state
* @remarks * @internal
* <zh/>
*
* <en/> Triggered when clicking the canvas
*/ */
protected clearSelected = () => { protected clearStates() {
if (this.endPoint) return; if (this.endPoint) return;
const { graph } = this.context; this.clearElementsStates();
const selects = getAllElementState(graph, () => []); }
graph.setElementState(selects, this.options.animation);
};
/** /**
* <zh/> * <zh/>
* *
* <en/> Update the state of the elements in the box selection area * <en/> Clear the state of all elements on the canvas
* @param points - <zh/> | <en/> The coordinates of the points forming the box selection area * @internal
*/ */
public updateElementState = (points: Points) => { protected clearElementsStates() {
const { graph } = this.context;
const states = Object.values(graph.getData()).reduce((acc, data) => {
return Object.assign(
{},
acc,
data.reduce((acc: Record<ID, []>, datum: ElementDatum) => {
acc[idOf(datum)] = [];
return acc;
}, {}),
);
}, {});
graph.setElementState(states, this.options.animation);
}
/**
* <zh/>
*
* <en/> Update the state of the selected elements
* @param points - <zh/> | <en/> The vertex of the selection area
* @internal
*/
protected updateElementsStates(points: Point[]) {
const { graph } = this.context; const { graph } = this.context;
const { enableElements, state, mode, onSelect } = this.options; const { enableElements, state, mode, onSelect } = this.options;
// 框选选中的 ids const selectedIds = this.selector(graph, points, enableElements);
const rectSelectIds = this.getPointsSelectIds(graph, points, enableElements);
// state mode 框选逻辑 let states: Record<ID, State | State[]> = {};
let stateChangeFn = (id: ID, oldState: string[]): string[] => (rectSelectIds.includes(id) ? [state] : []);
switch (mode) { switch (mode) {
case 'union': case 'union':
stateChangeFn = (id: ID, oldState: string[]) => (rectSelectIds.includes(id) ? [state] : oldState); selectedIds.forEach((id) => {
states[id] = [...graph.getElementState(id), state];
});
break; break;
case 'diff': case 'diff':
stateChangeFn = (id: ID, oldState: string[]) => { selectedIds.forEach((id) => {
if (rectSelectIds.includes(id)) { const prevStates = graph.getElementState(id);
return oldState.includes(state) ? [] : [state]; states[id] = prevStates.includes(state) ? prevStates.filter((s) => s !== state) : [...prevStates, state];
} });
return oldState;
};
break; break;
case 'intersect': case 'intersect':
stateChangeFn = (id: ID, oldState: string[]) => { selectedIds.forEach((id) => {
if (rectSelectIds.includes(id)) { const prevStates = graph.getElementState(id);
return oldState.includes(state) ? [state] : []; states[id] = prevStates.includes(state) ? [state] : [];
} });
return oldState; break;
}; case 'default':
default:
selectedIds.forEach((id) => {
states[id] = [state];
});
break; break;
} }
let states = getAllElementState(graph, stateChangeFn); if (isFunction(onSelect)) states = onSelect(states);
if (enableElements.includes('edge')) {
transformEdgeState(graph, states, state);
}
if (isFunction(onSelect)) {
states = onSelect(states);
}
graph.setElementState(states, this.options.animation);
};
private clearBrush = async () => { graph.setElementState(states, this.options.animation);
}
/**
* <zh/> combo combo
*
* <en/> Find the elements displayed in the specified area on the canvas. A node is selected if the center of its bbox is inside the rect; An edge is selected if both end nodes are inside the rect ;A combo is selected if the center of its bbox is inside the rect.
* @param graph - <zh/> | <en/> Graph instance
* @param points - <zh/> | <en/> The vertex of the selection area
* @param itemTypes - <zh/> | <en/> Element type
* @returns <zh/> ID | <en/> Selected element ID array
* @internal
*/
protected selector(graph: Graph, points: Point[], itemTypes: ElementType[]): ID[] {
if (!itemTypes || itemTypes.length === 0) return [];
const elements: ID[] = [];
const graphData = graph.getData();
itemTypes.forEach((itemType) => {
const data = graphData[`${itemType}s`];
data?.forEach((datum) => {
const id = idOf(datum);
if (graph.getElementVisibility(id) !== 'hidden' && isPointInPolygon(graph.getElementPosition(id), points)) {
elements.push(id);
}
});
});
// 如果边的两端节点都在框选范围内,则边也被选中 | If source node and target node are within the selection range, that edge is also selected
if (itemTypes.includes('edge')) {
const edges = graphData.edges;
edges?.forEach((edge) => {
const { source, target } = edge;
if (elements.includes(source) && elements.includes(target)) {
elements.push(idOf(edge));
}
});
}
return elements;
}
private clearBrush() {
this.rectShape?.remove(); this.rectShape?.remove();
this.rectShape = undefined; this.rectShape = undefined;
this.startPoint = undefined; this.startPoint = undefined;
this.endPoint = undefined; this.endPoint = undefined;
};
/**
* <zh/> ids
*
* <en/> Get the ids of the elements selected by the box selection
* @param graph - <zh/> | <en/> Graph instance
* @param points - <zh/> | <en/> The coordinates of the points forming the box selection area
* @param itemTypes - <zh/> | <en/> The type of the elements selected by the box selection
* @returns <zh/> ids | <en/> The ids of the elements selected by the box selection
*/
private getPointsSelectIds = (graph: Graph, points: Points, itemTypes: ElementType[]) => {
const selectedNodeIds: ID[] = [];
const selectedComboIds: ID[] = [];
if (itemTypes.includes('node')) {
graph.getNodeData().forEach((node) => {
const { id } = node;
if (
graph.getElementVisibility(id) !== 'hidden' && // hidden node is not selectable
this.selectElementFn(graph, id, points)
) {
selectedNodeIds.push(id);
}
});
}
if (itemTypes.includes('combo')) {
graph.getComboData().forEach((combo) => {
const { id } = combo;
if (
graph.getElementVisibility(id) !== 'hidden' && // hidden combo is not selectable
this.selectElementFn(graph, id, points)
) {
selectedComboIds.push(id);
}
});
}
return [...selectedNodeIds, ...selectedComboIds];
};
// 当前按键是否和 trigger 配置一致
protected isKeydown() {
const { trigger } = this.options;
const keys = (Array.isArray(trigger) ? trigger : [trigger]) as string[];
if (keys.length === 0 || keys.includes('drag')) return true;
return this.shortcut?.match(keys);
} }
/** /**
* <zh/> * <zh/> trigger
* *
* <en/> Validate whether to trigger the box selection * <en/> Is the current key consistent with the trigger configuration
* @param event - <zh/> | <en/> pointer event * @returns <zh/> | <en/> Is consistent
* @returns <zh/> | <en/> Whether to trigger the box selection * @internal
*/
protected isKeydown(): boolean {
const { trigger } = this.options;
const keys = (Array.isArray(trigger) ? trigger : [trigger]) as string[];
if (!trigger || keys.includes('drag')) return true;
return this.shortcut!.match(keys);
}
/**
* <zh/>
*
* <en/> Verify whether brush select is enabled
* @param event - <zh/> | <en/> Event
* @returns <zh/> | <en/> Whether to enable
* @internal
*/ */
protected validate(event: IPointerEvent) { protected validate(event: IPointerEvent) {
if (this.destroyed) return false; if (this.destroyed) return false;
@ -345,42 +332,48 @@ export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> ext
return !!enable; return !!enable;
} }
/** private bindEvents() {
* <zh/>
*
* <en/> Bind event
*/
protected bindEvents() {
const { graph } = this.context; const { graph } = this.context;
this.unbindEvents(); this.unbindEvents();
graph.on(CommonEvent.POINTER_DOWN, this.pointerDown); graph.on(CommonEvent.POINTER_DOWN, this.onPointerDown);
graph.on(CommonEvent.POINTER_MOVE, this.pointerMove); graph.on(CommonEvent.POINTER_MOVE, this.onPointerMove);
graph.on(CommonEvent.POINTER_UP, this.pointerUp); graph.on(CommonEvent.POINTER_UP, this.onPointerUp);
graph.on(`canvas:${CommonEvent.CLICK}`, this.clearSelected); graph.on(`canvas:${CommonEvent.CLICK}`, this.clearStates);
}
private unbindEvents() {
const { graph } = this.context;
graph.off(CommonEvent.POINTER_DOWN, this.onPointerDown);
graph.off(CommonEvent.POINTER_MOVE, this.onPointerMove);
graph.off(CommonEvent.POINTER_UP, this.onPointerUp);
graph.off(`canvas:${CommonEvent.CLICK}`, this.clearStates);
} }
/** /**
* <zh/> * <zh/>
* *
* <en/> Unbind event * <en/> Update configuration
* @param options - <zh/> | <en/> Options
* @internal
*/ */
protected unbindEvents() { public update(options: Partial<BrushSelectOptions>) {
const { graph } = this.context; this.options = deepMix(this.options, options);
graph.off(CommonEvent.POINTER_DOWN, this.pointerDown);
graph.off(CommonEvent.POINTER_MOVE, this.pointerMove);
graph.off(CommonEvent.POINTER_UP, this.pointerUp);
graph.off(`canvas:${CommonEvent.CLICK}`, this.clearSelected);
} }
/** /**
* <zh/> * <zh/>
* *
* <en/> Destroy * <en/> Destroy
* @internal
*/ */
public destroy() { public destroy() {
this.unbindEvents(); this.unbindEvents();
super.destroy(); super.destroy();
} }
} }
export const getCursorPoint = (event: IPointerEvent): Point => {
return [event.canvas.x, event.canvas.y];
};

View File

@ -1,12 +1,16 @@
import { isFunction } from '@antv/util'; import { isFunction } from '@antv/util';
import { CommonEvent } from '../constants'; import { CommonEvent } from '../constants';
import { ELEMENT_TYPES } from '../constants/element';
import type { RuntimeContext } from '../runtime/types'; import type { RuntimeContext } from '../runtime/types';
import type { Element, ID, IPointerEvent, ViewportAnimationEffectTiming } from '../types'; import type { IElementEvent, ViewportAnimationEffectTiming } from '../types';
import type { BaseBehaviorOptions } from './base-behavior'; import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior'; import { BaseBehavior } from './base-behavior';
const FOCUS_ELEMENT_TYPES = ['node', 'combo']; /**
* <zh/>
*
* <en/> Focus element behavior options
*/
export interface FocusElementOptions extends BaseBehaviorOptions { export interface FocusElementOptions extends BaseBehaviorOptions {
/** /**
* <zh/> * <zh/>
@ -17,12 +21,21 @@ export interface FocusElementOptions extends BaseBehaviorOptions {
/** /**
* <zh/> * <zh/>
* *
* <en/> Whether to enable the function of dragging the node * <en/> Whether to enable the function of focusing on the element
* @defaultValue true * @defaultValue true
*/ */
enable?: boolean | ((event: IElementEvent) => boolean); enable?: boolean | ((event: IElementEvent) => boolean);
} }
/**
* <zh/>
*
* <en/> Focus element behavior
* @remarks
* <zh/>
*
* <en/> When an element is clicked, the element is focused to the center of the view.
*/
export class FocusElement extends BaseBehavior<FocusElementOptions> { export class FocusElement extends BaseBehavior<FocusElementOptions> {
static defaultOptions: Partial<FocusElementOptions> = { static defaultOptions: Partial<FocusElementOptions> = {
animation: { animation: {
@ -41,29 +54,16 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
const { graph } = this.context; const { graph } = this.context;
this.unbindEvents(); this.unbindEvents();
FOCUS_ELEMENT_TYPES.forEach((type) => { ELEMENT_TYPES.forEach((type) => {
graph.on(`${type}:${CommonEvent.CLICK}`, this.clickFocusElement); graph.on(`${type}:${CommonEvent.CLICK}`, this.focus);
}); });
} }
private getSelectedNodeIDs(currTarget: ID[]) { private focus = async (event: IElementEvent) => {
return Array.from(
new Set(
this.context.graph
.getElementDataByState('node', this.options.state)
.map((node) => node.id)
.concat(currTarget),
),
);
}
private clickFocusElement = async (event: IElementEvent) => {
if (!this.validate(event)) return; if (!this.validate(event)) return;
const { animation } = this.options;
const { graph } = this.context; const { graph } = this.context;
const id = this.getSelectedNodeIDs([(event.target as Element).id]);
await graph.focusElement(id, animation); await graph.focusElement(event.target.id, this.options.animation);
}; };
private validate(event: IElementEvent) { private validate(event: IElementEvent) {
@ -76,8 +76,8 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
private unbindEvents() { private unbindEvents() {
const { graph } = this.context; const { graph } = this.context;
FOCUS_ELEMENT_TYPES.forEach((type) => { ELEMENT_TYPES.forEach((type) => {
graph.off(`${type}:${CommonEvent.CLICK}`, this.clickFocusElement); graph.off(`${type}:${CommonEvent.CLICK}`, this.focus);
}); });
} }
@ -86,5 +86,3 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
super.destroy(); super.destroy();
} }
} }
interface IElementEvent extends IPointerEvent<Element> {}

View File

@ -1,121 +1,53 @@
import { Path } from '@antv/g'; import { Path } from '@antv/g';
import { isBBoxIntersectPolygon } from '../utils/behaviors/lasso'; import type { IPointerEvent, Points } from '../types';
import { getAllElementState } from '../utils/behaviors/utils';
import { pointsToPath } from '../utils/path'; import { pointsToPath } from '../utils/path';
import { BrushSelect, DEFAULT_STYLE } from './brush-select';
import type { RuntimeContext } from '../runtime/types';
import type { ID, IPointerEvent, Points, State } from '../types';
import type { BrushSelectOptions } from './brush-select'; import type { BrushSelectOptions } from './brush-select';
import { BrushSelect, getCursorPoint } from './brush-select';
const SHOW_PATH_ID = 'g6-lasso-select-path-id';
export type States = Record<ID, State | State[]>;
export interface LassoSelectOptions extends BrushSelectOptions {} export interface LassoSelectOptions extends BrushSelectOptions {}
export class LassoSelect extends BrushSelect<LassoSelectOptions> { export class LassoSelect extends BrushSelect {
static defaultOptions: Partial<LassoSelectOptions> = {
...BrushSelect.defaultOptions,
style: DEFAULT_STYLE,
};
private points?: Points; private points?: Points;
private pathShape?: Path; private pathShape?: Path;
public selectElementFn = isBBoxIntersectPolygon; protected onPointerDown(event: IPointerEvent) {
if (!super.validate(event) || !super.isKeydown() || this.points) return;
constructor(context: RuntimeContext, options: LassoSelectOptions) {
super(context, Object.assign({}, LassoSelect.defaultOptions, options));
this.bindEvents();
}
/**
* </zh>
*
* <en/> Pointer down
* @param event - <zh/> | <en/> pointer event object
*/
protected pointerDown = async (event: IPointerEvent) => {
if (!this.validate(event) || !this.isKeydown() || this.points) return;
const { style, trigger } = this.options;
const triggers = (Array.isArray(trigger) ? trigger : [trigger]) as string[];
if (event.targetType !== 'canvas' && triggers.includes('drag')) return;
const { canvas } = this.context; const { canvas } = this.context;
this.pathShape = new Path({ this.pathShape = new Path({
id: SHOW_PATH_ID, id: 'g6-lasso-select',
style: { style: this.options.style,
...LassoSelect.defaultOptions.style,
fill: style.fill || DEFAULT_STYLE.fill,
...style,
pointerEvents: 'none',
},
}); });
canvas.appendChild(this.pathShape); canvas.appendChild(this.pathShape);
this.points = [[event.canvas.x, event.canvas.y]]; this.points = [getCursorPoint(event)];
}; }
/**
* </zh> protected onPointerMove(event: IPointerEvent) {
*
* <en/> Pointer move
* @param event - <zh/> | <en/> pointer event object
*/
protected pointerMove = async (event: IPointerEvent) => {
if (!this.points) return; if (!this.points) return;
const { immediately, mode } = this.options; const { immediately, mode } = this.options;
const { element } = this.context;
this.points.push([event.canvas.x, event.canvas.y]);
this.points.push(getCursorPoint(event));
this.pathShape?.setAttribute('path', pointsToPath(this.points)); this.pathShape?.setAttribute('path', pointsToPath(this.points));
if (immediately && mode === 'default') { if (immediately && mode === 'default' && this.points.length > 2) super.updateElementsStates(this.points);
this.lassoUpdateElementState(); }
}
await element?.draw({ animation: false, silence: true }); protected onPointerUp() {
};
/**
* </zh>
*
* <en/> Pointer up
*/
protected pointerUp = async () => {
if (!this.points) return; if (!this.points) return;
if (this.points.length < 2) { if (this.points.length < 2) {
await this.clearLasso(); this.clearLasso();
return; return;
} }
super.updateElementsStates(this.points);
this.lassoUpdateElementState(); this.clearLasso();
}
await this.clearLasso(); private clearLasso() {
};
/**
* </zh>
*
* <en/> Clear the status of the selected element
*/
protected clearSelected = () => {
if (this.points) return;
const { graph } = this.context;
const selects = getAllElementState(graph, () => []);
graph.setElementState(selects, this.options.animation);
};
private lassoUpdateElementState = () => {
if (!this.points || this.points?.length < 2) return;
this.updateElementState(this.points);
};
private clearLasso = async () => {
this.pathShape?.remove(); this.pathShape?.remove();
this.pathShape = undefined; this.pathShape = undefined;
this.points = undefined; this.points = undefined;
}; }
} }

View File

@ -3,7 +3,5 @@ import type { DragCanvasOptions } from './drag-canvas';
import type { ScrollCanvasOptions } from './scroll-canvas'; import type { ScrollCanvasOptions } from './scroll-canvas';
import type { ZoomCanvasOptions } from './zoom-canvas'; import type { ZoomCanvasOptions } from './zoom-canvas';
export type { States } from './brush-select';
export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions | ScrollCanvasOptions; export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions | ScrollCanvasOptions;
export type Behavior = BaseBehavior<any>; export type Behavior = BaseBehavior<any>;

View File

@ -1,28 +0,0 @@
import { isPointInPolygon } from '../point';
import type { Graph } from '../../runtime/graph';
import type { ID, Point, Points } from '../../types';
/**
* <zh/> rect
*
* <en/> Element center in rect.
* @param graph Graph
* @param id ID
* @param points Points
* @returns boolean
*/
export function isBBoxCenterInRect(graph: Graph, id: ID, points: Points) {
const bbox = graph.getElementRenderBounds(id);
if (!bbox) return false;
return isPointInPolygon(bbox.center, points);
}
/**
*
* @param start
* @param end
*/
export function getRectPoints(start: Point, end: Point): Points {
return [start, [start[0], end[1]], end, [end[0], start[1]]];
}

View File

@ -1,126 +0,0 @@
import { AABB } from '@antv/g';
import { getLinesIntersection } from '../line';
import { isPointInPolygon } from '../point';
import type { Graph } from '../../runtime/graph';
import type { ID, Points } from '../../types';
import type { LineSegment } from '../line';
/**
* <zh/> path
*
* <en/> Element center in path.
* @param graph Graph
* @param id ID
* @param points Points
* @returns boolean
*/
export const isBBoxIntersectPolygon = (graph: Graph, id: ID, points: Points) => {
const bbox = graph.getElementRenderBounds(id);
if (!bbox) return false;
const shapePoints = [
[bbox.min[0], bbox.min[1]],
[bbox.max[0], bbox.min[1]],
[bbox.max[0], bbox.max[1]],
[bbox.min[0], bbox.max[1]],
] as Points;
return isPolygonsIntersect(points, shapePoints);
};
/**
* <zh/> .
*
* <en/> Whether two polygons intersect.
* @param points1 Points
* @param points2 Points
* @returns boolean
*/
const isPolygonsIntersect = (points1: Points, points2: Points): boolean => {
const getBBox = (points: Points): Partial<AABB> => {
const xArr = points.map((p) => p[0]);
const yArr = points.map((p) => p[1]);
return {
min: [Math.min.apply(null, xArr), Math.min.apply(null, yArr), 0],
max: [Math.max.apply(null, xArr), Math.max.apply(null, yArr), 0],
};
};
const parseToLines = (points: Points) => {
const lines = [];
const count = points.length;
for (let i = 0; i < count - 1; i++) {
const point = points[i];
const next = points[i + 1];
lines.push([point, next]);
}
if (lines.length > 1) {
const first = points[0];
const last = points[count - 1];
lines.push([last, first]);
}
return lines;
};
// 空数组,或者一个点返回 false
if (points1.length < 2 || points2.length < 2) {
return false;
}
const bbox1 = getBBox(points1);
const bbox2 = getBBox(points2);
// 判定包围盒是否相交,比判定点是否在多边形内要快的多,可以筛选掉大多数情况
if (!intersectBBox(bbox1, bbox2)) {
return false;
}
let isIn = false;
// 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回
points2.forEach((point) => {
if (isPointInPolygon(point, points1)) {
isIn = true;
return false;
}
});
if (isIn) {
return true;
}
points1.forEach((point) => {
if (isPointInPolygon(point, points2)) {
isIn = true;
return false;
}
});
if (isIn) {
return true;
}
const lines1 = parseToLines(points1);
const lines2 = parseToLines(points2);
let isIntersect = false;
lines2.forEach((line) => {
if (lineIntersectPolygon(lines1, line)) {
isIntersect = true;
return false;
}
});
return isIntersect;
};
const intersectBBox = (box1: Partial<AABB>, box2: Partial<AABB>) => {
if (!box2?.min || !box1?.min || !box2?.max || !box1?.max) return false;
return (
box2.min[0] <= box1.max[0] && box2.max[0] >= box1.min[0] && box2.min[1] <= box1.max[1] && box2.max[1] >= box1.min[1]
);
};
const lineIntersectPolygon = (lines: Points[], line: Points) => {
let isIntersect = false;
lines.forEach((l) => {
if (l.length > 1 && line.length > 1 && getLinesIntersection(l as LineSegment, line as LineSegment)) {
isIntersect = true;
return false;
}
});
return isIntersect;
};

View File

@ -1,48 +0,0 @@
import { isFunction } from '@antv/util';
import type { States } from '../../behaviors/types';
import type { Graph } from '../../runtime/graph';
import type { ID } from '../../types';
import { idOf } from '../id';
/**
* <zh/> .
*
* <en/> Gets all element states.
* @param graph Graph
* @param callback (id: ID, state: string[]) => string[]
* @returns States
*/
export const getAllElementState = (graph: Graph, callback?: (id: ID, state: string[]) => string[]): States => {
const allElementState: States = {};
const datas = graph.getData();
Object.values(datas).forEach((data) => {
data.map((d: any) => {
const state = graph.getElementState(idOf(d));
allElementState[d.id] = isFunction(callback) ? callback(d.id, state) : state;
});
});
return allElementState;
};
/**
* <zh/> edge .
*
* <en/> Example Transform the edge state.
* @param graph Graph
* @param states States
* @param state 'active' | 'selected'
*/
export const transformEdgeState = (graph: Graph, states: States, state: 'active' | 'selected') => {
const edgeData = graph.getEdgeData();
edgeData.forEach(({ id, target, source }) => {
if (!id) return;
if (states[target].includes(state) && states[source].includes(state)) {
states[id] = [state];
} else {
states[id] = [];
}
});
};

View File

@ -435,7 +435,7 @@ export function getTrianglePorts(width: number, height: number, direction: Trian
* @param height - <zh/> | <en/> height * @param height - <zh/> | <en/> height
* @returns <zh/> | <en/> The points of a rectangle * @returns <zh/> | <en/> The points of a rectangle
*/ */
export function getRectPoints(width: number, height: number): Point[] { export function getBoundingPoints(width: number, height: number): Point[] {
return [ return [
[width / 2, -height / 2], [width / 2, -height / 2],
[width / 2, height / 2], [width / 2, height / 2],

View File

@ -172,44 +172,31 @@ export function getPolygonIntersectPoint(
} }
/** /**
* <zh/> 线 * <zh/>
* *
* <en/> Whether point is inside the polygon (ray algo) * <en/> Whether point is inside the polygon (ray algo)
* @param point - <zh/> | <en/> point * @param point - <zh/> | <en/> point
* @param points - <zh/> | <en/> polygon vertices * @param points - <zh/> | <en/> polygon vertices
* @param start - <zh/> | <en/> start index
* @param end - <zh/> | <en/> end index
* @returns <zh/> | <en/> whether inside the polygon * @returns <zh/> | <en/> whether inside the polygon
*/ */
export function isPointInPolygon(point: Point, points: Point[]): boolean { export function isPointInPolygon(point: Point, points: Point[], start?: number, end?: number): boolean {
const [x, y] = point; const x = point[0];
let isHit = false; const y = point[1];
const n = points.length; let inside = false;
// 判断两个 double 在 eps 精度下的大小关系 | Determine the size relationship between two doubles within eps precision if (start === undefined) start = 0;
const tolerance = 1e-6; if (end === undefined) end = points.length;
const len = end - start;
// svg 中点小于 3 个时,不显示,也无法被拾取 | When the number of points in the svg is less than 3, it is not displayed and cannot be picked up for (let i = 0, j = len - 1; i < len; j = i++) {
if (n <= 2) return false; const xi = points[i + start][0];
const yi = points[i + start][1];
const dcmp = (xValue: number) => { const xj = points[j + start][0];
if (Math.abs(xValue) < tolerance) { const yj = points[j + start][1];
return 0; const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
} if (intersect) inside = !inside;
return xValue < 0 ? -1 : 1;
};
for (let i = 0; i < n; i++) {
const p1 = points[i];
const p2 = points[(i + 1) % n];
// 点在多边形一条边上 | The point is on one side of the polygon
if (isCollinear(p1, p2, point)) return true;
// 前一个判断min(p1[1],p2[1])<P.y<=max(p1[1],p2[1]);后一个判断被测点 在 射线与边交点 的左边
if (
dcmp(p1[1] - y) > 0 !== dcmp(p2[1] - y) > 0 &&
dcmp(x - ((y - p1[1]) * (p1[0] - p2[0])) / (p1[1] - p2[1]) - p1[0]) < 0
) {
isHit = !isHit;
}
} }
return isHit; return inside;
} }
/** /**
@ -375,3 +362,13 @@ export function sortByClockwise(points: Point[], clockwise = true): Point[] {
return clockwise ? angle2 - angle1 : angle1 - angle2; return clockwise ? angle2 - angle1 : angle1 - angle2;
}); });
} }
/**
* <zh/>
* @param start - <zh/> | <en/> start point
* @param end - <zh/> | <en/> end point
* @returns <zh/> | <en/> an array consisting of these two points and their diagonal points
*/
export function getBoundingPoints(start: Point, end: Point): Point[] {
return [start, [start[0], end[1]], end, [end[0], start[1]]];
}

View File

@ -2,33 +2,24 @@ import { Graph } from '@antv/g6';
const data = { const data = {
nodes: [ nodes: [
{ id: 'node1', style: { x: 100, y: 150, parentId: 'combo1' } }, { id: 'node1', style: { x: 110, y: 150, parentId: 'combo1' } },
{ id: 'node2', style: { x: 180, y: 150, parentId: 'combo1' } }, { id: 'node2', style: { x: 190, y: 150, parentId: 'combo1' } },
{ id: 'node3', style: { x: 150, y: 200, parentId: 'combo2' } }, { id: 'node3', style: { x: 150, y: 260, parentId: 'combo2' } },
], ],
edges: [], edges: [{ source: 'node1', target: 'node2' }],
combos: [ combos: [
{ id: 'combo1', style: { parentId: 'combo2' } }, // collapsed: true { id: 'combo1', style: { parentId: 'combo2' } },
{ id: 'combo2', style: {} }, { id: 'combo2', style: {} },
], ],
}; };
const graph = new Graph({ const graph = new Graph({
container: 'container', container: 'container',
width: 500,
height: 400,
node: { node: {
style: { style: { labelText: (d) => d.id },
labelText: (d) => d.id,
},
},
combo: {
style: {
type: 'circle', // 👈🏻 Combo shape type.
},
}, },
data, data,
behaviors: ['drag-canvas', 'collapse-expand', 'focus-element', 'zoom-canvas'], behaviors: ['collapse-expand', 'focus-element'],
}); });
graph.render(); graph.render();

View File

@ -2,65 +2,33 @@ import { Graph } from '@antv/g6';
const data = { const data = {
nodes: [ nodes: [
{ { id: 'node1', style: { x: 110, y: 150, parentId: 'combo1' } },
id: 'node1', { id: 'node2', style: { x: 190, y: 150, parentId: 'combo1' } },
data: { { id: 'node3', style: { x: 150, y: 260, parentId: 'combo2' } },
x: 150,
y: 100,
},
},
{
id: 'node2',
data: { x: 250, y: 200 },
},
{
id: 'node3',
data: { x: 450, y: 200 },
},
], ],
edges: [ edges: [{ source: 'node1', target: 'node2' }],
{ combos: [
id: 'edge1', { id: 'combo1', style: { parentId: 'combo2' } },
source: 'node1', { id: 'combo2', style: {} },
target: 'node2',
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
},
{
id: 'edge3',
source: 'node2',
target: 'node3',
},
], ],
}; };
const graph = new Graph({ const graph = new Graph({
container: 'container', container: 'container',
width: 500,
height: 500,
data,
node: { node: {
style: { style: { labelText: (d) => d.id },
labelText: (d) => d.id,
labelPosition: 'bottom',
},
},
edge: {},
layout: {
type: 'force',
}, },
data,
animation: true, animation: true,
behaviors: [ behaviors: [
'drag-element', 'collapse-expand',
{ {
type: 'focus-element', type: 'focus-element',
animation: { animation: {
easing: 'ease-out', easing: 'ease-out',
duration: 3500, duration: 3500,
}, },
enable: (e) => e.targetType === 'node',
}, },
], ],
}); });

View File

@ -6,12 +6,12 @@
"demos": [ "demos": [
{ {
"filename": "clickFocus.ts", "filename": "clickFocus.ts",
"title": "节点聚焦", "title": "点击元素移动到画布中心",
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ph6tS4k6S-oAAAAAAAAAAAAADmJ7AQ/original" "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ph6tS4k6S-oAAAAAAAAAAAAADmJ7AQ/original"
}, },
{ {
"filename": "clickFocusAnimate.ts", "filename": "clickFocusAnimate.ts",
"title": "节点聚焦自定义动画", "title": "点击节点带有自定义动画地移动到画布中心",
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Fs_DT6n11zMAAAAAAAAAAAAADmJ7AQ/original" "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Fs_DT6n11zMAAAAAAAAAAAAADmJ7AQ/original"
} }
] ]