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: [
{
type: 'brush-select',
key: 'brush-select',
trigger: 'drag',
},
],
@ -50,5 +51,16 @@ export const behaviorBrushSelect: TestCase = async (context) => {
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;
};

View File

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

View File

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

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -121,7 +121,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

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" stroke="transparent" stroke-width="3"/>
<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="14" stroke="transparent" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/>
</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"/>
<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="1" stroke="transparent"/>
</g>
</g>
<g fill="none" marker-start="false" marker-end="false" transform="matrix(1,0,0,1,0,0)">
@ -68,14 +64,11 @@
</g>
<g fill="none" transform="matrix(1,0,0,1,250,200)">
<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"/>
</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"/>
<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 fill="none" transform="matrix(1,0,0,1,0,12)">
<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
</text>
</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 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)">
<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 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>
@ -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" stroke="transparent" stroke-width="3"/>
<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="14" stroke="transparent" stroke-dasharray="0,0" pointer-events="none" stroke-opacity="0.25"/>
</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"/>
<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="1" stroke="transparent"/>
</g>
</g>
</g>
@ -95,14 +91,11 @@
</g>
<g fill="none" transform="matrix(1,0,0,1,250,300)">
<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"/>
</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"/>
<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 fill="none" transform="matrix(1,0,0,1,0,12)">
<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
</text>
</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,250.979996,261.500000)">
<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"/>
</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"/>
<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>
</g>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -121,7 +121,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -90,7 +90,7 @@
</g>
</g>
<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>

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}`);
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_MOVE, { canvas: { x: 400, y: 400 } });

View File

@ -46,7 +46,10 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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_MOVE, { canvas: { x: 100, y: 300 } });
@ -62,7 +65,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
@ -80,7 +83,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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.POINTER_DOWN, { canvas: { x: 100, y: 100 }, targetType: 'canvas' });
@ -98,7 +101,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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_MOVE, { canvas: { x: 100, y: 500 } });
@ -114,7 +117,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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_MOVE, { canvas: { x: 100, y: 500 } });
@ -130,7 +133,7 @@ describe('behavior lasso select', () => {
graph.emit(`canvas:${CommonEvent.CLICK}`);
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_MOVE, { canvas: { x: 100, y: 500 } });

View File

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

View File

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

View File

@ -1,21 +1,16 @@
import type { RectStyleProps } from '@antv/g';
import { Rect } from '@antv/g';
import { isFunction } from '@antv/util';
import { deepMix, isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import { getRectPoints, isBBoxCenterInRect } from '../utils/behaviors/brush';
import { getAllElementState, transformEdgeState } from '../utils/behaviors/utils';
import { Shortcut } from '../utils/shortcut';
import { BaseBehavior } from './base-behavior';
import { idOf } from '../exports';
import type { Graph } from '../runtime/graph';
import type { RuntimeContext } from '../runtime/types';
import type { NodeStyle } from '../spec/element/node';
import type { ElementType, ID, IPointerEvent, Point, Points, State } from '../types';
import type { ElementDatum, ElementType, ID, IPointerEvent, Point, State } from '../types';
import { getBoundingPoints, isPointInPolygon } from '../utils/point';
import type { ShortcutKey } from '../utils/shortcut';
import { Shortcut } from '../utils/shortcut';
import type { BaseBehaviorOptions } from './base-behavior';
const SHOW_RECT_ID = 'g6-brush-select-rect-id';
export type States = Record<ID, State | State[]>;
import { BaseBehavior } from './base-behavior';
/**
* <zh/>
@ -23,6 +18,13 @@ export type States = Record<ID, State | State[]>;
* <en/> Brush select options
*/
export interface BrushSelectOptions extends BaseBehaviorOptions {
/**
* <zh/>
*
* <en/> Whether to enable animation.
* @defaultValue false
*/
animation?: boolean;
/**
* <zh/>
*
@ -33,96 +35,87 @@ export interface BrushSelectOptions extends BaseBehaviorOptions {
/**
* <zh/>
*
* <en/> Enable Elements type
* <en/> Enable Elements type.
* @defaultValue ['node', 'combo', 'edge']
*/
enableElements?: ElementType[];
/**
* <zh/>
* <zh/>
*
* <en/> Whether to enable animation.
* @defaultValue false
*/
animation?: boolean;
/**
* <zh/>
* <en/> Press this shortcut key to apply brush select with mouse click.
* @remarks
* <zh/> `trigger` `['drag']` `drag-canvas`
*
* <en/> Trigger click or drag
* @defaultValue ['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 ['shift']
*/
trigger?: ShortcutKey;
/**
* <zh/>
* - union : 选中元素添加 state
* - intersect : 进一步筛选已经 state
* - diff : 反转选中元素的 state
* - default : state , state
* <zh/>
*
* <en/> Box Select Select the mode
* - 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
* <en/> The state to switch to when selected.
* @defaultValue 'selected'
*/
state?: State;
/**
* <zh/> , default 使
* <zh/>
* - `'union'` state
* - `'intersect'` state
* - `'diff'` state
* - `'default'` state
*
// * TODO fixme
* <en/> Timely screening
* <en/> Brush select mode
* - `'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
*/
immediately?: boolean;
/**
* <zh/>
* <zh/>
*
* <en/> Brush select box style
* <en/> Timely screening.
*/
style?: NodeStyle;
style?: RectStyleProps;
/**
* <zh/>
* <zh/>
*
* <en/> Callback when selecting elements
* @param states - <zh/> | <en/> Selected element state
* @returns <zh/> | <en/> Element state
* <en/> Callback when brush select elements.
* @param states -
* @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/>
*
* <en/> Brush select elements
*/
export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> extends BaseBehavior<T> {
export class BrushSelect extends BaseBehavior<BrushSelectOptions> {
static defaultOptions: Partial<BrushSelectOptions> = {
enable: true,
trigger: ['drag'],
immediately: false,
state: 'selected',
mode: 'default',
animation: false,
enable: true,
enableElements: ['node', 'combo', 'edge'],
immediately: false,
mode: 'default',
state: 'selected',
trigger: ['shift'],
style: {
size: 0,
type: 'rect',
...DEFAULT_STYLE,
width: 0,
height: 0,
lineWidth: 1,
fill: '#EEF6FF',
stroke: '#DDEEFE',
fillOpacity: 0.4,
zIndex: 2,
pointerEvents: 'none',
},
};
@ -130,55 +123,35 @@ export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> ext
private startPoint?: Point;
private endPoint?: Point;
private rectShape?: Rect;
public shortcut?: Shortcut;
private shortcut?: Shortcut;
public selectElementFn: (graph: Graph, id: ID, points: Points) => boolean = isBBoxCenterInRect;
constructor(context: RuntimeContext, options: T) {
super(context, Object.assign({}, BrushSelect.defaultOptions, options));
constructor(context: RuntimeContext, options: BrushSelectOptions) {
super(context, deepMix({}, BrushSelect.defaultOptions, options));
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();
}
/**
* <zh/>
*
* <en/> pointer down
* @param event - <zh/> | <en/> pointer event
*/
protected pointerDown = async (event: IPointerEvent) => {
protected onPointerDown(event: IPointerEvent) {
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;
this.rectShape = new Rect({
id: SHOW_RECT_ID,
style: {
...BrushSelect.defaultOptions.style,
fill: style.fill || DEFAULT_STYLE.fill,
...style,
pointerEvents: 'none',
},
});
this.rectShape = new Rect({ id: 'g6-brush-select', style: this.options.style });
canvas.appendChild(this.rectShape);
this.startPoint = [event.canvas.x, event.canvas.y];
};
/**
* <zh/>
*
* <en/> pointer move
* @param event - <zh/> | <en/> pointer event
*/
protected pointerMove = async (event: IPointerEvent) => {
}
protected onPointerMove(event: IPointerEvent) {
if (!this.startPoint) return;
const { immediately, mode } = this.options;
this.endPoint = [event.canvas.x, event.canvas.y];
this.endPoint = getCursorPoint(event);
this.rectShape?.attr({
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]),
});
if (immediately && mode === 'default') {
this.updateElementState(getRectPoints(this.startPoint, this.endPoint));
}
};
/**
* <zh/>
*
* <en/> pointer up
* @param event - <zh/> | <en/> pointer event
*/
protected pointerUp = async (event: IPointerEvent) => {
if (immediately && mode === 'default') this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
}
protected onPointerUp(event: IPointerEvent) {
if (!this.startPoint) return;
if (!this.endPoint) {
await this.clearBrush();
this.clearBrush();
return;
}
const points = getRectPoints(this.startPoint, [event.canvas.x, event.canvas.y]);
this.updateElementState(points);
this.endPoint = getCursorPoint(event);
this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
await this.clearBrush();
};
this.clearBrush();
}
/**
* <zh/>
* <zh/>
*
* <en/> clear brush
* @remarks
* <zh/>
*
* <en/> Triggered when clicking the canvas
* <en/> Clear state
* @internal
*/
protected clearSelected = () => {
protected clearStates() {
if (this.endPoint) return;
const { graph } = this.context;
const selects = getAllElementState(graph, () => []);
graph.setElementState(selects, this.options.animation);
};
this.clearElementsStates();
}
/**
* <zh/>
* <zh/>
*
* <en/> Update the state of the elements in the box selection area
* @param points - <zh/> | <en/> The coordinates of the points forming the box selection area
* <en/> Clear the state of all elements on the canvas
* @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 { enableElements, state, mode, onSelect } = this.options;
// 框选选中的 ids
const rectSelectIds = this.getPointsSelectIds(graph, points, enableElements);
const selectedIds = this.selector(graph, points, enableElements);
// state mode 框选逻辑
let stateChangeFn = (id: ID, oldState: string[]): string[] => (rectSelectIds.includes(id) ? [state] : []);
let states: Record<ID, State | State[]> = {};
switch (mode) {
case 'union':
stateChangeFn = (id: ID, oldState: string[]) => (rectSelectIds.includes(id) ? [state] : oldState);
selectedIds.forEach((id) => {
states[id] = [...graph.getElementState(id), state];
});
break;
case 'diff':
stateChangeFn = (id: ID, oldState: string[]) => {
if (rectSelectIds.includes(id)) {
return oldState.includes(state) ? [] : [state];
}
return oldState;
};
selectedIds.forEach((id) => {
const prevStates = graph.getElementState(id);
states[id] = prevStates.includes(state) ? prevStates.filter((s) => s !== state) : [...prevStates, state];
});
break;
case 'intersect':
stateChangeFn = (id: ID, oldState: string[]) => {
if (rectSelectIds.includes(id)) {
return oldState.includes(state) ? [state] : [];
}
return oldState;
};
selectedIds.forEach((id) => {
const prevStates = graph.getElementState(id);
states[id] = prevStates.includes(state) ? [state] : [];
});
break;
case 'default':
default:
selectedIds.forEach((id) => {
states[id] = [state];
});
break;
}
let states = getAllElementState(graph, stateChangeFn);
if (enableElements.includes('edge')) {
transformEdgeState(graph, states, state);
}
if (isFunction(onSelect)) {
states = onSelect(states);
}
graph.setElementState(states, this.options.animation);
};
if (isFunction(onSelect)) states = onSelect(states);
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 = undefined;
this.startPoint = 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
* @param event - <zh/> | <en/> pointer event
* @returns <zh/> | <en/> Whether to trigger the box selection
* <en/> Is the current key consistent with the trigger configuration
* @returns <zh/> | <en/> Is consistent
* @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) {
if (this.destroyed) return false;
@ -345,42 +332,48 @@ export class BrushSelect<T extends BaseBehaviorOptions = BrushSelectOptions> ext
return !!enable;
}
/**
* <zh/>
*
* <en/> Bind event
*/
protected bindEvents() {
private bindEvents() {
const { graph } = this.context;
this.unbindEvents();
graph.on(CommonEvent.POINTER_DOWN, this.pointerDown);
graph.on(CommonEvent.POINTER_MOVE, this.pointerMove);
graph.on(CommonEvent.POINTER_UP, this.pointerUp);
graph.on(`canvas:${CommonEvent.CLICK}`, this.clearSelected);
graph.on(CommonEvent.POINTER_DOWN, this.onPointerDown);
graph.on(CommonEvent.POINTER_MOVE, this.onPointerMove);
graph.on(CommonEvent.POINTER_UP, this.onPointerUp);
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() {
const { graph } = this.context;
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);
public update(options: Partial<BrushSelectOptions>) {
this.options = deepMix(this.options, options);
}
/**
* <zh/>
*
* <en/> Destroy
* @internal
*/
public destroy() {
this.unbindEvents();
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 { CommonEvent } from '../constants';
import { ELEMENT_TYPES } from '../constants/element';
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 { BaseBehavior } from './base-behavior';
const FOCUS_ELEMENT_TYPES = ['node', 'combo'];
/**
* <zh/>
*
* <en/> Focus element behavior options
*/
export interface FocusElementOptions extends BaseBehaviorOptions {
/**
* <zh/>
@ -17,12 +21,21 @@ export interface FocusElementOptions extends BaseBehaviorOptions {
/**
* <zh/>
*
* <en/> Whether to enable the function of dragging the node
* <en/> Whether to enable the function of focusing on the element
* @defaultValue true
*/
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> {
static defaultOptions: Partial<FocusElementOptions> = {
animation: {
@ -41,29 +54,16 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
const { graph } = this.context;
this.unbindEvents();
FOCUS_ELEMENT_TYPES.forEach((type) => {
graph.on(`${type}:${CommonEvent.CLICK}`, this.clickFocusElement);
ELEMENT_TYPES.forEach((type) => {
graph.on(`${type}:${CommonEvent.CLICK}`, this.focus);
});
}
private getSelectedNodeIDs(currTarget: ID[]) {
return Array.from(
new Set(
this.context.graph
.getElementDataByState('node', this.options.state)
.map((node) => node.id)
.concat(currTarget),
),
);
}
private clickFocusElement = async (event: IElementEvent) => {
private focus = async (event: IElementEvent) => {
if (!this.validate(event)) return;
const { animation } = this.options;
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) {
@ -76,8 +76,8 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
private unbindEvents() {
const { graph } = this.context;
FOCUS_ELEMENT_TYPES.forEach((type) => {
graph.off(`${type}:${CommonEvent.CLICK}`, this.clickFocusElement);
ELEMENT_TYPES.forEach((type) => {
graph.off(`${type}:${CommonEvent.CLICK}`, this.focus);
});
}
@ -86,5 +86,3 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
super.destroy();
}
}
interface IElementEvent extends IPointerEvent<Element> {}

View File

@ -1,121 +1,53 @@
import { Path } from '@antv/g';
import { isBBoxIntersectPolygon } from '../utils/behaviors/lasso';
import { getAllElementState } from '../utils/behaviors/utils';
import type { IPointerEvent, Points } from '../types';
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';
const SHOW_PATH_ID = 'g6-lasso-select-path-id';
export type States = Record<ID, State | State[]>;
import { BrushSelect, getCursorPoint } from './brush-select';
export interface LassoSelectOptions extends BrushSelectOptions {}
export class LassoSelect extends BrushSelect<LassoSelectOptions> {
static defaultOptions: Partial<LassoSelectOptions> = {
...BrushSelect.defaultOptions,
style: DEFAULT_STYLE,
};
export class LassoSelect extends BrushSelect {
private points?: Points;
private pathShape?: Path;
public selectElementFn = isBBoxIntersectPolygon;
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;
protected onPointerDown(event: IPointerEvent) {
if (!super.validate(event) || !super.isKeydown() || this.points) return;
const { canvas } = this.context;
this.pathShape = new Path({
id: SHOW_PATH_ID,
style: {
...LassoSelect.defaultOptions.style,
fill: style.fill || DEFAULT_STYLE.fill,
...style,
pointerEvents: 'none',
},
id: 'g6-lasso-select',
style: this.options.style,
});
canvas.appendChild(this.pathShape);
this.points = [[event.canvas.x, event.canvas.y]];
};
/**
* </zh>
*
* <en/> Pointer move
* @param event - <zh/> | <en/> pointer event object
*/
protected pointerMove = async (event: IPointerEvent) => {
this.points = [getCursorPoint(event)];
}
protected onPointerMove(event: IPointerEvent) {
if (!this.points) return;
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));
if (immediately && mode === 'default') {
this.lassoUpdateElementState();
}
await element?.draw({ animation: false, silence: true });
};
/**
* </zh>
*
* <en/> Pointer up
*/
protected pointerUp = async () => {
if (immediately && mode === 'default' && this.points.length > 2) super.updateElementsStates(this.points);
}
protected onPointerUp() {
if (!this.points) return;
if (this.points.length < 2) {
await this.clearLasso();
this.clearLasso();
return;
}
super.updateElementsStates(this.points);
this.lassoUpdateElementState();
this.clearLasso();
}
await this.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 () => {
private clearLasso() {
this.pathShape?.remove();
this.pathShape = undefined;
this.points = undefined;
};
}
}

View File

@ -3,7 +3,5 @@ import type { DragCanvasOptions } from './drag-canvas';
import type { ScrollCanvasOptions } from './scroll-canvas';
import type { ZoomCanvasOptions } from './zoom-canvas';
export type { States } from './brush-select';
export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions | ScrollCanvasOptions;
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
* @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 [
[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)
* @param point - <zh/> | <en/> point
* @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
*/
export function isPointInPolygon(point: Point, points: Point[]): boolean {
const [x, y] = point;
let isHit = false;
const n = points.length;
// 判断两个 double 在 eps 精度下的大小关系 | Determine the size relationship between two doubles within eps precision
const tolerance = 1e-6;
// svg 中点小于 3 个时,不显示,也无法被拾取 | When the number of points in the svg is less than 3, it is not displayed and cannot be picked up
if (n <= 2) return false;
const dcmp = (xValue: number) => {
if (Math.abs(xValue) < tolerance) {
return 0;
}
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;
}
export function isPointInPolygon(point: Point, points: Point[], start?: number, end?: number): boolean {
const x = point[0];
const y = point[1];
let inside = false;
if (start === undefined) start = 0;
if (end === undefined) end = points.length;
const len = end - start;
for (let i = 0, j = len - 1; i < len; j = i++) {
const xi = points[i + start][0];
const yi = points[i + start][1];
const xj = points[j + start][0];
const yj = points[j + start][1];
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return isHit;
return inside;
}
/**
@ -375,3 +362,13 @@ export function sortByClockwise(points: Point[], clockwise = true): Point[] {
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 = {
nodes: [
{ id: 'node1', style: { x: 100, y: 150, parentId: 'combo1' } },
{ id: 'node2', style: { x: 180, y: 150, parentId: 'combo1' } },
{ id: 'node3', style: { x: 150, y: 200, parentId: 'combo2' } },
{ id: 'node1', style: { x: 110, y: 150, parentId: 'combo1' } },
{ id: 'node2', style: { x: 190, y: 150, parentId: 'combo1' } },
{ id: 'node3', style: { x: 150, y: 260, parentId: 'combo2' } },
],
edges: [],
edges: [{ source: 'node1', target: 'node2' }],
combos: [
{ id: 'combo1', style: { parentId: 'combo2' } }, // collapsed: true
{ id: 'combo1', style: { parentId: 'combo2' } },
{ id: 'combo2', style: {} },
],
};
const graph = new Graph({
container: 'container',
width: 500,
height: 400,
node: {
style: {
labelText: (d) => d.id,
},
},
combo: {
style: {
type: 'circle', // 👈🏻 Combo shape type.
},
style: { labelText: (d) => d.id },
},
data,
behaviors: ['drag-canvas', 'collapse-expand', 'focus-element', 'zoom-canvas'],
behaviors: ['collapse-expand', 'focus-element'],
});
graph.render();

View File

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

View File

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