欢迎访问 生活随笔!

生活随笔

当前位置: 首页 >

多边形裁剪(Polygon Clipping) 1

发布时间:2023/12/8 62 豆豆
生活随笔 收集整理的这篇文章主要介绍了 多边形裁剪(Polygon Clipping) 1 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

原文地址: https://sean.cm/a/polygon-clipping-pt1

Greiner-Hormann裁剪算法无法处理重合线。 所以我研究并写了另一篇适用于所有多边形的文章。

在此处阅读后续内容:多边形裁剪(第 2 部分)

问题

首先, 让我们定义问题,

假设您有两个多边形,每个多边形都以 2D 形式存在

var poly1 = [ // red[ 181, 270 ],[ 85, 418 ],[ 171, 477 ],[ 491, 365 ],[ 218, 381 ],[ 458, 260 ] ]; var poly2 = [ // blue[ 474, 488 ],[ 659, 363 ],[ 255, 283 ],[ 56, 340 ],[ 284, 488 ],[ 371, 342 ] ];

 奇偶规则

多边形遵循奇偶规则来确定一个点是否被视为区域“内部”。

基本规则是想象您正在用一条水平线上从左到右扫描。每次越过边缘时,都会在外部和内部之间切换。

 那么:给定这两个多边形,我们如何计算不同的布尔运算?

基本理念

 首先,让我们定义一些基本的规则.

顺时针vs逆时针(Forward vs. Backward Movement)

如果我们坐在多边形的任何一点上,我们总是可以向前一个点或者后一个点移动

顺时针只是意味着沿箭头方向移动,逆时针则相反

 插入点

 在处理过程中,我们需要在多边形中插入点。只要我们对如何插入它们很聪明,它就不会改变多边形的形状:

交叉点

识别和分类交叉点是算法中的魔法.

如果您考虑一下,我们将执行的每个操作(交集、联合、差异)都会产生一个包含多边形之间所有交点的多边形

 我们不关心同一多边形内的交叉点.

另外:如果你想象我们正沿着一个多边形行走并遇到一个十字路口,我们有3个选择.

1. 保持在同一个多边形上(这是毫无意义的)

2. 切换多边形,开始顺时针移动

3. 切换多边形,并开始逆时针移动

 

因此,如果我们能够智能地选择在每个交叉路口的方向,我们就可以追踪到正确的结果形状.

交叉点示例

想象一下, 我们想象一下,我们正在追踪两个多边形合并union的结果.

在每个交叉点,我们都希望朝着最终形状继续增长的方向移动.

我们可以这样做:

我们也可以在相反的方向得到相同的结果:

关于所有四个决定,我们可以说哪些是正确的?

在每个交叉点,我们总是朝着远离我们离开的多边形的方向前进.

因此,例如,如果我们沿着 Blue 行驶,然后遇到一个十字路口,我们应该继续沿着 Red 向远离Blue的方向行使.

会有什么不同?这是Red-Blue(从Red中减去Blue区域):

 而在另一个方向:

对此我们能说什么?

当从红色切换到蓝色时,我们进入红色。当从蓝色切换到红色时,我们远离红色.

所以我们有两个基本的决定:

1. 当从红色切换到蓝色时,我们是进入还是远离红色?

2.当从蓝色切换到红色时,我们是进入还是远离蓝色?

对于联合(union)来说, 答案总是离开. 但是对于Red-Blue(Red减Blue),我们想要进入红色, 远离蓝色。如果你玩玩,你会注意到交叉(intersection )意味着总是进入你要离开的

这给了我们下边的表

OperatorInto Red?Into Blue?
联合(Union)FalseFalse
Red减Blue(Red - Blue)TrueFalse
Blue减Red(Blue - Red)FalseTrue
交叉(Intersection)TrueTrue

交叉入口/ 交叉出口

我们不知道如何进入或离开——我们只知道沿着多边形顺时针,逆时针移动。我们如何把两者同意起来.

如果我们在一个交点的两边取两个点,并测试它们是否在另一个多边形内,我们可以保证一个点在外面,一个点在里面:

如果第一个点在外面,那么我们可以认为这条线是通过交点进入多边形的。如果第一个点在内,则该线通过交点离开多边形.

所以,我们真的只需要将每个交叉点标记为交叉入口/ 交叉出口

当我们沿着一条路径行驶时,每个路口都会切换我们是在里面还是外面。它必须.

因此,我们只需要计算第一个点是否在另一个多边形内部。如果是,那么第一个交叉点是一个交叉出口——否则第一个交叉点是一个交叉入口.

而且由于路径上的每个交叉点都在entry和exit之间切换,我们不必继续测试点是在内部还是外部(这很昂贵).

表现

最后,重要的是要认识到交叉点是相对于多边形的交叉入口或交叉出口.

这意味着每个交叉点有四种可能性.

 白色代表进入,黑色代表退出。左半球为红色,右半球为蓝色

 实际上,对于每个交点,我们将在每个多边形中插入一个点。所以每个交点会有两个点,一个存储在每个多边形中。每个点都会跟踪它是进入还是退出.

现在我们准备好代码啦.

步骤1. 将多边形转换为链表

双链表对于这个算法来说是一个有用的多边形表示,因为我们将同时插入点和遍历。通过使用双链表,我们不必担心插入会破坏遍历.

我们还需要跟踪一个点是否是一个交点,所以我们可以从false这里初始化它开始:

function UpgradePolygon(p){// converts a list of points into a double linked listvar root = null;for (var i = 0; i < p.length; i++){var node = {point: p[i],intersection: false,next: null,prev: null};if (root === null){// root just points to itself:// +-> (root) <-+// | |// +------------+node.next = node;node.prev = node;root = node;}else{// change this:// ...-- (prev) <--------------> (root) --...// to this:// ...-- (prev) <--> (node) <--> (root) --...var prev = root.prev;prev.next = node;node.prev = prev;node.next = root;root.prev = node;}}return root; }

步骤2. 计算并插入交叉点

接下来,我们需要遍历每个边组合,看看它们是否相交。如果它们确实彼此相交,那么我们需要在多边形中插入交点.

线交点

首先,我们需要一个辅助函数来计算两条线的交点:

function LinesIntersect(a0, a1, b0, b1){var adx = a1[0] - a0[0];var ady = a1[1] - a0[1];var bdx = b1[0] - b0[0];var bdy = b1[1] - b0[1];var axb = adx * bdy - ady * bdx;var ret = {cross: axb,alongA: Infinity,alongB: Infinity,point: [Infinity, Infinity]};if (axb === 0)return ret;var dx = a0[0] - b0[0];var dy = a0[1] - b0[1];ret.alongA = (bdx * dy - bdy * dx) / axb;ret.alongB = (adx * dy - ady * dx) / axb;ret.point = [a0[0] + ret.alongA * adx,a0[1] + ret.alongA * ady];return ret; }

它计算两条线的交点,并返回每条线上的交点“沿”多远。因此,例如,如果alongA是0.75,那么交集发生在从a0到 的75% 处a1. 

这些值是重要的,因为他们可能是负数或大于1,因此,如果两条线实际相交,我们需要测试alongA和alongB0和1(不含)之间.

下一个非交点

由于我们将在我们的链表中插入交点,所以有一个帮助函数来查找下一个非交点.

function NextNonIntersection(node){do{node = node.next;} while (node.intersection);return node; }

每个边组合(Edge Pair)

现在我们可以编写迭代每个边组合的代码:

var root1 = UpgradePolygon(poly1); var root2 = UpgradePolygon(poly2);var here1 = root1; var here2 = root2; do{do{//// TODO: test intersection between:// here1 -> NextNonIntersection(here1) and// here2 -> NextNonIntersection(here2)//here2 = NextNonIntersection(here2);} while (here2 !== root2);here1 = NextNonIntersection(here1); } while (here1 !== root1);

交叉点测试

给定两个节点,我们可以测试交集:

var next1 = NextNonIntersection(here1); var next2 = NextNonIntersection(here2);var i = LinesIntersect(here1.point, next1.point,here2.point, next2.point );if (i.alongA > 0 && i.alongA < 1 &&i.alongB > 0 && i.alongB < 1){//// TODO: insert intersection points in both polygons at// the correct location, referencing each other// }

插入交叉点

最后,如果两条边相交,那么我们要在两个非交点之间插入我们的交叉点.

为了将它插入正确的位置,我们必须跟踪alongA和alongB值以确保如果两个交点在同一条边上,它们以正确的顺序插入.

我们将要创建两个节点,一个用于每个多边形——但这些节点应该相互指向,以便我们稍后在遇到交叉点时可以在多边形之间“跳跃”

var node1 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongA,friend: null }; var node2 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongB,friend: null };// point the nodes at each other node1.friend = node2; node2.friend = node1;var inext, iprev;// find insertion between here1 and next1, based on dist inext = here1.next; while (inext !== next1 && inext.dist < node1.dist)inext = inext.next; iprev = inext.prev;// insert node1 between iprev and inext inext.prev = node1; node1.next = inext; node1.prev = iprev; iprev.next = node1;// find insertion between here2 and next2, based on dist inext = here2.next; while (inext !== next2 && inext.dist < node2.dist)inext = inext.next; iprev = inext.prev;// insert node2 between iprev and inext inext.prev = node2; node2.next = inext; node2.prev = iprev; iprev.next = node2;

步骤3. 计算交叉入口/交叉出口

我们知道交叉口在进入和退出之间交替。但是第一个交叉点是什么?是入口还是出口.

简单:如果多边形的第一个点在另一个多边形内,那么第一个交点必须是出口.

但是,计算一个点是否在多边形内部实际上有点复杂.

点在多边形内

function PointInPolygon(point, root){var odd = false;var x = point[0];var y = point[1];var here = root;do {var next = here.next;var hx = here.point[0];var hy = here.point[1];var nx = next.point[0];var ny = next.point[1];if (((hy < y && ny >= y) || (hy >= y && ny < y)) &&(hx <= x || nx <= x) &&(hx + (y - hy) / (ny - hy) * (nx - hx) < x)){odd = !odd;}here = next;} while (here !== root);return odd; }

PointInPolygon通过计算水平线相交的边数来工作。水平线从(-Infinity, y)到(x, y)。它只关心交叉点的数量是奇数还是偶数。它基于光线投射。

交替进入/退出

现在我们可以轻松计算出一个交叉点是入口还是出口:

function CalculateEntryExit(root, isEntry){var here = root;do{if (here.intersection){here.isEntry = isEntry;isEntry = !isEntry;}here = here.next;} while (here !== root); }var is1in2 = PointInPolygon(root1.point, root2); var is2in1 = PointInPolygon(root2.point, root1);CalculateEntryExit(root1, !is1in2); CalculateEntryExit(root2, !is2in1);

步骤4. 生成结果

我们已经走了很长一段路!这是我们到目前为止所拥有的.

我们已经计算并插入了交点,并将它们标记为每个多边形的入口或出口.

现在是有趣的部分!

从哪里开始

 我们从哪里开始追踪结果?我们不能只选择一个随机点,因为有些点实际上可以从结果中完全删除.

由于所有操作都包括每个交集,我们应该从寻找未处理的交集开始.

我们添加到最终结果中的每个交点,我们都标记为已处理.

然后,我们只是继续跟踪,直到我们不再有任何交集需要处理.

var result = []; var isect = root1; var into = [intoBlue, intoRed]; // explained below while (true){do{if (isect.intersection && !isect.processed)break;isect = isect.next;} while (isect !== root1);if (isect === root1)break;//// TODO: process isect// }

 转向哪个方向

最后,我们来到了症结所在:

当我们遇到十字路口时,我们怎么知道该往哪个方向转弯?

让我们来推理一下:

Is Entry?Move Into?Move Forward?
TrueTrueTrue
TrueFalseFalse
FalseTrueFalse
FalseFalseTrue

因此,如果 ,我们应该继续前进isEntry === intoPoly

由于我们所在的多边形来回切换,我们只需通过将intoBlue和存储intoRed在into列表中来使我们的决策动态化,并将 其curpoly用作索引.

var curpoly = 0; var clipped = [];var here = isect; do{// mark intersection as processedhere.processed = true;here.friend.processed = true;var moveForward = here.isEntry === into[curpoly];do{clipped.push(here.point);if (moveForward)here = here.next;elsehere = here.prev;} while (!here.intersection);// we've hit the next intersection so switch polygonshere = here.friend;curpoly = 1 - curpoly; } while (!here.processed);result.push(clipped);

没有交叉点

如果没有交叉点?

我们的结果集将是空的……这可能是正确的,也可能是错误的——这取决于操作.

一个简单的检查就足以修复它:

if (result.length <= 0){if (is1in2 === intoBlue)result.push(poly1);if (is2in1 === intoRed)result.push(poly2); }

演示

单击此处启动演示!

 您可以拖动每个多边形的点,并通过单击按钮切换操作。

 附录:限制

抱歉,这个算法有一个严重的局限性:

您不能拥有完美重叠的点或边.

如果你仔细想想,这是有道理的:整个算法都是基于交叉点的思想.

如果点或边直接重叠,那么您就不会得到那种好的跳跃效果.

最初的论文建议稍微“扰乱”点,这样线条就不会完全重叠。我最初认为这是一个小调整,不会有有问题.

但是,我错了.

扰动点会破坏数据——因此可能很重要的源数据的属性(例如,平滑边缘)变得无效.

幸运的是我研究了另一种处理一切的算法,并写了一篇后续文章

此处阅读后续内容:多边形裁剪(第 2 部分)

总结

以上是生活随笔为你收集整理的多边形裁剪(Polygon Clipping) 1的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。