本文涉及知识点
翻译及基础
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++**实现。
