@logicflow/core 2.2.0-alpha.2 → 2.2.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.turbo/turbo-build$colon$dev.log +2 -2
  2. package/.turbo/turbo-build.log +6 -6
  3. package/CHANGELOG.md +6 -0
  4. package/dist/index.css +3 -2
  5. package/dist/index.min.js +1 -1
  6. package/dist/index.min.js.map +1 -1
  7. package/es/LogicFlow.d.ts +9 -0
  8. package/es/constant/index.d.ts +1 -1
  9. package/es/constant/index.js +1 -1
  10. package/es/constant/theme.d.ts +136 -0
  11. package/es/constant/theme.js +680 -0
  12. package/es/index.css +3 -2
  13. package/es/model/GraphModel.d.ts +9 -2
  14. package/es/model/GraphModel.js +17 -6
  15. package/es/model/TransformModel.js +9 -9
  16. package/es/model/edge/BaseEdgeModel.js +7 -2
  17. package/es/model/edge/PolylineEdgeModel.d.ts +6 -0
  18. package/es/model/edge/PolylineEdgeModel.js +23 -1
  19. package/es/model/node/BaseNodeModel.d.ts +12 -1
  20. package/es/model/node/BaseNodeModel.js +6 -1
  21. package/es/model/node/HtmlNodeModel.d.ts +12 -0
  22. package/es/model/node/HtmlNodeModel.js +19 -0
  23. package/es/model/node/PolygonNodeModel.js +3 -3
  24. package/es/options.d.ts +1 -1
  25. package/es/style/index.css +3 -2
  26. package/es/style/index.less +3 -2
  27. package/es/style/raw.d.ts +1 -1
  28. package/es/style/raw.js +1 -1
  29. package/es/tool/MultipleSelectTool.js +6 -5
  30. package/es/util/edge.d.ts +1 -1
  31. package/es/util/edge.js +2 -2
  32. package/es/util/geometry.d.ts +8 -0
  33. package/es/util/geometry.js +79 -0
  34. package/es/util/theme.d.ts +2 -65
  35. package/es/util/theme.js +4 -281
  36. package/es/view/Control.d.ts +5 -0
  37. package/es/view/Control.js +44 -57
  38. package/es/view/edge/PolylineEdge.js +13 -2
  39. package/es/view/node/BaseNode.d.ts +1 -0
  40. package/es/view/node/BaseNode.js +14 -10
  41. package/es/view/node/HtmlNode.js +2 -4
  42. package/es/view/overlay/Grid.d.ts +12 -1
  43. package/es/view/overlay/Grid.js +85 -23
  44. package/es/view/overlay/OutlineOverlay.d.ts +1 -0
  45. package/es/view/overlay/OutlineOverlay.js +17 -16
  46. package/es/view/overlay/gridConfig.d.ts +46 -0
  47. package/es/view/overlay/gridConfig.js +99 -0
  48. package/es/view/shape/Polygon.d.ts +0 -7
  49. package/es/view/shape/Polygon.js +12 -43
  50. package/lib/LogicFlow.d.ts +9 -0
  51. package/lib/constant/index.d.ts +1 -1
  52. package/lib/constant/index.js +16 -2
  53. package/lib/constant/theme.d.ts +136 -0
  54. package/lib/constant/theme.js +683 -0
  55. package/lib/index.css +3 -2
  56. package/lib/model/GraphModel.d.ts +9 -2
  57. package/lib/model/GraphModel.js +18 -7
  58. package/lib/model/TransformModel.js +9 -9
  59. package/lib/model/edge/BaseEdgeModel.js +7 -2
  60. package/lib/model/edge/PolylineEdgeModel.d.ts +6 -0
  61. package/lib/model/edge/PolylineEdgeModel.js +23 -1
  62. package/lib/model/node/BaseNodeModel.d.ts +12 -1
  63. package/lib/model/node/BaseNodeModel.js +6 -1
  64. package/lib/model/node/HtmlNodeModel.d.ts +12 -0
  65. package/lib/model/node/HtmlNodeModel.js +19 -0
  66. package/lib/model/node/PolygonNodeModel.js +3 -3
  67. package/lib/options.d.ts +1 -1
  68. package/lib/style/index.css +3 -2
  69. package/lib/style/index.less +3 -2
  70. package/lib/style/raw.d.ts +1 -1
  71. package/lib/style/raw.js +1 -1
  72. package/lib/tool/MultipleSelectTool.js +6 -5
  73. package/lib/util/edge.d.ts +1 -1
  74. package/lib/util/edge.js +2 -2
  75. package/lib/util/geometry.d.ts +8 -0
  76. package/lib/util/geometry.js +81 -1
  77. package/lib/util/theme.d.ts +2 -65
  78. package/lib/util/theme.js +15 -292
  79. package/lib/view/Control.d.ts +5 -0
  80. package/lib/view/Control.js +44 -57
  81. package/lib/view/edge/PolylineEdge.js +13 -2
  82. package/lib/view/node/BaseNode.d.ts +1 -0
  83. package/lib/view/node/BaseNode.js +14 -10
  84. package/lib/view/node/HtmlNode.js +1 -3
  85. package/lib/view/overlay/Grid.d.ts +12 -1
  86. package/lib/view/overlay/Grid.js +83 -21
  87. package/lib/view/overlay/OutlineOverlay.d.ts +1 -0
  88. package/lib/view/overlay/OutlineOverlay.js +17 -16
  89. package/lib/view/overlay/gridConfig.d.ts +46 -0
  90. package/lib/view/overlay/gridConfig.js +104 -0
  91. package/lib/view/shape/Polygon.d.ts +0 -7
  92. package/lib/view/shape/Polygon.js +13 -45
  93. package/package.json +1 -1
  94. package/src/LogicFlow.tsx +10 -0
  95. package/src/constant/index.ts +2 -2
  96. package/src/constant/theme.ts +708 -0
  97. package/src/model/GraphModel.ts +19 -7
  98. package/src/model/TransformModel.ts +9 -9
  99. package/src/model/edge/BaseEdgeModel.ts +10 -2
  100. package/src/model/edge/PolylineEdgeModel.ts +26 -1
  101. package/src/model/node/BaseNodeModel.ts +9 -1
  102. package/src/model/node/HtmlNodeModel.ts +14 -0
  103. package/src/model/node/PolygonNodeModel.ts +2 -0
  104. package/src/options.ts +1 -1
  105. package/src/style/index.less +3 -2
  106. package/src/style/raw.ts +3 -2
  107. package/src/tool/MultipleSelectTool.tsx +6 -5
  108. package/src/util/edge.ts +2 -1
  109. package/src/util/geometry.ts +99 -0
  110. package/src/util/theme.ts +12 -303
  111. package/src/view/Control.tsx +61 -63
  112. package/src/view/edge/PolylineEdge.tsx +14 -2
  113. package/src/view/node/BaseNode.tsx +8 -3
  114. package/src/view/node/HtmlNode.tsx +27 -10
  115. package/src/view/overlay/Grid.tsx +187 -30
  116. package/src/view/overlay/OutlineOverlay.tsx +35 -47
  117. package/src/view/overlay/gridConfig.ts +103 -0
  118. package/src/view/shape/Polygon.tsx +12 -49
  119. package/stats.html +1 -1
