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

577 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.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();
}
}
}