【计算几何 排样】libnest2d的放置器

本文涉及知识点

数学 几何

翻译及基础

Contour 轮廓

circumference 圆的周长

noFitPolygon 是排样/装箱算法中的核心几何概念,全称 No-Fit Polygon(无拟合多边形),也叫 临界多边形。

目标函数(objective function)

template

cpp 复制代码
e.template sqlength<double>()

当一个对象/表达式的类型依赖于模板参数时,编译器在解析时不知道 sqlength 是:

一个普通成员变量?

一个静态成员?

还是一个成员模板?

为了消除歧义,C++ 要求在这种依赖上下文中,如果要用成员模板,必须显式写 template 关键字

is_integral

cpp 复制代码
template <class _Ty>
_INLINE_VAR constexpr bool is_integral_v = _Is_any_of_v<remove_cv_t<_Ty>, bool, char, signed char, unsigned char,
    wchar_t,
#ifdef __cpp_char8_t
    char8_t,
#endif // __cpp_char8_t
    char16_t, char32_t, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long>;
cpp 复制代码
template <class _Ty>
_INLINE_VAR constexpr bool is_floating_point_v = _Is_any_of_v<remove_cv_t<_Ty>, float, double, long double>;

std::reference_wrapper

cpp 复制代码
	int x = 14;
	auto i1 = std::ref(x);
	std::vector<std::reference_wrapper<int>> v;
	v.emplace_back(i1);
	v[0].get() = 5;
	std::cout << x << "\n";

代码执行后x=5。

基础几何

对于两个简单多边形P1,P2(无自交,允许凹),在内部不相交的前提下发生相切,公共点只可能是以下三种类型之一(或它们的组合):

边重合:两条边共线且部分重叠,公共点是一条线段(无限多点)。

顶点重合:一个多边形的顶点与另一个多边形的顶点重合,公共点是 1 个孤立点。

顶点在边上:一个多边形的顶点落在另一个多边形的边内部,公共点是 1 个孤立点。

引理一 :如果p在P1内部,P2边界。p沿P2的内外法矢量移动 Δ = 0 + \Delta=0^+ Δ=0+,到达p2p3。p2p3至少一个在P2内,不妨令p2在P2内,如果p2在P1内部,则P1,P2相交。如果p2在P1的边界,则线段pp2中点在P1和P2内。

如果p2p3在边界线上

引理二:P1和P2的任意交点p都在P1和P2的边界线上。如果p在P1和P2内部。则P1P2相交,而不是相切。p在P1(P2)边界上,P2(P1)内部。

pointlike

horizontalDistance

点pt水平向左移动多少,会在线段上。

cpp 复制代码
template<class P, class Unit = TCompute<P>>
inline std::pair<Unit, bool> horizontalDistance(
        const P& p, const _Segment<P>& s)
{
    namespace pl = pointlike;
    auto x = Unit(pl::x(p)), y = Unit(pl::y(p));
    auto x1 = Unit(pl::x(s.first())), y1 = Unit(pl::y(s.first()));
    auto x2 = Unit(pl::x(s.second())), y2 = Unit(pl::y(s.second()));

    Unit ret;

    if( (y < y1 && y < y2) || (y > y1 && y > y2) )
        return {0, false};
    if ((y == y1 && y == y2) && (x > x1 && x > x2))
        ret = std::min( x-x1, x -x2);
    else if( (y == y1 && y == y2) && (x < x1 && x < x2))
        ret = -std::min(x1 - x, x2 - x);
    else if(y == y1 && y == y2)
        ret = 0;
    else
        ret = x - x1 + (x1 - x2)*(y1 - y)/(y1 - y2);

    return {ret, true};
}
cpp 复制代码
 if( (y < y1 && y < y2) || (y > y1 && y > y2) )
        return {0, false};

纵坐标没有交叉,怎么移动都不重叠。

cpp 复制代码
if ((y == y1 && y == y2) && (x > x1 && x > x2))
        ret = std::min( x-x1, x -x2);
    else if( (y == y1 && y == y2) && (x < x1 && x < x2))
        ret = -std::min(x1 - x, x2 - x);
    else if(y == y1 && y == y2)
        ret = 0;

处理水平线段。{true,0}表示已经在线段上。

cpp 复制代码
ret = x - x1 + (x1 - x2)*(y1 - y)/(y1 - y2);

处理非水平线。

_BottomLeftPlacer

注意 :

一,此放置器限制了限制了包围盒的左下角是原点。

二,包围盒的坐标不能过小,否则会计算错误,是误差的原因。

三,由于移动误差是1,故物体间最小间距是1。

误差

cpp 复制代码
	const int M =2;
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	const Box bin(ClipperLib::IntPoint((0), (0)), ClipperLib::IntPoint((12 * M), (12 * M)));
	const std::size_t bins = nest<BottomLeftPlacer,FirstFitSelection>(items, bin, (0));

M=1需要5个箱子,M>1,需要2个箱子

