30 lines
3.9 KiB
TypeScript
30 lines
3.9 KiB
TypeScript
/**
|
|
* 与渲染框架无关的基本几何协议。所有计算输入必须在同一坐标空间;
|
|
* bounds 负责粗筛,hit 负责矩形精判,曲线 segments 由模型命中和 SVG 绘制共享。
|
|
* 函数不主动校验输入几何/容差/控制点,也不换算 viewport 或 presentation.unitScale,调用方先统一空间。
|
|
*/
|
|
import type {Geometry,Point} from './types.ts';
|
|
/** 轴对齐包围盒,用于空间索引粗筛;旋转节点的盒会大于其实际轮廓。 */
|
|
export type Rect={readonly x:number;readonly y:number;readonly width:number;readonly height:number};
|
|
/** 三次 Bezier 的四个绝对点,顺序为起点、第一控制点、第二控制点、终点。 */
|
|
export type Cubic=readonly[Point,Point,Point,Point];
|
|
/** 已解算到共同展示空间的连接点及朝外法向量;默认控制柄沿该法向量延伸。 */
|
|
export type Connector={readonly point:Point;readonly normal:Point};
|
|
/** knot.position 是绝对位置,in/out 和首尾 control 是相对锚点偏移。 */
|
|
export type BezierChain={readonly knots:readonly{readonly position:Point;readonly in:Point;readonly out:Point}[];readonly startControl?:Point;readonly endControl?:Point};
|
|
/** 绕节点中心旋转后的 AABB:宽=|cos|·w+|sin|·h,高=|sin|·w+|cos|·h。 */
|
|
export function rectangleBounds(g:Geometry):Rect{const angle=g.rotation*Math.PI/180,c=Math.abs(Math.cos(angle)),s=Math.abs(Math.sin(angle)),width=g.width*c+g.height*s,height=g.width*s+g.height*c;return{x:g.x+g.width/2-width/2,y:g.y+g.height/2-height/2,width,height};}
|
|
/** 将点反向旋转到矩形自身坐标,计算到矩形的距离,再与容差比较。
|
|
* 角外区域使用欧氏距离而非扩展 AABB;浮点误差余量随坐标尺度调整。
|
|
* 点在矩形内部时距离为 0;tolerance 由调用方保证非负,正常边界含在命中内。 */
|
|
export function hitRectangle(g:Geometry,point:Point,tolerance=0):boolean{const angle=-g.rotation*Math.PI/180,c=Math.cos(angle),s=Math.sin(angle),dx=point.x-g.x-g.width/2,dy=point.y-g.y-g.height/2,x=dx*c-dy*s,y=dx*s+dy*c,ox=Math.max(0,Math.abs(x)-g.width/2),oy=Math.max(0,Math.abs(y)-g.height/2);return Math.hypot(ox,oy)<=tolerance+Number.EPSILON*64*Math.max(1,Math.abs(point.x),Math.abs(point.y),g.width,g.height);}
|
|
/** 默认矩形节点协议;空 dataDependencies 表示形状仅依赖 geometry,不因 title 等数据变化重建。 */
|
|
export const rectangleGeometry=Object.freeze({kind:'node' as const,version:1 as const,dataDependencies:Object.freeze([] as string[]),bounds:({geometry}:{geometry:Geometry})=>rectangleBounds(geometry),hit:({geometry,point,tolerance}:{geometry:Geometry;point:Point;tolerance:number})=>hitRectangle(geometry,point,tolerance)});
|
|
/** Shared by the built-in model protocol and the SVG renderer; all arguments are in one display space. */
|
|
/** 由锚点链生成每段三次曲线:上一 knot.out 和下一 knot.in 分别作为控制偏移。
|
|
* 无显式首尾控制柄时,reach 按两端直线距离的 0.4 倍限制在 40–240,再乘相应法向量。
|
|
* 本函数不归一 normal;只有单位法向量才使 reach 等于控制柄的实际长度。
|
|
* add 把相对偏移变成绝对控制点;map 对每对相邻 anchors 输出一段,共 knots.length+1 段。
|
|
* 无 knot 时直接生成端点间一段;这里不根据 smooth/symmetric 标签自动修正控制柄。 */
|
|
export function bezierChainSegments(from:Connector,to:Connector,path?:BezierChain|null):Cubic[]{const reach=Math.min(240,Math.max(40,Math.hypot(to.point.x-from.point.x,to.point.y-from.point.y)*.4)),knots=path?.knots??[],anchors=[from.point,...knots.map(knot=>knot.position),to.point],add=(a:Point,b:Point)=>({x:a.x+b.x,y:a.y+b.y});return anchors.slice(0,-1).map((point,index)=>[point,add(point,index?knots[index-1].out:path?.startControl??{x:from.normal.x*reach,y:from.normal.y*reach}),add(anchors[index+1],index<knots.length?knots[index].in:path?.endControl??{x:to.normal.x*reach,y:to.normal.y*reach}),anchors[index+1]]);}
|