【问题标题】:Force directed layout for constrained rectangular shapes受约束矩形的强制定向布局
【发布时间】:2017-05-02 14:52:33
【问题描述】:

我需要均匀分布一组轴对齐的滑动矩形,这些矩形受最大宽度/高度和一些水平/垂直坐标的约束,具体取决于滑动形状本身的位置。矩形被限制在一个方向上,可以沿着另一个轴滑动,也不能重叠,也不能跨过。

此问题基于: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

【问题讨论】:

    标签: javascript geometry rectangles force-layout


    【解决方案1】:

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

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

    1. 为了尽可能确保相等的间距,您需要稍微更改规则(除了真实的物理),这样您就只计算了距离驱动力最近的障碍,而不是像现实世界中的所有障碍。此外,力仅受距离影响,而不像大多数物理力(包括静电)那样受接触/重叠面积的影响。

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

      并计算必须随距离缩放的驱动力。是否线性无关紧要,但对于从左/右或上/下均匀间隔的障碍物应该是合力为零。缩放主要改变动态行为。只有在约束阻止运动以实现均匀间距的状态下,最终结果才会受到它的影响。因此,您可以使用以下任何一种:

      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 - 垂直轴力

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

      这会带来一个问题,因为滑块也可以有自己的Fy,因此您应该始终只选择每侧最强的力。这意味着您需要记住每侧的距离/力,并始终选择最小距离或|最高|双方的力量。

      这就是 x0,x1 变量的来源,它们在可移动轴上保持到最近障碍物(包括孩子)的最小距离,并且只有在计算所有滑块后才转换为 Fx,Fy 驱动力。

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

      • 垂直,位于水平和垂直滑块之间(图像底部交互)
      • 宽平行,它位于两个相同类型的滑块之间,它们的长边接触(图像上的左右交互)
      • 短平行,它位于两个相同类型的滑块之间,它们的短边接触(图像上的顶部交互)
    2. 限制和系数

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

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

    3. 反馈

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

    【讨论】:

    • 嗨 Spektre,你能帮我完成这个项目吗?我有很多测试数据,有你的帮助我能及时完成。
    • @deblocker 我的空闲时间有限你需要什么?
    • @deblocker 还有几个问题:例如如何确定解决方案真的是最优的?正如 Nina 所建议的那样,可能是一些反映间距失真的权重函数......类似于 1. 获取相邻关系 2. 计算每行/列中间距的最大和平均差异 3. 决定已解决或未解决的状态......另一个问题是您需要指定输入格式正如我之前提到的,由于某些滑块已经相交,因此存在冲突,因此如何处理它们(对于相交的滑块,我忽略了它们,但对吗?)
    • @deblocker 我不使用 javascript,所以如果你需要比较一些东西,你应该创建一个包含你的测试用例的配置文件,这样我就可以将它加载到我的代码中。无论如何,看不到它对您有什么帮助,因为您的代码基于我的代码并且行为方式相同......(至少从外观上看)
    猜你喜欢
    • 2013-02-12
    • 2015-07-05
    • 1970-01-01
    • 2022-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多