Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-invalid-polyline-path-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@logicflow/extension': patch
---

fix: prevent curved-edge path generation from throwing for empty, single-point, or malformed point lists
5 changes: 5 additions & 0 deletions .changeset/fix-rounded-rect-edge-nan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@logicflow/core': patch
---

fix: keep polyline endpoints finite when dragging across rounded rectangle straight-side regions
2 changes: 2 additions & 0 deletions examples/dynamic-group-regression/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ cd examples/dynamic-group-regression && pnpm dev
| `resize-bounds` | LOCAL-resize-bounds(DG 缩小 resize 最小边界) |
| `resize-undo-twice` | #1532(关闭 · 暂不修复) |
| `resize-single-axis` | #1555(关闭 · 暂不修复) |
| `selection-copy-paste` | LOCAL-copy-paste(选区复制分组+连线后节点/连线分离) |

## 图数据约定

Expand All @@ -72,6 +73,7 @@ cd examples/dynamic-group-regression && pnpm dev

- 部分场景需**手动**拖折线控制点(#2401、#2399)后再点折叠。
- `#2412` 使用自定义节点类型 `locked-dynamic-group`。
- `selection-copy-paste` 使用 `SelectionSelect` 插件框选(点「开启框选」后在空白处拖拽),复制/粘贴按钮与键盘 `Cmd/Ctrl+C`、`Cmd/Ctrl+V` 走同一套 core 逻辑;复现要点:框选「分组+组内节点+连线」粘贴后,`addElements` 会让新分组另生成一套幽灵子节点,粘贴出的连线连的是另一批子节点,拖动新分组即分离(点「诊断」可看到重合矩形与归属信息)。
- 布局场景(`layout-format-escape`)支持通过控制面板组合测试:**Dagre (#2205) / ELK (#2332)**、全图/组内布局、`resizeGroup`、分组 `resizable` 与尺寸等。

## 相关文档
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DndPanel,
DynamicGroup,
MiniMap,
SelectionSelect,
type ShapeItem,
} from '@logicflow/extension'
import { Dagre, ElkLayout } from '@logicflow/layout'
Expand Down Expand Up @@ -51,7 +52,15 @@ const lfOptions: Partial<LogicFlow.Options> = {
allowRotate: false,
textEdit: true,
keyboard: { enabled: true },
plugins: [DynamicGroup, Control, DndPanel, Dagre, ElkLayout, MiniMap],
plugins: [
DynamicGroup,
Control,
DndPanel,
Dagre,
ElkLayout,
MiniMap,
SelectionSelect,
],
pluginsOptions: {
dynamicGroup: {
disallowEdgeConnectToGroup: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Alert, Button, Space, Typography } from 'antd'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { ScenarioControlsProps } from '@/scenarios/types'
import {
buildSelectionCopyGraph,
copySelection,
diagnoseCopyPaste,
pasteSelection,
selectAll,
type Clipboard,
} from '@/scenarios/selectionCopyPaste'

const { Text, Paragraph } = Typography

type LfWithSelection = NonNullable<ScenarioControlsProps['lf']> & {
openSelectionSelect?: () => void
closeSelectionSelect?: () => void
}

export default function SelectionCopyControls({
lf,
loadGraph,
}: ScenarioControlsProps) {
const clipboardRef = useRef<Clipboard>(null)
const lastPastedGroupRef = useRef<string | undefined>(undefined)
const [selectionOpen, setSelectionOpen] = useState(false)
const [hasClipboard, setHasClipboard] = useState(false)

// 离开场景时关闭框选,避免影响其它场景
useEffect(() => {
return () => {
const target = lf as LfWithSelection | undefined
target?.closeSelectionSelect?.()
}
}, [lf])

const toggleSelection = useCallback(() => {
const target = lf as LfWithSelection | undefined
if (!target) return
if (selectionOpen) {
target.closeSelectionSelect?.()
setSelectionOpen(false)
} else {
target.openSelectionSelect?.()
setSelectionOpen(true)
}
}, [lf, selectionOpen])

const handleSelectAll = useCallback(() => {
if (!lf) return
selectAll(lf)
}, [lf])

const handleCopy = useCallback(() => {
if (!lf) return
const clipboard = copySelection(lf)
clipboardRef.current = clipboard
setHasClipboard(!!clipboard)
if (!clipboard) {
alert('当前没有选中任何元素,请先框选或全选')
}
}, [lf])

const handlePaste = useCallback(() => {
if (!lf) return
if (!clipboardRef.current) {
alert('剪贴板为空,请先复制选中')
return
}
const { groupId } = pasteSelection(lf, clipboardRef.current)
lastPastedGroupRef.current = groupId
}, [lf])

const handleMovePasted = useCallback(() => {
if (!lf) return
const groupId = lastPastedGroupRef.current
if (!groupId || !lf.getNodeModelById(groupId)) {
alert('请先粘贴出一个新分组')
return
}
lf.graphModel.moveNode(groupId, 0, 200)
}, [lf])

const handleDiagnose = useCallback(() => {
if (!lf) return
const result = diagnoseCopyPaste(lf)
console.log('[selection-copy-paste] diagnosis', result)
alert(result.message)
}, [lf])

const handleReset = useCallback(() => {
clipboardRef.current = null
lastPastedGroupRef.current = undefined
setHasClipboard(false)
loadGraph(buildSelectionCopyGraph())
}, [loadGraph])

return (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Alert
type="warning"
showIcon
message="复现选区复制分离:先框选/全选,再复制 → 粘贴 → 诊断 → 拖动新分组"
/>
<Paragraph style={{ margin: 0 }}>
<Text type="secondary">
复制/粘贴按钮与键盘 Cmd/Ctrl+C、Cmd/Ctrl+V 走同一套 core 逻辑
(getSelectElements +
addElements)。开启框选后可直接在画布空白处拖拽选区。
</Text>
</Paragraph>
<Space wrap>
<Button
size="small"
type={selectionOpen ? 'primary' : 'default'}
onClick={toggleSelection}
>
{selectionOpen ? '关闭框选' : '开启框选'}
</Button>
<Button size="small" onClick={handleSelectAll}>
全选
</Button>
<Button size="small" type="primary" onClick={handleCopy}>
复制选中
</Button>
<Button
size="small"
type="primary"
disabled={!hasClipboard}
onClick={handlePaste}
>
粘贴
</Button>
<Button size="small" danger onClick={handleMovePasted}>
拖动新分组(演示分离)
</Button>
<Button size="small" onClick={handleDiagnose}>
诊断
</Button>
<Button size="small" onClick={handleReset}>
重置场景
</Button>
<Button
size="small"
type="dashed"
onClick={() => {
console.log(lf?.getGraphRawData())
alert('图数据已输出到控制台')
}}
>
打印 getGraphRawData
</Button>
</Space>
</Space>
)
}
6 changes: 6 additions & 0 deletions examples/dynamic-group-regression/src/scenarios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import LayoutFormatEscapeControls from '@/components/LayoutFormatEscapeControls'
import ResizeBoundsControls from '@/components/ResizeBoundsControls'
import TitleHeaderControls from '@/components/TitleHeaderControls'
import CascadeDeleteControls from '@/components/CascadeDeleteControls'
import SelectionCopyControls from '@/components/SelectionCopyControls'
import { layoutFormatEscapeScenario } from './layoutFormatEscape'
import {
resizeBoundsScenario,
syncResizeBoundsMembership,
} from './resizeBounds'
import { titleHeaderScenario } from './titleHeader'
import { selectionCopyPasteScenario } from './selectionCopyPaste'

function toggleGroup(lf: LogicFlow, groupId: string, collapse?: boolean) {
const model = lf.getNodeModelById(groupId) as {
Expand Down Expand Up @@ -786,6 +788,10 @@ export const scenarios: Scenario[] = [
Controls: ResizeBoundsControls,
afterRender: syncResizeBoundsMembership,
},
{
...selectionCopyPasteScenario,
Controls: SelectionCopyControls,
},
{
id: 'resize-undo-twice',
title: '分组 resize 后需撤销两次',
Expand Down
Loading