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 _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 items, Box box, double rotX, double rotY, double zoom, double wallPad = 0, double itemPad = 0) { _items = items ?? new List(); _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(); _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? 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(); } } }