jvs-ppt-ui 0.0.8 → 0.0.9

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.
package/src/utils/line.ts CHANGED
@@ -150,143 +150,246 @@ export function getLinePointByOffset(
150
150
  ] as [number, number]
151
151
  }
152
152
 
153
+ function getElementBox(el: any, fallbackPt: [number, number], logWidth = 1000, logHeight = 562.5) {
154
+ if (!el) {
155
+ return { left: fallbackPt[0] - 10, right: fallbackPt[0] + 10, top: fallbackPt[1] - 10, bottom: fallbackPt[1] + 10 }
156
+ }
157
+ const style = el.style || {}
158
+ const left = getPxVal(style.left || el.left, logWidth)
159
+ const top = getPxVal(style.top || el.top, logHeight)
160
+ const width = getPxVal(style.width || el.width, logWidth) || 20
161
+ const height = getPxVal(style.height || el.height, logHeight) || 20
162
+ return { left, top, right: left + width, bottom: top + height }
163
+ }
164
+
165
+ function getAnchorNormal(idx: number | undefined, isEllipse = false): [number, number] | null {
166
+ if (idx === undefined) return null
167
+ if (isEllipse) {
168
+ if (idx === 0) return [0, -1]
169
+ if (idx === 1) return [-1, -1]
170
+ if (idx === 2) return [-1, 0]
171
+ if (idx === 3) return [-1, 1]
172
+ if (idx === 4) return [0, 1]
173
+ if (idx === 5) return [1, 1]
174
+ if (idx === 6) return [1, 0]
175
+ if (idx === 7) return [1, -1]
176
+ } else {
177
+ const k = idx % 4
178
+ if (k === 0) return [0, -1]
179
+ if (k === 1) return [-1, 0]
180
+ if (k === 2) return [0, 1]
181
+ if (k === 3) return [1, 0]
182
+ }
183
+ return null
184
+ }
185
+
153
186
  function getDefaultOrthogonalTurnPoints(
154
187
  x1: number, y1: number,
155
188
  x2: number, y2: number,
156
189
  element: any,
157
190
  startEl?: any,
158
- endEl?: any
191
+ endEl?: any,
192
+ obstacles?: any[]
159
193
  ): [number, number][] {
160
- let absTurnPoints: [number, number][] = []
194
+ const pad = 20
195
+ const isStartEllipse = isEllipseShape(startEl)
196
+ const isEndEllipse = isEllipseShape(endEl)
161
197
 
162
- if (element.broken !== undefined) {
163
- if (element.startConnection && element.endConnection) {
164
- const idx1 = element.startConnection.idx
165
- const idx2 = element.endConnection.idx
166
-
167
- const isStartEllipse = isEllipseShape(startEl)
168
- const isEndEllipse = isEllipseShape(endEl)
169
-
170
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
171
- if (isStartEllipse) {
172
- isStartHoriz = (idx1 === 2 || idx1 === 6)
173
- }
174
-
175
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
176
- if (isEndEllipse) {
177
- isEndHoriz = (idx2 === 2 || idx2 === 6)
178
- }
198
+ const sBox = getElementBox(startEl, [x1, y1])
199
+ const eBox = getElementBox(endEl, [x2, y2])
200
+
201
+ const idx1 = element.startConnection?.idx
202
+ const idx2 = element.endConnection?.idx
203
+
204
+ let sNorm = getAnchorNormal(idx1, isStartEllipse)
205
+ let eNorm = getAnchorNormal(idx2, isEndEllipse)
206
+
207
+ let eEntryNorm: [number, number] | null = eNorm ? [-eNorm[0], -eNorm[1]] : null
208
+
209
+ if (!sNorm && !eEntryNorm) {
210
+ if (Math.abs(x2 - x1) >= Math.abs(y2 - y1)) {
211
+ return [[x2, y1]]
212
+ } else {
213
+ return [[x1, y2]]
214
+ }
215
+ }
216
+
217
+ if (!sNorm && eEntryNorm) {
218
+ sNorm = eEntryNorm[0] !== 0 ? [0, y2 >= y1 ? 1 : -1] : [x2 >= x1 ? 1 : -1, 0]
219
+ } else if (sNorm && !eEntryNorm) {
220
+ eEntryNorm = sNorm[0] !== 0 ? [0, y2 >= y1 ? 1 : -1] : [x2 >= x1 ? 1 : -1, 0]
221
+ }
222
+
223
+ if (!sNorm || !eEntryNorm) return [[(x1 + x2) / 2, y1], [(x1 + x2) / 2, y2]]
224
+
225
+ const sDx = sNorm[0]
226
+ const sDy = sNorm[1]
227
+ const eDx = eEntryNorm[0]
228
+ const eDy = eEntryNorm[1]
229
+
230
+ // 1. Can we do a clean 1-turn (L-shape)?
231
+ if (sDx !== 0 && sDy === 0 && eDx === 0 && eDy !== 0) {
232
+ if ((x2 - x1) * sDx > 0 && (y2 - y1) * eDy > 0) {
233
+ return [[x2, y1]]
234
+ }
235
+ }
236
+ if (sDy !== 0 && sDx === 0 && eDy === 0 && eDx !== 0) {
237
+ if ((y2 - y1) * sDy > 0 && (x2 - x1) * eDx > 0) {
238
+ return [[x1, y2]]
239
+ }
240
+ }
241
+
242
+ // 2. Parallel cases
243
+ if (sDx !== 0 && sDy === 0 && eDx !== 0 && eDy === 0) {
244
+ if (sDx === eDx) {
245
+ const mx = sDx > 0 ? Math.max(x1, x2, sBox.right, eBox.right) + pad : Math.min(x1, x2, sBox.left, eBox.left) - pad
246
+ return [[mx, y1], [mx, y2]]
247
+ } else if ((x2 - x1) * sDx > 0) {
248
+ const mx = (x1 + x2) / 2
249
+ return [[mx, y1], [mx, y2]]
250
+ }
251
+ }
179
252
 
180
- if (isStartHoriz && !isEndHoriz) {
181
- absTurnPoints = [[x2, y1]]
182
- } else if (!isStartHoriz && isEndHoriz) {
183
- absTurnPoints = [[x1, y2]]
184
- } else if (isStartHoriz && isEndHoriz) {
185
- let mx = (x1 + x2) / 2
186
- if (idx1 === idx2) {
187
- const isRight = (idx1 === 3 && !isStartEllipse) || (idx1 === 6 && isStartEllipse)
188
- const isLeft = (idx1 === 1 && !isStartEllipse) || (idx1 === 2 && isStartEllipse)
189
- if (isRight) mx = Math.max(x1, x2) + 30
190
- else if (isLeft) mx = Math.min(x1, x2) - 30
253
+ if (sDy !== 0 && sDx === 0 && eDy !== 0 && eDx === 0) {
254
+ if (sDy === eDy) {
255
+ const my = sDy > 0 ? Math.max(y1, y2, sBox.bottom, eBox.bottom) + pad : Math.min(y1, y2, sBox.top, eBox.top) - pad
256
+ return [[x1, my], [x2, my]]
257
+ } else if ((y2 - y1) * sDy > 0) {
258
+ const my = (y1 + y2) / 2
259
+ return [[x1, my], [x2, my]]
260
+ }
261
+ }
262
+
263
+ // 3. Perpendicular wrap-around (Start H, End V)
264
+ if (sDx !== 0 && eDy !== 0) {
265
+ const xExit = sDx > 0 ? Math.max(x1 + pad, sBox.right + pad) : Math.min(x1 - pad, sBox.left - pad)
266
+ let yMid: number
267
+ if (eDy > 0) {
268
+ if (y2 > sBox.bottom + pad) {
269
+ yMid = (sBox.bottom + y2) / 2
270
+ if (obstacles && obstacles.length > 0 && isSegmentObstructed(xExit, yMid, x2, yMid, obstacles)) {
271
+ const inBetween = obstacles.filter(obs => obs.bottom > sBox.bottom && obs.top < y2)
272
+ for (const obs of inBetween) {
273
+ const candidateAbove = (sBox.bottom + obs.top) / 2
274
+ if (!isSegmentObstructed(xExit, candidateAbove, x2, candidateAbove, obstacles)) {
275
+ yMid = candidateAbove
276
+ break
277
+ }
278
+ const candidateBelow = (obs.bottom + y2) / 2
279
+ if (!isSegmentObstructed(xExit, candidateBelow, x2, candidateBelow, obstacles)) {
280
+ yMid = candidateBelow
281
+ break
282
+ }
283
+ }
191
284
  }
192
- absTurnPoints = [[mx, y1], [mx, y2]]
193
285
  } else {
194
- let my = (y1 + y2) / 2
195
- if (idx1 === idx2) {
196
- const isBottom = (idx1 === 2 && !isStartEllipse) || (idx1 === 4 && isStartEllipse)
197
- const isTop = (idx1 === 0 && !isStartEllipse) || (idx1 === 0 && isStartEllipse)
198
- if (isBottom) my = Math.max(y1, y2) + 30
199
- else if (isTop) my = Math.min(y1, y2) - 30
200
- }
201
- absTurnPoints = [[x1, my], [x2, my]]
286
+ yMid = Math.min(sBox.top - pad, eBox.top - pad)
202
287
  }
203
288
  } else {
204
- if (Math.abs(x2 - x1) >= Math.abs(y2 - y1)) {
205
- absTurnPoints = [[x2, y1]]
289
+ if (y2 < sBox.top - pad) {
290
+ yMid = (sBox.top + y2) / 2
291
+ if (obstacles && obstacles.length > 0 && isSegmentObstructed(xExit, yMid, x2, yMid, obstacles)) {
292
+ const inBetween = obstacles.filter(obs => obs.top < sBox.top && obs.bottom > y2)
293
+ for (const obs of inBetween) {
294
+ const candidateBelow = (sBox.top + obs.bottom) / 2
295
+ if (!isSegmentObstructed(xExit, candidateBelow, x2, candidateBelow, obstacles)) {
296
+ yMid = candidateBelow
297
+ break
298
+ }
299
+ const candidateAbove = (obs.top + y2) / 2
300
+ if (!isSegmentObstructed(xExit, candidateAbove, x2, candidateAbove, obstacles)) {
301
+ yMid = candidateAbove
302
+ break
303
+ }
304
+ }
305
+ }
206
306
  } else {
207
- absTurnPoints = [[x1, y2]]
307
+ yMid = Math.max(sBox.bottom + pad, eBox.bottom + pad)
208
308
  }
209
309
  }
210
- } else if (element.broken2 !== undefined) {
211
- if (element.startConnection && !element.endConnection) {
212
- const isStartEllipse = isEllipseShape(startEl)
213
- const idx1 = element.startConnection.idx
214
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
215
- if (isStartEllipse) {
216
- isStartHoriz = (idx1 === 2 || idx1 === 6)
217
- }
218
- if (isStartHoriz) {
219
- const mx = (x1 + x2) / 2
220
- absTurnPoints = [[mx, y1], [mx, y2]]
221
- } else {
222
- const my = (y1 + y2) / 2
223
- absTurnPoints = [[x1, my], [x2, my]]
224
- }
225
- } else if (!element.startConnection && element.endConnection) {
226
- const isEndEllipse = isEllipseShape(endEl)
227
- const idx2 = element.endConnection.idx
228
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
229
- if (isEndEllipse) {
230
- isEndHoriz = (idx2 === 2 || idx2 === 6)
231
- }
232
- if (isEndHoriz) {
233
- const mx = (x1 + x2) / 2
234
- absTurnPoints = [[mx, y1], [mx, y2]]
235
- } else {
236
- const my = (y1 + y2) / 2
237
- absTurnPoints = [[x1, my], [x2, my]]
238
- }
239
- } else if (element.startConnection && element.endConnection) {
240
- const isStartEllipse = isEllipseShape(startEl)
241
- const isEndEllipse = isEllipseShape(endEl)
242
- const idx1 = element.startConnection.idx
243
- const idx2 = element.endConnection.idx
244
-
245
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
246
- if (isStartEllipse) {
247
- isStartHoriz = (idx1 === 2 || idx1 === 6)
248
- }
249
-
250
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
251
- if (isEndEllipse) {
252
- isEndHoriz = (idx2 === 2 || idx2 === 6)
253
- }
310
+ return [
311
+ [xExit, y1],
312
+ [xExit, yMid],
313
+ [x2, yMid]
314
+ ]
315
+ }
254
316
 
255
- if (isStartHoriz && isEndHoriz) {
256
- let mx = (x1 + x2) / 2
257
- if (idx1 === idx2) {
258
- const isRight = (idx1 === 3 && !isStartEllipse) || (idx1 === 6 && isStartEllipse)
259
- const isLeft = (idx1 === 1 && !isStartEllipse) || (idx1 === 2 && isStartEllipse)
260
- if (isRight) mx = Math.max(x1, x2) + 30
261
- else if (isLeft) mx = Math.min(x1, x2) - 30
262
- }
263
- absTurnPoints = [[mx, y1], [mx, y2]]
264
- } else if (!isStartHoriz && !isEndHoriz) {
265
- let my = (y1 + y2) / 2
266
- if (idx1 === idx2) {
267
- const isBottom = (idx1 === 2 && !isStartEllipse) || (idx1 === 4 && isStartEllipse)
268
- const isTop = (idx1 === 0 && !isStartEllipse) || (idx1 === 0 && isStartEllipse)
269
- if (isBottom) my = Math.max(y1, y2) + 30
270
- else if (isTop) my = Math.min(y1, y2) - 30
317
+ // 4. Perpendicular wrap-around (Start V, End H)
318
+ if (sDy !== 0 && eDx !== 0) {
319
+ const yExit = sDy > 0 ? Math.max(y1 + pad, sBox.bottom + pad) : Math.min(y1 - pad, sBox.top - pad)
320
+ let xMid: number
321
+ if (eDx > 0) {
322
+ if (x2 > sBox.right + pad) {
323
+ xMid = (sBox.right + x2) / 2
324
+ if (obstacles && obstacles.length > 0 && isSegmentObstructed(xMid, yExit, xMid, y2, obstacles)) {
325
+ const inBetween = obstacles.filter(obs => obs.right > sBox.right && obs.left < x2)
326
+ for (const obs of inBetween) {
327
+ const candidateLeft = (sBox.right + obs.left) / 2
328
+ if (!isSegmentObstructed(candidateLeft, yExit, candidateLeft, y2, obstacles)) {
329
+ xMid = candidateLeft
330
+ break
331
+ }
332
+ const candidateRight = (obs.right + x2) / 2
333
+ if (!isSegmentObstructed(candidateRight, yExit, candidateRight, y2, obstacles)) {
334
+ xMid = candidateRight
335
+ break
336
+ }
337
+ }
271
338
  }
272
- absTurnPoints = [[x1, my], [x2, my]]
273
- } else if (isStartHoriz && !isEndHoriz) {
274
- absTurnPoints = [[x2, y1]]
275
339
  } else {
276
- absTurnPoints = [[x1, y2]]
340
+ xMid = Math.min(sBox.left - pad, eBox.left - pad)
277
341
  }
278
342
  } else {
279
- const dir = getBroken2Direction(element)
280
- if (dir === 'horizontal') {
281
- const mx = (x1 + x2) / 2
282
- absTurnPoints = [[mx, y1], [mx, y2]]
343
+ if (x2 < sBox.left - pad) {
344
+ xMid = (sBox.left + x2) / 2
345
+ if (obstacles && obstacles.length > 0 && isSegmentObstructed(xMid, yExit, xMid, y2, obstacles)) {
346
+ const inBetween = obstacles.filter(obs => obs.left < sBox.left && obs.right > x2)
347
+ for (const obs of inBetween) {
348
+ const candidateRight = (sBox.left + obs.right) / 2
349
+ if (!isSegmentObstructed(candidateRight, yExit, candidateRight, y2, obstacles)) {
350
+ xMid = candidateRight
351
+ break
352
+ }
353
+ const candidateLeft = (obs.left + x2) / 2
354
+ if (!isSegmentObstructed(candidateLeft, yExit, candidateLeft, y2, obstacles)) {
355
+ xMid = candidateLeft
356
+ break
357
+ }
358
+ }
359
+ }
283
360
  } else {
284
- const my = (y1 + y2) / 2
285
- absTurnPoints = [[x1, my], [x2, my]]
361
+ xMid = Math.max(sBox.right + pad, eBox.right + pad)
286
362
  }
287
363
  }
364
+ return [
365
+ [x1, yExit],
366
+ [xMid, yExit],
367
+ [xMid, y2]
368
+ ]
369
+ }
370
+
371
+ // 5. Parallel facing opposite directions and e is behind s
372
+ if (sDx !== 0) {
373
+ const xExit = sDx > 0 ? Math.max(x1 + pad, sBox.right + pad) : Math.min(x1 - pad, sBox.left - pad)
374
+ const yAround = y1 < y2 ? Math.min(sBox.top - pad, eBox.top - pad) : Math.max(sBox.bottom + pad, eBox.bottom + pad)
375
+ const xEntry = eDx > 0 ? Math.min(x2 - pad, eBox.left - pad) : Math.max(x2 + pad, eBox.right + pad)
376
+ return [
377
+ [xExit, y1],
378
+ [xExit, yAround],
379
+ [xEntry, yAround],
380
+ [xEntry, y2]
381
+ ]
382
+ } else {
383
+ const yExit = sDy > 0 ? Math.max(y1 + pad, sBox.bottom + pad) : Math.min(y1 - pad, sBox.top - pad)
384
+ const xAround = x1 < x2 ? Math.min(sBox.left - pad, eBox.left - pad) : Math.max(sBox.right + pad, eBox.right + pad)
385
+ const yEntry = eDy > 0 ? Math.min(y2 - pad, eBox.top - pad) : Math.max(y2 + pad, eBox.bottom + pad)
386
+ return [
387
+ [x1, yExit],
388
+ [xAround, yExit],
389
+ [xAround, yEntry],
390
+ [x2, yEntry]
391
+ ]
288
392
  }
289
- return absTurnPoints
290
393
  }
291
394
 
292
395
  export function getLinePathTurningPoints(
@@ -297,25 +400,34 @@ export function getLinePathTurningPoints(
297
400
  ): [number, number][] {
298
401
  const isCustom = element.customControlPoints
299
402
 
300
- const leftPx = getPxVal(element.style.left, 1000)
301
- const topPx = getPxVal(element.style.top, 562.5)
403
+ const leftPx = getPxVal(element.style?.left || element.left, 1000)
404
+ const topPx = getPxVal(element.style?.top || element.top, 562.5)
302
405
  const rawStart = Array.isArray(element.start) ? element.start : [0, 0]
303
406
  const rawEnd = Array.isArray(element.end) ? element.end : [getPxVal(element.style?.width || element.width, 1000) || 0, getPxVal(element.style?.height || element.height, 562.5) || 0]
304
407
  const s: [number, number] = absStart || [leftPx + rawStart[0], topPx + rawStart[1]]
305
408
  const e: [number, number] = absEnd || [leftPx + rawEnd[0], topPx + rawEnd[1]]
306
409
 
307
- const startEl = (element.startConnection && elements) ? elements.find(item => item.id === element.startConnection.id) : null
308
- const endEl = (element.endConnection && elements) ? elements.find(item => item.id === element.endConnection.id) : null
309
- const isStartEllipse = isEllipseShape(startEl)
310
- const isEndEllipse = isEllipseShape(endEl)
410
+ const findElementRecursive = (list: any[], id: string): any => {
411
+ for (const it of list) {
412
+ if (!it) continue
413
+ if (String(it.id) === String(id)) return it
414
+ if (it.type === 'group' && Array.isArray(it.elements)) {
415
+ const found = findElementRecursive(it.elements, id)
416
+ if (found) return found
417
+ }
418
+ }
419
+ return null
420
+ }
421
+ const startEl = (element.startConnection && elements) ? findElementRecursive(elements, element.startConnection.id) : null
422
+ const endEl = (element.endConnection && elements) ? findElementRecursive(elements, element.endConnection.id) : null
311
423
 
312
- if (!isCustom && elements && elements.length > 0 && (element.broken !== undefined || element.broken2 !== undefined)) {
313
- const obstacles = getSlideObstacles(element, elements)
314
- if (obstacles.length > 0) {
315
- // 1. Calculate the default orthogonal path turning points first
316
- const defaultTurnPoints = getDefaultOrthogonalTurnPoints(s[0], s[1], e[0], e[1], element, startEl, endEl)
424
+ let absTurnPoints: [number, number][] = []
317
425
 
318
- // 2. Check if the default path is obstructed
426
+ if (!isCustom && (element.broken !== undefined || element.broken2 !== undefined)) {
427
+ const obstacles = (elements && elements.length > 0) ? getSlideObstacles(element, elements) : []
428
+ const defaultTurnPoints = getDefaultOrthogonalTurnPoints(s[0], s[1], e[0], e[1], element, startEl, endEl, obstacles)
429
+
430
+ if (obstacles.length > 0) {
319
431
  const defaultPathPoints = [s, ...defaultTurnPoints, e]
320
432
  let defaultObstructed = false
321
433
  for (let i = 0; i < defaultPathPoints.length - 1; i++) {
@@ -325,176 +437,36 @@ export function getLinePathTurningPoints(
325
437
  }
326
438
  }
327
439
 
328
- // 3. If default path is NOT obstructed, use it! It's perfectly stable and uses midpoint.
329
440
  if (!defaultObstructed) {
330
- if (absStart && absEnd) {
331
- return defaultTurnPoints
332
- } else {
333
- return defaultTurnPoints.map(pt => [pt[0] - leftPx, pt[1] - topPx])
334
- }
335
- }
336
-
337
- // 4. Otherwise, run A* to avoid the obstacle
338
- const idx1 = element.startConnection?.idx
339
- const idx2 = element.endConnection?.idx
340
- const astarPath = routeAvoidObstacles(s[0], s[1], e[0], e[1], idx1, idx2, obstacles, startEl, endEl)
341
- if (astarPath && astarPath.length >= 2) {
342
- const absTurnPoints = astarPath.slice(1, -1)
343
- if (absStart && absEnd) {
344
- return absTurnPoints
345
- } else {
346
- return absTurnPoints.map(pt => [pt[0] - leftPx, pt[1] - topPx])
347
- }
348
- }
349
- }
350
- }
351
-
352
- const x1 = s[0]
353
- const y1 = s[1]
354
- const x2 = e[0]
355
- const y2 = e[1]
356
-
357
- let absTurnPoints: [number, number][] = []
358
-
359
- if (element.broken !== undefined) {
360
- if (isCustom && element.broken) {
361
- const absB: [number, number] = [leftPx + element.broken[0], topPx + element.broken[1]]
362
- absTurnPoints = [absB]
363
- } else {
364
- if (element.startConnection && element.endConnection) {
365
- const idx1 = element.startConnection.idx
366
- const idx2 = element.endConnection.idx
367
-
368
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
369
- if (isStartEllipse) {
370
- isStartHoriz = (idx1 === 2 || idx1 === 6)
371
- }
372
-
373
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
374
- if (isEndEllipse) {
375
- isEndHoriz = (idx2 === 2 || idx2 === 6)
376
- }
377
-
378
- if (isStartHoriz && !isEndHoriz) {
379
- absTurnPoints = [[x2, y1]]
380
- } else if (!isStartHoriz && isEndHoriz) {
381
- absTurnPoints = [[x1, y2]]
382
- } else if (isStartHoriz && isEndHoriz) {
383
- let mx = (x1 + x2) / 2
384
- if (idx1 === idx2) {
385
- const isRight = (idx1 === 3 && !isStartEllipse) || (idx1 === 6 && isStartEllipse)
386
- const isLeft = (idx1 === 1 && !isStartEllipse) || (idx1 === 2 && isStartEllipse)
387
- if (isRight) mx = Math.max(x1, x2) + 30
388
- else if (isLeft) mx = Math.min(x1, x2) - 30
389
- }
390
- absTurnPoints = [[mx, y1], [mx, y2]]
391
- } else {
392
- let my = (y1 + y2) / 2
393
- if (idx1 === idx2) {
394
- const isBottom = (idx1 === 2 && !isStartEllipse) || (idx1 === 4 && isStartEllipse)
395
- const isTop = (idx1 === 0 && !isStartEllipse) || (idx1 === 0 && isStartEllipse)
396
- if (isBottom) my = Math.max(y1, y2) + 30
397
- else if (isTop) my = Math.min(y1, y2) - 30
398
- }
399
- absTurnPoints = [[x1, my], [x2, my]]
400
- }
441
+ absTurnPoints = defaultTurnPoints
401
442
  } else {
402
- if (Math.abs(x2 - x1) >= Math.abs(y2 - y1)) {
403
- absTurnPoints = [[x2, y1]]
443
+ const idx1 = element.startConnection?.idx
444
+ const idx2 = element.endConnection?.idx
445
+ const astarPath = routeAvoidObstacles(s[0], s[1], e[0], e[1], idx1, idx2, obstacles, startEl, endEl)
446
+ if (astarPath && astarPath.length >= 2) {
447
+ absTurnPoints = astarPath.slice(1, -1)
404
448
  } else {
405
- absTurnPoints = [[x1, y2]]
449
+ absTurnPoints = defaultTurnPoints
406
450
  }
407
451
  }
452
+ } else {
453
+ absTurnPoints = defaultTurnPoints
408
454
  }
409
- } else if (element.broken2 !== undefined) {
410
- if (isCustom && element.broken2) {
411
- const dir = getBroken2Direction(element)
412
- const absB2: [number, number] = [leftPx + element.broken2[0], topPx + element.broken2[1]]
413
- if (dir === 'horizontal') {
414
- absTurnPoints = [
415
- [absB2[0], y1],
416
- [absB2[0], y2]
417
- ]
418
- } else {
419
- absTurnPoints = [
420
- [x1, absB2[1]],
421
- [x2, absB2[1]]
422
- ]
423
- }
455
+ } else if (isCustom && element.broken !== undefined && element.broken) {
456
+ absTurnPoints = [[leftPx + element.broken[0], topPx + element.broken[1]]]
457
+ } else if (isCustom && element.broken2 !== undefined && element.broken2) {
458
+ const dir = getBroken2Direction(element)
459
+ const absB2: [number, number] = [leftPx + element.broken2[0], topPx + element.broken2[1]]
460
+ if (dir === 'horizontal') {
461
+ absTurnPoints = [
462
+ [absB2[0], s[1]],
463
+ [absB2[0], e[1]]
464
+ ]
424
465
  } else {
425
- if (element.startConnection && !element.endConnection) {
426
- const idx1 = element.startConnection.idx
427
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
428
- if (isStartEllipse) {
429
- isStartHoriz = (idx1 === 2 || idx1 === 6)
430
- }
431
- if (isStartHoriz) {
432
- const mx = (x1 + x2) / 2
433
- absTurnPoints = [[mx, y1], [mx, y2]]
434
- } else {
435
- const my = (y1 + y2) / 2
436
- absTurnPoints = [[x1, my], [x2, my]]
437
- }
438
- } else if (!element.startConnection && element.endConnection) {
439
- const idx2 = element.endConnection.idx
440
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
441
- if (isEndEllipse) {
442
- isEndHoriz = (idx2 === 2 || idx2 === 6)
443
- }
444
- if (isEndHoriz) {
445
- const mx = (x1 + x2) / 2
446
- absTurnPoints = [[mx, y1], [mx, y2]]
447
- } else {
448
- const my = (y1 + y2) / 2
449
- absTurnPoints = [[x1, my], [x2, my]]
450
- }
451
- } else if (element.startConnection && element.endConnection) {
452
- const idx1 = element.startConnection.idx
453
- const idx2 = element.endConnection.idx
454
-
455
- let isStartHoriz = (idx1 === 1 || idx1 === 3)
456
- if (isStartEllipse) {
457
- isStartHoriz = (idx1 === 2 || idx1 === 6)
458
- }
459
-
460
- let isEndHoriz = (idx2 === 1 || idx2 === 3)
461
- if (isEndEllipse) {
462
- isEndHoriz = (idx2 === 2 || idx2 === 6)
463
- }
464
-
465
- if (isStartHoriz && isEndHoriz) {
466
- let mx = (x1 + x2) / 2
467
- if (idx1 === idx2) {
468
- const isRight = (idx1 === 3 && !isStartEllipse) || (idx1 === 6 && isStartEllipse)
469
- const isLeft = (idx1 === 1 && !isStartEllipse) || (idx1 === 2 && isStartEllipse)
470
- if (isRight) mx = Math.max(x1, x2) + 30
471
- else if (isLeft) mx = Math.min(x1, x2) - 30
472
- }
473
- absTurnPoints = [[mx, y1], [mx, y2]]
474
- } else if (!isStartHoriz && !isEndHoriz) {
475
- let my = (y1 + y2) / 2
476
- if (idx1 === idx2) {
477
- const isBottom = (idx1 === 2 && !isStartEllipse) || (idx1 === 4 && isStartEllipse)
478
- const isTop = (idx1 === 0 && !isStartEllipse) || (idx1 === 0 && isStartEllipse)
479
- if (isBottom) my = Math.max(y1, y2) + 30
480
- else if (isTop) my = Math.min(y1, y2) - 30
481
- }
482
- absTurnPoints = [[x1, my], [x2, my]]
483
- } else if (isStartHoriz && !isEndHoriz) {
484
- absTurnPoints = [[x2, y1]]
485
- } else {
486
- absTurnPoints = [[x1, y2]]
487
- }
488
- } else {
489
- const dir = getBroken2Direction(element)
490
- if (dir === 'horizontal') {
491
- const mx = (x1 + x2) / 2
492
- absTurnPoints = [[mx, y1], [mx, y2]]
493
- } else {
494
- const my = (y1 + y2) / 2
495
- absTurnPoints = [[x1, my], [x2, my]]
496
- }
497
- }
466
+ absTurnPoints = [
467
+ [s[0], absB2[1]],
468
+ [e[0], absB2[1]]
469
+ ]
498
470
  }
499
471
  } else if (element.curve) {
500
472
  absTurnPoints = [[leftPx + element.curve[0], topPx + element.curve[1]]]
@@ -643,6 +615,8 @@ export function getSvgWidth(el: any) {
643
615
  }
644
616
  }
645
617
  }
618
+ const styleW = getPxVal(el.style?.width || el.width, 1000)
619
+ if (styleW && styleW > 0) return Math.max(24, styleW)
646
620
  const width = Math.abs((el.start ? el.start[0] : 0) - (el.end ? el.end[0] : 0))
647
621
  return width < 24 ? 24 : width
648
622
  }
@@ -664,6 +638,8 @@ export function getSvgHeight(el: any) {
664
638
  }
665
639
  }
666
640
  }
641
+ const styleH = getPxVal(el.style?.height || el.height, 562.5)
642
+ if (styleH && styleH > 0) return Math.max(24, styleH)
667
643
  const height = Math.abs((el.start ? el.start[1] : 0) - (el.end ? el.end[1] : 0))
668
644
  return height < 24 ? 24 : height
669
645
  }
@@ -702,9 +678,6 @@ export function isStandardShapeType(target: any): boolean {
702
678
  if (target.type && target.type !== 'shape' && target.type !== 'text') return false
703
679
 
704
680
  const shapType = String(target.shapType || target.pathFormula || '').toLowerCase()
705
- if (!shapType) {
706
- return !!target.pathFormula || target.viewBox?.[0] === 200
707
- }
708
681
 
709
682
  // 明确排除非标准自定义/自由多边形
710
683
  if (shapType === 'custom' || shapType.startsWith('custom:') || /freeform/i.test(shapType) || shapType === 'unknown') {
@@ -848,15 +821,12 @@ function getSlideObstacles(element: any, elements: any[]) {
848
821
  const logWidth = 1000
849
822
  const logHeight = 562.5
850
823
 
851
- const startId = element.startConnection?.id
852
- const endId = element.endConnection?.id
824
+ const lineId = String(element.id)
853
825
 
854
826
  for (const el of elements) {
855
- if (el.id === element.id) continue
856
- if (el.id !== startId && el.id !== endId) continue
827
+ if (!el || String(el.id) === lineId) continue
857
828
  if (el.type !== 'shape' && el.type !== 'text' && el.type !== 'image') continue
858
829
 
859
- if (!el) continue
860
830
  const style = el.style || {}
861
831
  const left = getPxVal(style.left || el.left, logWidth)
862
832
  const top = getPxVal(style.top || el.top, logHeight)
@@ -866,8 +836,8 @@ function getSlideObstacles(element: any, elements: any[]) {
866
836
  if (typeof document !== 'undefined' && (!width || !height || el.type === 'text')) {
867
837
  const domEl = document.getElementById(`canvas-el-${el.id}`) || document.getElementById(`screen-element-${el.id}`)
868
838
  if (domEl) {
869
- if (domEl.offsetWidth) width = domEl.offsetWidth
870
- if (domEl.offsetHeight) height = domEl.offsetHeight
839
+ if (domEl.offsetWidth && !width) width = domEl.offsetWidth
840
+ if (domEl.offsetHeight && !height) height = domEl.offsetHeight
871
841
  }
872
842
  }
873
843
 
@@ -889,7 +859,7 @@ function isSegmentObstructed(x3: number, y3: number, x4: number, y4: number, obs
889
859
  const minY = Math.min(y3, y4)
890
860
  const maxY = Math.max(y3, y4)
891
861
 
892
- const eps = 1.0
862
+ const eps = 0.5
893
863
 
894
864
  for (const obs of obstacles) {
895
865
  const obsLeft = obs.left + eps
@@ -899,13 +869,13 @@ function isSegmentObstructed(x3: number, y3: number, x4: number, y4: number, obs
899
869
 
900
870
  if (obsRight <= obsLeft || obsBottom <= obsTop) continue
901
871
 
902
- if (x3 === x4) {
872
+ if (Math.abs(x3 - x4) < 0.5) {
903
873
  if (x3 > obsLeft && x3 < obsRight) {
904
874
  if (minY < obsBottom && maxY > obsTop) {
905
875
  return true
906
876
  }
907
877
  }
908
- } else if (y3 === y4) {
878
+ } else if (Math.abs(y3 - y4) < 0.5) {
909
879
  if (y3 > obsTop && y3 < obsBottom) {
910
880
  if (minX < obsRight && maxX > obsLeft) {
911
881
  return true
@@ -1249,7 +1219,7 @@ export function updateLineGeometry(
1249
1219
  absControls.broken = [(absStart[0] + absEnd[0]) / 2, (absStart[1] + absEnd[1]) / 2]
1250
1220
  }
1251
1221
 
1252
- if (el.path !== undefined && !isBent) {
1222
+ if (el.path !== undefined) {
1253
1223
  delete el.path
1254
1224
  }
1255
1225
 
@@ -1424,17 +1394,13 @@ export function syncSlideConnections(slide: any, logWidth: number, logHeight: nu
1424
1394
  }
1425
1395
  const pt = getConnectionPoint(target, idx, logWidth, logHeight, targetDim)
1426
1396
  const isLineSelected = Boolean(selectedElementIds && selectedElementIds.includes(el.id))
1427
- const isTargetSelected = Boolean(selectedElementIds && selectedElementIds.includes(target.id))
1428
- const shouldSnapStart = (!selectedElementIds || selectedElementIds.length === 0) || (isTargetSelected && !isLineSelected)
1397
+ const shouldSnapStart = !isLineSelected
1429
1398
  if (shouldSnapStart) {
1430
1399
  if (Math.abs(pt[0] - currentStartAbs[0]) > 0.5 || Math.abs(pt[1] - currentStartAbs[1]) > 0.5) {
1431
1400
  newStartAbs = pt
1432
1401
  updated = true
1433
1402
  }
1434
1403
  }
1435
- } else {
1436
- // 目标形状不是系统已知标准形状(如自由曲线/自定义形状等),不给绑定锚点,原样呈现 PPT 原生排版位置
1437
- delete el.startConnection
1438
1404
  }
1439
1405
  }
1440
1406
 
@@ -1474,17 +1440,13 @@ export function syncSlideConnections(slide: any, logWidth: number, logHeight: nu
1474
1440
  }
1475
1441
  const pt = getConnectionPoint(target, idx, logWidth, logHeight, targetDim)
1476
1442
  const isLineSelected = Boolean(selectedElementIds && selectedElementIds.includes(el.id))
1477
- const isTargetSelected = Boolean(selectedElementIds && selectedElementIds.includes(target.id))
1478
- const shouldSnapEnd = (!selectedElementIds || selectedElementIds.length === 0) || (isTargetSelected && !isLineSelected)
1443
+ const shouldSnapEnd = !isLineSelected
1479
1444
  if (shouldSnapEnd) {
1480
1445
  if (Math.abs(pt[0] - currentEndAbs[0]) > 0.5 || Math.abs(pt[1] - currentEndAbs[1]) > 0.5) {
1481
1446
  newEndAbs = pt
1482
1447
  updated = true
1483
1448
  }
1484
1449
  }
1485
- } else {
1486
- // 目标形状不是系统已知标准形状,不给绑定锚点,原样呈现 PPT 原生排版位置
1487
- delete el.endConnection
1488
1450
  }
1489
1451
  }
1490
1452