Files
BoxPacker3D/Viewer3D.cs
T
2026-06-05 22:02:32 +02:00

496 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PackedItem>();
}
private List<PackedItem> _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<PackedItem>();
_box = null;
panel.Invalidate();
}
public void Render(List<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> 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<PackedItem> packed)
{
if (packed == null || packed.Count == 0) return new List<(double, double)>();
var yVals = new SortedSet<double>();
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;
}
}
}