受限矩形的力导向布局

Force directed layout for constrained rectangular shapes

我需要均匀分布一堆轴对齐的滑动矩形,这些矩形受最大 width/height 和一些 horizontal/vertical 坐标的约束,具体取决于滑动形状本身的位置。矩形在一个方向上受到约束,可以沿另一轴滑动,不能重叠,也不能越过。

此问题基于:How to implement a constraint solver for 2-D geometry? 和 Spektre 广为接受的力驱动约束求解器提案。

整个结构像往常一样构建为一个图形,其中矩形代表节点。

现在,我需要检查每个矩形的大小以获得正确的力计算并避免重叠,但我在理解如何将力场应用于二维形状以及如何应计算两个矩形之间的距离。也许是顶点或边?

相关代码在下面的函数Solver.solve()中,其中s.Z分别表示水平形状的高度,垂直形状的宽度:

for(var i=0, l=sliders.length; i<l; i++) {
    var si = sliders[i];
    for(var j=i+1, k=sliders.length; j<k; j++) {
        var sj = sliders[j];
        if(si._horizontal == sj._horizontal) {
            // longer side interaction
            if(si._horizontal == 1) {
                a0 = si.X + si.a; a1 = sj.X + sj.a;
                b0 = si.X + si.b; b1 = sj.X + sj.b;
                x0 = si.Y; x1 = sj.Y;
            } else {
                a0 = si.Y + si.a; a1 = sj.Y + sj.a;
                b0 = si.Y + si.b; b1 = sj.Y + sj.b;
                x0 = si.X; x1 = sj.X;
            }
            if(((a0 <= b1) && (b0 >= a1)) || ((a1 <= b0) && (b1 >= a0))) {
                x0 = x1 - x0;
                if((si.ia >= 0) && (x0 < 0.0) && ((fabs(si.x0) < si.Z) || (fabs(si.x0) > fabs(x0)))) si.x0 = -x0;
                if((si.ia >= 0) && (x0 > 0.0) && ((fabs(si.x1) < si.Z) || (fabs(si.x1) > fabs(x0)))) si.x1 = -x0;
                if((sj.ia >= 0) && (x0 < 0.0) && ((fabs(sj.x0) < sj.Z) || (fabs(sj.x0) > fabs(x0)))) sj.x0 = +x0;
                if((sj.ia >= 0) && (x0 > 0.0) && ((fabs(sj.x1) < sj.Z) || (fabs(sj.x1) > fabs(x0)))) sj.x1 = +x0;
            }
            // shorter side interaction
            if(si._horizontal == 1) {
                a0 = si.Y - si.Z; a1 = sj.Y + sj.Z;
                b0 = si.Y + si.Z; b1 = sj.Y + sj.Z;
                x0 = si.X; x1 = sj.X;
            } else {
                a0 = si.X - si.Z; a1 = sj.X + sj.Z;
                b0 = si.X + si.Z; b1 = sj.X + sj.Z;
                x0 = si.Y; x1 = sj.Y;
            }
            if(((a0 <= b1) && (b0 >= a1)) || ((a1 <= b0) && (b1 >= a0))) {
                if(x0 < x1) {
                    x0 += si.b; x1 += sj.a;
                } else{
                    x0 += si.a; x1 += sj.b;
                }
                x0 = x1 - x0;
                if(si.ia >= 0) {
                    var sa = this.sliders[si.ia];
                    if((sa.ia >= 0) && (x0 < 0.0) && ((fabs(sa.x0) < sa.Z) || (fabs(sa.x0) > fabs(x0)))) sa.x0 = -x0;
                    if((sa.ia >= 0) && (x0 > 0.0) && ((fabs(sa.x1) < sa.Z) || (fabs(sa.x1) > fabs(x0)))) sa.x1 = -x0;
                }
                if(sj.ia >= 0) {
                    var sa = sliders[sj.ia];
                    if((sa.ia >= 0) && (x0 < 0.0) && ((fabs(sa.x0) < sa.Z) || (fabs(sa.x0) > fabs(x0)))) sa.x0 = +x0;
                    if((sa.ia >= 0) && (x0 > 0.0) && ((fabs(sa.x1) < sa.Z) || (fabs(sa.x1) > fabs(x0)))) sa.x1 = +x0;
                }
            }
        }
    }
}
// set x0 as 1D vector to closest perpendicular neighbour before and x1 after
for(var i=0, l=sliders.length; i<l; i++) {
    var si = sliders[i];
    for(var j=i+1, k=sliders.length; j<k; j++) {
        var sj = sliders[j];
        if(si._horizontal != sj._horizontal) {
            // skip ignored sliders for this
            var ignore = false;
            for(var n=0, m=si.ic.length; n<m; n++) {
                if(si.ic[n] == j) {
                    ignore = true;
                    break;
                }
            }
            if(ignore === true) continue;
            if(si._horizontal == 1) {
                a0 = si.X + si.a; a1 = sj.X - sj.Z;
                b0 = si.X + si.b; b1 = sj.X + sj.Z;
                x0 = si.Y;
            } else {
                a0 = si.Y + si.a; a1 = sj.Y - sj.Z;
                b0 = si.Y + si.b; b1 = sj.Y + sj.Z;
                x0 = si.X;
            }
            if(((a0 <= b1) && (b0 >= a1)) || ((a1 <= b0) && (b1 >= a0))){
                if(si._horizontal == 1) {
                    a1 = sj.Y + sj.a;
                    b1 = sj.Y + sj.b;
                        } else {
                    a1 = sj.X + sj.a;
                    b1 = sj.X + sj.b;
                }
                a1 -= x0; b1 -= x0;
                if(fabs(a1) < fabs(b1)) x0 = -a1; else x0 = -b1;
                if((si.ia >= 0) && (x0 < 0.0) && ((fabs(si.x0) < si.Z) || (fabs(si.x0) > fabs(x0)))) si.x0 = +x0;
                if((si.ia >= 0) && (x0 > 0.0) && ((fabs(si.x1) < si.Z) || (fabs(si.x1) > fabs(x0)))) si.x1 = +x0;
                if(sj.ia < 0) continue;
                var sa = sliders[sj.ia];
                if((sa.ia >= 0) && (x0 < 0.0) && ((fabs(sa.x0) < sa.Z) || (fabs(sa.x0) > fabs(x0)))) sa.x0 = -x0;
                if((sa.ia >= 0) && (x0 > 0.0) && ((fabs(sa.x1) < sa.Z) || (fabs(sa.x1) > fabs(x0)))) sa.x1 = -x0;
            }
        }
    }
}

