107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import type { BatchOp } from '@/api/mapEdit'
|
|||
|
|
|
||
|
|
/**
|
||
|
|
* 对齐算法:纯前端计算每个选中对象的新坐标,再以 batch ops(patch)回写到反射 API。
|
||
|
|
* 仅支持有 (x, y) 坐标的对象(site / image / text / model 等 UIHelper 子类);
|
||
|
|
* track 是端点连线,没有自己的坐标,alignment 不影响 track。
|
||
|
|
*/
|
||
|
|
|
||
|
|
export interface AlignTarget {
|
||
|
|
kind: string
|
||
|
|
id: number
|
||
|
|
x: number
|
||
|
|
y: number
|
||
|
|
width?: number
|
||
|
|
height?: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export type AlignMode =
|
||
|
|
| 'left'
|
||
|
|
| 'right'
|
||
|
|
| 'top'
|
||
|
|
| 'bottom'
|
||
|
|
| 'centerH'
|
||
|
|
| 'centerV'
|
||
|
|
| 'center'
|
||
|
|
| 'distributeH'
|
||
|
|
| 'distributeV'
|
||
|
|
| 'toFirst'
|
||
|
|
| 'toLast'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 根据对齐模式计算 patch ops。返回值为 mapEdit.batch 直接消费的 BatchOp 数组。
|
||
|
|
* 输入要求至少 2 个对象(distribute 需要 ≥ 3 才有意义)。
|
||
|
|
*/
|
||
|
|
export function buildAlignOps(targets: AlignTarget[], mode: AlignMode): BatchOp[] {
|
||
|
|
if (targets.length < 2) return []
|
||
|
|
|
||
|
|
const ops: BatchOp[] = []
|
||
|
|
|
||
|
|
const xs = targets.map((t) => t.x)
|
||
|
|
const ys = targets.map((t) => t.y)
|
||
|
|
const minX = Math.min(...xs)
|
||
|
|
const maxX = Math.max(...xs)
|
||
|
|
const minY = Math.min(...ys)
|
||
|
|
const maxY = Math.max(...ys)
|
||
|
|
const avgX = xs.reduce((a, b) => a + b, 0) / xs.length
|
||
|
|
const avgY = ys.reduce((a, b) => a + b, 0) / ys.length
|
||
|
|
|
||
|
|
switch (mode) {
|
||
|
|
case 'left':
|
||
|
|
for (const t of targets) ops.push(patch(t, { x: minX }))
|
||
|
|
break
|
||
|
|
case 'right':
|
||
|
|
for (const t of targets) ops.push(patch(t, { x: maxX }))
|
||
|
|
break
|
||
|
|
case 'top':
|
||
|
|
// 工程坐标 y 向上为正:top 取 max
|
||
|
|
for (const t of targets) ops.push(patch(t, { y: maxY }))
|
||
|
|
break
|
||
|
|
case 'bottom':
|
||
|
|
for (const t of targets) ops.push(patch(t, { y: minY }))
|
||
|
|
break
|
||
|
|
case 'centerH':
|
||
|
|
for (const t of targets) ops.push(patch(t, { x: avgX }))
|
||
|
|
break
|
||
|
|
case 'centerV':
|
||
|
|
for (const t of targets) ops.push(patch(t, { y: avgY }))
|
||
|
|
break
|
||
|
|
case 'center':
|
||
|
|
for (const t of targets) ops.push(patch(t, { x: avgX, y: avgY }))
|
||
|
|
break
|
||
|
|
case 'distributeH': {
|
||
|
|
const sorted = [...targets].sort((a, b) => a.x - b.x)
|
||
|
|
const step = (sorted[sorted.length - 1]!.x - sorted[0]!.x) / (sorted.length - 1)
|
||
|
|
sorted.forEach((t, i) => {
|
||
|
|
const x = sorted[0]!.x + step * i
|
||
|
|
if (Math.abs(x - t.x) > 0.5) ops.push(patch(t, { x }))
|
||
|
|
})
|
||
|
|
break
|
||
|
|
}
|
||
|
|
case 'distributeV': {
|
||
|
|
const sorted = [...targets].sort((a, b) => a.y - b.y)
|
||
|
|
const step = (sorted[sorted.length - 1]!.y - sorted[0]!.y) / (sorted.length - 1)
|
||
|
|
sorted.forEach((t, i) => {
|
||
|
|
const y = sorted[0]!.y + step * i
|
||
|
|
if (Math.abs(y - t.y) > 0.5) ops.push(patch(t, { y }))
|
||
|
|
})
|
||
|
|
break
|
||
|
|
}
|
||
|
|
case 'toFirst': {
|
||
|
|
const ref = targets[0]!
|
||
|
|
for (const t of targets.slice(1)) ops.push(patch(t, { x: ref.x, y: ref.y }))
|
||
|
|
break
|
||
|
|
}
|
||
|
|
case 'toLast': {
|
||
|
|
const ref = targets[targets.length - 1]!
|
||
|
|
for (const t of targets.slice(0, -1)) ops.push(patch(t, { x: ref.x, y: ref.y }))
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ops
|
||
|
|
}
|
||
|
|
|
||
|
|
function patch(t: AlignTarget, data: Record<string, unknown>): BatchOp {
|
||
|
|
return { action: 'patch', kind: t.kind, id: t.id, data }
|
||
|
|
}
|