一个点在不在多边形里?(ray casting)
给一个多边形和一个点 P,怎么判断 P 在里面还是外面?最经典也最好用的办法是 ray casting(射线法):从 P 朝任意一个方向发一条射线,数它穿过多边形边界几次——穿了奇数次就在内部,偶数次(含 0)就在外部。就这一条规则,凹多边形、带洞的、形状不规则的多边形都适用。
本页射线一律朝正右方发(水平向右)。对每条边只问两件事:其一,这条边跨越 P 所在的水平线吗?(一个端点在线上方、另一个在下方)其二,若跨越,交点落在 P 的右侧吗?是 → 射线真的穿过它,计一次。数完所有边,看穿过次数的奇偶即可。下面这张图,拖动绿 / 红点 P(或在画布里点一下)就能实时看射线、被穿过的边(橙色)、交点和判定。
1 · 为什么「数交点的奇偶」就够了?
把射线想成从 P 一路走到无穷远。无穷远处一定在多边形外面。从 P 往那走,每穿过一次边界,内 / 外身份就翻转一次:外→内→外→内……所以「从 P 出发到无穷远穿了几次」的奇偶,就唯一决定了 P 的身份——奇数次:翻转了奇数回,和无穷远(外部)相反 → P 在内部;偶数次 → 同为外部。这就是 Jordan 曲线定理的算法版,和多边形凹凸、有几个凹槽都无关。
2 · even-odd vs nonzero:同一条边界,两套「内部」定义
上面数奇偶的规则叫 even-odd(奇偶规则)。还有一套 nonzero(非零绕数规则):它给射线穿过的每条边按方向记 +1 / −1,看总和是否为 0。对不自交的简单多边形两者结果一样;一旦边界自己穿过自己,它们就会分道扬镳。
把图形切到**「五角星(自交)」**,再把 P 拖到正中心那块小五边形里:射线会穿过 2 条边 → 偶数 → even-odd 判定为外部(中心是「镂空」的)。但 nonzero 会把中心算作内部。这正是 CSS / SVG 的 fill-rule 在做的选择(evenodd vs nonzero),而 Canvas 的
ctx.fill() 默认就是 nonzero。本页填充按 even-odd 上色,所以你能直接看到中心没被填。
3 · 边界情形:顶点与「点落在边上」
最容易出错的是射线正好擦过一个顶点——若处理不当,一个交点会被相邻两条边各数一次,奇偶就错了。上面代码用了一个技巧:(A.y > P.y) !== (B.y > P.y) 对两个端点用同一个严格 >,等价于把每条边看成「上端开、下端闭」的半开区间,于是擦过顶点时只有一条边会认领它,不会重复计数。另一个边界是 P 正好落在某条边上:这种「在边界上」算内还是外,得看业务定义,通常要单独做一次共线 + 区间判定,而不是指望 ray casting 替你决定。
4 · 它用在哪里
命中测试 (hit-testing):点一下地图,落在哪个行政区 / 哪个不规则按钮里。GIS:一批坐标各属于哪个区域(point-in-region)。游戏 / 图形:粗略碰撞、选区判定。排版与渲染:SVG / Canvas / CSS 用
fill-rule 决定一块自交路径里哪些区域要填色。工程实现通常还会先用包围盒 (bounding box) 快速排除明显在外的点,再对剩下的跑 ray casting。