using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using OfficeOpenXml.FormulaParsing.Excel.Functions.Math; using WIDESEAWCS_Core.Helper; using WIDESEAWCS_DTO.PlacedBlockDTO; using WIDESEAWCS_DTO.TaskInfo; using WIDESEAWCS_IBasicInfoService; namespace WIDESEAWCS_BasicInfoService { /// /// 货物放置服务,提供集装箱内货物摆放位置计算功能 /// /// /// 主要功能包括:
/// 1. 根据货物尺寸自动计算最优摆放位置
/// 2. 支持横向/纵向两种摆放方式
/// 3. 考虑吸盘尺寸、容器边界等物理限制
/// 4. 提供任务位置坐标转换功能
///
/// 核心参数:
/// - SPACING: 货物间最小间距
/// - MaxRotateLength: 最大旋转长度限制
/// - MaxY/MinY: Y轴坐标限制
/// - SuctionLength/Width: 吸盘尺寸参数 ///
public class PlaceBlockService { /// /// 获取配置文件中"Spacing"键对应的整数值,表示间距值 /// public static int SPACING = AppSettings.GetValue("Spacing").ObjToInt(); /// /// 获取或设置最大旋转长度配置值 /// public static int MaxRotateLength = AppSettings.GetValue("MaxRotateLength").ObjToInt(); /// /// 最大Y坐标限制(毫米) /// public const int MaxY = 600; /// /// 最小横向Y坐标限制(毫米) /// public static int MinY = AppSettings.GetValue("MinY").ObjToInt(); /// /// 旋转抓取Y轴偏移量(毫米) /// public static int RotateYOffset = AppSettings.GetValue("RotateYOffset").ObjToInt(); /// /// 吸盘横向长度 /// public const int SuctionLengthH = 920; /// /// 吸盘横向宽度 /// public const int SuctionWidthH = 530; /// /// 吸盘纵向长度 /// public const int SuctionLengthZ = 530; /// /// 吸盘纵向宽度 /// public const int SuctionWidthZ = 130; /// /// 容器尺寸 /// public ContainerSize ContainerSize { get; private set; } /// /// 已放置货物 /// public List PlacedBlocks { get; private set; } /// /// 容器地板的放置块实例 /// private readonly PlacedBlock containerFloor; /// /// 初始化放置区块服务 /// /// 容器尺寸 /// 已放置区块列表,可选参数 /// /// 构造函数会初始化容器尺寸和已放置区块列表。 /// 如果未提供placedBlocks或列表为空,将创建新的空列表。 /// 同时会创建表示容器底部的PlacedBlock对象。 /// public PlaceBlockService(ContainerSize containerSize, List? placedBlocks = null) { containerSize.Length = containerSize.Length; containerSize.Width = containerSize.Width; ContainerSize = containerSize; if (placedBlocks == null || placedBlocks.Count == 0) { PlacedBlocks = new List(); } else { PlacedBlocks = placedBlocks; } containerFloor = new PlacedBlock(new Point3D(0, 0, 0), ContainerSize.Length, ContainerSize.Width, 0); //containerFloor = new PlacedBlock(new Point3D(SPACING, SPACING, 0), ContainerSize.Length - 2 * SPACING, ContainerSize.Width - 2 * SPACING, 0); } /// /// 根据给定的长宽高寻找合适的放置位置 /// /// 块的长度 /// 块的宽度 /// 块的高度 /// 可放置的3D坐标点,若无法放置则返回null /// /// 方法会自动处理长宽参数,确保length >= width /// 在放置前会先检查块的有效性 /// public Point3D? PlaceBlock(int length, int width, int height) { int tempLength = length; int tempWidth = width; if (length < width) { length = tempWidth; width = tempLength; } if (!IsValidBlock(length, width, height)) return null; return FindStackablePosition(length, width, height); } /// /// 根据给定的长、宽、高和边缘值放置一个块,并返回可放置的位置 /// /// 块的长度 /// 块的宽度 /// 块的高度 /// 边缘值 /// 可放置的位置信息,若块无效则返回null public TaskPosition? PlaceBlock(int length, int width, int height, int edge) { int tempLength = length; int tempWidth = width; if (length < width) { length = tempWidth; width = tempLength; } if (!IsValidBlock(length, width, height)) return null; return FindStackablePosition(length, width, height, edge); } /// /// 检查给定的长宽高是否构成有效的块体尺寸 /// /// 长度 /// 宽度 /// 高度 /// 如果尺寸有效返回true,否则返回false /// /// 有效块体需满足:长宽高都大于0,且长宽不超过容器尺寸减去两倍间距,高度不超过容器高度 /// private bool IsValidBlock(int l, int w, int h) { return l > 0 && w > 0 && h > 0 && l <= ContainerSize.Length && w <= ContainerSize.Width && h < ContainerSize.Height; } //private bool IsValidBlock(int l, int w, int h) //{ // return l > 0 && w > 0 && h > 0 && // l <= ContainerSize.Length - 2 * SPACING && // w <= ContainerSize.Width - 2 * SPACING && // h < ContainerSize.Height; //} /// /// 在容器中查找可放置指定尺寸块体的有效位置 /// /// 块体长度 /// 块体宽度 /// 块体高度 /// 可放置位置的3D坐标点,若无合适位置则返回null /// /// 1. 通过分析已放置块体生成候选支撑层 /// 2. 考虑容器底部和支撑块的不同间隔规则 /// 3. 优先检查角落位置以提高搜索效率 /// private Point3D? FindStackablePosition(int l, int w, int h) { // 生成候选支撑层(包含容器底部) var candidateLayers = PlacedBlocks .Select(b => b.Position.Z + b.Height) .Append(0) .Distinct() .OrderBy(z => z) .ToList(); foreach (var baseZ in candidateLayers) { if (baseZ + h > ContainerSize.Height) continue; // 获取当前层的支撑块(包含虚拟容器底部) var supports = GetSupportBlocks(baseZ); foreach (var support in supports) { // 计算有效叠放区域(修正间隔逻辑) int xStart = support.Position.X; int yStart = support.Position.Y; // 容器底部支撑必须内缩间隔(即使尺寸相同) if (support == containerFloor) { xStart = support.Position.X; yStart = support.Position.Y; } int xEnd = support.Position.X + support.Length - l; int yEnd = support.Position.Y + support.Width - w; // 普通支撑块仅在尺寸不同时加间隔 if (support != containerFloor && (l != support.Length && w != support.Width)) { xStart += SPACING; yStart += SPACING; xEnd -= SPACING; yEnd -= SPACING; } // 最终容器边界约束 xEnd = Math.Min(xEnd, ContainerSize.Length - l - SPACING); yEnd = Math.Min(yEnd, ContainerSize.Width - w - SPACING); if (xStart > xEnd || yStart > yEnd) continue; // 优化搜索:优先角落位置 for (int x = xStart; x <= xEnd; x += 10) { for (int y = yStart; y <= yEnd; y += 10) { var candidate = new Point3D(x, y, baseZ); if (IsPositionValid(candidate, l, w, h)) { var placed = new PlacedBlock(candidate, l, w, h); //PlacedBlocks.Add(placed); return candidate; } } } } } return null; } /// /// 在容器中查找可堆叠位置 /// /// 物品长度 /// 物品宽度 /// 物品高度 /// 边缘类型 /// 返回找到的有效任务位置,若找不到则返回null /// /// 1. 生成候选支撑层(包含容器底部) /// 2. 遍历每层支撑块计算有效叠放区域 /// 3. 考虑容器边界约束和间隔规则 /// 4. 优先检查角落位置以提高搜索效率 /// private TaskPosition? FindStackablePosition(int l, int w, int h, int edge) { // 生成候选支撑层(包含容器底部) var candidateLayers = PlacedBlocks .Select(b => b.Position.Z + b.Height) .Append(0) .Distinct() .OrderBy(z => z) .ToList(); foreach (var baseZ in candidateLayers) { if (baseZ + h > ContainerSize.Height) continue; // 获取当前层的支撑块(包含虚拟容器底部) var supports = GetSupportBlocks(baseZ); foreach (var support in supports) { // 计算有效叠放区域(修正间隔逻辑) int xStart = support.Position.X; int yStart = support.Position.Y; int xEnd = support.Position.X + support.Length - l; int yEnd = support.Position.Y + support.Width - w; // 最终容器边界约束 xEnd = Math.Min(xEnd, ContainerSize.Length - l); yEnd = Math.Min(yEnd, ContainerSize.Width - w); if (xStart > xEnd || yStart > yEnd) continue; // 优化搜索:优先角落位置 for (int x = xStart; x <= xEnd; x += 10) { for (int y = yStart; y <= yEnd; y += 10) { var candidate = new Point3D(x, y, baseZ); if (IsPositionValid(candidate, l, w, h)) { TaskPosition taskPosition = GetTaskPosition(candidate, l, w, h, edge); if (IsPositionValid(taskPosition)) { return taskPosition; } } } } } } return null; } /// /// 获取指定高度层的支撑块 /// /// 需要支撑的高度层 /// 按面积从大到小排序的支撑块集合,当baseZ=0且无支撑块时返回容器底部 private IEnumerable GetSupportBlocks(int baseZ) { var blocks = PlacedBlocks .Where(b => b.Position.Z + b.Height == baseZ) .OrderBy(b => b.Position.X) .ThenBy(b => b.Position.Y) /*.OrderByDescending(b => b.Length * b.Width)*/ .ToList(); // 当baseZ=0时添加容器底部支撑 if (baseZ == 0 && blocks.Count == 0) { return new List { containerFloor }; } return blocks; } /// /// 检查指定位置是否有效,即该位置是否可以放置指定尺寸的块体 /// /// 要检查的位置坐标 /// 块体长度 /// 块体宽度 /// 块体高度 /// 如果位置有效且不与其他已放置块体重叠则返回true,否则返回false /// /// 检查条件包括: /// 1. 是否超出容器尺寸限制 /// 2. X坐标是否超过1600限制 /// 3. 当X坐标超过MaxY且长度超过MaxRotateLength时的限制 /// 4. 是否与已放置的块体发生重叠(考虑SPACING间距) /// private bool IsPositionValid(Point3D pos, int l, int w, int h) { if (pos.X + l > ContainerSize.Length || pos.Y + w > ContainerSize.Width) return false; if (pos.X > 1600) return false; if (pos.X > MaxY && l > MaxRotateLength) return false; var newBlock = new PlacedBlock(pos, l, w, h); return !PlacedBlocks.Any(existing => { bool xOverlap = newBlock.Position.X < existing.Position.X + existing.Length + SPACING && newBlock.Position.X + newBlock.Length + SPACING > existing.Position.X; bool yOverlap = newBlock.Position.Y < existing.Position.Y + existing.Width + SPACING && newBlock.Position.Y + newBlock.Width + SPACING > existing.Position.Y; bool zOverlap = newBlock.Position.Z < existing.Position.Z + existing.Height && newBlock.Position.Z + h > existing.Position.Z; return xOverlap && yOverlap && zOverlap; }); } //private bool IsPositionValid(Point3D pos, int l, int w, int h) //{ // // 边界检查(含容器边缘间隔) // if (pos.X < SPACING || // pos.Y < SPACING || // pos.X + l > ContainerSize.Length - SPACING || // pos.Y + w > ContainerSize.Width - SPACING) // return false; // if (pos.X > 1600) // return false; // if (pos.X > MaxY && l > MaxRotateLength) return false; // // 三维碰撞检测 // var newBlock = new PlacedBlock(pos, l, w, h); // return !PlacedBlocks.Any(existing => // { // bool xOverlap = newBlock.Position.X < existing.Position.X + existing.Length + SPACING && // newBlock.Position.X + newBlock.Length + SPACING > existing.Position.X; // bool yOverlap = newBlock.Position.Y < existing.Position.Y + existing.Width + SPACING && // newBlock.Position.Y + newBlock.Width + SPACING > existing.Position.Y; // bool zOverlap = newBlock.Position.Z < existing.Position.Z + existing.Height && // newBlock.Position.Z + h > existing.Position.Z; // return xOverlap && yOverlap && zOverlap; // }); //} /// /// 检查任务位置是否有效 /// /// 要检查的任务位置 /// 如果Y轴坐标在有效范围内返回true,否则返回false private bool IsPositionValid(TaskPosition pos) { return pos.PutPositionY <= MaxY && pos.PutPositionY >= 0 && pos.TakePositionY >= 0; } /// /// 根据给定的三维坐标和尺寸参数计算任务位置信息 /// /// 起始三维坐标点 /// 物体长度 /// 物体宽度 /// 物体高度 /// 边缘标识(0/1) /// 包含取放货位置信息的TaskPosition对象 /// /// 该方法根据物体尺寸自动选择单吸盘或双吸盘模式, /// 并计算吸盘中心点位置,同时处理边界条件限制。 /// 横向放置时最小Y坐标为155,纵向时为350,最大Y坐标为700。 /// public TaskPosition GetTaskPosition(Point3D point3D, int length, int width, int height, int edge) { //放货位置板材中心点 Point3D putCenter = new Point3D(point3D.X + length / 2, point3D.Y + width / 2, point3D.Z + height / 2); //取货位置板材中心点 Point3D takeCenter = new Point3D(length / 2, width / 2, height / 2); //吸盘长530 间隔660 最大920 吸盘宽130 int positionR = 1; int takePositionX = 0; int takePositionY = 0; int takePositionZ = 0; int putPositionX = 0; int putPositionY = 0; int putPositionZ = 0; //1.如果长度大于920,宽度大于等于300,则可以使用双吸盘横向吸取 if (length > 920) //横向双吸 { //吸盘尺寸 Point3D deviceCenter = new Point3D(530 / 2, 920 / 2, 0); positionR = 1; takePositionX = (takeCenter.Y - deviceCenter.X); takePositionY = (takeCenter.X - deviceCenter.Y); takePositionZ = 10; putPositionX = (putCenter.Y - deviceCenter.X); putPositionY = (putCenter.X - deviceCenter.Y); putPositionZ = point3D.Z; } else//横向单吸 { //吸盘尺寸 Point3D deviceCenter = new Point3D(530 / 2, 130 / 2, 0); positionR = 1; takePositionX = (takeCenter.Y - deviceCenter.X); takePositionY = (takeCenter.X - deviceCenter.Y); takePositionZ = 10; putPositionX = (putCenter.Y - deviceCenter.X); putPositionY = (putCenter.X - deviceCenter.Y); putPositionZ = point3D.Z; } if (takePositionY <= MinY) { takePositionY = 0; putPositionY = point3D.X + MinY; } else { takePositionY -= MinY; } if (putPositionY > MaxY) { int moreY = putPositionY - MaxY; if (takePositionY - moreY > 0) { takePositionY -= moreY; putPositionY = MaxY; } else if (Math.Abs(takePositionY - moreY) < SPACING) { takePositionY = 0; putPositionY = Math.Abs(takePositionY - moreY) + MinY; } else { int count = PlacedBlocks.Where(x => x.Position.Y == 10).Count(); putPositionY = point3D.X - 920 + 130 + RotateYOffset; takePositionY = length - MinY - 130 - RotateYOffset; if (putPositionY < 0 && takePositionY + putPositionY >= 0) { takePositionY += putPositionY; putPositionY = 0; } positionR = 2; } } if (positionR == 2 && edge == 0) { takePositionX = width - 530; putPositionX = point3D.Y; } else if (positionR == 2 && edge == 1) { takePositionX = 0; putPositionX = point3D.Y + width - 530; } else if (positionR == 1 && edge == 1) { if(width > ContainerSize.Width) { takePositionX = 0; putPositionX = point3D.Y; } else { takePositionX = width - 530; putPositionX = point3D.Y + (width - 530); } } else if (positionR == 1 && edge == 0) { if (putPositionX < 0) { takePositionX = 0; putPositionX = point3D.Y; } } if (takePositionY < 0 && Math.Abs(takePositionY) < SPACING) { takePositionY = 0; } TaskPosition taskPosition = new TaskPosition() { PositionR = positionR, TakePositionX = takePositionX, TakePositionY = takePositionY, TakePositionZ = takePositionZ, PutPositionX = putPositionX, PutPositionY = putPositionY, PutPositionZ = putPositionZ, TakeCenterPositionX = takeCenter.X, TakeCenterPositionY = takeCenter.Y, TakeCenterPositionZ = takeCenter.Z, PutCenterPositionX = putCenter.X, PutCenterPositionY = putCenter.Y, PutCenterPositionZ = putCenter.Z, PositionX = point3D.X, PositionY = point3D.Y, PositionZ = point3D.Z }; return taskPosition; } } }