35 lines
4.3 KiB
TypeScript
35 lines
4.3 KiB
TypeScript
/**
|
||
* 有限预算的三次曲线命中精判。先用控制点包围盒排除,再用弦线距离与平坦度误差判断;
|
||
* 不能确定时用 de Casteljau 二分。预算/数值精度不足明确保留可能命中,不能伪称已证明 miss。
|
||
* 上层须提供合法曲线/点、非负半径和有效预算,本函数没有独立参数校验或 registry 检查。
|
||
*/
|
||
import type {Point} from './types.ts';
|
||
import type {Cubic} from './geometry.ts';
|
||
/** 点到线段的最短距离:先按最大坐标量级归一化,投影限制在线段上,再恢复原尺度。
|
||
* 零长度段退化为点,归一化用于减少大坐标求差/长度平方造成的溢出。 */
|
||
const distance=(p:Point,a:Point,b:Point)=>{const magnitude=Math.max(1,Math.abs(p.x),Math.abs(p.y),Math.abs(a.x),Math.abs(a.y),Math.abs(b.x),Math.abs(b.y)),x=b.x/magnitude-a.x/magnitude,y=b.y/magnitude-a.y/magnitude,dx=p.x/magnitude-a.x/magnitude,dy=p.y/magnitude-a.y/magnitude,length=Math.hypot(x,y),ux=length?x/length:0,uy=length?y/length:0,t=Math.max(0,Math.min(length,dx*ux+dy*uy));return Math.hypot(dx-t*ux,dy-t*uy)*magnitude;};
|
||
/** 两点中点写成 a/2+b/2,减少先做 a+b 时溢出的风险。 */
|
||
const mid=(a:Point,b:Point)=>({x:a.x/2+b.x/2,y:a.y/2+b.y/2});
|
||
/** de Casteljau 在 t=0.5 分割,生成两条连接于同一个 center 的等价子曲线。 */
|
||
function split([a,b,c,d]:Cubic):[Cubic,Cubic]{const ab=mid(a,b),bc=mid(b,c),cd=mid(c,d),abc=mid(ab,bc),bcd=mid(bc,cd),center=mid(abc,bcd);return[[a,ab,abc,center],[center,bcd,cd,d]];}
|
||
/** Hull rejection + bounded de Casteljau refinement; ambiguous boundaries are explicit, never a proven miss. */
|
||
/** 返回 hit、certainty、距离估计、误差和预算状态;hit=true/possible 需调用方保守处理。
|
||
* maxSteps 限总工作,maxDepth 限细分深度,error 控制可接受平坦度;stack 用显式 DFS。
|
||
* 每轮:包围盒排除→near±flat 判定→足够平/深度已满则记 possible→否则入栈左右子曲线。
|
||
* near 是到端点弦线段距离,flat 包括两控制点到该弦的偏差及 roundoff。
|
||
* 剩余 stack 非空表示总预算耗尽,仍可能有未检查的命中区域。
|
||
* certainty 只在 hit=true 时区分 confirmed/possible;hit=false 的正常返回也固定带 possible,不是“可能没命中”的三态枚举。 */
|
||
export function hitCubic(curve:Cubic,point:Point,radius:number,options:{error:number;maxDepth:number;maxSteps:number}){
|
||
// 栈中保存尚未证明可排除的子曲线,steps 按实际取出的子曲线计数;possible 记住已停止细分的模糊区域。
|
||
const stack:{curve:Cubic;depth:number}[]=[{curve,depth:0}];let steps=0,possible=false,estimate=Infinity,error=0;
|
||
while(stack.length&&steps<options.maxSteps){const item=stack.pop()!;steps++;const xs=item.curve.map(p=>p.x),ys=item.curve.map(p=>p.y),dx=Math.max(0,Math.min(...xs)-point.x,point.x-Math.max(...xs)),dy=Math.max(0,Math.min(...ys)-point.y,point.y-Math.max(...ys));if(Math.hypot(dx,dy)>radius)continue;
|
||
// 弦距离 near 与 flat 给出保守区间:near-flat 超半径可排除,near+flat 在半径内可确认。
|
||
// 数值非有限时直接返回 possible 且 budget=true;该标志此时不只是“栈还有工作”的含义。
|
||
const[a,b,c,d]=item.curve,roundoff=Number.EPSILON*64*Math.max(1,Math.abs(point.x),Math.abs(point.y),...xs.map(Math.abs),...ys.map(Math.abs)),flat=Math.max(distance(b,a,d),distance(c,a,d))+roundoff,near=distance(point,a,d);if(!Number.isFinite(flat)||!Number.isFinite(near))return{hit:true,certainty:'possible' as const,steps,distance:Infinity,error:Infinity,budget:true};if(near-flat>radius)continue;if(near+flat<=radius)return{hit:true,certainty:'confirmed' as const,steps,distance:near,error:flat,budget:false};
|
||
// 达到平坦误差或深度阈值只保留 possible,budget 可仍为 false;继续二分时先压右再压左,按左优先 DFS。
|
||
if(flat<=options.error||item.depth>=options.maxDepth){possible=true;if(near<estimate){estimate=near;error=flat;}continue;}const[left,right]=split(item.curve);stack.push({curve:right,depth:item.depth+1},{curve:left,depth:item.depth+1});
|
||
}
|
||
// estimate/error 来自已记住的最小 near 模糊区,不是所有曲线的精确最近距离;全被排除时 estimate 仍是 Infinity。
|
||
return{hit:possible||stack.length>0,certainty:'possible' as const,steps,distance:estimate,error,budget:stack.length>0};
|
||
}
|