修正的代码

cpp 复制代码
	const int M =1;
	items.emplace_back(Rectangle((5* M), (5* M)));
	items.emplace_back(Rectangle((5*M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
	items.emplace_back(Rectangle((5 * M), (5 * M)));
    const Box bin(ClipperLib::IntPoint((0),(0)), ClipperLib::IntPoint((13 * M), (13 * M)));
	const std::size_t bins = nest<BottomLeftPlacer,FirstFitSelection>(items, bin, (0));

第一个物品到箱子的间隔是1,两个物品之间的间隔是1,第二个物品到箱子的间隔是1。共13,故箱子宽高必须是13.

itemsInTheWayOf

已放置物品中那些可能挡住当前物体,不能漏判,可以误判。

csharp 复制代码
        static_assert(OrientationType<RawShape>::Value ==
                      Orientation::CLOCKWISE,
                      "Counter clockwise toWallPoly() Unimplemented!");

端点必须是顺时针,否则编译不过。

通过下面的代码,可以看出红线可能替换左侧,也可能替换右侧。不然无需item,直接对scanpoly求交就可以了。

cpp 复制代码
   template<class C = Coord>
    static enable_if_t<std::is_floating_point<C>::value, bool>
    isInTheWayOf( const Item& item,
                  const Item& other,
                  const RawShape& scanpoly)
    {
        auto tsh = other.transformedShape();
        return ( sl::intersects(tsh, scanpoly) ||
                 sl::isInside(tsh, scanpoly) ) &&
               ( !sl::intersects(tsh, item.rawShape()) &&
                 !sl::isInside(tsh, item.rawShape()) );
    }

availableSpace

尝试移动当前物品各边,直到已放物品有点到当前边上。

尝试移动当前物品各点,直到点在已放物品的边上。

取上述所有尝试的最小移动量。由于是尝试,移动原位。

_trypack

尽可能下移,尽可能左移,不断循环。直到无法移动。

_NofitPolyPlacer

correctNfpPosition

把计算出来的 NFP(无拟合多边形、临界多边形)平移到正确的位置,使其相对于 stationary(静止物品)的位置符合 NFP 的参考点定义。感觉是将闵可夫斯基差平移成临界多边形。

创建nfp用的是nfpConvexOnly。

minimizeCircle

而是用数值优化来逼近求一个形状(RawShape)的最小包围圆(Minimum Enclosing Circle),返回圆心和半径。

NfpPConfig

cpp 复制代码
enum class Alignment {
        CENTER,
        BOTTOM_LEFT,
        BOTTOM_RIGHT,
        TOP_LEFT,
        TOP_RIGHT,
        DONT_ALIGN      //!> Warning: parts may end up outside the bin with the
                        //! default object function.
    };

枚举类型,对齐方式。

std::vector rotations; 尝试旋转那些角度。

Alignment alignment; 排样完成后,整个结果堆在箱子中的对齐方式。

Alignment starting_point;第一个物品从箱子的哪个位置开始放置;

std::function<double(const _Item&)> object_function; 自定义评分。输入是候选物品(带当前位置信息);

输出是该位置的评分;评分越低,表示位置越好(与默认行为一致);

float accuracy = 0.65f; 质量。1.0质量最高,0性能最好。

bool explore_holes = false;预留接口,物品的洞是否有效。当前版本不支持此参数,留作以后更正。

bool parallel = true; 是否并行以发挥多CPU优势。

std::function<void(const nfp::Shapes&, // merged pile

const ItemGroup&, // packed items

const ItemGroup& // remaining items

)> before_packing; 在每次为新物品搜索位置之前调用的回调。

EdgeCache

ContourCache

cpp 复制代码
struct ContourCache {
    mutable std::vector<double> corners;   // 缓存的采样角点(归一化距离)
    std::vector<Edge> emap;                // 边列表
    std::vector<double> distances;         // 累积距离(每条边结束处的总长度)
    double full_distance = 0;              // 总周长
};

ContourCache contour_; 外边界轮廓

std::vector holes_;洞的轮廓

inline Vertex coords(const ContourCache& cache, double distance) const 。distance是归一化的距离。

_NofitPolyPlacer

const double norm_; 比如归一化因子是箱体面积,位置分数最大值是箱体面积。分数除以norm_后范围是0,1。

overfit,这是一组函数重载,用于判断一个物品(或它的包围盒/凸包)是否超出箱子边界,并返回一个"超出程度"的数值。返回值<= 0 物品适合箱子(未超出),> 0 物品超出箱子,数值表示超出程度。

repack 尝试装箱

如果剩余空间(箱子面积-已占用面积) ≥ \ge ≥ 当前物品面积。

备份对象函数(功能和目标函数类似),最后恢复。

backup_cpy备份已经装箱的物品,在ofn函数中恢复。

分数计算:

cpp 复制代码
 double pack_rate = (pile_area)/parea;
 double waste = 1.0 - pack_rate;

parea是包围盒和凸包的平均面积。pile_area是摆放物品面积之和。 非线性优化分数越小越好。填充率,越接近 1 表示堆越紧凑

cpp 复制代码
 auto wscore = std::sqrt(waste);

开方,扩大低值区间,压缩高值区间。

dscore,待放物品和箱体中心的距离得分。重合距离0,距离是对角线长度的一半是1。

config_.object_function的分数:ratio*wscore + (1.0 - ratio) * dscore;紧促分数。

cpp 复制代码
auto count_diff = items.size() - items_.size();

count_diff ≥ 0 \ge 0 ≥0,只尝试了items.size()个物品,故最多items个物品装箱。

如果count_diff > 0,说明旧方案比当前方案可以放更多物品。

cpp 复制代码
 if(count_diff == 0) {
                    result = _trypack(item);

                    if(result) {
                        std::cout << "Success" << std::endl;
                        score = 0.0;
                    } else {
                        score += result.overfit() / norm;
                    }
                }

如果当前物品能够装箱,则是解。无需最优解,最优解在下个物品找。

如果当前物品不能装箱,则根据溢出部分计算分数。

_trypack

cpp 复制代码
        if(std::all_of(items_.begin(), items_.end(),
                [](const Item& item) { return item.isDisallowedArea(); })) 

如果已放置物品,全部是禁止区域,则无法移动,只能使用原始位置。否则大致流程如下:

伪代码 复制代码
保存物品初始平移和旋转。
遍历config_.rotations中的每个旋转角度
|设置物品旋转,放到箱子外。
|计算NFP(可能多个)
|为每个NFP建立边缓存
|构造目标函数_objfun(默认或自定义函数)
|对NFP轮廓和每个孔洞
|| 用每个采样点角点做为起点,运行局部优化器。并行方式运行三层循环。
||取最优结果
||做边界检查(凸包是否在箱子内)
||更新best_score/best_overfit
|应用最终平移和旋转
|返回PackResult(成功或带overfit)

物品可以放到nfp的外部,也可以放到空洞中。由于外部轮廓和空洞的方向相反,故局部优化函数相同。

没有配置目标函数
cpp 复制代码
   // Inside check has to be strict if no alignment was enabled
                    std::function<double(const Box&)> ins_check;
                    if(config_.alignment == Config::Alignment::DONT_ALIGN)
                        ins_check = [&binbb, norm](const Box& fullbb) {
                            double ret = 0;
                            if(!sl::isInside(fullbb, binbb))
                                ret += norm;
                            return ret;
                        };
                    else
                        ins_check = [&bin](const Box& fullbb) {
                            double miss = overfit(fullbb, bin);
                            miss = miss > 0? miss : 0;
                            return std::pow(miss, 2);
                        };

                    _objfunc = [norm, binbb, pbb, ins_check](const Item& item)
                    {
                        auto ibb = item.boundingBox();
                        auto fullbb = sl::boundingBox(pbb, ibb);

                        double score = pl::distance(ibb.center(),
                                                    binbb.center());
                        score /= norm;

                        score += ins_check(fullbb);

                        return score;
                    };

DONT_ALIGN配置下,不在箱子内,分数最大。否则,根据超出部分的平方计算分数。

_objfunc,根据中心距离计算得分。

扩展阅读

计算几何为骨,排样优化为魂
作品一:亲士套料工具箱 作品二: 亲士CAD工具箱
经典文章推荐:二维排样
万物皆数学
查阅鄙人的博文,请点击博文下载学院导航
活到老,学到老。明朝中后期,大约50%的进士能当上堂官(副部及更高);能当上堂官的举人只有十余人。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。

测试环境

操作系统:win7 开发环境: VS2019 C++17

或者 操作系统:win10 开发环境: VS2022 C++17

如无特殊说明,本算法用**C++**实现。

相关推荐
hans77488296819 小时前
仅根据BMI反推身高和体重?还真能做到!只需要简单的数论知识~
python·数学
Ro Jace1 天前
数学领域全景框架图
数学
Tisfy1 天前
LeetCode 3723.数位平方和的最大值:构造
数学·算法·leetcode·字符串·题解
Tisfy2 天前
LeetCode 2333.最小差值平方和:排序 + 数学计算(不用二分)
数学·算法·leetcode·题解·贪心·排序
fxss3 天前
宝宝学识屋上新|用喜欢的小动物,玩懂数数、加法和减法
数学
_wyt0015 天前
洛谷 P10112 [GESP202312 八级] 奖品分配 题解
c++·数学·gesp·八级
仿生狮子5 天前
OpenAI 攻下千禧年难题之后,开发者似乎无动于衷
前端·数学·vibecoding
HIT_Weston7 天前
26、【数学】【信息论】KL 弃峰怎么救:α-散度、退火、JS 与 Wasserstein
数学·信息论
闻缺陷则喜何志丹7 天前
【排样】亲士套料工具箱
数学·计算几何·亲士·排样·套料