using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; namespace BoxPacker3D { public class Viewer3D { private Panel panel; public Viewer3D(Panel panel) { this.panel = panel; panel.Paint += Panel_Paint; _packedItems = new List(); } private List _packedItems; private Box _box; private double _rotX = 30, _rotY = 30, _zoom = 1.0; private double _wallPad = 0, _itemPad = 0; public void Clear() { _packedItems = new List(); _box = null; panel.Invalidate(); } public void Render(List packedItems, Box box, double rotX, double rotY, double zoom = 1.0, double wallPad = 0, double itemPad = 0) { _packedItems = packedItems; _box = box; _rotX = rotX; _rotY = rotY; _zoom = Math.Max(0.1, zoom); _wallPad = wallPad; _itemPad = itemPad; panel.Invalidate(); } private void Panel_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Color.FromArgb(18, 18, 28)); if (_box == null) { DrawWelcome(g); return; } DrawScene(g, panel.Width, panel.Height); } private void DrawWelcome(Graphics g) { int w = panel.Width, h = panel.Height; using (var brush = new SolidBrush(Color.FromArgb(40, 40, 60))) g.FillRectangle(brush, 0, 0, w, h); using (var pen = new Pen(Color.FromArgb(35, 35, 55), 1)) { for (int x = 0; x < w; x += 40) g.DrawLine(pen, x, 0, x, h); for (int y = 0; y < h; y += 40) g.DrawLine(pen, 0, y, w, y); } using (var font = new Font("Segoe UI", 14f)) using (var brush = new SolidBrush(Color.FromArgb(80, 100, 160))) { var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString("Zadajte rozmery škatule a kusy,\npotom kliknite 🚀 SPUSTIŤ BALENIE", font, brush, new RectangleF(0, 0, w, h), sf); } } public Bitmap RenderToBitmap(List packedItems, Box box, double rotX, double rotY, double zoom, int width, int height, double wallPad = 0, double itemPad = 0, bool whiteBg = false) { var bmp = new Bitmap(width, height); using (var g = Graphics.FromImage(bmp)) { g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(whiteBg ? Color.White : Color.FromArgb(18, 18, 28)); DrawSceneEx(g, width, height, packedItems, box, rotX, rotY, zoom, wallPad, itemPad, null, whiteBg); } return bmp; } public Bitmap RenderLayerToBitmap(List packedItems, Box box, double rotX, double rotY, double zoom, double layerYMin, double layerYMax, int layerIndex, int totalLayers, int width, int height, double wallPad = 0, double itemPad = 0, bool whiteBg = false) { var bmp = new Bitmap(width, height); using (var g = Graphics.FromImage(bmp)) { g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(whiteBg ? Color.White : Color.FromArgb(18, 18, 28)); var layerItems = packedItems .Where(p => p.Y < layerYMax - 0.001 && p.Y + p.RH > layerYMin + 0.001) .ToList(); DrawSceneEx(g, width, height, layerItems, box, rotX, rotY, zoom, wallPad, itemPad, $"Vrstva {layerIndex + 1} / {totalLayers} (Y: {layerYMin:F1} – {layerYMax:F1} mm)", whiteBg); // Draw layer boundary indicators (subtle horizontal planes at YMin and YMax) DrawLayerBoundaries(g, width, height, box, layerYMin, layerYMax, rotX, rotY, zoom, whiteBg); } return bmp; } private void DrawLayerBoundaries(Graphics g, int w, int h, Box box, double yMin, double yMax, double rotX, double rotY, double zoom, bool whiteBg) { double radX = rotX * Math.PI / 180.0; double radY = rotY * Math.PI / 180.0; double maxDim = Math.Max(Math.Max(box.W, box.H), box.D); double scale = Math.Min(w, h) * 0.38 / maxDim * zoom; double cx = w / 2.0, cy = h / 2.0; // Draw two horizontal planes (YMin and YMax) as subtle dotted lines var planeColor = whiteBg ? Color.FromArgb(80, 255, 100, 100) : Color.FromArgb(60, 255, 180, 100); using (var pen = new Pen(planeColor, 1f) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }) { // YMin plane DrawHorizontalPlane(g, pen, box, yMin, scale, cx, cy, radX, radY); // YMax plane DrawHorizontalPlane(g, pen, box, yMax, scale, cx, cy, radX, radY); } } private void DrawHorizontalPlane(Graphics g, Pen pen, Box box, double y, double scale, double cx, double cy, double radX, double radY) { double yRel = y - box.H / 2; // relative to box center double hw = box.W / 2, hd = box.D / 2; // Four corners of the plane var p1 = Project(-hw, yRel, -hd, scale, cx, cy, radX, radY); var p2 = Project( hw, yRel, -hd, scale, cx, cy, radX, radY); var p3 = Project( hw, yRel, hd, scale, cx, cy, radX, radY); var p4 = Project(-hw, yRel, hd, scale, cx, cy, radX, radY); // Draw rectangle g.DrawLine(pen, p1, p2); g.DrawLine(pen, p2, p3); g.DrawLine(pen, p3, p4); g.DrawLine(pen, p4, p1); } private void DrawScene(Graphics g, int w, int h) => DrawSceneEx(g, w, h, _packedItems, _box, _rotX, _rotY, _zoom, _wallPad, _itemPad, null, false); private void DrawSceneEx(Graphics g, int w, int h, List items, Box box, double rotX, double rotY, double zoom, double wallPad, double itemPad, string layerLabel, bool whiteBg) { double radX = rotX * Math.PI / 180.0; double radY = rotY * Math.PI / 180.0; double maxDim = Math.Max(Math.Max(box.W, box.H), box.D); double scale = Math.Min(w, h) * 0.38 / maxDim * zoom; double cx = w / 2.0, cy = h / 2.0; var sorted = items .Select(pi => (pi, depth: Transform( pi.X + pi.RW / 2 - box.W / 2, pi.Y + pi.RH / 2 - box.H / 2, pi.Z + pi.RD / 2 - box.D / 2, radX, radY).Z)) .OrderBy(t => t.depth) .ToList(); DrawBoxOutline(g, scale, cx, cy, box, radX, radY, alpha: whiteBg ? 100 : 40, wallPad: wallPad, whiteBg: whiteBg); if (box.WallThickness > 0.001) DrawWallThicknessZone(g, scale, cx, cy, box, radX, radY, whiteBg); if (wallPad > 0.001) DrawPaddingZone(g, scale, cx, cy, box, radX, radY, wallPad, whiteBg); foreach (var (pi, _) in sorted) DrawCuboid(g, pi, scale, cx, cy, box, radX, radY); DrawBoxOutline(g, scale, cx, cy, box, radX, radY, alpha: whiteBg ? 220 : 180, wallPad: wallPad, whiteBg: whiteBg); DrawLegend(g, w, h, items, whiteBg); DrawInfo(g, w, h, box, items.Count, whiteBg); if (layerLabel != null) { using (var font = new Font("Segoe UI", 11f, FontStyle.Bold)) using (var brush = new SolidBrush(whiteBg ? Color.FromArgb(40, 80, 160) : Color.FromArgb(220, 200, 100, 30))) { var sf = new StringFormat { Alignment = StringAlignment.Center }; g.DrawString(layerLabel, font, brush, new RectangleF(0, 8, w, 28), sf); } } } private static (double X, double Y, double Z) Transform(double x, double y, double z, double radX, double radY) { double x1 = x * Math.Cos(radY) + z * Math.Sin(radY); double y1 = y; double z1 = -x * Math.Sin(radY) + z * Math.Cos(radY); double x2 = x1; double y2 = y1 * Math.Cos(radX) - z1 * Math.Sin(radX); double z2 = y1 * Math.Sin(radX) + z1 * Math.Cos(radX); return (x2, y2, z2); } private PointF Project(double x, double y, double z, double scale, double cx, double cy, double radX, double radY) { var (tx, ty, _) = Transform(x, y, z, radX, radY); return new PointF((float)(cx + tx * scale), (float)(cy - ty * scale)); } private void DrawCuboid(Graphics g, PackedItem pi, double scale, double cx, double cy, Box box, double radX, double radY) { double x0 = pi.X - box.W / 2, y0 = pi.Y - box.H / 2, z0 = pi.Z - box.D / 2; double x1 = x0 + pi.RW, y1 = y0 + pi.RH, z1 = z0 + pi.RD; var pts = Corners8(x0, y0, z0, x1, y1, z1, scale, cx, cy, radX, radY); var baseColor = pi.Item.Color; DrawFace(g, new[] { pts[4], pts[5], pts[6], pts[7] }, Shade(baseColor, 1.00f)); DrawFace(g, new[] { pts[5], pts[1], pts[2], pts[6] }, Shade(baseColor, 0.75f)); DrawFace(g, new[] { pts[3], pts[2], pts[6], pts[7] }, Shade(baseColor, 0.55f)); using (var pen = new Pen(Color.FromArgb(200, Color.Black), 0.8f)) { g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] }); g.DrawPolygon(pen, new[] { pts[5], pts[1], pts[2], pts[6] }); g.DrawPolygon(pen, new[] { pts[3], pts[2], pts[6], pts[7] }); } } private void DrawPaddingZone(Graphics g, double scale, double cx, double cy, Box box, double radX, double radY, double pad, bool whiteBg) { double edge = pad + box.WallThickness; double hw = box.W / 2 - edge, hh = box.H / 2 - edge, hd = box.D / 2 - edge; if (hw <= 0 || hh <= 0 || hd <= 0) return; var pts = new PointF[8]; pts[0] = Project(-hw, -hh, -hd, scale, cx, cy, radX, radY); pts[1] = Project(hw, -hh, -hd, scale, cx, cy, radX, radY); pts[2] = Project(hw, hh, -hd, scale, cx, cy, radX, radY); pts[3] = Project(-hw, hh, -hd, scale, cx, cy, radX, radY); pts[4] = Project(-hw, -hh, hd, scale, cx, cy, radX, radY); pts[5] = Project(hw, -hh, hd, scale, cx, cy, radX, radY); pts[6] = Project(hw, hh, hd, scale, cx, cy, radX, radY); pts[7] = Project(-hw, hh, hd, scale, cx, cy, radX, radY); var col = whiteBg ? Color.FromArgb(150, 200, 150, 50) : Color.FromArgb(60, 255, 220, 80); using (var pen = new Pen(col, 1f)) { pen.DashStyle = DashStyle.Dot; g.DrawPolygon(pen, new[] { pts[0], pts[1], pts[2], pts[3] }); g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] }); g.DrawLine(pen, pts[0], pts[4]); g.DrawLine(pen, pts[1], pts[5]); g.DrawLine(pen, pts[2], pts[6]); g.DrawLine(pen, pts[3], pts[7]); } } /// Draws inner wire cube + corner connectors to visualise the thickness of the box walls. private void DrawWallThicknessZone(Graphics g, double scale, double cx, double cy, Box box, double radX, double radY, bool whiteBg) { double t = box.WallThickness; double hwO = box.W / 2, hhO = box.H / 2, hdO = box.D / 2; double hwI = hwO - t, hhI = hhO - t, hdI = hdO - t; if (hwI <= 0 || hhI <= 0 || hdI <= 0) return; // Outer 8 corners (used for connector lines) var outer = new PointF[8]; outer[0] = Project(-hwO, -hhO, -hdO, scale, cx, cy, radX, radY); outer[1] = Project( hwO, -hhO, -hdO, scale, cx, cy, radX, radY); outer[2] = Project( hwO, hhO, -hdO, scale, cx, cy, radX, radY); outer[3] = Project(-hwO, hhO, -hdO, scale, cx, cy, radX, radY); outer[4] = Project(-hwO, -hhO, hdO, scale, cx, cy, radX, radY); outer[5] = Project( hwO, -hhO, hdO, scale, cx, cy, radX, radY); outer[6] = Project( hwO, hhO, hdO, scale, cx, cy, radX, radY); outer[7] = Project(-hwO, hhO, hdO, scale, cx, cy, radX, radY); // Inner 8 corners var inner = new PointF[8]; inner[0] = Project(-hwI, -hhI, -hdI, scale, cx, cy, radX, radY); inner[1] = Project( hwI, -hhI, -hdI, scale, cx, cy, radX, radY); inner[2] = Project( hwI, hhI, -hdI, scale, cx, cy, radX, radY); inner[3] = Project(-hwI, hhI, -hdI, scale, cx, cy, radX, radY); inner[4] = Project(-hwI, -hhI, hdI, scale, cx, cy, radX, radY); inner[5] = Project( hwI, -hhI, hdI, scale, cx, cy, radX, radY); inner[6] = Project( hwI, hhI, hdI, scale, cx, cy, radX, radY); inner[7] = Project(-hwI, hhI, hdI, scale, cx, cy, radX, radY); // Cardboard-brown tones (slightly darker on white backgrounds for print visibility) var innerCol = whiteBg ? Color.FromArgb(220, 120, 80, 40) : Color.FromArgb(200, 180, 140, 90); var connCol = whiteBg ? Color.FromArgb(160, 140, 95, 55) : Color.FromArgb(140, 180, 140, 90); // Inner wire cube (solid line) using (var pen = new Pen(innerCol, 1.3f)) { g.DrawPolygon(pen, new[] { inner[0], inner[1], inner[2], inner[3] }); g.DrawPolygon(pen, new[] { inner[4], inner[5], inner[6], inner[7] }); g.DrawLine(pen, inner[0], inner[4]); g.DrawLine(pen, inner[1], inner[5]); g.DrawLine(pen, inner[2], inner[6]); g.DrawLine(pen, inner[3], inner[7]); } // 8 corner connectors (outer corner → inner corner) using (var pen = new Pen(connCol, 0.9f)) { for (int i = 0; i < 8; i++) g.DrawLine(pen, outer[i], inner[i]); } } private void DrawBoxOutline(Graphics g, double scale, double cx, double cy, Box box, double radX, double radY, int alpha, double wallPad, bool whiteBg) { double hw = box.W / 2, hh = box.H / 2, hd = box.D / 2; var pts = new PointF[8]; pts[0] = Project(-hw, -hh, -hd, scale, cx, cy, radX, radY); pts[1] = Project(hw, -hh, -hd, scale, cx, cy, radX, radY); pts[2] = Project(hw, hh, -hd, scale, cx, cy, radX, radY); pts[3] = Project(-hw, hh, -hd, scale, cx, cy, radX, radY); pts[4] = Project(-hw, -hh, hd, scale, cx, cy, radX, radY); pts[5] = Project(hw, -hh, hd, scale, cx, cy, radX, radY); pts[6] = Project(hw, hh, hd, scale, cx, cy, radX, radY); pts[7] = Project(-hw, hh, hd, scale, cx, cy, radX, radY); var col = whiteBg ? Color.FromArgb(alpha, 40, 80, 180) : Color.FromArgb(alpha, 100, 200, 255); using (var pen = new Pen(col, 1.5f)) { pen.DashStyle = DashStyle.Dash; g.DrawPolygon(pen, new[] { pts[0], pts[1], pts[2], pts[3] }); g.DrawPolygon(pen, new[] { pts[4], pts[5], pts[6], pts[7] }); g.DrawLine(pen, pts[0], pts[4]); g.DrawLine(pen, pts[1], pts[5]); g.DrawLine(pen, pts[2], pts[6]); g.DrawLine(pen, pts[3], pts[7]); } if (alpha >= 100) { using var dimFont = new Font("Segoe UI", 11f, FontStyle.Bold); var colW = whiteBg ? Color.FromArgb(200, 190, 50, 50) : Color.FromArgb(220, 255, 110, 110); var colH = whiteBg ? Color.FromArgb(200, 50, 150, 50) : Color.FromArgb(220, 110, 230, 110); var colD = whiteBg ? Color.FromArgb(200, 50, 50, 190) : Color.FromArgb(220, 110, 130, 255); // midpoint helpers PointF Mid(PointF a, PointF b) => new PointF((a.X + b.X) / 2, (a.Y + b.Y) / 2); void DrawDimLabel(string text, PointF pos, Color dimCol) { var sz = g.MeasureString(text, dimFont); float tx = pos.X - sz.Width / 2; float ty = pos.Y - sz.Height / 2; // dark background rect for readability var bgCol = whiteBg ? Color.FromArgb(180, 255, 255, 255) : Color.FromArgb(180, 20, 20, 30); using var bgBr = new SolidBrush(bgCol); g.FillRectangle(bgBr, tx - 2, ty - 1, sz.Width + 4, sz.Height + 2); using var br = new SolidBrush(dimCol); g.DrawString(text, dimFont, br, tx, ty); } // šírka: midpoint of bottom-front edge, offset down var midW = Mid(pts[0], pts[1]); DrawDimLabel($"šírka {box.W:F0}", new PointF(midW.X, midW.Y + 24), colW); // dĺžka: midpoint of bottom-right edge, offset right+down var midD = Mid(pts[1], pts[5]); DrawDimLabel($"dĺžka {box.D:F0}", new PointF(midD.X + 24, midD.Y + 10), colD); // výška: midpoint of right-front edge, offset right var midH = Mid(pts[1], pts[2]); DrawDimLabel($"výška {box.H:F0}", new PointF(midH.X + 30, midH.Y), colH); } } private PointF[] Corners8(double x0, double y0, double z0, double x1, double y1, double z1, double scale, double cx, double cy, double radX, double radY) { return new[] { Project(x0, y0, z0, scale, cx, cy, radX, radY), Project(x1, y0, z0, scale, cx, cy, radX, radY), Project(x1, y1, z0, scale, cx, cy, radX, radY), Project(x0, y1, z0, scale, cx, cy, radX, radY), Project(x0, y0, z1, scale, cx, cy, radX, radY), Project(x1, y0, z1, scale, cx, cy, radX, radY), Project(x1, y1, z1, scale, cx, cy, radX, radY), Project(x0, y1, z1, scale, cx, cy, radX, radY), }; } private static void DrawFace(Graphics g, PointF[] corners, Color color) { using (var brush = new SolidBrush(Color.FromArgb(210, color))) g.FillPolygon(brush, corners); } private static Color Shade(Color c, float f) => Color.FromArgb( Math.Min(255, (int)(c.R * f)), Math.Min(255, (int)(c.G * f)), Math.Min(255, (int)(c.B * f))); private void DrawLegend(Graphics g, int w, int h, List items, bool whiteBg) { if (items == null || items.Count == 0) return; var unique = items .GroupBy(p => p.Item.Name) .Select(grp => (grp.Key, grp.First().Item.Color, grp.Count())) .ToList(); int lx = 10, ly = 10; var txtCol = whiteBg ? Color.Black : Color.White; using (var titleFont = new Font("Segoe UI", 8.5f, FontStyle.Bold)) using (var itemFont = new Font("Segoe UI", 8f)) { using (var b = new SolidBrush(txtCol)) g.DrawString("Legenda:", titleFont, b, lx, ly); ly += 18; foreach (var (name, color, cnt) in unique) { using (var b = new SolidBrush(color)) g.FillRectangle(b, lx, ly, 12, 12); using (var p = new Pen(Color.Black, 0.5f)) g.DrawRectangle(p, lx, ly, 12, 12); using (var b = new SolidBrush(txtCol)) g.DrawString($"{name} (×{cnt})", itemFont, b, lx + 16, ly); ly += 16; } } } private void DrawInfo(Graphics g, int w, int h, Box box, int count, bool whiteBg) { string info = $"Škatuľa: {box.W} × {box.D} × {box.H} mm | Kusov: {count}"; var txtCol = whiteBg ? Color.FromArgb(60, 60, 60) : Color.FromArgb(120, 120, 140); using (var font = new Font("Segoe UI", 8.5f)) using (var brush = new SolidBrush(txtCol)) { var sf = new StringFormat { Alignment = StringAlignment.Center }; g.DrawString(info, font, brush, new RectangleF(0, h - 22, w, 20), sf); } } public static List<(double YMin, double YMax)> GetLayers(List packed) { if (packed == null || packed.Count == 0) return new List<(double, double)>(); var yVals = new SortedSet(); yVals.Add(0); foreach (var p in packed) { yVals.Add(Math.Round(p.Y, 3)); yVals.Add(Math.Round(p.Y + p.RH, 3)); } var sorted = yVals.ToList(); var layers = new List<(double, double)>(); for (int i = 0; i < sorted.Count - 1; i++) { double yMin = sorted[i], yMax = sorted[i + 1]; bool hasItem = packed.Any(p => p.Y < yMax - 0.001 && p.Y + p.RH > yMin + 0.001); if (hasItem) layers.Add((yMin, yMax)); } var merged = new List<(double, double)>(); if (layers.Count == 0) return merged; double curMin = layers[0].Item1, curMax = layers[0].Item2; for (int i = 1; i < layers.Count; i++) { bool newItemStartsHere = packed.Any(p => Math.Abs(p.Y - layers[i].Item1) < 0.001); if (newItemStartsHere) { merged.Add((curMin, curMax)); curMin = layers[i].Item1; } curMax = layers[i].Item2; } merged.Add((curMin, curMax)); return merged; } } }