@@ -1,4 +1,5 @@
1
1
  import {
2
+ assign,
2
3
  find,
3
4
  forEach,
4
5
  map,
@@ -26,6 +27,8 @@ import {
26
27
  ModelType,
27
28
  OverlapMode,
28
29
  TextMode,
30
+ backgroundModeMap,
31
+ gridModeMap,
29
32
  } from '../constant'
30
33
  import LogicFlow from '../LogicFlow'
31
34
  import { Options as LFOptions } from '../options'
@@ -43,8 +46,6 @@ import {
43
46
  setupTheme,
44
47
  snapToGrid,
45
48
  updateTheme,
46
- backgroundModeMap,
47
- gridModeMap,
48
49
  } from '../util'
49
50
  import EventEmitter from '../event/eventEmitter'
50
51
  import { Grid } from '../view/overlay'
@@ -69,8 +70,9 @@ export class GraphModel {
69
70
 
70
71
  // 流程图主题配置
71
72
  @observable theme: LogicFlow.Theme
73
+ @observable themeMode: LogicFlow.ThemeMode | string = 'default'
72
74
  // 初始化样式
73
- customStyles: object
75
+ customStyles: LogicFlow.Theme
74
76
  // 网格配置
75
77
  @observable grid: Grid.GridOptions
76
78
  // 事件中心
@@ -167,6 +169,10 @@ export class GraphModel {
167
169
  customTrajectory,
168
170
  customTargetAnchor,
169
171
  } = options
172
+ this.themeMode = options.themeMode || 'default'
173
+ const initialGrid = gridModeMap[this.themeMode] || gridModeMap['default']
174
+ const initialBackground =
175
+ backgroundModeMap[this.themeMode] || backgroundModeMap['default']
170
176
  this.rootEl = container
171
177
  this.partial = !!partial
172
178
  this.background = background
@@ -175,11 +181,16 @@ export class GraphModel {
175
181
  // TODO:需要让用户设置成 0 吗?后面可以讨论一下
176
182
  this.gridSize = grid.size || 1 // 默认 gridSize 设置为 1
177
183
  }
178
- this.customStyles = options.style || {}
179
- this.grid = Grid.getGridOptions(grid ?? false)
184
+ this.customStyles = (options.style || {}) as LogicFlow.Theme
180
185
  this.theme = setupTheme(options.style, options.themeMode)
186
+ this.grid = Grid.getGridOptions(assign({}, initialGrid, grid))
181
187
  this.theme.grid = cloneDeep(this.grid)
182
- this.theme.background = cloneDeep(this.background)
188
+ if (background) {
189
+ this.background = cloneDeep(assign({}, initialBackground, background))
190
+ this.theme.background = cloneDeep(
191
+ assign({}, initialBackground, background),
192
+ )
193
+ }
183
194
  this.edgeType = options.edgeType || 'polyline'
184
195
  this.animation = setupAnimation(animation)
185
196
  this.overlapMode = options.overlapMode || OverlapMode.DEFAULT
@@ -1516,9 +1527,10 @@ export class GraphModel {
1516
1527
  */
1517
1528
  @action setTheme(
1518
1529
  style: Partial<LogicFlow.Theme>,
1519
- themeMode?: 'radius' | 'dark' | 'colorful' | 'default' | string,
1530
+ themeMode?: LogicFlow.ThemeMode | string,
1520
1531
  ) {
1521
1532
  if (themeMode) {
1533
+ this.themeMode = themeMode
1522
1534
  // 修改背景颜色
1523
1535
  backgroundModeMap[themeMode] &&
1524
1536
  this.updateBackgroundOptions({
@@ -46,15 +46,15 @@ const translateLimitsMap = {
46
46
  }
47
47
 
48
48
  export class TransformModel implements TransformInterface {
49
- MINI_SCALE_SIZE = 0.2
50
- MAX_SCALE_SIZE = 16
51
- @observable SCALE_X = 1
52
- @observable SKEW_Y = 0
53
- @observable SKEW_X = 0
54
- @observable SCALE_Y = 1
55
- @observable TRANSLATE_X = 0
56
- @observable TRANSLATE_Y = 0
57
- @observable ZOOM_SIZE = 0.04
49
+ MINI_SCALE_SIZE = 0.2 // 缩小的最小值
50
+ MAX_SCALE_SIZE = 16 // 放大的最大值
51
+ @observable SCALE_X = 1 // x轴缩放比例
52
+ @observable SKEW_Y = 0 // y轴倾斜角度
53
+ @observable SKEW_X = 0 // x轴倾斜角度
54
+ @observable SCALE_Y = 1 // y轴缩放比例
55
+ @observable TRANSLATE_X = 0 // x轴平移距离
56
+ @observable TRANSLATE_Y = 0 // y轴平移距离
57
+ @observable ZOOM_SIZE = 0.04 // 缩放比例变化量
58
58
  eventCenter: EventEmitter
59
59
 
60
60
  // 限制画布可移动区域
@@ -265,8 +265,16 @@ export class BaseEdgeModel<P extends PropertiesType = PropertiesType>
265
265
  */
266
266
  getOutlineStyle(): LogicFlow.OutlineTheme {
267
267
  const { graphModel } = this
268
- const { outline } = graphModel.theme
269
- return cloneDeep(outline)
268
+ const { edgeOutline } = graphModel.theme
269
+ let attributes = { ...edgeOutline }
270
+ if (this.isHovered) {
271
+ const hoverStyle = edgeOutline.hover || {}
272
+ attributes = {
273
+ ...attributes,
274
+ ...hoverStyle,
275
+ }
276
+ }
277
+ return cloneDeep(attributes)
270
278
  }
271
279
 
272
280
  /**
@@ -34,13 +34,38 @@ export class PolylineEdgeModel extends BaseEdgeModel {
34
34
  @observable dbClickPosition?: Point
35
35
 
36
36
  initEdgeData(data: LogicFlow.EdgeConfig): void {
37
- this.offset = get(data, 'properties.offset', 30)
37
+ const providedOffset = get(data, 'properties.offset')
38
+ // 当用户未传入 offset 时,按“箭头与折线重叠长度 + 5”作为默认值
39
+ // 其中“重叠长度”采用箭头样式中的 offset(沿边方向的长度)
40
+ this.offset =
41
+ typeof providedOffset === 'number'
42
+ ? providedOffset
43
+ : this.getDefaultOffset()
38
44
  if (data.pointsList) {
39
45
  this.pointsList = data.pointsList
40
46
  }
41
47
  super.initEdgeData(data)
42
48
  }
43
49
 
50
+ setAttributes() {
51
+ const { offset: newOffset } = this.properties
52
+ if (newOffset && newOffset !== this.offset) {
53
+ this.offset = newOffset
54
+ this.updatePoints()
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 计算默认 offset:箭头与折线重叠长度 + 5
60
+ * 重叠长度采用箭头样式中的 offset(沿边方向的长度)
61
+ */
62
+ private getDefaultOffset(): number {
63
+ const arrowStyle = this.getArrowStyle()
64
+ const arrowOverlap =
65
+ typeof arrowStyle.offset === 'number' ? arrowStyle.offset : 0
66
+ return arrowOverlap + 5
67
+ }
68
+
44
69
  getEdgeStyle() {
45
70
  const { polyline } = this.graphModel.theme
46
71
  const style = super.getEdgeStyle()
@@ -433,7 +433,15 @@ export class BaseNodeModel<P extends PropertiesType = PropertiesType>
433
433
 
434
434
  getResizeOutlineStyle() {
435
435
  const { resizeOutline } = this.graphModel.theme
436
- return cloneDeep(resizeOutline)
436
+ let attributes = { ...resizeOutline }
437
+ if (this.isHovered) {
438
+ const hoverStyle = resizeOutline.hover || {}
439
+ attributes = {
440
+ ...attributes,
441
+ ...hoverStyle,
442
+ }
443
+ }
444
+ return cloneDeep(attributes)
437
445
  }
438
446
 
439
447
  /**
@@ -1,3 +1,4 @@
1
+ import { cloneDeep } from 'lodash-es'
1
2
  import BaseNodeModel from './BaseNodeModel'
2
3
  import { Model } from '../BaseModel'
3
4
  import { ModelType } from '../../constant'
@@ -45,6 +46,19 @@ export class HtmlNodeModel<
45
46
  { x: x - width / 2, y, id: `${this.id}_3` },
46
47
  ]
47
48
  }
49
+
50
+ getNodeStyle() {
51
+ const style = super.getNodeStyle()
52
+ const { baseNode, html } = this.graphModel.theme
53
+ const { style: customStyle = {} } = this.properties
54
+ const finalStyle = {
55
+ ...style,
56
+ ...cloneDeep(baseNode),
57
+ ...cloneDeep(html),
58
+ ...cloneDeep(customStyle),
59
+ }
60
+ return finalStyle
61
+ }
48
62
  }
49
63
 
50
64
  export default HtmlNodeModel
@@ -67,12 +67,14 @@ export class PolygonNodeModel<
67
67
  const {
68
68
  graphModel: {
69
69
  theme: { polygon },
70
+ customStyles: { polygon: customPolygon },
70
71
  },
71
72
  } = this
72
73
  const { style: customStyle = {} } = this.properties
73
74
  return {
74
75
  ...style,
75
76
  ...cloneDeep(polygon),
77
+ ...cloneDeep(customPolygon),
76
78
  ...cloneDeep(customStyle),
77
79
  }
78
80
  }
package/src/options.ts CHANGED
@@ -110,7 +110,7 @@ export namespace Options {
110
110
 
111
111
  customTargetAnchor?: customTargetAnchorType
112
112
  customTrajectory?: (props: CustomAnchorLineProps) => h.JSX.Element
113
- themeMode?: 'radius' | 'dark' | 'colorful' // 主题模式
113
+ themeMode?: LogicFlow.ThemeMode // 主题模式
114
114
 
115
115
  parentTransform?: TransformModel // 父级变换模型,用于嵌套变换
116
116
 
@@ -215,8 +215,9 @@
215
215
 
216
216
  .lf-multiple-select {
217
217
  position: absolute;
218
- border: 2px dashed #187dffcc;
219
- box-shadow: 0 0 3px 0 #187dff80;
218
+ border: 2px dashed #4271dfcc;
219
+ border-radius: 12px;
220
+ box-shadow: 0 0 3px 0 #4271df80;
220
221
  cursor: move;
221
222
  }
222
223
 
package/src/style/raw.ts CHANGED
@@ -183,8 +183,9 @@ export const content = `.lf-graph {
183
183
  }
184
184
  .lf-multiple-select {
185
185
  position: absolute;
186
- border: 2px dashed #187dffcc;
187
- box-shadow: 0 0 3px 0 #187dff80;
186
+ border: 2px dashed #4271dfcc;
187
+ border-radius: 12px;
188
+ box-shadow: 0 0 3px 0 #4271df80;
188
189
  cursor: move;
189
190
  }
190
191
  .lf-edge-adjust-point {
@@ -86,9 +86,10 @@ export default class MultipleSelect extends Component<IToolProps> {
86
86
 
87
87
  render() {
88
88
  const {
89
- graphModel: { selectElements, transformModel },
89
+ graphModel: { selectElements, transformModel, theme },
90
90
  } = this.props
91
91
  const { SCALE_X, SCALE_Y } = this.props.lf.getTransform()
92
+ const { xPadding = 8, yPadding = 8 } = theme.multiSelect || {}
92
93
  if (selectElements.size <= 1) return
93
94
  let x = Number.MAX_SAFE_INTEGER
94
95
  let y = Number.MAX_SAFE_INTEGER
@@ -113,10 +114,10 @@ export default class MultipleSelect extends Component<IToolProps> {
113
114
  ;[x, y] = transformModel.CanvasPointToHtmlPoint([x, y])
114
115
  ;[x1, y1] = transformModel.CanvasPointToHtmlPoint([x1, y1])
115
116
  const style = {
116
- left: `${x - (20 * SCALE_X) / 2}px`,
117
- top: `${y - (20 * SCALE_Y) / 2}px`,
118
- width: `${x1 - x + 20 * SCALE_X}px`,
119
- height: `${y1 - y + 20 * SCALE_Y}px`,
117
+ left: `${x - (20 * SCALE_X) / 2 - xPadding / 2}px`,
118
+ top: `${y - (20 * SCALE_Y) / 2 - yPadding / 2}px`,
119
+ width: `${x1 - x + 20 * SCALE_X + xPadding}px`,
120
+ height: `${y1 - y + 20 * SCALE_Y + yPadding}px`,
120
121
  'border-width': `${2 * SCALE_X}px`,
121
122
  }
122
123
  return (
package/src/util/edge.ts CHANGED
@@ -159,6 +159,7 @@ export const mergeBBox = (b1: BoxBounds, b2: BoxBounds): BoxBounds => {
159
159
  export const getBBoxOfPoints = (
160
160
  points: Point[] = [],
161
161
  offset?: number,
162
+ heightOffset?: number,
162
163
  ): BoxBounds => {
163
164
  const xList: number[] = []
164
165
  const yList: number[] = []
@@ -174,7 +175,7 @@ export const getBBoxOfPoints = (
174
175
  let height = maxY - minY
175
176
  if (offset) {
176
177
  width += offset
177
- height += offset
178
+ height += heightOffset || offset
178
179
  }
179
180
  return {
180
181
  centerX: (minX + maxX) / 2,
@@ -1,5 +1,6 @@
1
1
  import LogicFlow from '../LogicFlow'
2
2
  import PointTuple = LogicFlow.PointTuple
3
+ import Point = LogicFlow.Point
3
4
 
4
5
  export function snapToGrid(point: number, gridSize: number, snapGrid: boolean) {
5
6
  // 开启节网格对齐时才根据网格尺寸校准坐标
@@ -53,3 +54,101 @@ export function normalizePolygon(
53
54
  // 缩放顶点
54
55
  return translatedPoints.map(([x, y]) => [x * scaleFactor, y * scaleFactor])
55
56
  }
57
+
58
+ /**
59
+ * 通用圆角生成:为菱形、多边形、折线在转折处生成与矩形视觉一致的圆角
60
+ * - 圆角基于角平分线,切点距顶点的距离 t = r * tan(theta/2)
61
+ * - 半径会根据相邻边长度进行钳制,避免超过边长造成断裂
62
+ * - 多边形/菱形保持闭合;折线保持开口
63
+ */
64
+
65
+ export const generateRoundedCorners = (
66
+ points: Point[],
67
+ radius: number,
68
+ isClosedShape: boolean, // 是否是闭合图形
69
+ ): Point[] => {
70
+ const n = points.length
71
+ if (n < 2 || radius <= 0) return points.slice()
72
+
73
+ const toVec = (a: Point, b: Point) => ({ x: b.x - a.x, y: b.y - a.y })
74
+ const len = (v: { x: number; y: number }) => Math.hypot(v.x, v.y)
75
+ const norm = (v: { x: number; y: number }) => {
76
+ const l = len(v) || 1
77
+ return { x: v.x / l, y: v.y / l }
78
+ }
79
+
80
+ const result: Point[] = []
81
+
82
+ // 用二次贝塞尔近似圆角,控制点取角点,避免复杂圆心计算
83
+ const makeRoundCorner = (prev: Point, curr: Point, next: Point): Point[] => {
84
+ const vPrev = toVec(curr, prev)
85
+ const vNext = toVec(curr, next)
86
+ const dPrev = len(vPrev)
87
+ const dNext = len(vNext)
88
+ if (dPrev < 1e-6 || dNext < 1e-6) return [curr]
89
+
90
+ const uPrev = norm(vPrev)
91
+ const uNext = norm(vNext)
92
+ const t = Math.min(radius, dPrev * 0.45, dNext * 0.45)
93
+
94
+ const start = { x: curr.x + uPrev.x * t, y: curr.y + uPrev.y * t }
95
+ const end = { x: curr.x + uNext.x * t, y: curr.y + uNext.y * t }
96
+
97
+ // 二次贝塞尔采样:B(s) = (1-s)^2*start + 2(1-s)s*curr + s^2*end
98
+ const steps = 10 // 3段近似,简洁且效果稳定
99
+ const pts: Point[] = [start]
100
+ for (let k = 1; k < steps; k++) {
101
+ const s = k / steps
102
+ const a = 1 - s
103
+ pts.push({
104
+ x: a * a * start.x + 2 * a * s * curr.x + s * s * end.x,
105
+ y: a * a * start.y + 2 * a * s * curr.y + s * s * end.y,
106
+ })
107
+ }
108
+ pts.push(end)
109
+ return pts
110
+ }
111
+
112
+ for (let i = 0; i < n; i++) {
113
+ const prevIdx = i === 0 ? (isClosedShape ? n - 1 : 0) : i - 1
114
+ const nextIdx = i === n - 1 ? (isClosedShape ? 0 : n - 1) : i + 1
115
+ const prev = points[prevIdx]
116
+ const curr = points[i]
117
+ const next = points[nextIdx]
118
+
119
+ const isEndpoint = !isClosedShape && (i === 0 || i === n - 1)
120
+ if (isEndpoint) {
121
+ // 折线两端不处理圆角
122
+ result.push(curr)
123
+ } else {
124
+ const arc = makeRoundCorner(prev, curr, next)
125
+ arc.forEach((p) => result.push(p))
126
+ }
127
+ }
128
+
129
+ // 去重处理:避免连续重复点
130
+ const dedup: Point[] = []
131
+ for (let i = 0; i < result.length; i++) {
132
+ const p = result[i]
133
+ if (
134
+ dedup.length === 0 ||
135
+ Math.hypot(
136
+ p.x - dedup[dedup.length - 1].x,
137
+ p.y - dedup[dedup.length - 1].y,
138
+ ) > 1e-6
139
+ ) {
140
+ dedup.push(p)
141
+ }
142
+ }
143
+
144
+ // 闭合图形:确保首尾不重复闭合
145
+ if (isClosedShape && dedup.length > 1) {
146
+ const first = dedup[0]
147
+ const last = dedup[dedup.length - 1]
148
+ if (Math.hypot(first.x - last.x, first.y - last.y) < 1e-6) {
149
+ dedup.pop()
150
+ }
151
+ }
152
+
153
+ return dedup
154
+ }