refactor: focus-element, lasso-select, brush-select behavior (#5707)
* refactor: focus-element behavior * refactor: lasso select, brush select * fix: remove unnecessary utils
@ -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;
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ export const behaviorFocusElement: TestCase = async (context) => {
|
||||
edge: {
|
||||
style: { endArrow: true },
|
||||
},
|
||||
behaviors: [{ type: 'focus-element' }],
|
||||
behaviors: ['focus-element'],
|
||||
});
|
||||
|
||||
await graph.render();
|
||||
|
@ -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();
|
||||
|
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 } });
|
||||
|
@ -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 } });
|
||||
|
@ -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],
|
||||
|
@ -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', () => {
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -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> {}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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]]];
|
||||
}
|
@ -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;
|
||||
};
|
@ -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] = [];
|
||||
}
|
||||
});
|
||||
};
|
@ -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],
|
||||
|
@ -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]]];
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
|