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
+576
View File
@@ -0,0 +1,576 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.WinForms;
namespace BoxPacker3D
{
public class GlViewer : IDisposable
{
private GLControl _gl;
private bool _glReady = false;
private List<PackedItem> _items = new();
private Box? _box = null;
private float _rotX = 30f, _rotY = 30f, _zoom = 1f;
private double _wallPad = 0, _itemPad = 0;
private bool _dragging;
private Point _lastMouse;
public GlViewer(Panel container)
{
var settings = new GLControlSettings
{
API = OpenTK.Windowing.Common.ContextAPI.OpenGL,
APIVersion = new Version(2, 1),
Profile = OpenTK.Windowing.Common.ContextProfile.Any,
Flags = OpenTK.Windowing.Common.ContextFlags.Default,
NumberOfSamples = 4
};
_gl = new GLControl(settings)
{
Dock = DockStyle.Fill,
BackColor = Color.FromArgb(18, 18, 28)
};
_gl.Load += OnLoad;
_gl.Paint += OnPaint;
_gl.Resize += OnResize;
_gl.MouseDown += OnMouseDown;
_gl.MouseMove += OnMouseMove;
_gl.MouseUp += OnMouseUp;
_gl.MouseWheel += OnMouseWheel;
container.Controls.Add(_gl);
}
public Control Control => _gl;
public void Render(List<PackedItem> items, Box box,
double rotX, double rotY, double zoom,
double wallPad = 0, double itemPad = 0)
{
_items = items ?? new List<PackedItem>();
_box = box;
_rotX = (float)rotX;
_rotY = (float)rotY;
_zoom = (float)Math.Max(0.1, zoom);
_wallPad = wallPad;
_itemPad = itemPad;
if (_glReady) _gl.Invalidate();
}
public void Clear()
{
_items = new List<PackedItem>();
_box = null;
if (_glReady) _gl.Invalidate();
}
public void SetView(float rotX, float rotY, float zoom)
{
_rotX = rotX; _rotY = rotY; _zoom = zoom;
if (_glReady) _gl.Invalidate();
}
private void OnLoad(object? sender, EventArgs e)
{
_gl.MakeCurrent();
GL.ClearColor(0.071f, 0.071f, 0.110f, 1f);
GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.CullFace);
GL.CullFace(CullFaceMode.Back);
GL.ShadeModel(ShadingModel.Smooth);
GL.Enable(EnableCap.Lighting);
GL.Enable(EnableCap.Light0);
GL.Enable(EnableCap.ColorMaterial);
GL.ColorMaterial(MaterialFace.FrontAndBack, ColorMaterialParameter.AmbientAndDiffuse);
GL.Light(LightName.Light0, LightParameter.Position, new float[] { 1f, 2f, 1.5f, 0f });
GL.Light(LightName.Light0, LightParameter.Diffuse, new float[] { 1f, 1f, 1f, 1f });
GL.Light(LightName.Light0, LightParameter.Ambient, new float[] { 0.25f, 0.25f, 0.25f, 1f });
GL.Light(LightName.Light0, LightParameter.Specular, new float[] { 0.4f, 0.4f, 0.4f, 1f });
GL.Enable(EnableCap.Multisample);
GL.Enable(EnableCap.LineSmooth);
GL.Hint(HintTarget.LineSmoothHint, HintMode.Nicest);
// Set initial viewport immediately so first render is centered
GL.Viewport(0, 0, _gl.Width, _gl.Height);
_glReady = true;
_gl.Invalidate(); // Force first paint
}
private void OnResize(object? sender, EventArgs e)
{
if (!_glReady) return;
_gl.MakeCurrent();
GL.Viewport(0, 0, _gl.Width, _gl.Height);
_gl.Invalidate();
}
private void OnPaint(object? sender, PaintEventArgs e)
{
if (!_glReady) return;
_gl.MakeCurrent();
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
if (_box == null) { DrawWelcome(); _gl.SwapBuffers(); return; }
SetupProjection();
SetupModelView();
DrawBoxOutline(_box, alpha: 0.3f);
if (_box.WallThickness > 0.001) DrawWallThickness(_box);
if (_wallPad > 0.001) DrawPaddingOutline(_box, _wallPad);
DrawItems();
DrawBoxOutline(_box, alpha: 0.9f, frontOnly: true);
DrawAxesLabels();
if (_box != null) DrawDimensionLabelsGL();
_gl.SwapBuffers();
}
private void SetupProjection()
{
GL.MatrixMode(MatrixMode.Projection);
GL.LoadIdentity();
float aspect = _gl.Width > 0 && _gl.Height > 0
? (float)_gl.Width / _gl.Height : 1f;
// Increase far plane to 50000 to handle large boxes at low zoom
// Near plane 1.0 (instead of 0.1) for better depth precision
var proj = Matrix4.CreatePerspectiveFieldOfView(
MathHelper.DegreesToRadians(45f), aspect, 1f, 50000f);
GL.LoadMatrix(ref proj);
}
private void SetupModelView()
{
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadIdentity();
double maxDim = _box == null ? 100
: Math.Max(Math.Max(_box.W, _box.H), _box.D);
float dist = (float)(maxDim * 2.2 / _zoom);
GL.Translate(0f, 0f, -dist);
GL.Rotate(_rotX, 1f, 0f, 0f);
GL.Rotate(_rotY, 0f, 1f, 0f);
if (_box != null)
GL.Translate(-(float)(_box.W / 2), -(float)(_box.H / 2), -(float)(_box.D / 2));
}
private void DrawItems()
{
foreach (var pi in _items)
{
float r = pi.Item.Color.R / 255f;
float g = pi.Item.Color.G / 255f;
float b = pi.Item.Color.B / 255f;
DrawSolidBox(
(float)pi.X, (float)pi.Y, (float)pi.Z,
(float)pi.RW, (float)pi.RH, (float)pi.RD,
r, g, b, 1f);
}
}
private void DrawSolidBox(float x, float y, float z,
float w, float h, float d,
float r, float g, float b, float a)
{
GL.Color4(r, g, b, a);
GL.Begin(PrimitiveType.Quads);
GL.Normal3(0f, 0f, 1f);
GL.Vertex3(x, y, z + d);
GL.Vertex3(x + w, y, z + d);
GL.Vertex3(x + w, y + h, z + d);
GL.Vertex3(x, y + h, z + d);
GL.Normal3(0f, 0f, -1f);
GL.Vertex3(x + w, y, z);
GL.Vertex3(x, y, z);
GL.Vertex3(x, y + h, z);
GL.Vertex3(x + w, y + h, z);
GL.Normal3(-1f, 0f, 0f);
GL.Vertex3(x, y, z);
GL.Vertex3(x, y, z + d);
GL.Vertex3(x, y + h, z + d);
GL.Vertex3(x, y + h, z);
GL.Normal3(1f, 0f, 0f);
GL.Vertex3(x + w, y, z + d);
GL.Vertex3(x + w, y, z);
GL.Vertex3(x + w, y + h, z);
GL.Vertex3(x + w, y + h, z + d);
GL.Normal3(0f, -1f, 0f);
GL.Vertex3(x, y, z);
GL.Vertex3(x + w, y, z);
GL.Vertex3(x + w, y, z + d);
GL.Vertex3(x, y, z + d);
GL.Normal3(0f, 1f, 0f);
GL.Vertex3(x, y + h, z + d);
GL.Vertex3(x + w, y + h, z + d);
GL.Vertex3(x + w, y + h, z);
GL.Vertex3(x, y + h, z);
GL.End();
GL.Disable(EnableCap.Lighting);
GL.LineWidth(1.2f);
GL.Color4(0f, 0f, 0f, 0.55f);
DrawWireBox(x, y, z, w, h, d);
GL.Enable(EnableCap.Lighting);
}
private void DrawWireBox(float x, float y, float z, float w, float h, float d)
{
GL.Begin(PrimitiveType.LineLoop);
GL.Vertex3(x, y, z); GL.Vertex3(x + w, y, z);
GL.Vertex3(x + w, y + h, z); GL.Vertex3(x, y + h, z);
GL.End();
GL.Begin(PrimitiveType.LineLoop);
GL.Vertex3(x, y, z + d); GL.Vertex3(x + w, y, z + d);
GL.Vertex3(x + w, y + h, z + d); GL.Vertex3(x, y + h, z + d);
GL.End();
GL.Begin(PrimitiveType.Lines);
GL.Vertex3(x, y, z); GL.Vertex3(x, y, z + d);
GL.Vertex3(x + w, y, z); GL.Vertex3(x + w, y, z + d);
GL.Vertex3(x + w, y + h, z); GL.Vertex3(x + w, y + h, z + d);
GL.Vertex3(x, y + h, z); GL.Vertex3(x, y + h, z + d);
GL.End();
}
private void DrawBoxOutline(Box box, float alpha, bool frontOnly = false)
{
GL.Disable(EnableCap.Lighting);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.LineWidth(frontOnly ? 2f : 1f);
GL.LineStipple(1, 0x0F0F);
GL.Enable(EnableCap.LineStipple);
GL.Color4(0.39f, 0.78f, 1f, alpha);
DrawWireBox(0, 0, 0, (float)box.W, (float)box.H, (float)box.D);
GL.Disable(EnableCap.LineStipple);
GL.Disable(EnableCap.Blend);
GL.Enable(EnableCap.Lighting);
}
/// Draws the inner cardboard wall surface and corner connectors between outer/inner cubes.
/// Visually represents the thickness of the box material.
private void DrawWallThickness(Box box)
{
float t = (float)box.WallThickness;
float w = (float)box.W, h = (float)box.H, d = (float)box.D;
float iw = w - 2 * t, ih = h - 2 * t, id = d - 2 * t;
if (iw <= 0 || ih <= 0 || id <= 0) return;
GL.Disable(EnableCap.Lighting);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
// 1) Inner wire cube (solid line, brownish cardboard tone)
GL.LineWidth(1.5f);
GL.Color4(0.72f, 0.55f, 0.35f, 0.85f);
DrawWireBox(t, t, t, iw, ih, id);
// 2) Corner connectors 8 lines from outer corner to inner corner
// (visualises the wall as a prism at each corner)
GL.LineWidth(1.0f);
GL.Color4(0.72f, 0.55f, 0.35f, 0.55f);
GL.Begin(PrimitiveType.Lines);
void Link(float ox, float oy, float oz, float ix, float iy, float iz)
{ GL.Vertex3(ox, oy, oz); GL.Vertex3(ix, iy, iz); }
Link(0, 0, 0, t, t, t);
Link(w, 0, 0, w - t, t, t);
Link(w, h, 0, w - t, h - t, t);
Link(0, h, 0, t, h - t, t);
Link(0, 0, d, t, t, d - t);
Link(w, 0, d, w - t, t, d - t);
Link(w, h, d, w - t, h - t, d - t);
Link(0, h, d, t, h - t, d - t);
GL.End();
// 3) Subtle translucent shaded floor shows where inner base begins
GL.Color4(0.72f, 0.55f, 0.35f, 0.08f);
GL.Begin(PrimitiveType.Quads);
GL.Vertex3(t, t, t);
GL.Vertex3(w - t, t, t);
GL.Vertex3(w - t, t, d - t);
GL.Vertex3(t, t, d - t);
GL.End();
GL.Disable(EnableCap.Blend);
GL.Enable(EnableCap.Lighting);
}
private void DrawPaddingOutline(Box box, double pad)
{
float edge = (float)(pad + box.WallThickness);
float w = (float)box.W - 2 * edge;
float h = (float)box.H - 2 * edge;
float d = (float)box.D - 2 * edge;
if (w <= 0 || h <= 0 || d <= 0) return;
GL.Disable(EnableCap.Lighting);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.LineWidth(1.2f);
GL.LineStipple(2, 0xAAAA);
GL.Enable(EnableCap.LineStipple);
GL.Color4(1f, 0.86f, 0.31f, 0.5f);
DrawWireBox(edge, edge, edge, w, h, d);
GL.Disable(EnableCap.LineStipple);
GL.Disable(EnableCap.Blend);
GL.Enable(EnableCap.Lighting);
}
private void DrawAxesLabels()
{
GL.Disable(EnableCap.Lighting);
GL.LineWidth(2f);
float aw = _box == null ? 10f : (float)(_box.W * 0.12f);
GL.Begin(PrimitiveType.Lines);
GL.Color3(1f, 0.3f, 0.3f); GL.Vertex3(0, 0, 0); GL.Vertex3(aw, 0, 0);
GL.Color3(0.3f, 1f, 0.3f); GL.Vertex3(0, 0, 0); GL.Vertex3(0, aw, 0);
GL.Color3(0.3f, 0.5f, 1f); GL.Vertex3(0, 0, 0); GL.Vertex3(0, 0, aw);
GL.End();
GL.Enable(EnableCap.Lighting);
}
private void DrawDimensionLabelsGL()
{
if (_box == null) return;
int sw = _gl.Width, sh = _gl.Height;
var labels = new (float wx, float wy, float wz, string text, float r, float g, float b, float ox, float oy)[]
{
((float)(_box.W / 2), 0f, 0f, $"šírka {_box.W:F0}", 1f, 0.43f, 0.43f, 0f, 26f),
((float)_box.W, 0f, (float)(_box.D/2), $"dĺžka {_box.D:F0}", 0.43f,0.51f, 1f, 26f, 8f),
((float)_box.W, (float)(_box.H/2), 0f, $"výška {_box.H:F0}", 0.43f,0.9f, 0.43f, 30f, 0f),
};
foreach (var lbl in labels)
{
var pt = Project3DToScreen(lbl.wx, lbl.wy, lbl.wz);
if (pt == null) continue;
float sx = pt.Value.X + lbl.ox;
float sy = pt.Value.Y + lbl.oy;
DrawTextGL(lbl.text, sx, sy, sw, sh, lbl.r, lbl.g, lbl.b);
}
}
private void DrawTextGL(string text, float sx, float sy, int sw, int sh, float r, float g, float b)
{
using var bmp = new Bitmap(1, 1);
using var measure = Graphics.FromImage(bmp);
using var font = new Font("Segoe UI", 11f, FontStyle.Bold);
var sz = measure.MeasureString(text, font);
int tw = (int)sz.Width + 6, th = (int)sz.Height + 4;
if (tw <= 0 || th <= 0) return;
using var textBmp = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
using (var tg = Graphics.FromImage(textBmp))
{
tg.Clear(Color.FromArgb(0, 0, 0, 0));
using var bgBr = new SolidBrush(Color.FromArgb(180, 15, 15, 25));
tg.FillRectangle(bgBr, 0, 0, tw, th);
using var br = new SolidBrush(Color.FromArgb(255, (int)(r*255), (int)(g*255), (int)(b*255)));
tg.DrawString(text, font, br, 3, 2);
}
// flip vertically (OpenGL vs GDI+ Y axis)
textBmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
int tex = GL.GenTexture();
GL.BindTexture(TextureTarget.Texture2D, tex);
var data = textBmp.LockBits(
new Rectangle(0, 0, tw, th),
System.Drawing.Imaging.ImageLockMode.ReadOnly,
System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
tw, th, 0, OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, data.Scan0);
textBmp.UnlockBits(data);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
// switch to 2D ortho
GL.Disable(EnableCap.DepthTest);
GL.Disable(EnableCap.Lighting);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Enable(EnableCap.Texture2D);
GL.MatrixMode(MatrixMode.Projection);
GL.PushMatrix();
GL.LoadIdentity();
GL.Ortho(0, sw, 0, sh, -1, 1);
GL.MatrixMode(MatrixMode.Modelview);
GL.PushMatrix();
GL.LoadIdentity();
// screen Y is flipped: sy is from top, GL ortho is from bottom
float glY = sh - sy - th;
GL.Color4(1f, 1f, 1f, 1f);
GL.Begin(PrimitiveType.Quads);
GL.TexCoord2(0f, 0f); GL.Vertex2(sx, glY);
GL.TexCoord2(1f, 0f); GL.Vertex2(sx + tw, glY);
GL.TexCoord2(1f, 1f); GL.Vertex2(sx + tw, glY + th);
GL.TexCoord2(0f, 1f); GL.Vertex2(sx, glY + th);
GL.End();
GL.PopMatrix();
GL.MatrixMode(MatrixMode.Projection);
GL.PopMatrix();
GL.MatrixMode(MatrixMode.Modelview);
GL.Disable(EnableCap.Texture2D);
GL.Disable(EnableCap.Blend);
GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.Lighting);
GL.DeleteTexture(tex);
}
private PointF? Project3DToScreen(float wx, float wy, float wz)
{
if (_box == null) return null;
int sw = _gl.Width, sh = _gl.Height;
if (sw <= 0 || sh <= 0) return null;
double maxDim = Math.Max(Math.Max(_box.W, _box.H), _box.D);
float dist = (float)(maxDim * 2.2 / _zoom);
float aspect = (float)sw / sh;
float fovY = (float)(Math.PI / 4); // 45°
// translate to box center
float tx = wx - (float)(_box.W / 2);
float ty = wy - (float)(_box.H / 2);
float tz = wz - (float)(_box.D / 2);
// rotate Y
float radY = _rotY * (float)(Math.PI / 180);
float cosY = (float)Math.Cos(radY), sinY = (float)Math.Sin(radY);
float rx = tx * cosY + tz * sinY;
float ry = ty;
float rz = -tx * sinY + tz * cosY;
// rotate X
float radX = _rotX * (float)(Math.PI / 180);
float cosX = (float)Math.Cos(radX), sinX = (float)Math.Sin(radX);
float fx = rx;
float fy = ry * cosX - rz * sinX;
float fz = ry * sinX + rz * cosX - dist;
if (fz >= -0.1f) return null;
float f = 1f / (float)Math.Tan(fovY / 2);
float ndcX = f / aspect * fx / (-fz);
float ndcY = f * fy / (-fz);
return new PointF(
(ndcX + 1f) / 2f * sw,
(1f - ndcY) / 2f * sh);
}
private void DrawDimensionLabels(Graphics g)
{
if (_box == null) return;
using var font = new Font("Segoe UI", 11f, FontStyle.Bold);
var colW = Color.FromArgb(255, 255, 110, 110);
var colD = Color.FromArgb(255, 110, 140, 255);
var colH = Color.FromArgb(255, 110, 230, 110);
void Label(PointF? pt, string text, Color col, float ox, float oy)
{
if (pt == null) return;
var sz = g.MeasureString(text, font);
float tx = pt.Value.X + ox - sz.Width / 2;
float ty = pt.Value.Y + oy - sz.Height / 2;
using var bgBr = new SolidBrush(Color.FromArgb(170, 15, 15, 25));
g.FillRectangle(bgBr, tx - 3, ty - 1, sz.Width + 6, sz.Height + 2);
using var br = new SolidBrush(col);
g.DrawString(text, font, br, tx, ty);
}
// midpoints of three outer edges
Label(Project3DToScreen((float)(_box.W / 2), 0f, 0f),
$"šírka {_box.W:F0}", colW, 0, 22);
Label(Project3DToScreen((float)_box.W, 0f, (float)(_box.D / 2)),
$"dĺžka {_box.D:F0}", colD, 22, 8);
Label(Project3DToScreen((float)_box.W, (float)(_box.H / 2), 0f),
$"výška {_box.H:F0}", colH, 28, 0);
}
private void DrawWelcome()
{
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
}
private void OnMouseDown(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{ _dragging = true; _lastMouse = e.Location; }
}
private void OnMouseMove(object? sender, MouseEventArgs e)
{
if (!_dragging) return;
float dx = e.X - _lastMouse.X;
float dy = e.Y - _lastMouse.Y;
_rotY += dx * 0.5f;
_rotX += dy * 0.5f;
_lastMouse = e.Location;
_gl.Invalidate();
ViewChanged?.Invoke(this, new ViewArgs(_rotX, _rotY, _zoom));
}
private void OnMouseUp(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left) _dragging = false;
}
private void OnMouseWheel(object? sender, MouseEventArgs e)
{
_zoom *= e.Delta > 0 ? 1.1f : 0.9f;
_zoom = Math.Clamp(_zoom, 0.1f, 10f);
_gl.Invalidate();
ViewChanged?.Invoke(this, new ViewArgs(_rotX, _rotY, _zoom));
}
public event EventHandler<ViewArgs>? ViewChanged;
public class ViewArgs : EventArgs
{
public float RotX, RotY, Zoom;
public ViewArgs(float rx, float ry, float z) { RotX = rx; RotY = ry; Zoom = z; }
}
public void Dispose()
{
_gl.Dispose();
}
}
}