107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
/**
|
||
* 战场防御线:反弓曲线路径 + 锯齿齿形
|
||
*/
|
||
|
||
export type LngLat = [number, number]
|
||
|
||
/** 二次贝塞尔 B(t) = (1-t)²P0 + 2(1-t)t P1 + t² P2 */
|
||
function quadraticBezier(P0: LngLat, P1: LngLat, P2: LngLat, t: number): LngLat {
|
||
const u = 1 - t
|
||
return [
|
||
u * u * P0[0] + 2 * u * t * P1[0] + t * t * P2[0],
|
||
u * u * P0[1] + 2 * u * t * P1[1] + t * t * P2[1],
|
||
]
|
||
}
|
||
|
||
/**
|
||
* 生成反弓曲线路径:通过 path 各点,弧面向进攻方向(西侧)凸出
|
||
* @param path 关键点 [[lng, lat], ...](如大不里士→萨南达季→克尔曼沙赫)
|
||
* @param bulgeWest 向西凸出量(经度,正值表示弧顶在西侧/面向进攻方)
|
||
* @param samplesPerSegment 每段贝塞尔采样点数
|
||
*/
|
||
export function createCurvedDefensePath(
|
||
path: LngLat[],
|
||
bulgeWest: number = 0.35,
|
||
samplesPerSegment: number = 24
|
||
): LngLat[] {
|
||
if (path.length < 2) return path
|
||
const out: LngLat[] = []
|
||
for (let i = 0; i < path.length - 1; i++) {
|
||
const a = path[i]
|
||
const b = path[i + 1]
|
||
const midLng = (a[0] + b[0]) / 2
|
||
const midLat = (a[1] + b[1]) / 2
|
||
const control: LngLat = [midLng - bulgeWest, midLat]
|
||
const startK = i === 0 ? 0 : 1
|
||
for (let k = startK; k <= samplesPerSegment; k++) {
|
||
out.push(quadraticBezier(a, control, b, k / samplesPerSegment))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* 沿路径线性插值增加点(直线段)
|
||
*/
|
||
export function interpolatePath(path: LngLat[], stepsPerSegment: number = 4): LngLat[] {
|
||
if (path.length < 2) return path
|
||
const out: LngLat[] = []
|
||
for (let i = 0; i < path.length - 1; i++) {
|
||
const a = path[i]
|
||
const b = path[i + 1]
|
||
const startK = i === 0 ? 0 : 1
|
||
for (let k = startK; k <= stepsPerSegment; k++) {
|
||
const t = k / stepsPerSegment
|
||
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* 锯齿防御线:沿 path 每 toothWidth(比例)生成齿,齿高 toothHeight,朝向法向(敌方侧)
|
||
* @param path 路径点 [[lng, lat], ...](可先经 interpolatePath 插值)
|
||
* @param toothWidth 齿宽(与段长同单位,经纬度)
|
||
* @param toothHeight 齿高(法向伸出)
|
||
*/
|
||
export function createDefenseLine(
|
||
path: number[][],
|
||
toothWidth: number,
|
||
toothHeight: number
|
||
): number[][] {
|
||
const coords: number[][] = []
|
||
|
||
for (let i = 0; i < path.length - 1; i++) {
|
||
const start = path[i] as [number, number]
|
||
const end = path[i + 1] as [number, number]
|
||
|
||
const dx = end[0] - start[0]
|
||
const dy = end[1] - start[1]
|
||
const L = Math.sqrt(dx * dx + dy * dy)
|
||
if (L < 1e-9) continue
|
||
|
||
const ux = dx / L
|
||
const uy = dy / L
|
||
const nx = -uy
|
||
const ny = ux
|
||
|
||
const numTeeth = Math.max(1, Math.floor(L / toothWidth))
|
||
const actualWidth = L / numTeeth
|
||
|
||
for (let j = 0; j < numTeeth; j++) {
|
||
const t1 = j * actualWidth
|
||
const t2 = t1 + actualWidth * 0.5
|
||
const t3 = (j + 1) * actualWidth
|
||
|
||
coords.push([start[0] + ux * t1, start[1] + uy * t1])
|
||
coords.push([
|
||
start[0] + ux * t2 + nx * toothHeight,
|
||
start[1] + uy * t2 + ny * toothHeight,
|
||
])
|
||
coords.push([start[0] + ux * t3, start[1] + uy * t3])
|
||
}
|
||
}
|
||
coords.push(path[path.length - 1] as [number, number])
|
||
return coords
|
||
}
|