Add project files.

This commit is contained in:
roman
2026-06-05 22:02:32 +02:00
parent f09ad1b7c6
commit 274d607d54
25 changed files with 5462 additions and 0 deletions
+495
View File
@@ -0,0 +1,495 @@
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;
}
}
}