如何计算矩形的力,使力场均匀分布,即矩形之间的距离尽可能大?想想矩形真的很热,并且必须最多间隔,相对于它们的自定义 x/y 约束。

任何帮助将不胜感激。

编辑:

样本:https://plnkr.co/edit/3xGmAKsly2qCGMp3fPrJ?p=preview

如果我答对了这个问题 OP 要求提供一组驱动滑块的规则,以便最终模拟状态导致有效的解决方案。

所以这是我的方法,它实际上是从 OP 中的链接答案重述我的求解器代码,但它不适合那里,因为我已经达到 30 KB 的限制并且我觉得它需要更多的解释然后只是评论代码所以这里是:

  1. 部队

    为了尽可能确保相等的间距,您需要稍微更改一下规则(除了实际物理之外),这样您就只考虑距离驱动力最近的障碍,而不是像现实世界中那样考虑所有障碍。此外,力仅受距离影响,而不像大多数物理力(包括静电力)那样也受 contact/overlap 面积的加权。

    因此在 i-th 滑块(黄色)的迭代过程中,找到所有 4 个方向(红色)中到最近障碍物的距离:

    并计算必须与距离成比例的驱动力。线性与否并不重要,但对于来自 left/right 或 up/down 的均匀间隔的障碍物,合力应该为零。缩放主要改变动力学行为。只有在约束阻止移动以实现均匀间距的状态下,最终结果才会受它影响。因此,您可以使用以下任何一个:

    F = c * (d1-d0)
    F = c * (d1^2 - d0^2)
    F = c * (1/d1^2 - 1/d0^2)
    

    其中c是一些幅度系数,d0,d1是同一轴上最近的2个障碍物距离。

    现在您将获得 2 个力(每个轴一个)

    • Fx-横轴力
    • Fy - 垂直轴力

    简而言之就是这样。但是约束有问题。例如 selected 滑块(黄色)是垂直的。这意味着它只能在 x 轴上移动。所以你把 Fx 驱动力给它。 Fy 力应该驱动它的父级滑块(蓝色),它是水平的并且可以在 y 轴上移动(如果不是固定的或粗糙的)。

    这会引入一个问题,因为该滑块也可以有自己的 Fy,因此您应该始终 select 仅从每一侧施加最强的力。这意味着你需要记住每一边的 distances/forces 和 select 始终是最小距离或 |最高|各方力量。

    这就是 x0,x1 变量的来源,它们保持可移动轴到最近障碍物(包括儿童)的最小距离,并且仅在所有滑块的计算转换为 Fx,Fy 驾驶之后部队。

    为了检测邻居和碰撞,我识别出 3 种交互类型

    • 垂直 介于水平和垂直滑块之间(图像底部交互)
    • wide parallel 两个相同类型的滑块之间的长边接触(图像上的左、右交互)
    • short parallel 相同类型的两个滑块之间的短边接触(图像上的顶部交互)
  2. 极限和系数

    我还介绍了一些速度限制和系数(不只是为了倾倒以避免振荡)。速度和加速度限制会影响求解时间和稳定性。使用它们还有另一个原因(我不这样做),那就是保留滑块的顺序,这样它们就不会互相跳过。这可以通过简单地限制最高速度来完成,这样每次迭代时,任何滑块的移动量都不会超过滑块厚度的一半。因此,如果发生碰撞,碰撞例程将启动。

    我在我的代码中跳过了它,因为我的配置已设置,因此滑块的顺序与其索引号一致。因此,如果我检测到任何滑块位于某个测试滑块的左侧,而其索引较高,则意味着它已跳过,我通过恢复最后位置来处理它......这是廉价的 hack 并且不会在复杂的集合中工作链中的许多子滑块不只是一个。

  3. 意见反馈

    由于限制,有时您的驱动力可能会丢失(移动到固定或卡住的父滑块),以防这种行为破坏结果,您应该将相反的力以另一种方式传播到它导致它的邻居。对于简单的构造来说,这不是必需的,因为它已经包含了驱动力,但对于更复杂的子链来说,它可能会构成威胁(但这是我的疯狂想法,我在这方面可能是错误的)。 在自然界中,这是通过整合所有相邻对象的力而不是最近的对象的力来提供的,但我认为这会导致不相等的间距。