refactor(gui): replace JPanel with AWT Canvas for rendering master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 12 Mar 2026 20:16:28 +0000 (22:16 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 12 Mar 2026 20:16:28 +0000 (22:16 +0200)
Convert ViewPanel from Swing JPanel to AWT Canvas to use BufferStrategy
for page-flipping rendering. This provides better control over the
rendering pipeline and avoids Swing's double-buffering overhead.

Also fixes a typo in Line.java (greenWithAplha -> greenWithAlpha) and
makes Color.a field final.

src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java

index 25a73f5..cca1c1b 100755 (executable)
@@ -8,27 +8,29 @@ import eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager;
 import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack;
 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
 
-import javax.swing.*;
 import java.awt.*;
+import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
-import java.awt.event.ComponentListener;
+import java.awt.image.BufferStrategy;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * Java Swing panel that provides a 3D rendering canvas with built-in camera navigation.
+ * AWT Canvas that provides a 3D rendering surface with built-in camera navigation.
  *
  * <p>{@code ViewPanel} is the primary entry point for embedding the Sixth 3D engine into
- * a Java Swing application. It manages the render loop, maintains a scene graph
+ * a Java application. It manages the render loop, maintains a scene graph
  * ({@link ShapeCollection}), and handles user input for camera navigation.</p>
  *
+ * <p>Uses {@link BufferStrategy} for efficient page-flipping and tear-free rendering.</p>
+ *
  * <p><b>Quick start - creating a 3D view in a window:</b></p>
  * <pre>{@code
  * // Option 1: Use ViewFrame (creates a maximized JFrame for you)
  * ViewFrame frame = new ViewFrame();
  * ViewPanel viewPanel = frame.getViewPanel();
  *
- * // Option 2: Embed ViewPanel in your own Swing layout
+ * // Option 2: Embed ViewPanel in your own window
  * JFrame frame = new JFrame("My 3D App");
  * ViewPanel viewPanel = new ViewPanel();
  * frame.add(viewPanel);
@@ -66,8 +68,10 @@ import java.util.concurrent.ConcurrentHashMap;
  * @see Camera the camera/viewer
  * @see FrameListener for per-frame callbacks
  */
-public class ViewPanel extends JPanel implements ComponentListener {
+public class ViewPanel extends Canvas {
     private static final long serialVersionUID = 1683277888885045387L;
+    private static final int NUM_BUFFERS = 2;
+
     private final InputManager inputManager = new InputManager(this);
     private final KeyboardFocusStack keyboardFocusStack;
     private final Camera camera = new Camera();
@@ -107,25 +111,33 @@ public class ViewPanel extends JPanel implements ComponentListener {
 
     private long nextFrameTime;
 
+    private BufferStrategy bufferStrategy;
+
+    private boolean bufferStrategyInitialized = false;
+
     public ViewPanel() {
         frameListeners.add(camera);
         frameListeners.add(inputManager);
 
         keyboardFocusStack = new KeyboardFocusStack(this);
 
-        initializePanelLayout();
+        initializeCanvas();
 
         startRenderThread();
 
-        addComponentListener(this);
+        addComponentListener(new ComponentAdapter() {
+            @Override
+            public void componentResized(final ComponentEvent e) {
+                viewRepaintNeeded = true;
+            }
+
+            @Override
+            public void componentShown(final ComponentEvent e) {
+                viewRepaintNeeded = true;
+            }
+        });
     }
 
-    /**
-     * Returns the camera that represents the viewer's position and
-     * orientation in the 3D world. Use this to programmatically move the camera.
-     *
-     * @return the camera for this view
-     */
     public Camera getCamera() {
         return camera;
     }
@@ -181,28 +193,8 @@ public class ViewPanel extends JPanel implements ComponentListener {
     }
 
     @Override
-    public void componentHidden(final ComponentEvent e) {
-
-    }
-
-    @Override
-    public void componentMoved(final ComponentEvent e) {
-
-    }
-
-    @Override
-    public void componentResized(final ComponentEvent e) {
-        viewRepaintNeeded = true;
-    }
-
-    @Override
-    public void componentShown(final ComponentEvent e) {
-        viewRepaintNeeded = true;
-    }
-
-    @Override
-    public Dimension getMaximumSize() {
-        return getPreferredSize();
+    public Dimension getPreferredSize() {
+        return new Dimension(640, 480);
     }
 
     @Override
@@ -211,32 +203,57 @@ public class ViewPanel extends JPanel implements ComponentListener {
     }
 
     @Override
-    public java.awt.Dimension getPreferredSize() {
-        return new java.awt.Dimension(640, 480);
+    public Dimension getMaximumSize() {
+        return getPreferredSize();
     }
 
     public RenderingContext getRenderingContext() {
         return renderingContext;
     }
 
-    private void initializePanelLayout() {
-        setFocusCycleRoot(true);
-        setOpaque(true);
+    private void initializeCanvas() {
         setFocusable(true);
-        setDoubleBuffered(false);
         setVisible(true);
-        requestFocusInWindow();
+        requestFocus();
+    }
+
+    private void ensureBufferStrategy() {
+        if (bufferStrategyInitialized && bufferStrategy != null)
+            return;
+
+        if (!isDisplayable())
+            return;
+
+        try {
+            createBufferStrategy(NUM_BUFFERS);
+            bufferStrategy = getBufferStrategy();
+            bufferStrategyInitialized = true;
+        } catch (final Exception e) {
+            bufferStrategy = null;
+            bufferStrategyInitialized = false;
+        }
     }
 
     private void renderFrame() {
-        // paint root geometry collection to the offscreen render buffer
+        ensureBufferStrategy();
+
+        if (bufferStrategy == null)
+            return;
+
         clearCanvas();
         rootShapeCollection.paint(this, renderingContext);
 
-        // draw rendered offscreen buffer to visible screen
-        final Graphics graphics = getGraphics();
-        if (graphics != null)
-            graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
+        Graphics2D g = null;
+        try {
+            g = (Graphics2D) bufferStrategy.getDrawGraphics();
+            g.drawImage(renderingContext.bufferedImage, 0, 0, null);
+        } finally {
+            if (g != null)
+                g.dispose();
+        }
+
+        if (!bufferStrategy.contentsLost())
+            bufferStrategy.show();
     }
 
     private void clearCanvas() {
@@ -354,7 +371,6 @@ public class ViewPanel extends JPanel implements ComponentListener {
             renderFrame();
             viewRepaintNeeded = renderingContext.handlePossibleComponentMouseEvent();
         }
-
     }
 
     private void maintainRenderingContext() {
@@ -404,4 +420,4 @@ public class ViewPanel extends JPanel implements ComponentListener {
         frameListeners.remove(frameListener);
     }
 
-}
+}
\ No newline at end of file
index 9fb4e16..a13a5cf 100644 (file)
@@ -10,7 +10,7 @@ import eu.svjatoslav.sixth.e3d.gui.FrameListener;
 import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
 import eu.svjatoslav.sixth.e3d.math.Rotation;
 
-import javax.swing.*;
+import java.awt.*;
 import java.awt.event.*;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -20,7 +20,7 @@ import java.util.Map;
 /**
  * Manages mouse and keyboard input for the 3D view.
  *
- * <p>Handles Swing mouse/keyboard events, tracks pressed keys and mouse state,
+ * <p>Handles mouse/keyboard events, tracks pressed keys and mouse state,
  * and forwards events to the appropriate handlers. Also provides default camera
  * control via mouse dragging (look around) and mouse wheel (vertical movement).</p>
  *
@@ -54,11 +54,11 @@ public class InputManager implements
         return viewUpdateNeeded;
     }
 
-    private void bind(final JPanel panel) {
-        panel.addMouseMotionListener(this);
-        panel.addKeyListener(this);
-        panel.addMouseListener(this);
-        panel.addMouseWheelListener(this);
+    private void bind(final Component component) {
+        component.addMouseMotionListener(this);
+        component.addKeyListener(this);
+        component.addMouseListener(this);
+        component.addMouseWheelListener(this);
     }
 
     private boolean handleKeyboardEvents() {
index e7cb25f..dfa42eb 100644 (file)
@@ -84,7 +84,7 @@ public final class Color {
      * 0 - transparent.
      * 255 - opaque.
      */
-    public int a;
+    public final int a;
 
     private java.awt.Color cachedAwtColor;
 
index 079f0ab..7550d09 100644 (file)
@@ -164,7 +164,7 @@ public class Line extends AbstractCoordinateShape {
         final int backgroundAlpha = 255 - alpha;
 
         final int redWithAlpha = color.r * alpha;
-        final int greenWithAplha = color.g * alpha;
+        final int greenWithAlpha = color.g * alpha;
         final int blueWithAlpha = color.b * alpha;
 
         for (int relativeX = 0; relativeX <= lineWidth; relativeX++) {
@@ -182,7 +182,7 @@ public class Line extends AbstractCoordinateShape {
                     final int destB = dest & 0xff;
 
                     final int newR = ((destR * backgroundAlpha) + redWithAlpha) / 256;
-                    final int newG = ((destG * backgroundAlpha) + greenWithAplha) / 256;
+                    final int newG = ((destG * backgroundAlpha) + greenWithAlpha) / 256;
                     final int newB = ((destB * backgroundAlpha) + blueWithAlpha) / 256;
 
                     pixels[offset] = (newR << 16) | (newG << 8) | newB;