diff --git a/sites/x6-sites/docs/api/registry/attr.zh.md b/sites/x6-sites/docs/api/registry/attr.zh.md
index 8ca2e975b79..3fbb9244aa9 100644
--- a/sites/x6-sites/docs/api/registry/attr.zh.md
+++ b/sites/x6-sites/docs/api/registry/attr.zh.md
@@ -1,6 +1,6 @@
---
title: 属性
-order: 11
+order: 13
redirect_from:
- /zh/docs
- /zh/docs/api
diff --git a/sites/x6-sites/docs/api/registry/filter.zh.md b/sites/x6-sites/docs/api/registry/filter.zh.md
index ff00afbbaac..b4fb161e220 100644
--- a/sites/x6-sites/docs/api/registry/filter.zh.md
+++ b/sites/x6-sites/docs/api/registry/filter.zh.md
@@ -1,6 +1,6 @@
---
title: 滤镜
-order: 12
+order: 15
redirect_from:
- /zh/docs
- /zh/docs/api
diff --git a/sites/x6-sites/docs/api/registry/highlighter.zh.md b/sites/x6-sites/docs/api/registry/highlighter.zh.md
index 1b8f46c448b..a2c4f7f800b 100644
--- a/sites/x6-sites/docs/api/registry/highlighter.zh.md
+++ b/sites/x6-sites/docs/api/registry/highlighter.zh.md
@@ -1,6 +1,6 @@
---
title: 高亮器
-order: 11
+order: 14
redirect_from:
- /zh/docs
- /zh/docs/api
diff --git a/sites/x6-sites/docs/api/registry/port-label-layout.zh.md b/sites/x6-sites/docs/api/registry/port-label-layout.zh.md
index 5df5125fb1c..923d28f19a4 100644
--- a/sites/x6-sites/docs/api/registry/port-label-layout.zh.md
+++ b/sites/x6-sites/docs/api/registry/port-label-layout.zh.md
@@ -1,6 +1,6 @@
---
title: PortLabelLayout
-order: 14
+order: 12
redirect_from:
- /zh/docs
- /zh/docs/api
diff --git a/sites/x6-sites/docs/api/registry/port-layout.zh.md b/sites/x6-sites/docs/api/registry/port-layout.zh.md
index 97d017ca42d..65f4b01229a 100644
--- a/sites/x6-sites/docs/api/registry/port-layout.zh.md
+++ b/sites/x6-sites/docs/api/registry/port-layout.zh.md
@@ -1,6 +1,6 @@
---
-title: PortLayout
-order: 13
+title: 连接桩布局
+order: 11
redirect_from:
- /zh/docs
- /zh/docs/api
@@ -51,9 +51,7 @@ graph.addNode(
下面我们一起来看看如何使用内置的连接桩布局算法,以及如何自定并注册自定义布局算法。
-## presets
-
-在 `Registry.PortLayout.presets` 命名空间下提供了以下几个内置的布局算法。
+## 内置布局
### absolute
@@ -100,7 +98,7 @@ graph.addNode({
})
```
-
+
### left, right, top, bottom
@@ -145,7 +143,7 @@ graph.addNode({
})
```
-
+
### line
@@ -200,7 +198,7 @@ graph.addNode({
})
```
-
+
### ellipse
@@ -257,7 +255,7 @@ Array.from({ length: 10 }).forEach((_, index) => {
})
```
-
+
### ellipseSpread
@@ -312,9 +310,9 @@ Array.from({ length: 36 }).forEach(function (_, index) {
})
```
-
+
-## registry
+## 自定义连接桩布局
连接桩布局算法是一个函数具有如下签名的函数,返回每个连接桩相对于节点的相对位置。例如,某节点在画布的位置是 `{ x: 30, y: 40 }`,如果返回的某个连接桩的位置是 `{ x: 2, y: 4 }`,那么该连接桩渲染到画布后的位置是 `{ x: 32, y: 44 }`。
@@ -351,47 +349,11 @@ function sin(portsPositionArgs, elemBBox) {
布局算法实现后,需要注册到系统,注册后就可以像内置布局算法那样来使用。
-### register
-
-```ts
-register(entities: { [name: string]: Definition }, force?: boolean): void
-register(name: string, entity: Definition, force?: boolean): Definition
-```
-
-注册自定义布局算法。
-
-### unregister
-
-```ts
-unregister(name: string): Definition | null
-```
-
-删除注册的自定义布局算法。
-
-实际上,我们将 `registry` 的 `register` 和 `unregister` 方法分别挂载为 `Graph` 的两个静态方法 `Graph.registerPortLayout` 和 `Graph.unregisterPortLayout`,所以我们定义的正弦布局可以像下面这样注册到系统:
```ts
Graph.registerPortLayout('sin', sin)
```
-或者:
-
-```ts
-Graph.registerPortLayout('sin', (portsPositionArgs, elemBBox) => {
- return portsPositionArgs.map((_, index) => {
- const step = -Math.PI / 8
- const y = Math.sin(index * step) * 50
- return {
- position: {
- x: index * 12,
- y: y + elemBBox.height,
- },
- angle: 0,
- }
- })
-})
-```
-
注册以后,我们就可以像内置布局算法那样来使用:
```ts
@@ -429,4 +391,4 @@ Array.from({ length: 24 }).forEach(() => {
})
```
-
+
diff --git a/sites/x6-sites/src/api/port-label-layout/inside-outside/index.less b/sites/x6-sites/src/api/port-label-layout/inside-outside/index.less
new file mode 100644
index 00000000000..419153c6251
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/inside-outside/index.less
@@ -0,0 +1,42 @@
+.port-label-inside-outside-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/inside-outside/index.tsx b/sites/x6-sites/src/api/port-label-layout/inside-outside/index.tsx
new file mode 100644
index 00000000000..bb2a4cc83ef
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/inside-outside/index.tsx
@@ -0,0 +1,98 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ shape: 'ellipse',
+ x: 70,
+ y: 85,
+ width: 260,
+ height: 100,
+ attrs: {
+ label: {
+ text: 'outside',
+ fill: '#888',
+ fontSize: 12,
+ },
+ body: {
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fff',
+ rx: 6,
+ ry: 6,
+ },
+ },
+ ports: {
+ groups: {
+ a: {
+ position: {
+ name: 'ellipseSpread',
+ args: {
+ compensateRotate: true,
+ },
+ },
+ label: {
+ position: {
+ name: 'outside',
+ },
+ },
+ attrs: {
+ circle: {
+ fill: '#ffffff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ r: 10,
+ magnet: true,
+ },
+ text: {
+ fill: '#6a6c8a',
+ fontSize: 12,
+ },
+ },
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 10 }).forEach((_, index) => {
+ this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' })
+ })
+ }
+
+ onAttrsChanged = ({ position, offset }: State) => {
+ this.node.prop('ports/groups/a/label/position', {
+ name: position,
+ args: { offset },
+ })
+
+ this.node.attr('label/text', position)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/inside-outside/settings.tsx b/sites/x6-sites/src/api/port-label-layout/inside-outside/settings.tsx
new file mode 100644
index 00000000000..2b58eb40b1e
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/inside-outside/settings.tsx
@@ -0,0 +1,103 @@
+import React from 'react'
+import { Radio, Slider, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ position: string
+ offset: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ position: 'outside',
+ offset: 15,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onOffsetChanged = (offset: number) => {
+ this.setState({ offset }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onPositionChange = (e: any) => {
+ this.setState(
+ {
+ position: e.target.value,
+ },
+ () => {
+ this.notifyChange()
+ },
+ )
+ }
+
+ render() {
+ const radioStyle = {
+ display: 'block',
+ height: '36px',
+ lineHeight: '36px',
+ }
+
+ return (
+
+
+
+
+
+ inside
+
+
+ outside
+
+
+ insideOriented
+
+
+ outsideOriented
+
+
+
+
+
+
+ offset
+
+
+
+
+
+ {this.state.offset}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/radial/index.less b/sites/x6-sites/src/api/port-label-layout/radial/index.less
new file mode 100644
index 00000000000..51d1104836b
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/radial/index.less
@@ -0,0 +1,42 @@
+.port-label-radial-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/radial/index.tsx b/sites/x6-sites/src/api/port-label-layout/radial/index.tsx
new file mode 100644
index 00000000000..afccd64d79f
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/radial/index.tsx
@@ -0,0 +1,98 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ shape: 'ellipse',
+ x: 70,
+ y: 50,
+ width: 260,
+ height: 100,
+ attrs: {
+ label: {
+ text: 'outside',
+ fill: '#888',
+ fontSize: 12,
+ },
+ body: {
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fff',
+ rx: 6,
+ ry: 6,
+ },
+ },
+ ports: {
+ groups: {
+ a: {
+ position: {
+ name: 'ellipseSpread',
+ args: {
+ compensateRotate: true,
+ },
+ },
+ label: {
+ position: {
+ name: 'outside',
+ },
+ },
+ attrs: {
+ circle: {
+ fill: '#ffffff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ r: 10,
+ magnet: true,
+ },
+ text: {
+ fill: '#6a6c8a',
+ fontSize: 12,
+ },
+ },
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 10 }).forEach((_, index) => {
+ this.node.addPort({ attrs: { text: { text: `P ${index}` } }, group: 'a' })
+ })
+ }
+
+ onAttrsChanged = ({ position, offset }: State) => {
+ this.node.prop('ports/groups/a/label/position', {
+ name: position,
+ args: { offset },
+ })
+
+ this.node.attr('label/text', position)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/radial/settings.tsx b/sites/x6-sites/src/api/port-label-layout/radial/settings.tsx
new file mode 100644
index 00000000000..291df7bfd1e
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/radial/settings.tsx
@@ -0,0 +1,97 @@
+import React from 'react'
+import { Radio, Slider, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ position: string
+ offset: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ position: 'radial',
+ offset: 20,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onOffsetChanged = (offset: number) => {
+ this.setState({ offset }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onPositionChange = (e: any) => {
+ this.setState(
+ {
+ position: e.target.value,
+ },
+ () => {
+ this.notifyChange()
+ },
+ )
+ }
+
+ render() {
+ const radioStyle = {
+ display: 'block',
+ height: '36px',
+ lineHeight: '36px',
+ }
+
+ return (
+
+
+
+
+
+ radial
+
+
+ radialOriented
+
+
+
+
+
+
+ offset
+
+
+
+
+
+ {this.state.offset}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/side/index.less b/sites/x6-sites/src/api/port-label-layout/side/index.less
new file mode 100644
index 00000000000..dda8d3dbf27
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/side/index.less
@@ -0,0 +1,42 @@
+.port-label-side-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/side/index.tsx b/sites/x6-sites/src/api/port-label-layout/side/index.tsx
new file mode 100644
index 00000000000..fba03ce290d
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/side/index.tsx
@@ -0,0 +1,108 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ shape: 'rect',
+ x: 80,
+ y: 68,
+ width: 240,
+ height: 80,
+ attrs: {
+ label: {
+ text: 'left',
+ fill: '#6a6c8a',
+ },
+ body: {
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fff',
+ rx: 6,
+ ry: 6,
+ },
+ },
+ ports: {
+ groups: {
+ a: {
+ position: {
+ name: 'top',
+ args: {
+ dr: 0,
+ dx: 0,
+ dy: -10,
+ },
+ },
+ label: {
+ position: {
+ name: 'left',
+ },
+ },
+ attrs: {
+ circle: {
+ fill: '#ffffff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ r: 10,
+ },
+ text: {
+ fill: '#6a6c8a',
+ },
+ },
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 3 }).forEach((_, index) => {
+ const label =
+ index === 2
+ ? {
+ position: { args: { x: 20, y: -20 } },
+ }
+ : {}
+ const stroke = index === 2 ? { stroke: 'red' } : {}
+ const fill = index === 2 ? { fill: 'red' } : {}
+ this.node.addPort({
+ label,
+ group: 'a',
+ attrs: {
+ circle: { magnet: true, ...stroke },
+ text: { text: `P${index}`, ...fill },
+ },
+ })
+ })
+ }
+
+ onAttrsChanged = ({ position }: State) => {
+ this.node.prop('ports/groups/a/label/position/name', position)
+ this.node.attr('label/text', position)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-label-layout/side/settings.tsx b/sites/x6-sites/src/api/port-label-layout/side/settings.tsx
new file mode 100644
index 00000000000..a6ca02615f3
--- /dev/null
+++ b/sites/x6-sites/src/api/port-label-layout/side/settings.tsx
@@ -0,0 +1,94 @@
+import React from 'react'
+import { Radio, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ position: string
+ dx: number
+ dy: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ position: 'left',
+ dx: 0,
+ dy: 0,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onDxChanged = (dx: number) => {
+ this.setState({ dx }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDyChanged = (dy: number) => {
+ this.setState({ dy }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onPositionChange = (e: any) => {
+ this.setState(
+ {
+ position: e.target.value,
+ },
+ () => {
+ this.notifyChange()
+ },
+ )
+ }
+
+ render() {
+ const radioStyle = {
+ display: 'block',
+ height: '36px',
+ lineHeight: '36px',
+ }
+
+ return (
+
+
+
+
+
+ left
+
+
+ right
+
+
+ top
+
+
+ bottom
+
+
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/absolute/index.less b/sites/x6-sites/src/api/port-layout/absolute/index.less
new file mode 100644
index 00000000000..14d47bea90a
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/absolute/index.less
@@ -0,0 +1,42 @@
+.port-absolute-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/absolute/index.tsx b/sites/x6-sites/src/api/port-layout/absolute/index.tsx
new file mode 100644
index 00000000000..71d7e57e47e
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/absolute/index.tsx
@@ -0,0 +1,129 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ x: 120,
+ y: 48,
+ width: 280,
+ height: 120,
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'body',
+ },
+ ],
+ attrs: {
+ body: {
+ refWidth: '100%',
+ refHeight: '100%',
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ },
+ ports: {
+ groups: {
+ group1: {
+ attrs: {
+ circle: {
+ r: 6,
+ magnet: true,
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fff',
+ },
+ text: {
+ fontSize: 12,
+ fill: '#888',
+ },
+ },
+ position: {
+ name: 'absolute',
+ },
+ },
+ },
+ items: [
+ {
+ id: 'port1',
+ group: 'group1',
+ args: { x: 0, y: 60 },
+ attrs: {
+ text: { text: '{ x: 0, y: 60 }' },
+ },
+ },
+ {
+ id: 'port2',
+ group: 'group1',
+ args: { x: 0.6, y: 32, angle: 45 },
+ markup: [
+ {
+ tagName: 'path',
+ selector: 'path',
+ },
+ ],
+ zIndex: 10,
+ attrs: {
+ path: {
+ d: 'M -6 -8 L 0 8 L 6 -8 Z',
+ magnet: true,
+ fill: '#8f8f8f',
+ },
+ text: { text: '{ x: 0.6, y: 32, angle: 45 }', fill: '#888' },
+ },
+ },
+ {
+ id: 'port3',
+ group: 'group1',
+ args: { x: '100%', y: '100%' },
+ attrs: {
+ text: { text: "{ x: '100%', y: '100%' }" },
+ },
+ label: {
+ position: {
+ name: 'right',
+ },
+ },
+ },
+ ],
+ },
+ })
+ }
+
+ onAttrsChanged = (args: State) => {
+ this.node.portProp('port2', {
+ args,
+ attrs: {
+ text: { text: `{ x: ${args.x}, y: ${args.y}, angle: ${args.angle} }` },
+ },
+ } as any)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/absolute/settings.tsx b/sites/x6-sites/src/api/port-layout/absolute/settings.tsx
new file mode 100644
index 00000000000..78186ad5deb
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/absolute/settings.tsx
@@ -0,0 +1,100 @@
+import React from 'react'
+import { Slider, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ x: number
+ y: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ x: 0.6,
+ y: 32,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onXChanged = (x: number) => {
+ this.setState({ x }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onYChanged = (y: number) => {
+ this.setState({ y }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ render() {
+ return (
+
+
+
+ x
+
+
+
+
+
+ {this.state.x}
+
+
+
+
+ y
+
+
+
+
+
+ {this.state.y}
+
+
+
+
+ angle
+
+
+
+
+
+ {this.state.angle}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse-spread/index.less b/sites/x6-sites/src/api/port-layout/ellipse-spread/index.less
new file mode 100644
index 00000000000..c774001d8f1
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse-spread/index.less
@@ -0,0 +1,42 @@
+.port-ellipse-spread-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse-spread/index.tsx b/sites/x6-sites/src/api/port-layout/ellipse-spread/index.tsx
new file mode 100644
index 00000000000..93e40851f24
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse-spread/index.tsx
@@ -0,0 +1,122 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ x: 120,
+ y: 90,
+ width: 360,
+ height: 200,
+ shape: 'ellipse',
+
+ attrs: {
+ body: {
+ refWidth: '100%',
+ refHeight: '100%',
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ },
+ ports: {
+ groups: {
+ group1: {
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'rect',
+ },
+ {
+ tagName: 'circle',
+ selector: 'dot',
+ },
+ ],
+ attrs: {
+ rect: {
+ magnet: true,
+ stroke: '#8f8f8f',
+ fill: 'rgba(255,255,255,0.8)',
+ strokeWidth: 1,
+ width: 16,
+ height: 16,
+ x: -8,
+ y: -8,
+ },
+ dot: {
+ fill: '#8f8f8f',
+ r: 2,
+ },
+ text: {
+ fontSize: 12,
+ fill: '#888',
+ },
+ },
+ label: {
+ position: 'radial',
+ },
+ position: {
+ name: 'ellipseSpread',
+ args: {
+ start: 45,
+ },
+ },
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 10 }).forEach((_, index) => {
+ this.node.addPort({
+ id: `${index}`,
+ group: 'group1',
+ attrs: { text: { text: index } },
+ })
+ })
+
+ this.node.portProp('0', {
+ attrs: {
+ rect: { stroke: '#8f8f8f' },
+ dot: { fill: '#8f8f8f' },
+ },
+ })
+ }
+
+ onAttrsChanged = ({ start, compensateRotate, ...args }: State) => {
+ this.node.prop('ports/groups/group1/position/args', {
+ start,
+ compensateRotate,
+ })
+
+ this.node.portProp('0', {
+ args,
+ } as any)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse-spread/settings.tsx b/sites/x6-sites/src/api/port-layout/ellipse-spread/settings.tsx
new file mode 100644
index 00000000000..609a62f8b79
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse-spread/settings.tsx
@@ -0,0 +1,171 @@
+import React from 'react'
+import { Slider, Checkbox, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ start: number
+ compensateRotate: boolean
+ dr: number
+ dx: number
+ dy: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ start: 45,
+ compensateRotate: false,
+ dr: 0,
+ dx: 0,
+ dy: 0,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onStartChanged = (start: number) => {
+ this.setState({ start }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onCompensateRotateChange = (e: any) => {
+ this.setState({ compensateRotate: e.target.checked }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDxChanged = (dx: number) => {
+ this.setState({ dx }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDrChanged = (dr: number) => {
+ this.setState({ dr }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDyChanged = (dy: number) => {
+ this.setState({ dy }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ render() {
+ return (
+
+
+
+ start
+
+
+
+
+
+ {this.state.start}
+
+
+
+
+
+ compensateRotate
+
+
+
+
+
+ dr
+
+
+
+
+
+ {this.state.dr}
+
+
+
+
+ dx
+
+
+
+
+
+ {this.state.dx}
+
+
+
+
+ dy
+
+
+
+
+
+ {this.state.dy}
+
+
+
+
+ angle
+
+
+
+
+
+ {this.state.angle}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse/index.less b/sites/x6-sites/src/api/port-layout/ellipse/index.less
new file mode 100644
index 00000000000..86d4864e992
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse/index.less
@@ -0,0 +1,42 @@
+.port-ellipse-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse/index.tsx b/sites/x6-sites/src/api/port-layout/ellipse/index.tsx
new file mode 100644
index 00000000000..218b39f8f1f
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse/index.tsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import { Graph, Node, Point } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+function getPathData(deg: number) {
+ const center = new Point(180, 100)
+ const start = new Point(180, 0)
+ const ratio = center.x / center.y
+ const p = start
+ .clone()
+ .rotate(90 - deg, center)
+ .scale(ratio, 1, center)
+ return `M ${center.x} ${center.y} ${p.x} ${p.y}`
+}
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ x: 120,
+ y: 90,
+ width: 360,
+ height: 200,
+ shape: 'ellipse',
+ markup: [
+ { tagName: 'ellipse', selector: 'body' },
+ { tagName: 'text', selector: 'label' },
+ { tagName: 'path', selector: 'line' },
+ ],
+ attrs: {
+ body: {
+ refWidth: '100%',
+ refHeight: '100%',
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ line: {
+ d: getPathData(45),
+ stroke: '#8f8f8f',
+ strokeDasharray: '5 5',
+ },
+ },
+ ports: {
+ groups: {
+ group1: {
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'rect',
+ },
+ {
+ tagName: 'circle',
+ selector: 'dot',
+ },
+ ],
+ attrs: {
+ rect: {
+ magnet: true,
+ stroke: '#8f8f8f',
+ fill: 'rgba(255,255,255,0.8)',
+ strokeWidth: 1,
+ width: 16,
+ height: 16,
+ x: -8,
+ y: -8,
+ },
+ dot: {
+ fill: '#8f8f8f',
+ r: 2,
+ },
+ text: {
+ fontSize: 12,
+ fill: '#888',
+ },
+ },
+ label: {
+ position: 'radial',
+ },
+ position: {
+ name: 'ellipse',
+ args: {
+ start: 45,
+ },
+ },
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 10 }).forEach((_, index) => {
+ this.node.addPort({
+ id: `${index}`,
+ group: 'group1',
+ attrs: { text: { text: index } },
+ })
+ })
+
+ this.node.portProp('0', {
+ attrs: {
+ rect: { stroke: '#8f8f8f' },
+ dot: { fill: '#8f8f8f' },
+ },
+ })
+ }
+
+ onAttrsChanged = ({ start, step, compensateRotate, ...args }: State) => {
+ this.node.prop('ports/groups/group1/position/args', {
+ start,
+ step,
+ compensateRotate,
+ })
+
+ this.node.attr({
+ line: { d: getPathData(start) },
+ })
+
+ this.node.portProp('0', {
+ args,
+ } as any)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/ellipse/settings.tsx b/sites/x6-sites/src/api/port-layout/ellipse/settings.tsx
new file mode 100644
index 00000000000..267fde79d28
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/ellipse/settings.tsx
@@ -0,0 +1,196 @@
+import React from 'react'
+import { Slider, Checkbox, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ start: number
+ step: number
+ compensateRotate: boolean
+ dr: number
+ dx: number
+ dy: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ start: 45,
+ step: 20,
+ compensateRotate: false,
+ dr: 0,
+ dx: 0,
+ dy: 0,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onStartChanged = (start: number) => {
+ this.setState({ start }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onStepChanged = (step: number) => {
+ this.setState({ step }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onCompensateRotateChange = (e: any) => {
+ this.setState({ compensateRotate: e.target.checked }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDxChanged = (dx: number) => {
+ this.setState({ dx }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDrChanged = (dr: number) => {
+ this.setState({ dr }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDyChanged = (dy: number) => {
+ this.setState({ dy }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ render() {
+ return (
+
+
+
+ start
+
+
+
+
+
+ {this.state.start}
+
+
+
+
+ step
+
+
+
+
+
+ {this.state.step}
+
+
+
+
+
+ compensateRotate
+
+
+
+
+
+ dr
+
+
+
+
+
+ {this.state.dr}
+
+
+
+
+ dx
+
+
+
+
+
+ {this.state.dx}
+
+
+
+
+ dy
+
+
+
+
+
+ {this.state.dy}
+
+
+
+
+ angle
+
+
+
+
+
+ {this.state.angle}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/line/index.less b/sites/x6-sites/src/api/port-layout/line/index.less
new file mode 100644
index 00000000000..521da07b3c0
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/line/index.less
@@ -0,0 +1,42 @@
+.port-line-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/line/index.tsx b/sites/x6-sites/src/api/port-layout/line/index.tsx
new file mode 100644
index 00000000000..88aa6bea7fb
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/line/index.tsx
@@ -0,0 +1,127 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ x: 160,
+ y: 80,
+ width: 280,
+ height: 120,
+ markup: [
+ {
+ tagName: 'rect',
+ selector: 'body',
+ },
+ {
+ tagName: 'path',
+ selector: 'line',
+ },
+ ],
+ attrs: {
+ body: {
+ refWidth: '100%',
+ refHeight: '100%',
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ line: {
+ d: 'M 0 0 280 120',
+ stroke: '#8f8f8f',
+ strokeDasharray: '5 5',
+ },
+ },
+ ports: {
+ groups: {
+ group1: {
+ attrs: {
+ circle: {
+ r: 6,
+ magnet: true,
+ stroke: '#8f8f8f',
+ strokeWidth: 2,
+ fill: '#fff',
+ },
+ text: {
+ fontSize: 12,
+ fill: '#888',
+ },
+ },
+ position: {
+ name: 'line',
+ args: {
+ start: { x: 0, y: 0 },
+ end: { x: 280, y: 120 },
+ },
+ },
+ },
+ },
+ items: [
+ {
+ id: 'port1',
+ group: 'group1',
+ },
+ {
+ id: 'port2',
+ group: 'group1',
+ args: { angle: 45 },
+ markup: [
+ {
+ tagName: 'path',
+ selector: 'path',
+ },
+ ],
+ attrs: {
+ path: {
+ d: 'M -6 -8 L 0 8 L 6 -8 Z',
+ magnet: true,
+ fill: '#8f8f8f',
+ },
+ },
+ },
+ {
+ id: 'port3',
+ group: 'group1',
+ args: {},
+ },
+ ],
+ },
+ })
+ }
+
+ onAttrsChanged = ({ strict, ...args }: State) => {
+ this.node.prop('ports/groups/group1/position', {
+ name: 'line',
+ args: { strict },
+ })
+ this.node.portProp('port2', { args } as any)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/line/settings.tsx b/sites/x6-sites/src/api/port-layout/line/settings.tsx
new file mode 100644
index 00000000000..0c6afdfb979
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/line/settings.tsx
@@ -0,0 +1,122 @@
+import React from 'react'
+import { Slider, Switch, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ strict: boolean
+ dx: number
+ dy: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ strict: false,
+ dx: 0,
+ dy: 0,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onStrictChange = (strict: boolean) => {
+ this.setState({ strict }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDxChanged = (dx: number) => {
+ this.setState({ dx }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDyChanged = (dy: number) => {
+ this.setState({ dy }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ render() {
+ return (
+
+
+
+ strict
+
+
+
+
+
+
+
+ dx
+
+
+
+
+
+ {this.state.dx}
+
+
+
+
+ dy
+
+
+
+
+
+ {this.state.dy}
+
+
+
+
+ angle
+
+
+
+
+
+ {this.state.angle}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/side/index.less b/sites/x6-sites/src/api/port-layout/side/index.less
new file mode 100644
index 00000000000..22bb7935836
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/side/index.less
@@ -0,0 +1,42 @@
+.port-side-app {
+ display: flex;
+ padding: 0;
+ padding: 16px 8px;
+ font-family: sans-serif;
+
+ .app-side {
+ bottom: 0;
+ padding: 0 8px;
+ }
+
+ .app-content {
+ flex: 1;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card {
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+
+ .ant-card-head-title {
+ text-align: center;
+ }
+
+ .ant-row {
+ margin: 16px 0;
+ text-align: left;
+ }
+
+ .slider-value {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 3px 7px;
+ color: #333;
+ font-size: 12px;
+ line-height: 1.25;
+ background: #eee;
+ border-radius: 10px;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/side/index.tsx b/sites/x6-sites/src/api/port-layout/side/index.tsx
new file mode 100644
index 00000000000..8c7b8fd5084
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/side/index.tsx
@@ -0,0 +1,110 @@
+import React from 'react'
+import { Graph, Node } from '@antv/x6'
+import { Settings, State } from './settings'
+import './index.less'
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+ private node: Node
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ this.node = graph.addNode({
+ x: 160,
+ y: 110,
+ width: 280,
+ height: 120,
+ attrs: {
+ body: {
+ refWidth: '100%',
+ refHeight: '100%',
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ label: { text: 'left', fill: '#888' },
+ },
+ ports: {
+ groups: {
+ group1: {
+ attrs: {
+ circle: {
+ r: 6,
+ magnet: true,
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fff',
+ },
+ text: {
+ fontSize: 12,
+ fill: '#888',
+ },
+ },
+ position: {
+ name: 'left',
+ },
+ },
+ },
+ items: [
+ {
+ id: 'port1',
+ group: 'group1',
+ },
+ {
+ id: 'port2',
+ group: 'group1',
+ args: { angle: 45 },
+ markup: [
+ {
+ tagName: 'path',
+ selector: 'path',
+ },
+ ],
+ attrs: {
+ path: {
+ d: 'M -6 -8 L 0 8 L 6 -8 Z',
+ magnet: true,
+ fill: '#8f8f8f',
+ },
+ },
+ },
+ {
+ id: 'port3',
+ group: 'group1',
+ args: {},
+ },
+ ],
+ },
+ })
+ }
+
+ onAttrsChanged = ({ position, strict, ...args }: State) => {
+ this.node.prop('ports/groups/group1/position', {
+ name: position,
+ args: { strict },
+ })
+ this.node.attr('label/text', position)
+ this.node.portProp('port2', { args } as any)
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/side/settings.tsx b/sites/x6-sites/src/api/port-layout/side/settings.tsx
new file mode 100644
index 00000000000..0ebdbfff5e2
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/side/settings.tsx
@@ -0,0 +1,151 @@
+import React from 'react'
+import { Radio, Switch, Slider, Card, Row, Col } from 'antd'
+
+export interface Props {
+ onChange: (state: State) => void
+}
+
+export interface State {
+ position: string
+ strict: boolean
+ dx: number
+ dy: number
+ angle: number
+}
+
+export class Settings extends React.Component {
+ state: State = {
+ position: 'left',
+ strict: false,
+ dx: 0,
+ dy: 0,
+ angle: 45,
+ }
+
+ notifyChange() {
+ this.props.onChange(this.state)
+ }
+
+ onDxChanged = (dx: number) => {
+ this.setState({ dx }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onDyChanged = (dy: number) => {
+ this.setState({ dy }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onAngleChanged = (angle: number) => {
+ this.setState({ angle }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onStrictChange = (strict: boolean) => {
+ this.setState({ strict }, () => {
+ this.notifyChange()
+ })
+ }
+
+ onPositionChange = (e: any) => {
+ this.setState(
+ {
+ position: e.target.value,
+ },
+ () => {
+ this.notifyChange()
+ },
+ )
+ }
+
+ render() {
+ return (
+
+
+
+ position
+
+
+
+ left
+ right
+ top
+ bottom
+
+
+
+
+
+ strict
+
+
+
+
+
+
+
+ dx
+
+
+
+
+
+ {this.state.dx}
+
+
+
+
+ dy
+
+
+
+
+
+ {this.state.dy}
+
+
+
+
+ angle
+
+
+
+
+
+ {this.state.angle}
+
+
+
+ )
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/sin/index.less b/sites/x6-sites/src/api/port-layout/sin/index.less
new file mode 100644
index 00000000000..0df96e61630
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/sin/index.less
@@ -0,0 +1,14 @@
+.port-sin-app {
+ display: flex;
+ width: 100%;
+ padding: 0;
+ font-family: sans-serif;
+
+ .app-content {
+ flex: 1;
+ height: 240px;
+ margin-right: 8px;
+ margin-left: 8px;
+ box-shadow: 0 0 10px 1px #e9e9e9;
+ }
+}
diff --git a/sites/x6-sites/src/api/port-layout/sin/index.tsx b/sites/x6-sites/src/api/port-layout/sin/index.tsx
new file mode 100644
index 00000000000..faa6c7c9cfa
--- /dev/null
+++ b/sites/x6-sites/src/api/port-layout/sin/index.tsx
@@ -0,0 +1,80 @@
+import React from 'react'
+import { Graph } from '@antv/x6'
+import './index.less'
+
+Graph.registerPortLayout(
+ 'sin',
+ (portsPositionArgs, elemBBox) => {
+ return portsPositionArgs.map((_, index) => {
+ const step = -Math.PI / 8
+ const y = Math.sin(index * step) * 50
+ return {
+ position: {
+ x: index * 12,
+ y: y + elemBBox.height,
+ },
+ angle: 0,
+ }
+ })
+ },
+ true,
+)
+
+export default class Example extends React.Component {
+ private container: HTMLDivElement
+
+ componentDidMount() {
+ const graph = new Graph({
+ container: this.container,
+ background: {
+ color: '#F2F7FA',
+ },
+ })
+
+ const rect = graph.addNode({
+ x: 120,
+ y: 40,
+ width: 280,
+ height: 120,
+ attrs: {
+ body: {
+ fill: '#fff',
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ },
+ },
+ ports: {
+ groups: {
+ sin: {
+ attrs: {
+ circle: {
+ r: 6,
+ magnet: true,
+ stroke: '#8f8f8f',
+ strokeWidth: 1,
+ fill: '#fe854f',
+ },
+ },
+ position: 'sin',
+ },
+ },
+ },
+ })
+
+ Array.from({ length: 24 }).forEach(() => {
+ rect.addPort({ group: 'sin' })
+ })
+ }
+
+ refContainer = (container: HTMLDivElement) => {
+ this.container = container
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}