Package relocation: make package structure real and acyclic
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 20 Sep 2026 08:46:18 +0000 (11:46 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 20 Sep 2026 08:46:18 +0000 (11:46 +0300)
Moved (git mv, move-only, no behavior change):
- gui -> renderer.raster: RenderingContext, SegmentRenderingContext,
  HiZPyramid, CullingStatistics, StereoEye, Frustum, Vertex (math)
- gui -> diag: ThreadActivityRecorder, DebugLogBuffer
- gui -> geometry: Camera (decoupled from gui.FrameListener: dead
  ViewPanel param dropped, panel registers a lambda adapter)
- geometry -> renderer.raster.shapes.composite.base: Plane, BspTree,
  PolygonType (CSG machinery, used only by shapes)
- renderer.octree -> geometry: IntegerPoint (zero-dep coordinate type)
- ShapeCollection's ViewPanel overloads deleted (trivial delegates;
  callers pass getCamera()); RenderingContext.bufferedImage made public
  (AWT shell blits it); javadoc {@link}s re-qualified.

Resulting layering: math <- geometry <- renderer.raster <- gui, with
diag a standalone leaf. Remaining known renderer->gui edges (next
refactor): DeveloperTools field + mouse-picking API types
(MouseEvent/MouseInteractionController/ViewSpaceTracker/TextPointer)
and demo-only RayTracer->ViewPanel.

Verified: engine 37/37 tests, all 3 consumer repos (demos, aukio,
aukio-environment-fo4) compile, demo goldens bit-identical to the
pre-relocation run.

68 files changed:
AGENTS.org
Documentation/index.org
src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java
src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java
src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java
src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java
src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java
src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java [deleted file]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java
src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java
src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java
src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java
src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java
src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java

index f383da2..df9f9b9 100644 (file)
@@ -18,7 +18,7 @@ navigation.
 |-------------------------------+-----------------------------------------------------+-----------------------------------------------------------|
 | *Create a window*             | ~ViewFrame~ (~gui/ViewFrame.java~)                  | ~new ViewFrame()~ → ~.getViewPanel()~                     |
 | *Add shapes to scene*         | ~ShapeCollection~ (~raster/ShapeCollection.java~)   | ~viewPanel.getRootShapeCollection().addShape(shape)~      |
-| *Position camera*             | ~Camera~ (~gui/Camera.java~)                        | ~camera.getTransform().setTranslation(Point3D)~           |
+| *Position camera*             | ~Camera~ (~geometry/Camera.java~)                        | ~camera.getTransform().setTranslation(Point3D)~           |
 | *Create a wireframe cube*     | ~WireframeCube~ (~shapes/composite/wireframe/~)     | ~new WireframeCube(center, halfSize, appearance)~         |
 | *Create a solid cube*         | ~SolidPolygonCube~ (~shapes/composite/solid/~)      | ~new SolidPolygonCube(center, halfSize, color)~           |
 | *Create a line*               | ~Line~ (~shapes/basic/line/~)                       | ~new Line(p1, p2, color, width)~                          |
@@ -344,7 +344,7 @@ scene.addShape(clickableCube);
 |-----------------+--------------------------+------------------------------+--------------------------------------------------------------------------------------------------------|
 | ~ViewPanel~     | ~gui/ViewPanel.java~     | AWT Canvas, render loop      | ~.getRootShapeCollection()~, ~.getCamera()~, ~.getLightingManager()~, ~.addFrameListener()~, ~.stop()~ |
 | ~ViewFrame~     | ~gui/ViewFrame.java~     | JFrame wrapper               | ~new ViewFrame()~ → ~.getViewPanel()~                                                                  |
-| ~Camera~        | ~gui/Camera.java~        | Viewer position/orientation  | ~.getTransform()~, ~.setTransform()~                                                                   |
+| ~Camera~        | ~geometry/Camera.java~        | Viewer position/orientation  | ~.getTransform()~, ~.setTransform()~                                                                   |
 | ~FrameListener~ | ~gui/FrameListener.java~ | Per-frame callback interface | ~.onFrame(panel, deltaMs)~ → return true to repaint                                                    |
 
 ** Input (~gui/humaninput/~)
index 4068d09..c40ce32 100644 (file)
@@ -238,7 +238,7 @@ p1.rotate(new Point3D(0,0,0), Math.PI/4, 0);             // Rotate p1 in place,
 
 *** Vertex — Rendering-Ready Coordinates
 
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
 rendering. As a shape transforms through the render pipeline, each
 vertex tracks its position in multiple spaces:
 
@@ -304,7 +304,7 @@ visible when surfaces are not rendered. A triangle has 3 edges, a cube
 has 12 edges, and complex meshes have thousands.
 
 In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each
-Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a
+Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] endpoints and stores two properties: a
 [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a
 [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches
 between two modes based on the projected screen width: thin lines below
@@ -434,14 +434,14 @@ determines how bright a surface appears.
 
 | Use case             | API                                          | Computation                | Location          |
 |----------------------+----------------------------------------------+----------------------------+-------------------|
-| BSP/CSG operations   | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]]       | Lazy-cached once           | =Plane=           |
-| Per-frame shading    | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame     | =SolidPolygon=    |
+| BSP/CSG operations   | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#normal][Plane.normal]]       | Lazy-cached once           | =Plane=           |
+| Per-frame shading    | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame     | =SolidPolygon=    |
 | Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]]            | Uses normal via =dot(L,N)= | =LightingManager= |
 
 **Implementation notes:**
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
 
 ** Mesh
 :PROPERTIES:
@@ -1200,7 +1200,7 @@ the rendered screenshots.
 | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]]      | Golden-PNG comparison, diff writer, CLI            |
 | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]]        | Scene state as a reproducible text block           |
 | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]]  | ~transformShapes(Camera, ...)~ headless overload   |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame             |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame             |
 
 * Source code
 :PROPERTIES:
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java
new file mode 100644 (file)
index 0000000..ba81b5b
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.diag;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Circular buffer for debug log messages.
+ *
+ * <p>Captures log messages to a fixed-size circular buffer for display
+ * in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel}.</p>
+ *
+ * <p>This allows capturing early initialization logs before the user opens
+ * the Developer Tools panel. When the panel is opened, the buffered history
+ * becomes immediately visible.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel
+ */
+public class DebugLogBuffer {
+
+    private static final DateTimeFormatter TIME_FORMATTER =
+            DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+
+    private final String[] buffer;
+    private final int capacity;
+    private volatile int head = 0;
+    private volatile int count = 0;
+
+    /**
+     * Creates a new DebugLogBuffer with the specified capacity.
+     *
+     * @param capacity the maximum number of log entries to retain
+     */
+    public DebugLogBuffer(final int capacity) {
+        this.capacity = capacity;
+        this.buffer = new String[capacity];
+    }
+
+    /**
+     * Logs a message with a timestamp prefix.
+     *
+     * @param message the message to log
+     */
+    public void log(final String message) {
+        final String timestamped = LocalDateTime.now().format(TIME_FORMATTER) + " " + message;
+
+        synchronized (this) {
+            buffer[head] = timestamped;
+            head = (head + 1) % capacity;
+            if (count < capacity) {
+                count++;
+            }
+        }
+    }
+
+    /**
+     * Returns all buffered log entries in chronological order.
+     *
+     * @return a list of timestamped log entries
+     */
+    public synchronized List<String> getEntries() {
+        final List<String> entries = new ArrayList<>(count);
+
+        if (count < capacity) {
+            for (int i = 0; i < count; i++) {
+                entries.add(buffer[i]);
+            }
+        } else {
+            for (int i = 0; i < capacity; i++) {
+                final int index = (head + i) % capacity;
+                entries.add(buffer[index]);
+            }
+        }
+
+        return entries;
+    }
+
+    /**
+     * Clears all buffered log entries.
+     */
+    public synchronized void clear() {
+        head = 0;
+        count = 0;
+    }
+
+    /**
+     * Returns the current number of log entries in the buffer.
+     *
+     * @return the number of entries
+     */
+    public synchronized int size() {
+        return count;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java
new file mode 100644 (file)
index 0000000..203d061
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.diag;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Low-overhead recorder of per-thread work intervals for the thread
+ * timeline display in the Developer Tools window.
+ *
+ * <p>Task submission sites (transform chunks, paint tiles, tile binning)
+ * and render-thread phases (orchestration, waiting, blit) record their
+ * [start, end) intervals here when enabled. The timeline component paints
+ * each thread as a row, time on the X axis, the interval kind as color —
+ * the software-renderer equivalent of a GPU frame-profiler occupancy
+ * view. Idle time is simply the absence of intervals (black).</p>
+ *
+ * <p>Overhead when disabled: one volatile read per task. When enabled:
+ * two {@code System.nanoTime()} calls, one atomic increment and four
+ * array stores per task (~100 ns), negligible at hundreds of tasks per
+ * frame.</p>
+ *
+ * <p>Frame parity distinguishes "current frame" work from "next frame"
+ * work in the colors: transform/paint/binning kinds come in an even and
+ * an odd variant, selected by the parity that was current when the task
+ * was SUBMITTED (captured at task creation, so overlapping frames keep
+ * their own color).</p>
+ */
+public final class ThreadActivityRecorder {
+
+    /** Kind base: vertex transform chunk (add frame parity 0..2). */
+    public static final int KIND_TRANSFORM = 0;
+    /** Kind base: paint tile task (add frame parity 0..2). */
+    public static final int KIND_PAINT = 3;
+    /** Kind base: tile binning task (add frame parity 0..2). */
+    public static final int KIND_BIN = 6;
+    /** Kind: render thread orchestration (tree walk, submission, merges). */
+    public static final int KIND_RENDER = 9;
+    /** Kind: render thread blocked waiting for worker tasks. */
+    public static final int KIND_AWAIT = 10;
+    /** Kind: render thread blitting the finished frame to screen. */
+    public static final int KIND_BLIT = 11;
+    /** Kind: a pass's asynchronous continuation (drain, sort, bin, paint submission). */
+    public static final int KIND_PREP = 12;
+    /** Kind: continuation sub-phase: draining and merging transform chunks. */
+    public static final int KIND_DRAIN = 13;
+    /** Kind: continuation sub-phase: depth sort. */
+    public static final int KIND_SORT = 14;
+
+    /** Number of distinct kinds (three parities each for transform/paint/bin, plus 9..14). */
+    public static final int KIND_COUNT = 15;
+
+    private static final int CAPACITY = 1 << 18;
+    private static final int MASK = CAPACITY - 1;
+
+    private static final long[] starts = new long[CAPACITY];
+    private static final long[] ends = new long[CAPACITY];
+    private static final byte[] kinds = new byte[CAPACITY];
+    private static final byte[] rows = new byte[CAPACITY];
+    private static final AtomicInteger cursor = new AtomicInteger();
+
+    private static volatile boolean enabled = false;
+    private static volatile int frameParity = 0;
+
+    private static final ConcurrentHashMap<String, Integer> rowByThreadName = new ConcurrentHashMap<>();
+    private static final CopyOnWriteArrayList<String> rowNames = new CopyOnWriteArrayList<>();
+    private static final AtomicInteger nextRow = new AtomicInteger();
+
+    private ThreadActivityRecorder() {
+    }
+
+    /**
+     * @return true when recording is active (checked by task submission sites)
+     */
+    public static boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Enables or disables recording. Enabling starts with a clean buffer
+     * and fresh thread-row assignment.
+     *
+     * @param value true to start recording
+     */
+    public static void setEnabled(final boolean value) {
+        if (value && !enabled) {
+            clear();
+        }
+        enabled = value;
+    }
+
+    /**
+     * Drops all recorded intervals and thread-row assignments.
+     */
+    public static void clear() {
+        cursor.set(0);
+        rowByThreadName.clear();
+        rowNames.clear();
+        nextRow.set(0);
+        // starts[]==0 marks an empty slot for the timeline sweep
+        java.util.Arrays.fill(starts, 0L);
+    }
+
+    /**
+     * Sets the parity (0/1) of the frame currently being prepared.
+     * Called by the render thread at the start of each frame; task
+     * submission sites capture it into their tasks so overlapping frames
+     * keep distinct colors.
+     *
+     * @param parity frame parity, 0 or 1
+     */
+    public static void setFrameParity(final int parity) {
+        frameParity = parity;
+    }
+
+    /**
+     * @return parity of the frame currently being prepared
+     */
+    public static int frameParity() {
+        return frameParity;
+    }
+
+    /**
+     * Records one work interval on the calling thread.
+     *
+     * @param kind   interval kind (one of the KIND_* bases, plus parity
+     *               for transform/paint/binning)
+     * @param t0     interval start, from {@link System#nanoTime()}
+     * @param t1     interval end, from {@link System#nanoTime()}
+     */
+    public static void record(final int kind, final long t0, final long t1) {
+        // Rows are keyed by thread NAME, not Thread object: after a
+        // stop()/start() cycle the executor is recreated with fresh
+        // threads under the same names, and they must reuse the same
+        // rows — otherwise the timeline fills with dead threads' rows
+        // and pushes the live workers below the visible area.
+        final String threadName = Thread.currentThread().getName();
+        Integer row = rowByThreadName.get(threadName);
+        if (row == null) {
+            row = rowByThreadName.computeIfAbsent(threadName, t -> {
+                final int r = nextRow.getAndIncrement();
+                rowNames.add(t);
+                return r;
+            });
+        }
+        if (row > 127) {
+            return;
+        }
+        final int i = cursor.getAndIncrement() & MASK;
+        starts[i] = t0;
+        ends[i] = t1;
+        kinds[i] = (byte) kind;
+        rows[i] = (byte) (int) row;
+    }
+
+    // ---- Snapshot access for the timeline component ----
+
+    /** @return total number of intervals recorded since the last clear */
+    public static int cursor() {
+        return cursor.get();
+    }
+
+    /** @return ring buffer capacity */
+    public static int capacity() {
+        return CAPACITY;
+    }
+
+    /** @return interval start array (index space of the ring buffer) */
+    public static long[] starts() {
+        return starts;
+    }
+
+    /** @return interval end array (index space of the ring buffer) */
+    public static long[] ends() {
+        return ends;
+    }
+
+    /** @return interval kind array (index space of the ring buffer) */
+    public static byte[] kinds() {
+        return kinds;
+    }
+
+    /** @return interval thread-row array (index space of the ring buffer) */
+    public static byte[] rows() {
+        return rows;
+    }
+
+    /** @return number of distinct thread rows seen so far */
+    public static int rowCount() {
+        return nextRow.get();
+    }
+
+    /**
+     * @param row thread row index
+     * @return display name for the row ("render" for the render thread,
+     *         otherwise the thread name with the e3d- prefix stripped)
+     */
+    public static String rowName(final int row) {
+        if (row >= rowNames.size()) {
+            return "?";
+        }
+        final String name = rowNames.get(row);
+        if ("e3d-render".equals(name)) {
+            return "render";
+        }
+        return name.startsWith("e3d-") ? name.substring(4) : name;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/BspTree.java
deleted file mode 100644 (file)
index f6542b2..0000000
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A Binary Space Partitioning (BSP) tree for CSG operations.
- *
- * <p>BSP trees are the data structure that makes CSG boolean operations possible.
- * Each node divides 3D space into two half-spaces using a plane, enabling
- * efficient spatial queries and polygon clipping.</p>
- *
- * <p><b>BSP Tree Structure:</b></p>
- * <pre>
- *                 [Node: plane P]
- *                /               \
- *        [Front subtree]     [Back subtree]
- *     (same side as P's     (opposite side
- *        normal)             of P's normal)
- * </pre>
- *
- * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
- * @see Plane the plane type used for spatial partitioning
- * @see SolidPolygon the polygon type stored in BSP nodes
- */
-public class BspTree {
-
-    /**
-     * Polygons that lie on this node's partitioning plane.
-     */
-    public final List<SolidPolygon> polygons = new ArrayList<>();
-
-    /**
-     * The partitioning plane for this node.
-     */
-    public Plane plane;
-
-    /**
-     * The front child subtree.
-     */
-    public BspTree front;
-
-    /**
-     * The back child subtree.
-     */
-    public BspTree back;
-
-    /**
-     * Creates an empty BSP tree with no plane or children.
-     */
-    public BspTree() {
-    }
-
-    /**
-     * Creates a BSP tree from a list of polygons.
-     *
-     * @param polygons the polygons to partition into a BSP tree
-     */
-    public BspTree(final List<SolidPolygon> polygons) {
-        addPolygons(polygons);
-    }
-
-    /**
-     * Creates a deep clone of this BSP tree.
-     *
-     * @return a new BspTree with cloned data
-     */
-    public BspTree clone() {
-        final BspTree tree = new BspTree();
-
-        tree.plane = plane != null ? plane.clone() : null;
-        tree.front = front != null ? front.clone() : null;
-        tree.back = back != null ? back.clone() : null;
-
-        for (final SolidPolygon p : polygons) {
-            tree.polygons.add(p.deepClone());
-        }
-
-        return tree;
-    }
-
-    /**
-     * Inverts this BSP tree, converting "inside" to "outside" and vice versa.
-     */
-    public void invert() {
-        for (final SolidPolygon polygon : polygons) polygon.flip();
-
-        if (plane != null) plane.flip();
-        if (front != null) front.invert();
-        if (back != null) back.invert();
-
-        final BspTree temp = front;
-        front = back;
-        back = temp;
-    }
-
-    /**
-     * Clips a list of polygons against this BSP tree, returning only the
-     * portions that lie outside the solid represented by this tree.
-     *
-     * <p>This is a core CSG operation used for boolean subtraction and
-     * intersection. The method recursively traverses the BSP tree, splitting
-     * polygons at each partitioning plane and discarding interior fragments.</p>
-     *
-     * <p><b>Algorithm:</b></p>
-     * <ol>
-     *   <li>At each node, split polygons by the partitioning plane</li>
-     *   <li>Recursively clip front fragments against the front subtree</li>
-     *   <li>Recursively clip back fragments against the back subtree</li>
-     *   <li>Combine and return all surviving fragments</li>
-     * </ol>
-     *
-     * <p><b>Leaf nodes:</b> If this node has no plane (leaf node), all polygons
-     * are considered outside and returned unchanged.</p>
-     *
-     * @param polygons the polygons to clip against this BSP tree
-     * @return a new list containing only the portions outside this solid
-     */
-    public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
-        // Leaf node: no partitioning plane means all polygons are outside
-        if (plane == null) {
-            return new ArrayList<>(polygons);
-        }
-
-        // Split polygons by this node's partitioning plane
-        final List<SolidPolygon> frontList = new ArrayList<>();
-        final List<SolidPolygon> backList = new ArrayList<>();
-
-        for (final SolidPolygon polygon : polygons)
-            // Split by plane: coplanar polygons are classified by their normal direction
-            // (same-facing normal → frontList, opposite-facing normal → backList)
-            plane.splitPolygon(polygon, frontList, backList, frontList, backList);
-
-        // Recursively clip front fragments against front subtree
-        List<SolidPolygon> resultFront = frontList;
-        if (front != null) resultFront = front.clipPolygons(frontList);
-
-        // Recursively clip back fragments against back subtree
-        List<SolidPolygon> resultBack;
-        if (back != null) resultBack = back.clipPolygons(backList);
-        else resultBack = new ArrayList<>();
-
-        // Combine surviving fragments from both subtrees
-        final List<SolidPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
-        result.addAll(resultFront);
-        result.addAll(resultBack);
-        return result;
-    }
-
-    /**
-     * Clips this BSP tree against another BSP tree.
-     *
-     * @param bsp the BSP tree to clip against
-     */
-    public void clipTo(final BspTree bsp) {
-        final List<SolidPolygon> newPolygons = bsp.clipPolygons(polygons);
-        polygons.clear();
-        polygons.addAll(newPolygons);
-
-        if (front != null) front.clipTo(bsp);
-        if (back != null) back.clipTo(bsp);
-    }
-
-    /**
-     * Collects all polygons from this BSP tree into a flat list.
-     *
-     * @return a new list containing all polygons in this tree
-     */
-    public List<SolidPolygon> allPolygons() {
-        final List<SolidPolygon> result = new ArrayList<>(polygons);
-
-        if (front != null) result.addAll(front.allPolygons());
-        if (back != null) result.addAll(back.allPolygons());
-
-        return result;
-    }
-
-    /**
-     * Adds polygons to this BSP tree, partitioning space recursively.
-     *
-     * <p>This method is the core BSP tree construction algorithm. It builds or
-     * extends the tree by choosing a partition plane and classifying each polygon:</p>
-     *
-     * <ul>
-     *   <li><b>Coplanar</b> — polygons on the partition plane are stored in this node</li>
-     *   <li><b>Front</b> — polygons in the front half-space (same side as plane normal)
-     *       go to the front child subtree</li>
-     *   <li><b>Back</b> — polygons in the back half-space (opposite to plane normal)
-     *       go to the back child subtree</li>
-     *   <li><b>Spanning</b> — polygons crossing the plane are split into front and back
-     *       fragments, each going to its respective subtree</li>
-     * </ul>
-     *
-     * <p>For an empty tree, the first polygon's plane becomes the partition plane.
-     * Child nodes are created lazily when polygons need to be stored in them.</p>
-     *
-     * <p>Can be called multiple times to incrementally extend an existing tree,
-     * though the original partition planes remain unchanged.</p>
-     *
-     * @param polygons the polygons to insert into this BSP tree
-     * @see Plane#splitPolygon the method that classifies and splits individual polygons
-     */
-    public void addPolygons(final List<SolidPolygon> polygons) {
-        if (polygons.isEmpty()) return;
-
-        if (plane == null) plane = polygons.get(0).getPlane().clone();
-
-        final List<SolidPolygon> frontList = new ArrayList<>();
-        final List<SolidPolygon> backList = new ArrayList<>();
-
-        for (final SolidPolygon polygon : polygons)
-            plane.splitPolygon(polygon, this.polygons, this.polygons, frontList, backList);
-
-        if (!frontList.isEmpty()) {
-            if (front == null) front = new BspTree();
-            front.addPolygons(frontList);
-        }
-
-        if (!backList.isEmpty()) {
-            if (back == null) back = new BspTree();
-            back.addPolygons(backList);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java
new file mode 100644 (file)
index 0000000..c4c534b
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.geometry;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
+import eu.svjatoslav.aukio.e3d.math.Quaternion;
+import eu.svjatoslav.aukio.e3d.math.Transform;
+
+/**
+ * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
+ *
+ * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
+ * a looking direction (defined by a quaternion), and a movement system with
+ * velocity, acceleration, and friction for smooth camera navigation.</p>
+ *
+ * <p>By default, the user can navigate using arrow keys (handled by
+ * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker}),
+ * and the mouse controls the look direction (handled by
+ * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager}).</p>
+ *
+ * <p><b>Programmatic camera control:</b></p>
+ * <pre>{@code
+ * Camera camera = viewPanel.getCamera();
+ *
+ * // Set camera position
+ * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
+ *
+ * // Set camera orientation using a quaternion
+ * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
+ *
+ * // Copy camera state from another camera
+ * Camera snapshot = new Camera(camera);
+ * }</pre>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getCamera()
+ * @see eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation
+ */
+public class Camera {
+
+    /**
+     * Camera movement speed limit, relative to the world. When camera coordinates are
+     * updated within the world, camera orientation relative to the world is
+     * taken into account.
+     */
+    public static final double SPEED_LIMIT = 30;
+
+    /** World units moved per millisecond per unit of velocity.
+     * Public: direct-drive controllers (SpaceMouse) reuse it to match
+     * the keyboard/mouse feel. */
+    public static final double SPEED_MULTIPLIER = .02d;
+    /**
+     * Determines amount of friction user experiences every millisecond while moving around in space.
+     */
+    private static final double MILLISECOND_FRICTION = 1.005;
+    /**
+     * Camera movement speed, relative to camera itself. When camera coordinates
+     * are updated within the world, camera orientation relative to the world is
+     * taken into account.
+     */
+    private final Point3D movementVector = new Point3D();
+    private final Point3D previousLocation = new Point3D();
+    /**
+     * Camera acceleration factor for movement speed. Higher values result in faster acceleration.
+     */
+    public double cameraAcceleration = 0.1;
+    /**
+     * The transform containing camera location and orientation.
+     */
+    private final Transform transform;
+
+    /**
+     * Creates a camera at the world origin with no rotation.
+     */
+    public Camera() {
+        transform = new Transform();
+    }
+
+    /**
+     * Creates a copy of an existing camera, cloning its position and orientation.
+     *
+     * @param sourceView the camera to copy
+     */
+    public Camera(final Camera sourceView) {
+        transform = sourceView.getTransform().clone();
+    }
+
+    /**
+     * Creates a camera with the specified transform (position and orientation).
+     *
+     * @param transform the initial transform defining position and rotation
+     */
+    public Camera(final Transform transform){
+        this.transform = transform;
+    }
+
+    /**
+     * Per-frame camera physics tick (movement integration, friction).
+     * Registered on the panel via a {@code FrameListener} adapter in
+     * {@code ViewPanel} — decoupled from the gui interface so the camera
+     * stays in the geometry package.
+     *
+     * @param millisecondsSinceLastFrame frame delta in milliseconds
+     * @return true when the camera moved enough to need a repaint
+     */
+    public boolean onFrame(final int millisecondsSinceLastFrame) {
+
+        previousLocation.clone(transform.getTranslation());
+        translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame);
+        applyFrictionToMovement(millisecondsSinceLastFrame);
+        return isFrameRepaintNeeded();
+    }
+
+    private boolean isFrameRepaintNeeded() {
+        final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation);
+        return distanceMoved > 0.03;
+    }
+
+    /**
+     * Clamps the camera's movement speed to {@link #SPEED_LIMIT}.
+     * Called after modifying the movement vector to prevent excessive velocity.
+     */
+    public void enforceSpeedLimit() {
+        final double currentSpeed = movementVector.getVectorLength();
+
+        if (currentSpeed <= SPEED_LIMIT)
+            return;
+
+        movementVector.divide(currentSpeed / SPEED_LIMIT);
+    }
+
+    /**
+     * Returns the current movement velocity vector, relative to the camera's orientation.
+     * Modify this vector to programmatically move the camera.
+     *
+     * @return the movement vector (mutable reference)
+     */
+    public Point3D getMovementVector() {
+        return movementVector;
+    }
+
+    /**
+     * Returns the current movement speed (magnitude of the movement vector).
+     *
+     * @return the scalar speed value
+     */
+    public double getMovementSpeed() {
+        return movementVector.getVectorLength();
+    }
+
+    /**
+     * Apply friction to camera movement vector.
+     *
+     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+     *                                         Therefore, we take frame rendering time into account when translating
+     *                                         camera between consecutive frames.
+     */
+    private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) {
+        for (int i = 0; i < millisecondsPassedSinceLastFrame; i++)
+            applyMillisecondFrictionToUserMovementVector();
+    }
+
+    /**
+     * Apply friction to camera movement vector.
+     */
+    private void applyMillisecondFrictionToUserMovementVector() {
+        movementVector.x /= MILLISECOND_FRICTION;
+        movementVector.y /= MILLISECOND_FRICTION;
+        movementVector.z /= MILLISECOND_FRICTION;
+    }
+
+    /**
+     * Translate coordinates based on camera movement vector and camera orientation in the world.
+     *
+     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+     *                                         Therefore, we take frame rendering time into account when translating
+     *                                         camera between consecutive frames.
+     */
+    private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
+        final Matrix3x3 m = transform.getRotation().toMatrix();
+
+        final double forwardX = m.m20;
+        final double forwardY = m.m21;
+        final double forwardZ = m.m22;
+
+        final double rightX = m.m00;
+        final double rightY = m.m01;
+        final double rightZ = m.m02;
+
+        final Point3D location = transform.getTranslation();
+        final double ms = millisecondsPassedSinceLastFrame;
+
+        location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms;
+        location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms;
+        location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms;
+
+        location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms;
+        location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms;
+        location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms;
+
+        location.y += movementVector.y * SPEED_MULTIPLIER * ms;
+    }
+
+    /**
+     * Returns the transform containing this camera's location and orientation.
+     *
+     * @return the transform (mutable reference)
+     */
+    public Transform getTransform() {
+        return transform;
+    }
+
+    /**
+     * Orients the camera to look at a target point in world coordinates.
+     *
+     * <p>Calculates the required XZ and YZ rotation angles to point the camera
+     * from its current position toward the target. Useful for programmatic
+     * camera control, cinematic sequences, and following objects.</p>
+     *
+     * <p><b>Example:</b></p>
+     * <pre>{@code
+     * Camera camera = viewPanel.getCamera();
+     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
+     * }</pre>
+     *
+     * @param target the world-space point to look at
+     */
+    public void lookAt(final Point3D target) {
+        final Point3D pos = transform.getTranslation();
+        final double dx = target.x - pos.x;
+        final double dy = target.y - pos.y;
+        final double dz = target.z - pos.z;
+
+        final double angleXZ = -Math.atan2(dx, dz);
+        final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
+        final double angleYZ = -Math.atan2(dy, horizontalDist);
+
+        transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ));
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Frustum.java
deleted file mode 100644 (file)
index d1075c6..0000000
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-
-/**
- * View frustum for frustum culling - eliminates objects outside the camera's view.
- *
- * <p>The frustum is a truncated pyramid-shaped volume that represents everything
- * the camera can see. Objects completely outside this volume can be skipped
- * during rendering, significantly improving performance for large scenes.</p>
- *
- * <p><b>Frustum planes:</b></p>
- * <ul>
- *   <li>Left, Right, Top, Bottom - define the viewport edges</li>
- *   <li>Near - closest visible distance from camera</li>
- *   <li>Far - farthest visible distance from camera</li>
- * </ul>
- *
- * <p><b>Usage:</b></p>
- * <pre>{@code
- * Frustum frustum = new Frustum();
- * frustum.update(camera, screenWidth, screenHeight);
- *
- * Box objectBounds = shape.getBoundingBox();
- * if (frustum.intersectsAABB(objectBounds)) {
- *     // Object is potentially visible - render it
- * } else {
- *     // Object outside frustum - skip rendering
- * }
- * }</pre>
- *
- * <p><b>AABB intersection algorithm:</b></p>
- * <p>Uses the optimized "P-vertex" approach: for each plane, we test only
- * the AABB corner most aligned with the plane normal. If this corner is
- * behind the plane, the entire AABB is outside the frustum.</p>
- *
- * @see Box axis-aligned bounding box for culling tests
- * @see Camera provides position and orientation for frustum computation
- */
-public class Frustum {
-
-    /**
-     * Index for the left clipping plane.
-     */
-    public static final int LEFT = 0;
-    /**
-     * Index for the right clipping plane.
-     */
-    public static final int RIGHT = 1;
-    /**
-     * Index for the top clipping plane.
-     */
-    public static final int TOP = 2;
-    /**
-     * Index for the bottom clipping plane.
-     */
-    public static final int BOTTOM = 3;
-    /**
-     * Index for the near clipping plane.
-     */
-    public static final int NEAR = 4;
-    /**
-     * Index for the far clipping plane.
-     */
-    public static final int FAR = 5;
-
-    /**
-     * The six clipping planes defining the frustum volume.
-     * Each plane is stored as (normal, distance) in Hesse normal form.
-     * Planes are in world space coordinates.
-     */
-    private final Plane[] planes = new Plane[6];
-
-    /**
-     * Default near plane distance from camera (in world units).
-     * Objects closer than this are culled.
-     */
-    private double nearDistance = 1.0;
-
-    /**
-     * Default far plane distance from camera (in world units).
-     * Objects farther than this are culled.
-     */
-    private double farDistance = 10000.0;
-
-    /**
-     * Creates a new frustum with uninitialized planes.
-     * Call {@link #update} before using for culling.
-     */
-    public Frustum() {
-        for (int i = 0; i < 6; i++) {
-            planes[i] = new Plane(new Point3D(0, 0, 1), 0);
-        }
-    }
-
-    /**
-     * Updates the frustum planes in view space (camera at origin, looking along +Z).
-     *
-     * <p>This method should be called once per frame before rendering, after the
-     * camera position and orientation have been updated.</p>
-     *
-     * <p><b>View space coordinate system:</b></p>
-     * <ul>
-     *   <li>Camera at origin (0, 0, 0)</li>
-     *   <li>Forward = +Z axis (looking into the screen)</li>
-     *   <li>Right = +X axis</li>
-     *   <li>Up = -Y axis (since Y-down means smaller Y is higher visually)</li>
-     * </ul>
-     *
-     * <p><b>Plane normals point INTO the frustum</b> (toward the visible volume).
-     * A point is inside if dot(normal, point) >= distance for all planes.</p>
-     *
-     * <p><b>FOV calculation:</b> The Aukio 3D engine uses projectionScale = width/3.
-     * This means tan(halfHFOV) = (width/2) / projectionScale = 1.5, giving a
-     * horizontal FOV of approximately 112 degrees.</p>
-     *
-     * @param camera the camera (used only for aspect ratio derivation from width/height)
-     * @param width  the viewport width in pixels (defines projectionScale)
-     * @param height the viewport height in pixels (used for vertical FOV)
-     */
-    public void update(final Camera camera, final int width, final int height) {
-        // Frustum is computed in VIEW SPACE (camera at origin, looking along +Z)
-        // This matches the coordinate system after applying camera transforms
-
-        // Aukio 3D uses projectionScale = width/3
-        // tan(halfFOV) = (halfSize) / projectionScale
-        final double projectionScale = width / 3.0;
-        final double tanHalfHFOV = (width / 2.0) / projectionScale;  // = 1.5 (very wide FOV)
-        final double tanHalfVFOV = (height / 2.0) / projectionScale; // depends on aspect ratio
-
-        // Compute cosine and sine of half-FOV angles
-        // cosHalfFOV = 1 / sqrt(1 + tanHalfFOV^2)
-        // sinHalfFOV = tanHalfFOV * cosHalfFOV
-        final double cosHalfHFOV = 1.0 / Math.sqrt(1.0 + tanHalfHFOV * tanHalfHFOV);
-        final double sinHalfHFOV = tanHalfHFOV * cosHalfHFOV;
-        final double cosHalfVFOV = 1.0 / Math.sqrt(1.0 + tanHalfVFOV * tanHalfVFOV);
-        final double sinHalfVFOV = tanHalfVFOV * cosHalfVFOV;
-
-        // Near and far distances
-        nearDistance = 1.0;
-        farDistance = 10000.0;
-
-        // All side planes pass through origin (camera position in view space)
-        // Plane equation: dot(normal, point) >= distance means inside
-
-        // Left plane: inward normal pointing right-forward
-        // Bounds: x >= -tanHalfHFOV * z (to the right of left edge)
-        planes[LEFT].normal = new Point3D(cosHalfHFOV, 0, sinHalfHFOV);
-        planes[LEFT].distance = 0;
-
-        // Right plane: inward normal pointing left-forward
-        // Bounds: x <= tanHalfHFOV * z (to the left of right edge)
-        planes[RIGHT].normal = new Point3D(-cosHalfHFOV, 0, sinHalfHFOV);
-        planes[RIGHT].distance = 0;
-
-        // Top plane: inward normal pointing down-forward (Y-down system, top is smaller Y)
-        // Bounds: y <= tanHalfVFOV * z (below top edge, smaller Y)
-        planes[TOP].normal = new Point3D(0, -cosHalfVFOV, sinHalfVFOV);
-        planes[TOP].distance = 0;
-
-        // Bottom plane: inward normal pointing up-forward (larger Y is below)
-        // Bounds: y >= -tanHalfVFOV * z (above bottom edge, larger Y)
-        planes[BOTTOM].normal = new Point3D(0, cosHalfVFOV, sinHalfVFOV);
-        planes[BOTTOM].distance = 0;
-
-        // Near plane: inward normal pointing forward (+Z)
-        // Bounds: z >= nearDistance (in front of near plane)
-        planes[NEAR].normal = new Point3D(0, 0, 1);
-        planes[NEAR].distance = nearDistance;
-
-        // Far plane: inward normal pointing backward (-Z)
-        // Bounds: z <= farDistance (behind far plane)
-        planes[FAR].normal = new Point3D(0, 0, -1);
-        planes[FAR].distance = -farDistance;
-    }
-
-    /**
-     * Tests whether an axis-aligned bounding box intersects the frustum.
-     *
-     * <p>This is a conservative test: returns {@code true} if the box is
-     * potentially visible (inside or partially inside the frustum), and
-     * {@code false} only if the box is completely outside all frustum planes.</p>
-     *
-     * <p><b>Optimized algorithm:</b></p>
-     * <p>For each plane, we test only the AABB corner most aligned with the
-     * plane normal (the "P-vertex"). If this corner is behind the plane,
-     * the entire AABB must be outside the frustum.</p>
-     *
-     * @param box the axis-aligned bounding box to test (in view space coordinates)
-     * @return {@code true} if the box intersects or is inside the frustum,
-     *         {@code false} if completely outside
-     */
-    public boolean intersectsAABB(final Box box) {
-        // Get box min/max for each axis
-        final double minX = box.getMinX();
-        final double maxX = box.getMaxX();
-        final double minY = box.getMinY();
-        final double maxY = box.getMaxY();
-        final double minZ = box.getMinZ();
-        final double maxZ = box.getMaxZ();
-
-        for (int i = 0; i < 6; i++) {
-            final Plane plane = planes[i];
-            final Point3D n = plane.normal;
-            final double d = plane.distance;
-
-            // Find the P-vertex: the corner most aligned with the plane normal
-            // If normal component is positive, use max; if negative, use min
-            final double px = (n.x > 0) ? maxX : minX;
-            final double py = (n.y > 0) ? maxY : minY;
-            final double pz = (n.z > 0) ? maxZ : minZ;
-
-            // Test if P-vertex is outside the frustum (behind the plane)
-            // For inward-pointing normals: inside = dot(N,P) >= distance
-            // So outside = dot(N,P) < distance
-            if (n.x * px + n.y * py + n.z * pz < d) {
-                return false; // AABB entirely outside this plane
-            }
-        }
-
-        return true; // AABB intersects or inside all planes
-    }
-
-    /**
-     * Returns the near clipping plane distance.
-     *
-     * @return the near distance in world units
-     */
-    public double getNearDistance() {
-        return nearDistance;
-    }
-
-    /**
-     * Returns the far clipping plane distance.
-     *
-     * @return the far distance in world units
-     */
-    public double getFarDistance() {
-        return farDistance;
-    }
-
-    /**
-     * Sets the near and far clipping distances.
-     *
-     * @param near the near plane distance (objects closer are culled)
-     * @param far  the far plane distance (objects farther are culled)
-     */
-    public void setClipDistances(final double near, final double far) {
-        this.nearDistance = near;
-        this.farDistance = far;
-    }
-
-    /**
-     * Returns a specific frustum plane for debugging or advanced usage.
-     *
-     * @param planeIndex one of LEFT, RIGHT, TOP, BOTTOM, NEAR, FAR
-     * @return the plane at the specified index
-     */
-    public Plane getPlane(final int planeIndex) {
-        return planes[planeIndex];
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java
new file mode 100644 (file)
index 0000000..955267b
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.geometry;
+
+/**
+ * Point in 3D space with integer coordinates. Used for octree voxel positions.
+ */
+public class IntegerPoint
+{
+    /** X coordinate. */
+    public int x;
+    /** Y coordinate. */
+    public int y;
+    /** Z coordinate. */
+    public int z = 0;
+
+    /**
+     * Creates a point at the origin (0, 0, 0).
+     */
+    public IntegerPoint()
+    {
+    }
+
+    /**
+     * Creates a point with the specified coordinates.
+     *
+     * @param x the X coordinate
+     * @param y the Y coordinate
+     * @param z the Z coordinate
+     */
+    public IntegerPoint(final int x, final int y, final int z)
+    {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Plane.java
deleted file mode 100644 (file)
index 1d5c289..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-import eu.svjatoslav.aukio.e3d.math.Vertex;
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents an infinite plane in 3D space using the Hesse normal form.
- *
- * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
- * in CSG. They divide 3D space into two half-spaces.</p>
- *
- * @see SolidPolygon polygons that reference their containing plane
- * @see BspTree BSP trees that use planes for spatial partitioning
- */
-public class Plane {
-
-    /**
-     * Epsilon value used for floating-point comparisons in BSP operations.
-     * Smaller values provide higher precision but may cause issues with
-     * near-coplanar polygons. 1e-5 is a good balance for most 3D geometry.
-     */
-    public static final double EPSILON = 1e-12;
-
-    /**
-     * The unit normal vector perpendicular to the plane surface.
-     */
-    public Point3D normal;
-
-    /**
-     * The signed distance from the origin to the plane along the normal.
-     */
-    public double distance;
-
-    /**
-     * Creates a plane with the given normal and distance.
-     *
-     * @param normal   the unit normal vector
-     * @param distance the signed distance from origin to the plane
-     */
-    public Plane(final Point3D normal, final double distance) {
-        this.normal = normal;
-        this.distance = distance;
-    }
-
-    /**
-     * Computes the unit normal vector for a triangle defined by three points.
-     *
-     * <p>Zero-allocation method: fills the result point instead of creating a new one.
-     * This is the shared implementation used by both {@link #fromPoints} and
-     * {@link SolidPolygon} for shading calculations.</p>
-     *
-     * <p>The normal is computed as the cross product of two edge vectors (b-a and c-a),
-     * then normalized to unit length.</p>
-     *
-     * @param a     first point (base point for edge vectors)
-     * @param b     second point
-     * @param c     third point
-     * @param result Point3D to receive the unit normal vector (modified in place)
-     * @return true if normal computed successfully, false if points are collinear
-     *         (cross product magnitude less than EPSILON)
-     */
-    public static boolean computeNormal(final Point3D a, final Point3D b,
-                                        final Point3D c, final Point3D result) {
-        // Edge vectors from a to b and a to c
-        final double ax = b.x - a.x;
-        final double ay = b.y - a.y;
-        final double az = b.z - a.z;
-
-        final double bx = c.x - a.x;
-        final double by = c.y - a.y;
-        final double bz = c.z - a.z;
-
-        // Cross product: (edge1 × edge2)
-        double nx = ay * bz - az * by;
-        double ny = az * bx - ax * bz;
-        double nz = ax * by - ay * bx;
-
-        // Normalize
-        final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
-        if (length < EPSILON) {
-            result.x = result.y = result.z = 0;
-            return false;
-        }
-
-        result.x = nx / length;
-        result.y = ny / length;
-        result.z = nz / length;
-        return true;
-    }
-
-    /**
-     * Creates a plane from three non-collinear points.
-     *
-     * <p>Uses {@link #computeNormal} for the normal calculation, then computes
-     * the signed distance from origin using the dot product.</p>
-     *
-     * @param a the first point on the plane
-     * @param b the second point on the plane
-     * @param c the third point on the plane
-     * @return a new Plane passing through the three points
-     * @throws ArithmeticException if the points are collinear (cannot define a plane)
-     */
-    public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
-        final Point3D n = new Point3D();
-        if (!computeNormal(a, b, c, n)) {
-            throw new ArithmeticException(
-                    "Cannot create plane from collinear points: cross product is zero");
-        }
-        return new Plane(n, n.dot(a));
-    }
-
-    /**
-     * Creates a deep clone of this plane.
-     *
-     * @return a new Plane with the same normal and distance
-     */
-    public Plane clone() {
-        return new Plane(new Point3D(normal.x, normal.y, normal.z), distance);
-    }
-
-    /**
-     * Flips the plane orientation by negating the normal and distance.
-     */
-    public void flip() {
-        normal = normal.withNegated();
-        distance = -distance;
-    }
-
-    /**
-     * Splits a polygon by this plane, classifying and potentially dividing it.
-     *
-     * @param polygon       the polygon to classify and potentially split
-     * @param coplanarFront list to receive coplanar polygons with same-facing normals
-     * @param coplanarBack  list to receive coplanar polygons with opposite-facing normals
-     * @param front         list to receive polygons in the front half-space
-     * @param back          list to receive polygons in the back half-space
-     */
-    public void splitPolygon(final SolidPolygon polygon,
-                             final List<SolidPolygon> coplanarFront,
-                             final List<SolidPolygon> coplanarBack,
-                             final List<SolidPolygon> front,
-                             final List<SolidPolygon> back) {
-
-        PolygonType polygonType = PolygonType.COPLANAR;
-        final int vertexCount = polygon.getVertexCount();
-        final PolygonType[] types = new PolygonType[vertexCount];
-
-        for (int i = 0; i < vertexCount; i++) {
-            final Vertex v = polygon.vertices.get(i);
-            final double t = normal.dot(v.coordinate) - distance;
-            final PolygonType type = (t < -EPSILON) ? PolygonType.BACK
-                    : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR;
-            polygonType = polygonType.combine(type);
-            types[i] = type;
-        }
-
-        switch (polygonType) {
-            case COPLANAR:
-                ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
-                break;
-
-            case FRONT:
-                front.add(polygon);
-                break;
-
-            case BACK:
-                back.add(polygon);
-                break;
-
-            case SPANNING:
-                // Split spanning polygon by clipping each edge against the plane.
-                // Vertices on each side go to their respective lists.
-                // Edges crossing the plane create intersection vertices added to both lists.
-                final List<Vertex> frontVertices = new ArrayList<>();
-                final List<Vertex> backVertices = new ArrayList<>();
-
-                for (int i = 0; i < vertexCount; i++) {
-                    final int nextIndex = (i + 1) % vertexCount;
-                    final PolygonType currentType = types[i];
-                    final PolygonType nextType = types[nextIndex];
-                    final Vertex currentVertex = polygon.vertices.get(i);
-                    final Vertex nextVertex = polygon.vertices.get(nextIndex);
-
-                    // Add current vertex to the polygon on its side of the plane
-                    if (currentType.isFront()) {
-                        frontVertices.add(currentVertex.clone());
-                    }
-                    if (currentType.isBack()) {
-                        backVertices.add(currentVertex.clone());
-                    }
-
-                    // If edge crosses the plane, create intersection vertex for both polygons
-                    if (currentType != nextType
-                            && currentType != PolygonType.COPLANAR
-                            && nextType != PolygonType.COPLANAR) {
-                        // Calculate interpolation parameter t (0 = current, 1 = next)
-                        // t represents where along the edge the plane intersection occurs
-                        final double t = (distance - normal.dot(currentVertex.coordinate))
-                                / normal.dot(nextVertex.coordinate.withSubtracted(currentVertex.coordinate));
-
-                        final Vertex intersectionVertex = currentVertex.interpolate(nextVertex, t);
-                        frontVertices.add(intersectionVertex);
-                        backVertices.add(intersectionVertex.clone());
-                    }
-                }
-
-                if (frontVertices.size() >= 3) {
-                    final SolidPolygon frontPoly = SolidPolygon.fromVertices(
-                            frontVertices, polygon.getColor(), polygon.isShadingEnabled());
-                    front.add(frontPoly);
-                }
-                if (backVertices.size() >= 3) {
-                    final SolidPolygon backPoly = SolidPolygon.fromVertices(
-                            backVertices, polygon.getColor(), polygon.isShadingEnabled());
-                    back.add(backPoly);
-                }
-                break;
-        }
-    }
-}
\ No newline at end of file
index 91f7aa9..8b227c6 100755 (executable)
@@ -4,7 +4,7 @@
  */
 package eu.svjatoslav.aukio.e3d.geometry;
 
-import eu.svjatoslav.aukio.e3d.renderer.octree.IntegerPoint;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
 
 import static java.lang.Math.*;
 
@@ -57,7 +57,7 @@ import static java.lang.Math.*;
  * }</pre>
  *
  * @see Point2D the 2D equivalent
- * @see eu.svjatoslav.aukio.e3d.math.Vertex wraps a Point3D with transform support
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.Vertex wraps a Point3D with transform support
  */
 public class Point3D implements Cloneable {
 
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/PolygonType.java
deleted file mode 100644 (file)
index effc9d4..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.geometry;
-
-/**
- * Classification of a polygon's position relative to a plane.
- * Used in BSP tree operations to determine how polygons should be split.
- */
-public enum PolygonType {
-    /** Polygon lies on the plane. */
-    COPLANAR,
-    /** Polygon is entirely in front of the plane. */
-    FRONT,
-    /** Polygon is entirely behind the plane. */
-    BACK,
-    /** Polygon straddles the plane (vertices on both sides). */
-    SPANNING;
-
-    /**
-     * Combines this type with another to compute the aggregate classification.
-     * When vertices are on both sides of a plane, the result is SPANNING.
-     *
-     * @param other the other polygon type to combine with
-     * @return the combined classification
-     */
-    public PolygonType combine(final PolygonType other) {
-        if (this == other || other == COPLANAR) {
-            return this;
-        }
-        if (this == COPLANAR) {
-            return other;
-        }
-        // FRONT + BACK = SPANNING
-        return SPANNING;
-    }
-
-    /**
-     * Checks if this type represents a vertex in front of the plane.
-     *
-     * @return true if FRONT or COPLANAR (treated as front for classification)
-     */
-    public boolean isFront() {
-        return this == FRONT || this == COPLANAR;
-    }
-
-    /**
-     * Checks if this type represents a vertex behind the plane.
-     *
-     * @return true if BACK or COPLANAR (treated as back for classification)
-     */
-    public boolean isBack() {
-        return this == BACK || this == COPLANAR;
-    }
-}
\ No newline at end of file
index e684fe4..665b7cb 100644 (file)
@@ -3,6 +3,7 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 
 import eu.svjatoslav.aukio.e3d.diag.EngineConfig;
 import eu.svjatoslav.aukio.e3d.diag.PersistentLog;
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/Camera.java
deleted file mode 100644 (file)
index a212821..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
-import eu.svjatoslav.aukio.e3d.math.Quaternion;
-import eu.svjatoslav.aukio.e3d.math.Transform;
-
-/**
- * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
- *
- * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
- * a looking direction (defined by a quaternion), and a movement system with
- * velocity, acceleration, and friction for smooth camera navigation.</p>
- *
- * <p>By default, the user can navigate using arrow keys (handled by
- * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker}),
- * and the mouse controls the look direction (handled by
- * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager}).</p>
- *
- * <p><b>Programmatic camera control:</b></p>
- * <pre>{@code
- * Camera camera = viewPanel.getCamera();
- *
- * // Set camera position
- * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
- *
- * // Set camera orientation using a quaternion
- * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
- *
- * // Copy camera state from another camera
- * Camera snapshot = new Camera(camera);
- * }</pre>
- *
- * @see ViewPanel#getCamera()
- * @see eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation
- */
-public class Camera implements FrameListener {
-
-    /**
-     * Camera movement speed limit, relative to the world. When camera coordinates are
-     * updated within the world, camera orientation relative to the world is
-     * taken into account.
-     */
-    public static final double SPEED_LIMIT = 30;
-
-    /** World units moved per millisecond per unit of velocity.
-     * Public: direct-drive controllers (SpaceMouse) reuse it to match
-     * the keyboard/mouse feel. */
-    public static final double SPEED_MULTIPLIER = .02d;
-    /**
-     * Determines amount of friction user experiences every millisecond while moving around in space.
-     */
-    private static final double MILLISECOND_FRICTION = 1.005;
-    /**
-     * Camera movement speed, relative to camera itself. When camera coordinates
-     * are updated within the world, camera orientation relative to the world is
-     * taken into account.
-     */
-    private final Point3D movementVector = new Point3D();
-    private final Point3D previousLocation = new Point3D();
-    /**
-     * Camera acceleration factor for movement speed. Higher values result in faster acceleration.
-     */
-    public double cameraAcceleration = 0.1;
-    /**
-     * The transform containing camera location and orientation.
-     */
-    private final Transform transform;
-
-    /**
-     * Creates a camera at the world origin with no rotation.
-     */
-    public Camera() {
-        transform = new Transform();
-    }
-
-    /**
-     * Creates a copy of an existing camera, cloning its position and orientation.
-     *
-     * @param sourceView the camera to copy
-     */
-    public Camera(final Camera sourceView) {
-        transform = sourceView.getTransform().clone();
-    }
-
-    /**
-     * Creates a camera with the specified transform (position and orientation).
-     *
-     * @param transform the initial transform defining position and rotation
-     */
-    public Camera(final Transform transform){
-        this.transform = transform;
-    }
-
-    @Override
-    public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
-
-        previousLocation.clone(transform.getTranslation());
-        translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame);
-        applyFrictionToMovement(millisecondsSinceLastFrame);
-        return isFrameRepaintNeeded();
-    }
-
-    private boolean isFrameRepaintNeeded() {
-        final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation);
-        return distanceMoved > 0.03;
-    }
-
-    /**
-     * Clamps the camera's movement speed to {@link #SPEED_LIMIT}.
-     * Called after modifying the movement vector to prevent excessive velocity.
-     */
-    public void enforceSpeedLimit() {
-        final double currentSpeed = movementVector.getVectorLength();
-
-        if (currentSpeed <= SPEED_LIMIT)
-            return;
-
-        movementVector.divide(currentSpeed / SPEED_LIMIT);
-    }
-
-    /**
-     * Returns the current movement velocity vector, relative to the camera's orientation.
-     * Modify this vector to programmatically move the camera.
-     *
-     * @return the movement vector (mutable reference)
-     */
-    public Point3D getMovementVector() {
-        return movementVector;
-    }
-
-    /**
-     * Returns the current movement speed (magnitude of the movement vector).
-     *
-     * @return the scalar speed value
-     */
-    public double getMovementSpeed() {
-        return movementVector.getVectorLength();
-    }
-
-    /**
-     * Apply friction to camera movement vector.
-     *
-     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
-     *                                         Therefore, we take frame rendering time into account when translating
-     *                                         camera between consecutive frames.
-     */
-    private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) {
-        for (int i = 0; i < millisecondsPassedSinceLastFrame; i++)
-            applyMillisecondFrictionToUserMovementVector();
-    }
-
-    /**
-     * Apply friction to camera movement vector.
-     */
-    private void applyMillisecondFrictionToUserMovementVector() {
-        movementVector.x /= MILLISECOND_FRICTION;
-        movementVector.y /= MILLISECOND_FRICTION;
-        movementVector.z /= MILLISECOND_FRICTION;
-    }
-
-    /**
-     * Translate coordinates based on camera movement vector and camera orientation in the world.
-     *
-     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
-     *                                         Therefore, we take frame rendering time into account when translating
-     *                                         camera between consecutive frames.
-     */
-    private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
-        final Matrix3x3 m = transform.getRotation().toMatrix();
-
-        final double forwardX = m.m20;
-        final double forwardY = m.m21;
-        final double forwardZ = m.m22;
-
-        final double rightX = m.m00;
-        final double rightY = m.m01;
-        final double rightZ = m.m02;
-
-        final Point3D location = transform.getTranslation();
-        final double ms = millisecondsPassedSinceLastFrame;
-
-        location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms;
-        location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms;
-        location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms;
-
-        location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms;
-        location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms;
-        location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms;
-
-        location.y += movementVector.y * SPEED_MULTIPLIER * ms;
-    }
-
-    /**
-     * Returns the transform containing this camera's location and orientation.
-     *
-     * @return the transform (mutable reference)
-     */
-    public Transform getTransform() {
-        return transform;
-    }
-
-    /**
-     * Orients the camera to look at a target point in world coordinates.
-     *
-     * <p>Calculates the required XZ and YZ rotation angles to point the camera
-     * from its current position toward the target. Useful for programmatic
-     * camera control, cinematic sequences, and following objects.</p>
-     *
-     * <p><b>Example:</b></p>
-     * <pre>{@code
-     * Camera camera = viewPanel.getCamera();
-     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
-     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
-     * }</pre>
-     *
-     * @param target the world-space point to look at
-     */
-    public void lookAt(final Point3D target) {
-        final Point3D pos = transform.getTranslation();
-        final double dx = target.x - pos.x;
-        final double dy = target.y - pos.y;
-        final double dz = target.z - pos.z;
-
-        final double angleXZ = -Math.atan2(dx, dz);
-        final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
-        final double angleYZ = -Math.atan2(dy, horizontalDist);
-
-        transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ));
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/CullingStatistics.java
deleted file mode 100644 (file)
index 3b112c5..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Statistics for frustum culling, tracking composite-level culling efficiency.
- *
- * <p>Updated each frame during the rendering pipeline:</p>
- * <ul>
- *   <li>{@link #totalComposites} - incremented before each composite's frustum test</li>
- *   <li>{@link #culledComposites} - incremented when a composite fails the frustum test</li>
- * </ul>
- *
- * <p>Thread safety: counters are {@link AtomicInteger} because the parallel
- * transform phase increments them from multiple worker threads.</p>
- *
- * <p>Displayed in the {@link DeveloperToolsPanel} to help developers understand
- * culling efficiency and optimize scene graphs.</p>
- *
- * @see DeveloperToolsPanel
- * @see eu.svjatoslav.aukio.e3d.geometry.Frustum
- */
-public class CullingStatistics {
-
-    /**
-     * Total number of composite shapes tested against the frustum this frame.
-     * Incremented before each composite's AABB frustum test.
-     * Does not include the root composite (which is never frustum-tested).
-     */
-    public final AtomicInteger totalComposites = new AtomicInteger(0);
-
-    /**
-     * Number of composite shapes that were entirely outside the frustum and skipped.
-     * When a composite is culled, all its children (shapes and nested composites)
-     * are skipped without individual testing.
-     */
-    public final AtomicInteger culledComposites = new AtomicInteger(0);
-
-    /**
-     * Resets all statistics to zero.
-     * Called at the start of each frame before computing new statistics.
-     */
-    public void reset() {
-        totalComposites.set(0);
-        culledComposites.set(0);
-    }
-
-    /**
-     * Returns the percentage of composites that were culled.
-     *
-     * @return the culled percentage (0-100), or 0 if there are no composites
-     */
-    public double getCulledPercentage() {
-        final int total = totalComposites.get();
-        if (total == 0) {
-            return 0.0;
-        }
-        return 100.0 * culledComposites.get() / total;
-    }
-}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DebugLogBuffer.java
deleted file mode 100644 (file)
index 5104b37..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Circular buffer for debug log messages.
- *
- * <p>Captures log messages to a fixed-size circular buffer for display
- * in the {@link DeveloperToolsPanel}.</p>
- *
- * <p>This allows capturing early initialization logs before the user opens
- * the Developer Tools panel. When the panel is opened, the buffered history
- * becomes immediately visible.</p>
- *
- * @see DeveloperToolsPanel
- */
-public class DebugLogBuffer {
-
-    private static final DateTimeFormatter TIME_FORMATTER =
-            DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
-
-    private final String[] buffer;
-    private final int capacity;
-    private volatile int head = 0;
-    private volatile int count = 0;
-
-    /**
-     * Creates a new DebugLogBuffer with the specified capacity.
-     *
-     * @param capacity the maximum number of log entries to retain
-     */
-    public DebugLogBuffer(final int capacity) {
-        this.capacity = capacity;
-        this.buffer = new String[capacity];
-    }
-
-    /**
-     * Logs a message with a timestamp prefix.
-     *
-     * @param message the message to log
-     */
-    public void log(final String message) {
-        final String timestamped = LocalDateTime.now().format(TIME_FORMATTER) + " " + message;
-
-        synchronized (this) {
-            buffer[head] = timestamped;
-            head = (head + 1) % capacity;
-            if (count < capacity) {
-                count++;
-            }
-        }
-    }
-
-    /**
-     * Returns all buffered log entries in chronological order.
-     *
-     * @return a list of timestamped log entries
-     */
-    public synchronized List<String> getEntries() {
-        final List<String> entries = new ArrayList<>(count);
-
-        if (count < capacity) {
-            for (int i = 0; i < count; i++) {
-                entries.add(buffer[i]);
-            }
-        } else {
-            for (int i = 0; i < capacity; i++) {
-                final int index = (head + i) % capacity;
-                entries.add(buffer[index]);
-            }
-        }
-
-        return entries;
-    }
-
-    /**
-     * Clears all buffered log entries.
-     */
-    public synchronized void clear() {
-        head = 0;
-        count = 0;
-    }
-
-    /**
-     * Returns the current number of log entries in the buffer.
-     *
-     * @return the number of entries
-     */
-    public synchronized int size() {
-        return count;
-    }
-}
\ No newline at end of file
index b8b5a3a..179a4f0 100644 (file)
@@ -3,6 +3,11 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.CullingStatistics;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
 
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/HiZPyramid.java
deleted file mode 100644 (file)
index a12608b..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-package eu.svjatoslav.aukio.e3d.gui;
-
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Hierarchical depth pyramid for whole-block occlusion culling
- * (Hi-Z). Built from the just-painted frame's depth buffer; queried
- * during the NEXT frame's transform to skip blocks that are fully
- * hidden behind what was drawn last frame.
- *
- * <p>Semantics: each tile stores the MINIMUM w (= 1/z, i.e. the
- * FARTHEST written depth) over its pixels. A block whose nearest
- * possible point (max w over its AABB corners) is farther than a
- * tile's farthest written depth is behind something at every written
- * pixel of that tile — and tiles with any unwritten (sky) pixel hold
- * -infinity and never occlude. That makes the test conservative: it
- * may keep a hidden block, it never culls a visible one — under a
- * static camera. (The first version max-pooled, storing the NEAREST
- * depth per tile; that culled houses visible BETWEEN nearer tree
- * trunks — per-pixel gaps inside a tile are invisible to a max.)
- * Camera motion reuses the stale pyramid, which can cull a
- * newly-visible block wrongly; the block's pixels then contain far
- * background depth, so the next frame's pyramid no longer occludes
- * it — wrong culls self-heal in one frame (sanctioned design
- * concession), and the query margin absorbs small-motion
- * parallax.</p>
- *
- * <p>Empty tiles (never depth-written) store -infinity and never
- * occlude, so the first frame after startup (or after any pass that
- * leaves the pyramid unbuilt) culls nothing.</p>
- *
- * <p>Knobs: {@code -Daukio.hiz=false} disables culling (build still
- * happens — it is cheap — so flipping the flag needs no warm-up);
- * {@code -Daukio.hiz.margin=0.02} sets the relative w-space safety
- * margin against parallax between frames.</p>
- */
-public final class HiZPyramid {
-
-    /** Level-0 tile edge in pixels; level i tiles cover TILE*2^i px. */
-    private static final int TILE = 8;
-
-    private static final boolean ENABLED = Boolean.parseBoolean(
-            System.getProperty("aukio.hiz", "true"));
-    private static final double MARGIN = Double.parseDouble(
-            System.getProperty("aukio.hiz.margin", "0.02"));
-
-    /** Blocks occlusion-tested this process (telemetry). */
-    public final AtomicLong blocksTested = new AtomicLong();
-    /** Blocks culled as fully occluded (telemetry). */
-    public final AtomicLong blocksCulled = new AtomicLong();
-
-    private float[] tiles = new float[0];
-    private int[] levelOff = new int[0];
-    private int[] levelW = new int[0];
-    private int[] levelH = new int[0];
-    private int levels;
-    private int bufW = -1, bufH = -1;
-
-    /** Rebuilds the pyramid from a freshly painted depth buffer. */
-    public synchronized void buildFrom(final float[] depth,
-                                       final int width, final int height) {
-        if (width != bufW || height != bufH) {
-            allocate(width, height);
-        }
-
-        // Level 0: min-pool depth into TILE x TILE tiles. Depth
-        // outside the painted area is -infinity (cleared), so a tile
-        // with any unpainted pixel never occludes.
-        final int w0 = levelW[0], h0 = levelH[0];
-        final int off0 = levelOff[0];
-        for (int ty = 0; ty < h0; ty++) {
-            final int yEnd = Math.min((ty + 1) * TILE, height);
-            for (int tx = 0; tx < w0; tx++) {
-                final int xEnd = Math.min((tx + 1) * TILE, width);
-                float m = Float.POSITIVE_INFINITY;
-                for (int y = ty * TILE; y < yEnd; y++) {
-                    final int row = y * width;
-                    for (int x = tx * TILE; x < xEnd; x++)
-                        if (depth[row + x] < m)
-                            m = depth[row + x];
-                }
-                tiles[off0 + ty * w0 + tx] = m;
-            }
-        }
-
-        // Higher levels: min of 2x2 children.
-        for (int l = 1; l < levels; l++) {
-            final int pw = levelW[l - 1], ph = levelH[l - 1];
-            final int poff = levelOff[l - 1];
-            final int cw = levelW[l], ch = levelH[l];
-            final int coff = levelOff[l];
-            for (int ty = 0; ty < ch; ty++)
-                for (int tx = 0; tx < cw; tx++) {
-                    float m = Float.POSITIVE_INFINITY;
-                    for (int dy = 0; dy < 2; dy++)
-                        for (int dx = 0; dx < 2; dx++) {
-                            final int sx = tx * 2 + dx, sy = ty * 2 + dy;
-                            if (sx < pw && sy < ph) {
-                                final float v = tiles[poff + sy * pw + sx];
-                                if (v < m)
-                                    m = v;
-                            }
-                        }
-                    tiles[coff + ty * cw + tx] = m;
-                }
-        }
-    }
-
-    /**
-     * Conservative whole-block occlusion test.
-     *
-     * @param x1..y2    screen-space AABB of the block (will be clamped
-     *                  to the buffer; a fully off-screen box returns
-     *                  false)
-     * @param nearestW  the block's nearest possible depth = MAX 1/z
-     *                  over its corners
-     * @return true when the block is certainly hidden behind last
-     *         frame's occluders (within the parallax margin)
-     */
-    public synchronized boolean occluded(final double x1, final double y1,
-                            final double x2, final double y2,
-                            final double nearestW) {
-        if (!ENABLED || levels == 0)
-            return false;
-
-        int bx1 = (int) Math.floor(x1), by1 = (int) Math.floor(y1);
-        int bx2 = (int) Math.ceil(x2), by2 = (int) Math.ceil(y2);
-        if (bx1 < 0) bx1 = 0;
-        if (by1 < 0) by1 = 0;
-        if (bx2 >= bufW) bx2 = bufW - 1;
-        if (by2 >= bufH) by2 = bufH - 1;
-        if (bx1 > bx2 || by1 > by2)
-            return false;
-
-        // Coarsest level where the box still covers <= 2 tiles per axis.
-        int level = 0;
-        while (level + 1 < levels) {
-            final int s = TILE << (level + 1);
-            final int tw = (bx2 / s) - (bx1 / s) + 1;
-            final int th = (by2 / s) - (by1 / s) + 1;
-            if (tw > 2 || th > 2)
-                break;
-            level++;
-        }
-
-        final int s = TILE << level;
-        final int tx1 = bx1 / s, ty1 = by1 / s;
-        final int tx2 = bx2 / s, ty2 = by2 / s;
-        final int w = levelW[level], off = levelOff[level];
-
-        float minStored = Float.POSITIVE_INFINITY;
-        for (int ty = ty1; ty <= ty2; ty++)
-            for (int tx = tx1; tx <= tx2; tx++) {
-                final float v = tiles[off + ty * w + tx];
-                if (v < minStored)
-                    minStored = v;
-            }
-
-        // Occluded only when the block's nearest point is clearly
-        // behind the farthest written depth in the range; the
-        // relative margin absorbs parallax between frames.
-        return nearestW < minStored * (1.0 - MARGIN);
-    }
-
-    private void allocate(final int width, final int height) {
-        bufW = width;
-        bufH = height;
-        int lw = (width + TILE - 1) / TILE;
-        int lh = (height + TILE - 1) / TILE;
-        int count = 0;
-        levels = 0;
-        while (true) {
-            levels++;
-            count += lw * lh;
-            if (lw == 1 && lh == 1)
-                break;
-            lw = Math.max(1, (lw + 1) / 2);
-            lh = Math.max(1, (lh + 1) / 2);
-        }
-        tiles = new float[count];
-        levelOff = new int[levels];
-        levelW = new int[levels];
-        levelH = new int[levels];
-        lw = (width + TILE - 1) / TILE;
-        lh = (height + TILE - 1) / TILE;
-        int off = 0;
-        for (int l = 0; l < levels; l++) {
-            levelOff[l] = off;
-            levelW[l] = lw;
-            levelH[l] = lh;
-            off += lw * lh;
-            lw = Math.max(1, (lw + 1) / 2);
-            lh = Math.max(1, (lh + 1) / 2);
-        }
-        Arrays.fill(tiles, Float.NEGATIVE_INFINITY);
-    }
-}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/RenderingContext.java
deleted file mode 100644 (file)
index 627bc81..0000000
+++ /dev/null
@@ -1,707 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import eu.svjatoslav.aukio.e3d.geometry.Frustum;
-import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
-import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
-
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.awt.image.DataBufferInt;
-import java.awt.image.WritableRaster;
-import java.util.concurrent.ExecutorService;
-import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
-import java.util.function.Consumer;
-
-/**
- * Contains all state needed to render a single frame: the pixel buffer, graphics context,
- * screen dimensions, and mouse event tracking.
- *
- * <p>A new {@code RenderingContext} is created whenever the view panel is resized.
- * During rendering, shapes use this context to:</p>
- * <ul>
- *   <li>Access the raw pixel array ({@link #pixels}) for direct pixel manipulation</li>
- *   <li>Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing</li>
- *   <li>Read screen dimensions ({@link #width}, {@link #height}) and the
- *       {@link #centerCoordinate} for coordinate projection</li>
- *   <li>Use the {@link #projectionScale} factor for perspective projection</li>
- * </ul>
- *
- * <p>The context also manages mouse interaction detection: as shapes are painted
- * back-to-front, each shape can report itself as the object under the mouse cursor.
- * After painting completes, the topmost shape receives the mouse event.</p>
- *
- * @see ViewPanel the panel that creates and manages this context
- * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext)
- */
-public class RenderingContext {
-
-    /**
-     * The {@link BufferedImage} pixel format used for the rendering buffer.
-     * TYPE_INT_RGB provides optimal performance for Java2D blitting.
-     */
-    public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB;
-
-    /**
-     * Number of horizontal segments (bands) for parallel rendering.
-     * Bands are finer than the paint thread count: paint threads steal
-     * bands off a shared ticket until all bands are done, so a thread
-     * that finishes a cheap band immediately picks up more work.
-     * Derived from the render thread count via
-     * {@link ViewPanel#setNumRenderThreads(int)}.
-     *
-     * <p>Equals {@code tilesX * tilesY * viewportCount}: the tile grid
-     * covers one viewport, and in stereo mode a second grid covers the
-     * other eye (segment indices for the right eye start at
-     * {@code tilesX * tilesY}).</p>
-     */
-    public final int numRenderSegments;
-
-    /** Tile columns per viewport (1 = horizontal bands only). */
-    public final int tilesX;
-
-    /** Tile rows per viewport. */
-    public final int tilesY;
-
-    /** Number of side-by-side viewports (2 in stereo mode, else 1). */
-    public final int viewportCount;
-
-    /**
-     * Java2D graphics context for drawing text, anti-aliased shapes, and other
-     * high-level graphics operations onto the render buffer.
-     */
-    public final Graphics2D graphics;
-
-    /**
-     * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band.
-     * Used for thread-safe text and shape rendering without synchronization.
-     * Only initialized in the main RenderingContext; null in segment views.
-     */
-    private Graphics2D[] segmentGraphics;
-
-    /**
-     * Pixels of the rendering area.
-     * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}.
-     */
-    public final int[] pixels;
-
-    /**
-     * Per-pixel depth (biased 1/z, larger = nearer), always allocated —
-     * the painter path was deleted 2026-09-17 and the z-buffer is the
-     * only visibility mechanism. Shared with segment/pass copies like
-     * {@link #pixels}. Cleared per tile by the paint workers.
-     */
-    public float[] depth;
-
-    /**
-     * Active paint pass, set internally by
-     * {@code RenderAggregator.paintSorted}: 0 = not painting, 1 =
-     * opaque pass (opaque-class triangles only, depth test + write),
-     * 2 = alpha pass (alpha-carrying
-     * triangles only, depth test, no depth write). Shapes read it to
-     * decide whether they belong to the current pass.
-     */
-    public int depthPass;
-
-    /**
-     * Depth tolerance in world units for the z-buffer test, in the form
-     * {@code zw > stored - DEPTH_MARGIN_DZ * zw * zw} (tolerance behind
-     * stored, per-pixel at fragment depth). Default 0 = strict depth: any
-     * nonzero window exports per-triangle painter-sort errors into
-     * per-pixel occlusion errors (dirt whose triangles sort late beats
-     * road pavement that strictly wins at margin 0 — user bugreport
-     * 2026-09-16, road pose). Tunable via -Daukio.zbuffer.margin.
-     */
-    public static final double DEPTH_MARGIN_DZ =
-            Double.parseDouble(System.getProperty("aukio.zbuffer.margin", "0"));
-
-    /**
-     * Width of the rendering area in pixels.
-     */
-    public final int width;
-
-    /**
-     * Height of the rendering area in pixels.
-     */
-    public final int height;
-
-    /**
-     * Center of the screen in screen space (pixels).
-     * This is the point where (0,0) coordinate of the world space is rendered.
-     */
-    public final Point2D centerCoordinate;
-
-    /**
-     * Scale factor for perspective projection, derived from screen width.
-     * Used to convert normalized device coordinates to screen pixels.
-     * This is mutable to support stereo rendering where each eye has a different viewport width.
-     */
-    public double projectionScale;
-
-    /**
-     * Minimum Y coordinate (inclusive) to render. Used for multi-threaded rendering
-     * where each thread renders a horizontal segment.
-     */
-    public final int renderMinY;
-
-    /**
-     * Maximum Y coordinate (exclusive) to render. Used for multi-threaded rendering
-     * where each thread renders a horizontal segment.
-     */
-    public final int renderMaxY;
-
-    final BufferedImage bufferedImage;
-    /**
-     * Unique id of the current transform cycle, assigned by
-     * {@code ShapeCollection.transformShapes()} from a global counter.
-     * Unlike {@link #frameNumber} (per-context, can repeat across context
-     * instances), this never collides, so per-cycle memoization such as
-     * composite subtree weights can safely key on it.
-     */
-    public long transformCycleId;
-
-    /**
-     * Which projection buffer slot this context writes/reads: 0, 1 or 2.
-     * Cycles per render pass (per eye in stereo) when the
-     * triple-buffered pipeline is active, so the transform phase of a
-     * pass never overwrites the vertex state either of the two previous
-     * passes' paints may still be reading. Always 0 when the pipeline is
-     * off (tests, single-pass rendering).
-     */
-    public int vertexSlot = 0;
-
-    /**
-     * Near-plane distance in camera-space Z units. Polygons whose vertices
-     * straddle this plane are clipped against it (new intersection vertices
-     * are generated with interpolated UVs); polygons fully behind it are
-     * culled. Must be &gt; 0 so the perspective divide stays safe.
-     */
-    public double nearPlaneDistance = 1.0;
-
-    /**
-     * Number of frame that is currently being rendered.
-     * Every frame has its own number.
-     */
-    public int frameNumber = 0;
-
-    /**
-     * Projected-size cull threshold in screen pixels: shapes whose screen
-     * bounds span less than this in both axes are not queued for
-     * rendering. 0 (the default) disables the cull. Set globally with
-     * {@code -Daukio.cull.subpixel=<px>}.
-     */
-    public double subpixelCullingThreshold = Double.parseDouble(
-            System.getProperty("aukio.cull.subpixel", "0"));
-
-    /**
-     * Epoch of the subpixel-culling verdict cache, stamped per frame by
-     * {@code ShapeCollection.transformShapesBegin}: the epoch advances
-     * when the camera moves significantly (or after a bounded number of
-     * frames), which invalidates all cached skip verdicts and forces one
-     * re-evaluation pass. Meaningless when the cull is off.
-     */
-    public int subpixelCullingEpoch;
-
-    /**
-     * UI component that mouse is currently hovering over.
-     */
-    private MouseInteractionController objectPreviouslyUnderMouseCursor;
-    /**
-     * Mouse click event that needs to be processed.
-     * This event is processed only once per frame.
-     * If there are multiple objects under the mouse cursor, the top-most object will receive the event.
-     * If there are no objects under the mouse cursor, the event will be ignored.
-     * If there is no event, this field will be null.
-     * This field is set to null after the event is processed.
-     */
-    private MouseEvent mouseEvent;
-    /**
-     * UI component that mouse is currently hovering over.
-     */
-    private MouseInteractionController currentObjectUnderMouseCursor;
-    /**
-     * Texture coordinates of the mouse cursor on the hit shape (primary
-     * texture pixels), or NaN when the hit shape has no texture.
-     */
-    private double currentMouseTextureU = Double.NaN;
-    private double currentMouseTextureV = Double.NaN;
-    /**
-     * Developer tools for this rendering context.
-     * Controls diagnostic features like logging and visualization.
-     */
-    public DeveloperTools developerTools;
-
-    /**
-     * Debug log buffer for capturing diagnostic output.
-     * Shapes can log messages here that appear in the Developer Tools panel.
-     */
-    public DebugLogBuffer debugLogBuffer;
-
-    /**
-     * Global lighting manager for the scene.
-     * All shaded polygons use this to calculate lighting. Contains all light sources
-     * and ambient light settings for the world.
-     */
-    public LightingManager lightingManager;
-
-    /**
-     * Which eye is being rendered in stereo mode. NONE for normal single-view rendering.
-     */
-    public StereoEye stereoEye = StereoEye.NONE;
-
-    /**
-     * Width of the viewport for the current eye in stereo mode.
-     * Equals {@link #width} when not in stereo mode.
-     */
-    public int stereoViewportWidth;
-
-    /**
-     * X offset of the current eye's viewport within the full buffer.
-     * 0 for left eye, width/2 for right eye, 0 in normal mode.
-     */
-    public int stereoViewportOffsetX;
-
-    /**
-     * Minimum X coordinate (inclusive) for rendering.
-     * In stereo mode, this is {@link #stereoViewportOffsetX}.
-     * In normal mode, this is 0.
-     */
-    public int renderMinX;
-
-    /**
-     * Maximum X coordinate (exclusive) for rendering.
-     * In stereo mode, this is {@link #stereoViewportOffsetX} + {@link #stereoViewportWidth}.
-     * In normal mode, this is {@link #width}.
-     */
-    public int renderMaxX;
-
-    /**
-     * View frustum for frustum culling.
-     * Updated each frame from camera state and screen dimensions.
-     * Shapes can test their bounding boxes against this frustum to determine
-     * if they are potentially visible before expensive vertex transformations.
-     */
-    public Frustum frustum;
-
-    /**
-     * World-space position of the viewer for this pass, copied from the
-     * camera in {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection#transformShapesBegin}.
-     * Used by BSP painter ordering (viewpoint for tree traversal).
-     * Fresh instance per context, including per-pass copies, so overlapping
-     * pipeline passes each see their own viewpoint.
-     */
-    public final eu.svjatoslav.aukio.e3d.geometry.Point3D viewerPosition =
-            new eu.svjatoslav.aukio.e3d.geometry.Point3D();
-
-    /**
-     * Statistics for frustum culling performance tracking.
-     * Updated each frame: total shapes counted at start, visible shapes
-     * incremented during rendering, culled composites tracked during transform.
-     */
-    public CullingStatistics cullingStatistics;
-
-    /**
-     * Hi-Z occlusion pyramid, rebuilt from the depth buffer after every
-     * painted frame (live {@code ViewPanel} path only — headless
-     * {@code Snapshot} renders leave it empty so golden renders never
-     * cull). Read during the next frame's transform by
-     * {@code TriangleMeshBlock} to skip fully occluded blocks. Shared
-     * with pass/segment copies like {@link #depth}.
-     */
-    public HiZPyramid occlusionPyramid;
-
-    /**
-     * Executor for the parallel transform phase. When non-null, composites
-     * with enough children split their render lists into chunks transformed
-     * concurrently. When null, the transform phase runs serially on the
-     * render thread.
-     */
-    public ExecutorService transformExecutor;
-
-    /**
-     * Per-frame coordinator for the non-blocking parallel transform fork.
-     * Set by {@code ShapeCollection.transformShapes()} for the duration of
-     * the root transform when {@link #transformExecutor} is available;
-     * composites at any nesting level submit chunk tasks to it. Null outside
-     * the transform phase and when transforming serially.
-     */
-    public ParallelTransformCoordinator transformCoordinator;
-
-    /**
-     * Present gate for this framebuffer: fires when the frame currently
-     * held in this buffer has been presented to the display (or dropped
-     * from the presentation mailbox). The render thread installs a fresh
-     * gate at the start of each frame that reuses the buffer, and the
-     * frame's paint continuation awaits the PREVIOUS gate before writing
-     * pixels — without it, painting frame F+3 would overwrite the buffer
-     * while the present thread is still blitting frame F from it.
-     */
-    public volatile java.util.concurrent.CountDownLatch presentGate = new java.util.concurrent.CountDownLatch(0);
-
-    /**
-     * Chunk tasks submitted during the last transform phase, across all
-     * nesting levels. Diagnostics: proves nested composites forked.
-     */
-    public int lastTransformTaskCount;
-
-    /**
-     * Creates a new rendering context for full-screen rendering.
-     *
-     * <p>Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.</p>
-     *
-     * @param width             the rendering area width in pixels
-     * @param height            the rendering area height in pixels
-     * @param numRenderSegments number of parallel render segments (threads)
-     */
-    public RenderingContext(final int width, final int height, final int numRenderSegments) {
-        this(width, height, 0, height, 1, numRenderSegments, 1);
-    }
-
-    /**
-     * Creates a new rendering context with a rectangular tile grid.
-     *
-     * <p>Equivalent to the band-only constructors when {@code tilesX == 1}.
-     * In stereo mode ({@code viewportCount == 2}) each viewport gets its own
-     * tile grid; segment indices for viewport v start at
-     * {@code v * tilesX * tilesY}.</p>
-     *
-     * @param width         the rendering area width in pixels
-     * @param height        the rendering area height in pixels
-     * @param tilesX        tile columns per viewport (1 = bands only)
-     * @param tilesY        tile rows per viewport
-     * @param viewportCount number of side-by-side viewports (2 = stereo)
-     */
-    public RenderingContext(final int width, final int height,
-                            final int tilesX, final int tilesY,
-                            final int viewportCount) {
-        this(width, height, 0, height, tilesX, tilesY, viewportCount);
-    }
-
-    private RenderingContext(final int width, final int height,
-                             final int renderMinY, final int renderMaxY,
-                             final int tilesX, final int tilesY,
-                             final int viewportCount) {
-        this.width = width;
-        this.height = height;
-        this.renderMinY = renderMinY;
-        this.renderMaxY = renderMaxY;
-        this.tilesX = tilesX;
-        this.tilesY = tilesY;
-        this.viewportCount = viewportCount;
-        this.numRenderSegments = tilesX * tilesY * viewportCount;
-        this.centerCoordinate = new Point2D(width / 2d, height / 2d);
-        this.projectionScale = width / 3d;
-        this.stereoViewportWidth = width;
-        this.stereoViewportOffsetX = 0;
-        this.renderMinX = 0;
-        this.renderMaxX = width;
-
-        // Eagerly allocated so the developer-tools panel always finds it:
-        // transformPass() hands the pipeline a per-pass COPY of this context,
-        // and the copy constructor shares this reference. Lazy creation in
-        // ShapeCollection.transformShapesBegin() would only ever populate the
-        // throwaway pass copy, leaving this frame context null forever
-        // (the culling display then showed "-" permanently).
-        this.cullingStatistics = new CullingStatistics();
-        this.occlusionPyramid = new HiZPyramid();
-
-        bufferedImage = new BufferedImage(width, height, bufferedImageType);
-
-        final WritableRaster raster = bufferedImage.getRaster();
-        final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer();
-        pixels = dbi.getData();
-
-        // Z-buffer: one w-depth (biased 1/z) value per pixel, cleared per
-        // tile in the paint workers. Depth turns the queue order into a
-        // performance heuristic only; correctness comes from the per-pixel
-        // test. (The queue itself stays painter back-to-front — Z
-        // descending, see RenderAggregator.) Always allocated: the
-        // z-buffer path is the only renderer.
-        depth = new float[width * height];
-
-        graphics = (Graphics2D) bufferedImage.getGraphics();
-        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-
-        segmentGraphics = createSegmentGraphics();
-    }
-
-    /**
-     * Protected constructor for creating segment views.
-     * Shares the pixel buffer and graphics context with the parent.
-     *
-     * @param parent     the parent rendering context
-     * @param renderMinY minimum Y coordinate (inclusive) for this segment
-     * @param renderMaxY maximum Y coordinate (exclusive) for this segment
-     */
-    protected RenderingContext(final RenderingContext parent,
-                               final int renderMinY, final int renderMaxY) {
-        this.width = parent.width;
-        this.height = parent.height;
-        this.renderMinY = renderMinY;
-        this.renderMaxY = renderMaxY;
-        this.tilesX = parent.tilesX;
-        this.tilesY = parent.tilesY;
-        this.viewportCount = parent.viewportCount;
-        this.numRenderSegments = parent.numRenderSegments;
-        this.centerCoordinate = parent.centerCoordinate;
-        this.projectionScale = parent.projectionScale;
-        this.stereoViewportWidth = parent.stereoViewportWidth;
-        this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
-        this.stereoEye = parent.stereoEye;
-        this.renderMinX = parent.renderMinX;
-        this.renderMaxX = parent.renderMaxX;
-        this.bufferedImage = parent.bufferedImage;
-        this.pixels = parent.pixels;
-        this.depth = parent.depth;
-        this.graphics = parent.graphics;
-        this.vertexSlot = parent.vertexSlot;
-        this.nearPlaneDistance = parent.nearPlaneDistance;
-        this.developerTools = parent.developerTools;
-        this.debugLogBuffer = parent.debugLogBuffer;
-        this.lightingManager = parent.lightingManager;
-        this.occlusionPyramid = parent.occlusionPyramid;
-        this.segmentGraphics = null;
-    }
-
-    /**
-     * Creates an independent pass context for one pipeline pass (one eye
-     * in stereo): shares the frame's pixel buffer, graphics and services,
-     * but owns the per-pass projection fields (center, scale, stereo
-     * viewport, slot, frame/cycle stamps). The next pass's setup writes
-     * to its own copy, so it cannot disturb this pass's in-flight
-     * transform chunks or its asynchronous sort/bin/paint continuation.
-     *
-     * @param parent the frame rendering context to copy from
-     */
-    public RenderingContext(final RenderingContext parent) {
-        this.width = parent.width;
-        this.height = parent.height;
-        this.renderMinY = parent.renderMinY;
-        this.renderMaxY = parent.renderMaxY;
-        this.tilesX = parent.tilesX;
-        this.tilesY = parent.tilesY;
-        this.viewportCount = parent.viewportCount;
-        this.numRenderSegments = parent.numRenderSegments;
-        this.centerCoordinate = new Point2D(parent.centerCoordinate.x, parent.centerCoordinate.y);
-        this.projectionScale = parent.projectionScale;
-        this.stereoViewportWidth = parent.stereoViewportWidth;
-        this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
-        this.stereoEye = parent.stereoEye;
-        this.renderMinX = parent.renderMinX;
-        this.renderMaxX = parent.renderMaxX;
-        this.bufferedImage = parent.bufferedImage;
-        this.pixels = parent.pixels;
-        this.depth = parent.depth;
-        this.graphics = parent.graphics;
-        this.vertexSlot = parent.vertexSlot;
-        this.nearPlaneDistance = parent.nearPlaneDistance;
-        this.frameNumber = parent.frameNumber;
-        this.transformCycleId = parent.transformCycleId;
-        this.transformExecutor = parent.transformExecutor;
-        this.developerTools = parent.developerTools;
-        this.debugLogBuffer = parent.debugLogBuffer;
-        this.lightingManager = parent.lightingManager;
-        this.cullingStatistics = parent.cullingStatistics;
-        this.occlusionPyramid = parent.occlusionPyramid;
-        this.subpixelCullingThreshold = parent.subpixelCullingThreshold;
-        this.subpixelCullingEpoch = parent.subpixelCullingEpoch;
-        this.setMouseEvent(parent.getMouseEvent());
-        // Share the pre-clipped per-tile graphics: glyph rendering
-        // (user-facing text) draws through them by segment index. Null
-        // here made every glyph paint die with an NPE mid-tile (broken
-        // tiles whenever text faced the reader).
-        this.segmentGraphics = parent.segmentGraphics;
-        // frustum stays null: created fresh per pass in transformShapesBegin
-    }
-
-    /**
-     * Resets per-frame state in preparation for rendering a new frame.
-     * Increments the frame number and clears the mouse event state.
-     */
-    public void prepareForNewFrameRendering() {
-        frameNumber++;
-        mouseEvent = null;
-        currentObjectUnderMouseCursor = null;
-    }
-
-    /**
-     * Creates Graphics2D contexts for each render segment, pre-clipped to
-     * its tile rectangle. Segment index layout: viewport v, tile row ty,
-     * tile column tx -> v * tilesX * tilesY + ty * tilesX + tx.
-     *
-     * @return array of Graphics2D objects, one per segment
-     */
-    private Graphics2D[] createSegmentGraphics() {
-        final Graphics2D[] contexts = new Graphics2D[numRenderSegments];
-        final int viewportWidth = width / viewportCount;
-        final int tileW = viewportWidth / tilesX;
-        final int tileH = height / tilesY;
-
-        for (int v = 0; v < viewportCount; v++) {
-            final int viewportX = v * viewportWidth;
-            for (int ty = 0; ty < tilesY; ty++) {
-                final int minY = ty * tileH;
-                final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH;
-                for (int tx = 0; tx < tilesX; tx++) {
-                    final int minX = viewportX + tx * tileW;
-                    final int maxX = (tx == tilesX - 1)
-                            ? viewportX + viewportWidth : minX + tileW;
-
-                    final Graphics2D g = bufferedImage.createGraphics();
-                    g.setClip(minX, minY, maxX - minX, maxY - minY);
-                    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-                    g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
-                    contexts[v * tilesX * tilesY + ty * tilesX + tx] = g;
-                }
-            }
-        }
-
-        return contexts;
-    }
-
-    /**
-     * Returns the backing image whose pixel buffer the rasterizer paints into.
-     *
-     * <p>Exposed for headless rendering: after a transform/sort/paint pass the
-     * image holds the finished frame and can be saved or compared directly.</p>
-     *
-     * @return the backing buffered image
-     */
-    public BufferedImage getImage() {
-        return bufferedImage;
-    }
-
-    /**
-     * Returns the Graphics2D context for a specific render segment.
-     * Each segment's Graphics2D is pre-clipped to its Y bounds.
-     *
-     * @param segmentIndex the segment index (0 to numRenderSegments-1)
-     * @return the Graphics2D for that segment
-     * @throws NullPointerException if called on a segment view (not the main context)
-     */
-    public Graphics2D getSegmentGraphics(final int segmentIndex) {
-        return segmentGraphics[segmentIndex];
-    }
-
-    /**
-     * Disposes all Graphics2D resources associated with this context.
-     * Should be called when the context is no longer needed (e.g., on resize).
-     */
-    public void dispose() {
-        if (segmentGraphics != null) {
-            for (final Graphics2D g : segmentGraphics) {
-                if (g != null) {
-                    g.dispose();
-                }
-            }
-        }
-        if (graphics != null) {
-            graphics.dispose();
-        }
-    }
-
-    /**
-     * Executes a graphics operation in a thread-safe manner.
-     * This must be used for all Graphics2D operations (text, lines, etc.)
-     * during multi-threaded rendering.
-     *
-     * @param operation the graphics operation to execute
-     */
-    public void executeWithGraphics(final Consumer<Graphics2D> operation) {
-        synchronized (graphics) {
-            operation.accept(graphics);
-        }
-    }
-
-    /**
-     * Returns the pending mouse event for this frame, or {@code null} if none.
-     *
-     * @return the mouse event to process, or {@code null}
-     */
-    public MouseEvent getMouseEvent() {
-        return mouseEvent;
-    }
-
-    /**
-     * Sets the mouse event to be processed during this frame's rendering.
-     *
-     * @param mouseEvent the mouse event with position and button information
-     */
-    public void setMouseEvent(MouseEvent mouseEvent) {
-        this.mouseEvent = mouseEvent;
-    }
-
-    /**
-     * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}.
-     * Because objects are rendered back to front. The last method caller will set the top-most object, if
-     * there are multiple objects under mouse cursor.
-     *
-     * @param currentObjectUnderMouseCursor the object that is currently under the mouse cursor
-     */
-    public synchronized void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) {
-        setCurrentObjectUnderMouseCursor(currentObjectUnderMouseCursor,
-                Double.NaN, Double.NaN);
-    }
-
-    /**
-     * Called when given object was detected under mouse cursor, with the
-     * texture coordinates of the hit point (for textured shapes).
-     *
-     * @param currentObjectUnderMouseCursor the object under the mouse cursor
-     * @param textureU texture-space X of the hit point in primary-texture pixels
-     * @param textureV texture-space Y of the hit point in primary-texture pixels
-     */
-    public synchronized void setCurrentObjectUnderMouseCursor(
-            final MouseInteractionController currentObjectUnderMouseCursor,
-            final double textureU, final double textureV) {
-        this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
-        this.currentMouseTextureU = textureU;
-        this.currentMouseTextureV = textureV;
-    }
-
-    /**
-     * Returns the current object under the mouse cursor.
-     * Used by segment rendering to collect mouse results.
-     *
-     * @return the current object under mouse cursor, or null
-     */
-    public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
-        return currentObjectUnderMouseCursor;
-    }
-
-    /**
-     * Handles mouse events for components and returns whether a view repaint is needed.
-     *
-     * @return {@code true} if view update is needed as a consequence of this mouse event
-     */
-    public boolean handlePossibleComponentMouseEvent() {
-        if (mouseEvent == null) return false;
-
-        boolean viewRepaintNeeded = false;
-
-        if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) {
-            // Mouse cursor has just entered or left component.
-            viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited();
-            viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered();
-            objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor;
-        }
-
-        if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
-            // Mouse button was clicked on some component.
-            viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(
-                    mouseEvent.button, currentMouseTextureU, currentMouseTextureV);
-        } else if (currentObjectUnderMouseCursor != null)
-            // hover: let the component track the pointer position
-            viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover(
-                    currentMouseTextureU, currentMouseTextureV);
-
-        return viewRepaintNeeded;
-    }
-
-}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.java
deleted file mode 100644 (file)
index a7cc17b..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
-import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
-
-import java.awt.*;
-import java.util.function.Consumer;
-
-/**
- * A view of a RenderingContext for rendering a horizontal screen segment.
- *
- * <p>This class wraps a parent RenderingContext and provides its own Y-bounds
- * for multi-threaded rendering. All operations delegate to the parent context,
- * but with segment-specific Y bounds for pixel operations.</p>
- *
- * <p>Mouse tracking is local to each segment and must be combined after all
- * segments complete rendering.</p>
- *
- * @see RenderingContext
- */
-public class SegmentRenderingContext extends RenderingContext {
-
-    private final RenderingContext parent;
-    private final int segmentIndex;
-    private MouseInteractionController segmentMouseHit;
-    private double segmentMouseHitU = Double.NaN;
-    private double segmentMouseHitV = Double.NaN;
-
-    /**
-     * Creates a segment view of a parent rendering context.
-     *
-     * @param parent       the parent rendering context to delegate to
-     * @param renderMinY   minimum Y coordinate (inclusive) for this segment
-     * @param renderMaxY   maximum Y coordinate (exclusive) for this segment
-     * @param segmentIndex the index of this segment (0 to numRenderSegments-1)
-     */
-    public SegmentRenderingContext(final RenderingContext parent,
-                                    final int renderMinY, final int renderMaxY,
-                                    final int segmentIndex) {
-        super(parent, renderMinY, renderMaxY);
-        this.parent = parent;
-        this.segmentIndex = segmentIndex;
-    }
-
-    @Override
-    public void executeWithGraphics(final Consumer<Graphics2D> operation) {
-        operation.accept(parent.getSegmentGraphics(segmentIndex));
-    }
-
-    @Override
-    public MouseEvent getMouseEvent() {
-        return parent.getMouseEvent();
-    }
-
-    @Override
-    public void setMouseEvent(final MouseEvent mouseEvent) {
-        parent.setMouseEvent(mouseEvent);
-    }
-
-    @Override
-    public synchronized void setCurrentObjectUnderMouseCursor(final MouseInteractionController controller) {
-        setCurrentObjectUnderMouseCursor(controller, Double.NaN, Double.NaN);
-    }
-
-    @Override
-    public synchronized void setCurrentObjectUnderMouseCursor(
-            final MouseInteractionController controller,
-            final double textureU, final double textureV) {
-        this.segmentMouseHit = controller;
-        this.segmentMouseHitU = textureU;
-        this.segmentMouseHitV = textureV;
-    }
-
-    /**
-     * Returns the mouse hit detected in this segment.
-     *
-     * @return the MouseInteractionController that was under the mouse in this segment, or null
-     */
-    public MouseInteractionController getSegmentMouseHit() {
-        return segmentMouseHit;
-    }
-
-    /**
-     * Texture-space X of the hit point (primary texture pixels), NaN if none.
-     */
-    public double getSegmentMouseHitU() {
-        return segmentMouseHitU;
-    }
-
-    /**
-     * Texture-space Y of the hit point (primary texture pixels), NaN if none.
-     */
-    public double getSegmentMouseHitV() {
-        return segmentMouseHitV;
-    }
-
-    @Override
-    public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
-        return segmentMouseHit;
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/StereoEye.java
deleted file mode 100644 (file)
index aa9c99f..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-/**
- * Identifies which eye is being rendered in stereoscopic mode.
- *
- * @see RenderingContext#stereoEye
- */
-public enum StereoEye {
-    /** Normal single-view rendering (no stereo). */
-    NONE,
-    /** Left eye view in side-by-side stereo mode. */
-    LEFT,
-    /** Right eye view in side-by-side stereo mode. */
-    RIGHT
-}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadActivityRecorder.java
deleted file mode 100644 (file)
index eff1e39..0000000
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.gui;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Low-overhead recorder of per-thread work intervals for the thread
- * timeline display in the Developer Tools window.
- *
- * <p>Task submission sites (transform chunks, paint tiles, tile binning)
- * and render-thread phases (orchestration, waiting, blit) record their
- * [start, end) intervals here when enabled. The timeline component paints
- * each thread as a row, time on the X axis, the interval kind as color —
- * the software-renderer equivalent of a GPU frame-profiler occupancy
- * view. Idle time is simply the absence of intervals (black).</p>
- *
- * <p>Overhead when disabled: one volatile read per task. When enabled:
- * two {@code System.nanoTime()} calls, one atomic increment and four
- * array stores per task (~100 ns), negligible at hundreds of tasks per
- * frame.</p>
- *
- * <p>Frame parity distinguishes "current frame" work from "next frame"
- * work in the colors: transform/paint/binning kinds come in an even and
- * an odd variant, selected by the parity that was current when the task
- * was SUBMITTED (captured at task creation, so overlapping frames keep
- * their own color).</p>
- */
-public final class ThreadActivityRecorder {
-
-    /** Kind base: vertex transform chunk (add frame parity 0..2). */
-    public static final int KIND_TRANSFORM = 0;
-    /** Kind base: paint tile task (add frame parity 0..2). */
-    public static final int KIND_PAINT = 3;
-    /** Kind base: tile binning task (add frame parity 0..2). */
-    public static final int KIND_BIN = 6;
-    /** Kind: render thread orchestration (tree walk, submission, merges). */
-    public static final int KIND_RENDER = 9;
-    /** Kind: render thread blocked waiting for worker tasks. */
-    public static final int KIND_AWAIT = 10;
-    /** Kind: render thread blitting the finished frame to screen. */
-    public static final int KIND_BLIT = 11;
-    /** Kind: a pass's asynchronous continuation (drain, sort, bin, paint submission). */
-    public static final int KIND_PREP = 12;
-    /** Kind: continuation sub-phase: draining and merging transform chunks. */
-    public static final int KIND_DRAIN = 13;
-    /** Kind: continuation sub-phase: depth sort. */
-    public static final int KIND_SORT = 14;
-
-    /** Number of distinct kinds (three parities each for transform/paint/bin, plus 9..14). */
-    public static final int KIND_COUNT = 15;
-
-    private static final int CAPACITY = 1 << 18;
-    private static final int MASK = CAPACITY - 1;
-
-    private static final long[] starts = new long[CAPACITY];
-    private static final long[] ends = new long[CAPACITY];
-    private static final byte[] kinds = new byte[CAPACITY];
-    private static final byte[] rows = new byte[CAPACITY];
-    private static final AtomicInteger cursor = new AtomicInteger();
-
-    private static volatile boolean enabled = false;
-    private static volatile int frameParity = 0;
-
-    private static final ConcurrentHashMap<String, Integer> rowByThreadName = new ConcurrentHashMap<>();
-    private static final CopyOnWriteArrayList<String> rowNames = new CopyOnWriteArrayList<>();
-    private static final AtomicInteger nextRow = new AtomicInteger();
-
-    private ThreadActivityRecorder() {
-    }
-
-    /**
-     * @return true when recording is active (checked by task submission sites)
-     */
-    public static boolean isEnabled() {
-        return enabled;
-    }
-
-    /**
-     * Enables or disables recording. Enabling starts with a clean buffer
-     * and fresh thread-row assignment.
-     *
-     * @param value true to start recording
-     */
-    public static void setEnabled(final boolean value) {
-        if (value && !enabled) {
-            clear();
-        }
-        enabled = value;
-    }
-
-    /**
-     * Drops all recorded intervals and thread-row assignments.
-     */
-    public static void clear() {
-        cursor.set(0);
-        rowByThreadName.clear();
-        rowNames.clear();
-        nextRow.set(0);
-        // starts[]==0 marks an empty slot for the timeline sweep
-        java.util.Arrays.fill(starts, 0L);
-    }
-
-    /**
-     * Sets the parity (0/1) of the frame currently being prepared.
-     * Called by the render thread at the start of each frame; task
-     * submission sites capture it into their tasks so overlapping frames
-     * keep distinct colors.
-     *
-     * @param parity frame parity, 0 or 1
-     */
-    public static void setFrameParity(final int parity) {
-        frameParity = parity;
-    }
-
-    /**
-     * @return parity of the frame currently being prepared
-     */
-    public static int frameParity() {
-        return frameParity;
-    }
-
-    /**
-     * Records one work interval on the calling thread.
-     *
-     * @param kind   interval kind (one of the KIND_* bases, plus parity
-     *               for transform/paint/binning)
-     * @param t0     interval start, from {@link System#nanoTime()}
-     * @param t1     interval end, from {@link System#nanoTime()}
-     */
-    public static void record(final int kind, final long t0, final long t1) {
-        // Rows are keyed by thread NAME, not Thread object: after a
-        // stop()/start() cycle the executor is recreated with fresh
-        // threads under the same names, and they must reuse the same
-        // rows — otherwise the timeline fills with dead threads' rows
-        // and pushes the live workers below the visible area.
-        final String threadName = Thread.currentThread().getName();
-        Integer row = rowByThreadName.get(threadName);
-        if (row == null) {
-            row = rowByThreadName.computeIfAbsent(threadName, t -> {
-                final int r = nextRow.getAndIncrement();
-                rowNames.add(t);
-                return r;
-            });
-        }
-        if (row > 127) {
-            return;
-        }
-        final int i = cursor.getAndIncrement() & MASK;
-        starts[i] = t0;
-        ends[i] = t1;
-        kinds[i] = (byte) kind;
-        rows[i] = (byte) (int) row;
-    }
-
-    // ---- Snapshot access for the timeline component ----
-
-    /** @return total number of intervals recorded since the last clear */
-    public static int cursor() {
-        return cursor.get();
-    }
-
-    /** @return ring buffer capacity */
-    public static int capacity() {
-        return CAPACITY;
-    }
-
-    /** @return interval start array (index space of the ring buffer) */
-    public static long[] starts() {
-        return starts;
-    }
-
-    /** @return interval end array (index space of the ring buffer) */
-    public static long[] ends() {
-        return ends;
-    }
-
-    /** @return interval kind array (index space of the ring buffer) */
-    public static byte[] kinds() {
-        return kinds;
-    }
-
-    /** @return interval thread-row array (index space of the ring buffer) */
-    public static byte[] rows() {
-        return rows;
-    }
-
-    /** @return number of distinct thread rows seen so far */
-    public static int rowCount() {
-        return nextRow.get();
-    }
-
-    /**
-     * @param row thread row index
-     * @return display name for the row ("render" for the render thread,
-     *         otherwise the thread name with the e3d- prefix stripped)
-     */
-    public static String rowName(final int row) {
-        if (row >= rowNames.size()) {
-            return "?";
-        }
-        final String name = rowNames.get(row);
-        if ("e3d-render".equals(name)) {
-            return "render";
-        }
-        return name.startsWith("e3d-") ? name.substring(4) : name;
-    }
-}
index aafa7ae..ad9bf35 100644 (file)
@@ -3,6 +3,7 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
 
 import javax.swing.*;
 import java.awt.*;
index 96bd4bf..0a69725 100755 (executable)
@@ -3,6 +3,12 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye;
+import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 
 import eu.svjatoslav.aukio.e3d.diag.Diagnostics;
 import eu.svjatoslav.aukio.e3d.diag.EngineConfig;
@@ -317,7 +323,7 @@ public class ViewPanel extends Canvas {
      * Creates a new view panel with default settings.
      */
     public ViewPanel() {
-        frameListeners.add(camera);
+        frameListeners.add((panel, deltaMs) -> camera.onFrame(deltaMs));
         frameListeners.add(inputManager);
 
         // persistent log + telemetry (idempotent); the view telemetry
@@ -1117,7 +1123,7 @@ public class ViewPanel extends Canvas {
             // Walks the tree and forks heavy composites into chunk tasks,
             // but does NOT wait for them: draining happens inside the
             // pass's continuation on a worker thread.
-            rootShapeCollection.transformShapesBegin(this, passContext);
+            rootShapeCollection.transformShapesBegin(getCamera(), passContext);
             return passContext;
         } finally {
             location.x = originalX;
index 40f602a..24ddb36 100644 (file)
@@ -3,10 +3,11 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 
 /**
  * Tracks an object's position in view/camera space for distance and angle calculations.
index cd7db6c..b0b7d29 100644 (file)
@@ -5,7 +5,7 @@
 package eu.svjatoslav.aukio.e3d.gui.humaninput;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.gui.FrameListener;
 import eu.svjatoslav.aukio.e3d.gui.ViewFrame;
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
index dc9360f..f89d60e 100644 (file)
@@ -3,6 +3,7 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.gui.humaninput;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
 
index dcdc78b..152e69c 100644 (file)
@@ -4,7 +4,7 @@
  */
 package eu.svjatoslav.aukio.e3d.gui.humaninput;
 
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
 import eu.svjatoslav.aukio.e3d.gui.FrameListener;
 
index 1dc70e4..1bb7041 100644 (file)
  * <ul>
  *   <li>{@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} - The main rendering surface (JPanel)</li>
  *   <li>{@link eu.svjatoslav.aukio.e3d.gui.ViewFrame} - A JFrame with embedded ViewPanel</li>
- *   <li>{@link eu.svjatoslav.aukio.e3d.gui.Camera} - Represents the viewer's position and orientation</li>
+ *   <li>{@link eu.svjatoslav.aukio.e3d.geometry.Camera} - Represents the viewer's position and orientation</li>
  *   <li>{@link eu.svjatoslav.aukio.e3d.gui.DeveloperTools} - Debugging and profiling utilities</li>
  * </ul>
  *
  * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel
- * @see eu.svjatoslav.aukio.e3d.gui.Camera
+ * @see eu.svjatoslav.aukio.e3d.geometry.Camera
  */
 
-package eu.svjatoslav.aukio.e3d.gui;
\ No newline at end of file
+package eu.svjatoslav.aukio.e3d.gui;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
index 89469c6..1951873 100644 (file)
@@ -4,7 +4,7 @@
  */
 package eu.svjatoslav.aukio.e3d.gui.spacemouse;
 
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.gui.FrameListener;
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
 import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
index 3d32901..5c4d781 100644 (file)
@@ -9,7 +9,7 @@ import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
 import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 
 import java.util.Locale;
 
index 010c15e..d91db0a 100644 (file)
@@ -5,8 +5,8 @@
 package eu.svjatoslav.aukio.e3d.headless;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
 import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
 
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/Vertex.java
deleted file mode 100644 (file)
index 0eb8bcd..0000000
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.math;
-
-import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-
-/**
- * A vertex in 3D space with transformation and screen projection support.
- *
- * <p>A vertex represents a corner point of a polygon or polyhedron. In addition to
- * the 3D coordinate, it stores the transformed position (relative to viewer) and
- * the projected screen coordinates for rendering.</p>
- *
- * <p><b>Coordinate spaces:</b></p>
- * <ul>
- *   <li>{@link #coordinate} - Original position in local/model space</li>
- *   <li>{@link #transformedCoordinate} - Position relative to viewer (camera space)</li>
- *   <li>{@link #onScreenCoordinate} - 2D screen position after perspective projection</li>
- * </ul>
- *
- * <p><b>Example:</b></p>
- * <pre>{@code
- * Vertex v = new Vertex(new Point3D(10, 20, 30));
- * v.calculateLocationRelativeToViewer(transformStack, renderContext);
- * if (v.transformedCoordinate.z > 0) {
- *     // Vertex is in front of the camera
- * }
- * }</pre>
- *
- * @see Point3D
- * @see TransformStack
- */
-public class Vertex {
-
-    /**
-     * Vertex coordinate in local/model 3D space.
-     */
-    public Point3D coordinate;
-
-    /**
-     * Vertex coordinate relative to the viewer after transformation (camera
-     * space), per buffer slot. Slot parity lets the transform phase of the
-     * NEXT frame write slot B while the paint phase of the current frame
-     * still reads slot A (double-buffered pipeline). Never access directly:
-     * use {@link #transformedCoordinate(RenderingContext)}.
-     */
-    private final Point3D transformedCoordinate0 = new Point3D();
-    private final Point3D transformedCoordinate1 = new Point3D();
-    private final Point3D transformedCoordinate2 = new Point3D();
-
-    /**
-     * Vertex position on screen in pixels, per buffer slot.
-     * Use {@link #onScreenCoordinate(RenderingContext)}.
-     */
-    private final Point2D onScreenCoordinate0 = new Point2D();
-    private final Point2D onScreenCoordinate1 = new Point2D();
-    private final Point2D onScreenCoordinate2 = new Point2D();
-
-    /**
-     * Texture coordinate for UV mapping (optional).
-     */
-    public Point2D textureCoordinate;
-
-    /**
-     * Normal vector for this vertex (optional).
-     * Used by CSG operations for smooth interpolation during polygon splitting.
-     * Null for non-CSG usage; existing rendering code ignores this field.
-     */
-    public Point3D normal;
-
-
-    /**
-     * The transform cycle when each slot was last transformed (for
-     * caching). Keyed by {@link RenderingContext#transformCycleId}, NOT
-     * by frameNumber: in stereo mode both eye passes share the same
-     * parity context and therefore the same frameNumber, while using
-     * different slots — a frameNumber-based key lets one eye's pass
-     * falsely hit the cache entry the other eye wrote for the same slot
-     * an odd number of passes earlier, leaving opposite-eye (wrong
-     * viewport-offset) screen coordinates in the slot: the affected eye
-     * paints fully off-viewport, i.e. a black half-frame (observed as
-     * violent left/right flashing, 2026-09-09). transformCycleId is
-     * unique per pass, so a hit always means "already transformed within
-     * THIS pass" — the only correct dedupe semantics.
-     */
-    private long lastTransformCycle0 = -1;
-    private long lastTransformCycle1 = -1;
-    private long lastTransformCycle2 = -1;
-
-    /**
-     * Creates a vertex at the origin (0, 0, 0) with no texture coordinate.
-     */
-    public Vertex() {
-        this(new Point3D());
-    }
-
-    /**
-     * Creates a vertex at the specified position with no texture coordinate.
-     *
-     * @param coordinate the 3D position of this vertex
-     */
-    public Vertex(final Point3D coordinate) {
-        this(coordinate, null);
-    }
-
-    /**
-     * Creates a vertex at the specified position with an optional texture coordinate.
-     *
-     * @param coordinate        the 3D position of this vertex
-     * @param textureCoordinate the UV texture coordinate, or {@code null} for none
-     */
-    public Vertex(final Point3D coordinate, final Point2D textureCoordinate) {
-        this.coordinate = coordinate;
-        this.textureCoordinate = textureCoordinate;
-    }
-
-    /**
-     * Returns the camera-space coordinate for the rendering context's buffer
-     * slot. Valid only after this vertex was transformed for that slot's
-     * current frame.
-     *
-     * @param renderContext the rendering context (selects the buffer slot)
-     * @return the transformed coordinate (camera space) for the active slot
-     */
-    public Point3D transformedCoordinate(final RenderingContext renderContext) {
-        // Dual fields, not a slot array: measured 2026-09-05 (400-sphere
-        // scene) that array indexing adds a second dependent load per access
-        // and cost ~60% of transform phase time; a perfectly-predicted
-        // branch + direct field load is free.
-        final int slot = renderContext.vertexSlot;
-        return slot == 0 ? transformedCoordinate0
-                : slot == 1 ? transformedCoordinate1 : transformedCoordinate2;
-    }
-
-    /**
-     * Returns the screen-space position for the rendering context's buffer
-     * slot.
-     *
-     * @param renderContext the rendering context (selects the buffer slot)
-     * @return the on-screen coordinate (pixels) for the active slot
-     */
-    public Point2D onScreenCoordinate(final RenderingContext renderContext) {
-        final int slot = renderContext.vertexSlot;
-        return slot == 0 ? onScreenCoordinate0
-                : slot == 1 ? onScreenCoordinate1 : onScreenCoordinate2;
-    }
-
-
-    /**
-     * Transforms this vertex from model space to screen space.
-     *
-     * <p>This method applies the transform stack to compute the vertex position
-     * relative to the viewer, then projects it to 2D screen coordinates.
-     * Results are cached per-frame per-slot to avoid redundant calculations.</p>
-     *
-     * @param transforms    the transform stack to apply (world-to-camera transforms)
-     * @param renderContext the rendering context providing projection parameters
-     */
-    public void calculateLocationRelativeToViewer(final TransformStack transforms,
-                                                  final RenderingContext renderContext) {
-
-        final Point3D transformedCoordinate;
-        final Point2D onScreenCoordinate;
-        switch (renderContext.vertexSlot) {
-            case 0:
-                if (lastTransformCycle0 == renderContext.transformCycleId)
-                    return;
-                lastTransformCycle0 = renderContext.transformCycleId;
-                transformedCoordinate = transformedCoordinate0;
-                onScreenCoordinate = onScreenCoordinate0;
-                break;
-            case 1:
-                if (lastTransformCycle1 == renderContext.transformCycleId)
-                    return;
-                lastTransformCycle1 = renderContext.transformCycleId;
-                transformedCoordinate = transformedCoordinate1;
-                onScreenCoordinate = onScreenCoordinate1;
-                break;
-            default:
-                if (lastTransformCycle2 == renderContext.transformCycleId)
-                    return;
-                lastTransformCycle2 = renderContext.transformCycleId;
-                transformedCoordinate = transformedCoordinate2;
-                onScreenCoordinate = onScreenCoordinate2;
-                break;
-        }
-        transforms.transform(coordinate, transformedCoordinate);
-        onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale);
-        onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
-        onScreenCoordinate.add(renderContext.centerCoordinate);
-        onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
-    }
-
-    /**
-     * Writes a camera-space position directly into this vertex's slot state
-     * and projects it to screen coordinates, bypassing the transform stack.
-     *
-     * <p>Used by near-plane clipping ({@code AbstractCoordinateShape}), which
-     * creates intersection vertices that exist ONLY in camera space — there
-     * is no model-space coordinate to transform. The given z must be &gt; 0
-     * (clip against the near plane guarantees z == nearPlaneDistance).</p>
-     *
-     * @param x             camera-space X
-     * @param y             camera-space Y
-     * @param z             camera-space Z (depth in front of viewer, &gt; 0)
-     * @param renderContext the rendering context (selects the buffer slot and
-     *                      provides projection parameters)
-     */
-    public void setCameraSpaceCoordinate(final double x, final double y, final double z,
-                                         final RenderingContext renderContext) {
-        final Point3D transformedCoordinate;
-        final Point2D onScreenCoordinate;
-        switch (renderContext.vertexSlot) {
-            case 0:
-                transformedCoordinate = transformedCoordinate0;
-                onScreenCoordinate = onScreenCoordinate0;
-                break;
-            case 1:
-                transformedCoordinate = transformedCoordinate1;
-                onScreenCoordinate = onScreenCoordinate1;
-                break;
-            default:
-                transformedCoordinate = transformedCoordinate2;
-                onScreenCoordinate = onScreenCoordinate2;
-                break;
-        }
-        transformedCoordinate.x = x;
-        transformedCoordinate.y = y;
-        transformedCoordinate.z = z;
-        onScreenCoordinate.x = ((x / z) * renderContext.projectionScale);
-        onScreenCoordinate.y = ((y / z) * renderContext.projectionScale);
-        onScreenCoordinate.add(renderContext.centerCoordinate);
-        onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
-    }
-
-    // ========== CSG support methods ==========
-
-    /**
-     * Creates a deep copy of this vertex.
-     * Clones the coordinate, normal (if present), and texture coordinate (if present).
-     * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame).
-     *
-     * @return a new Vertex with cloned data
-     */
-    public Vertex clone() {
-        final Vertex result = new Vertex(new Point3D(coordinate),
-                textureCoordinate != null ? new Point2D(textureCoordinate) : null);
-        if (normal != null) {
-            result.normal = new Point3D(normal);
-        }
-        return result;
-    }
-
-    /**
-     * Flips the orientation of this vertex by negating the normal vector.
-     * Called when the orientation of a polygon is flipped during CSG operations.
-     * If normal is null, this method does nothing.
-     */
-    public void flip() {
-        if (normal != null) {
-            normal = normal.withNegated();
-        }
-    }
-
-    /**
-     * Creates a new vertex between this vertex and another by linearly interpolating
-     * all properties using parameter t.
-     *
-     * <p>Interpolates: position, normal (if present), and texture coordinate (if present).</p>
-     *
-     * @param other the other vertex to interpolate towards
-     * @param t     the interpolation parameter (0 = this vertex, 1 = other vertex)
-     * @return a new Vertex representing the interpolated position
-     */
-    public Vertex interpolate(final Vertex other, final double t) {
-        final Vertex result = new Vertex(
-                coordinate.interpolate(other.coordinate, t),
-                (textureCoordinate != null && other.textureCoordinate != null)
-                        ? new Point2D(
-                        textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
-                        textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t)
-                        : null
-        );
-        if (normal != null && other.normal != null) {
-            result.normal = normal.interpolate(other.normal, t);
-        }
-        return result;
-    }
-}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/IntegerPoint.java
deleted file mode 100644 (file)
index 01acee1..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Aukio 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.aukio.e3d.renderer.octree;
-
-/**
- * Point in 3D space with integer coordinates. Used for octree voxel positions.
- */
-public class IntegerPoint
-{
-    /** X coordinate. */
-    public int x;
-    /** Y coordinate. */
-    public int y;
-    /** Z coordinate. */
-    public int z = 0;
-
-    /**
-     * Creates a point at the origin (0, 0, 0).
-     */
-    public IntegerPoint()
-    {
-    }
-
-    /**
-     * Creates a point with the specified coordinates.
-     *
-     * @param x the X coordinate
-     * @param y the Y coordinate
-     * @param z the Z coordinate
-     */
-    public IntegerPoint(final int x, final int y, final int z)
-    {
-        this.x = x;
-        this.y = y;
-        this.z = z;
-    }
-}
index 5ae69d8..d07f08d 100755 (executable)
@@ -3,6 +3,7 @@
  * This project is released under Creative Commons Zero (CC0) license.
  */
 package eu.svjatoslav.aukio.e3d.renderer.octree;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
 import eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.Ray;
index 4509e7e..f79029e 100755 (executable)
@@ -9,7 +9,7 @@
  * <ul>
  *   <li>{@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume} - the main octree data structure
  *       for storing and querying voxel cells</li>
- *   <li>{@link eu.svjatoslav.aukio.e3d.renderer.octree.IntegerPoint} - integer 3D coordinate used
+ *   <li>{@link eu.svjatoslav.aukio.e3d.geometry.IntegerPoint} - integer 3D coordinate used
  *       for voxel addressing</li>
  * </ul>
  *
@@ -17,4 +17,5 @@
  */
 
 package eu.svjatoslav.aukio.e3d.renderer.octree;
+import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint;
 
index fa36212..5813889 100644 (file)
@@ -5,7 +5,7 @@
 package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
 
 import static eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE;
index ed67441..d4721d0 100755 (executable)
@@ -5,7 +5,7 @@
 package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.math.Matrix3x3;
 import eu.svjatoslav.aukio.e3d.math.Transform;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java
new file mode 100644 (file)
index 0000000..95bfc4e
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Statistics for frustum culling, tracking composite-level culling efficiency.
+ *
+ * <p>Updated each frame during the rendering pipeline:</p>
+ * <ul>
+ *   <li>{@link #totalComposites} - incremented before each composite's frustum test</li>
+ *   <li>{@link #culledComposites} - incremented when a composite fails the frustum test</li>
+ * </ul>
+ *
+ * <p>Thread safety: counters are {@link AtomicInteger} because the parallel
+ * transform phase increments them from multiple worker threads.</p>
+ *
+ * <p>Displayed in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel} to help developers understand
+ * culling efficiency and optimize scene graphs.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.Frustum
+ */
+public class CullingStatistics {
+
+    /**
+     * Total number of composite shapes tested against the frustum this frame.
+     * Incremented before each composite's AABB frustum test.
+     * Does not include the root composite (which is never frustum-tested).
+     */
+    public final AtomicInteger totalComposites = new AtomicInteger(0);
+
+    /**
+     * Number of composite shapes that were entirely outside the frustum and skipped.
+     * When a composite is culled, all its children (shapes and nested composites)
+     * are skipped without individual testing.
+     */
+    public final AtomicInteger culledComposites = new AtomicInteger(0);
+
+    /**
+     * Resets all statistics to zero.
+     * Called at the start of each frame before computing new statistics.
+     */
+    public void reset() {
+        totalComposites.set(0);
+        culledComposites.set(0);
+    }
+
+    /**
+     * Returns the percentage of composites that were culled.
+     *
+     * @return the culled percentage (0-100), or 0 if there are no composites
+     */
+    public double getCulledPercentage() {
+        final int total = totalComposites.get();
+        if (total == 0) {
+            return 0.0;
+        }
+        return 100.0 * culledComposites.get() / total;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java
new file mode 100644 (file)
index 0000000..47f7f39
--- /dev/null
@@ -0,0 +1,269 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.geometry.Box;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane;
+
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
+
+/**
+ * View frustum for frustum culling - eliminates objects outside the camera's view.
+ *
+ * <p>The frustum is a truncated pyramid-shaped volume that represents everything
+ * the camera can see. Objects completely outside this volume can be skipped
+ * during rendering, significantly improving performance for large scenes.</p>
+ *
+ * <p><b>Frustum planes:</b></p>
+ * <ul>
+ *   <li>Left, Right, Top, Bottom - define the viewport edges</li>
+ *   <li>Near - closest visible distance from camera</li>
+ *   <li>Far - farthest visible distance from camera</li>
+ * </ul>
+ *
+ * <p><b>Usage:</b></p>
+ * <pre>{@code
+ * Frustum frustum = new Frustum();
+ * frustum.update(camera, screenWidth, screenHeight);
+ *
+ * Box objectBounds = shape.getBoundingBox();
+ * if (frustum.intersectsAABB(objectBounds)) {
+ *     // Object is potentially visible - render it
+ * } else {
+ *     // Object outside frustum - skip rendering
+ * }
+ * }</pre>
+ *
+ * <p><b>AABB intersection algorithm:</b></p>
+ * <p>Uses the optimized "P-vertex" approach: for each plane, we test only
+ * the AABB corner most aligned with the plane normal. If this corner is
+ * behind the plane, the entire AABB is outside the frustum.</p>
+ *
+ * @see Box axis-aligned bounding box for culling tests
+ * @see Camera provides position and orientation for frustum computation
+ */
+public class Frustum {
+
+    /**
+     * Index for the left clipping plane.
+     */
+    public static final int LEFT = 0;
+    /**
+     * Index for the right clipping plane.
+     */
+    public static final int RIGHT = 1;
+    /**
+     * Index for the top clipping plane.
+     */
+    public static final int TOP = 2;
+    /**
+     * Index for the bottom clipping plane.
+     */
+    public static final int BOTTOM = 3;
+    /**
+     * Index for the near clipping plane.
+     */
+    public static final int NEAR = 4;
+    /**
+     * Index for the far clipping plane.
+     */
+    public static final int FAR = 5;
+
+    /**
+     * The six clipping planes defining the frustum volume.
+     * Each plane is stored as (normal, distance) in Hesse normal form.
+     * Planes are in world space coordinates.
+     */
+    private final Plane[] planes = new Plane[6];
+
+    /**
+     * Default near plane distance from camera (in world units).
+     * Objects closer than this are culled.
+     */
+    private double nearDistance = 1.0;
+
+    /**
+     * Default far plane distance from camera (in world units).
+     * Objects farther than this are culled.
+     */
+    private double farDistance = 10000.0;
+
+    /**
+     * Creates a new frustum with uninitialized planes.
+     * Call {@link #update} before using for culling.
+     */
+    public Frustum() {
+        for (int i = 0; i < 6; i++) {
+            planes[i] = new Plane(new Point3D(0, 0, 1), 0);
+        }
+    }
+
+    /**
+     * Updates the frustum planes in view space (camera at origin, looking along +Z).
+     *
+     * <p>This method should be called once per frame before rendering, after the
+     * camera position and orientation have been updated.</p>
+     *
+     * <p><b>View space coordinate system:</b></p>
+     * <ul>
+     *   <li>Camera at origin (0, 0, 0)</li>
+     *   <li>Forward = +Z axis (looking into the screen)</li>
+     *   <li>Right = +X axis</li>
+     *   <li>Up = -Y axis (since Y-down means smaller Y is higher visually)</li>
+     * </ul>
+     *
+     * <p><b>Plane normals point INTO the frustum</b> (toward the visible volume).
+     * A point is inside if dot(normal, point) >= distance for all planes.</p>
+     *
+     * <p><b>FOV calculation:</b> The Aukio 3D engine uses projectionScale = width/3.
+     * This means tan(halfHFOV) = (width/2) / projectionScale = 1.5, giving a
+     * horizontal FOV of approximately 112 degrees.</p>
+     *
+     * @param camera the camera (used only for aspect ratio derivation from width/height)
+     * @param width  the viewport width in pixels (defines projectionScale)
+     * @param height the viewport height in pixels (used for vertical FOV)
+     */
+    public void update(final Camera camera, final int width, final int height) {
+        // Frustum is computed in VIEW SPACE (camera at origin, looking along +Z)
+        // This matches the coordinate system after applying camera transforms
+
+        // Aukio 3D uses projectionScale = width/3
+        // tan(halfFOV) = (halfSize) / projectionScale
+        final double projectionScale = width / 3.0;
+        final double tanHalfHFOV = (width / 2.0) / projectionScale;  // = 1.5 (very wide FOV)
+        final double tanHalfVFOV = (height / 2.0) / projectionScale; // depends on aspect ratio
+
+        // Compute cosine and sine of half-FOV angles
+        // cosHalfFOV = 1 / sqrt(1 + tanHalfFOV^2)
+        // sinHalfFOV = tanHalfFOV * cosHalfFOV
+        final double cosHalfHFOV = 1.0 / Math.sqrt(1.0 + tanHalfHFOV * tanHalfHFOV);
+        final double sinHalfHFOV = tanHalfHFOV * cosHalfHFOV;
+        final double cosHalfVFOV = 1.0 / Math.sqrt(1.0 + tanHalfVFOV * tanHalfVFOV);
+        final double sinHalfVFOV = tanHalfVFOV * cosHalfVFOV;
+
+        // Near and far distances
+        nearDistance = 1.0;
+        farDistance = 10000.0;
+
+        // All side planes pass through origin (camera position in view space)
+        // Plane equation: dot(normal, point) >= distance means inside
+
+        // Left plane: inward normal pointing right-forward
+        // Bounds: x >= -tanHalfHFOV * z (to the right of left edge)
+        planes[LEFT].normal = new Point3D(cosHalfHFOV, 0, sinHalfHFOV);
+        planes[LEFT].distance = 0;
+
+        // Right plane: inward normal pointing left-forward
+        // Bounds: x <= tanHalfHFOV * z (to the left of right edge)
+        planes[RIGHT].normal = new Point3D(-cosHalfHFOV, 0, sinHalfHFOV);
+        planes[RIGHT].distance = 0;
+
+        // Top plane: inward normal pointing down-forward (Y-down system, top is smaller Y)
+        // Bounds: y <= tanHalfVFOV * z (below top edge, smaller Y)
+        planes[TOP].normal = new Point3D(0, -cosHalfVFOV, sinHalfVFOV);
+        planes[TOP].distance = 0;
+
+        // Bottom plane: inward normal pointing up-forward (larger Y is below)
+        // Bounds: y >= -tanHalfVFOV * z (above bottom edge, larger Y)
+        planes[BOTTOM].normal = new Point3D(0, cosHalfVFOV, sinHalfVFOV);
+        planes[BOTTOM].distance = 0;
+
+        // Near plane: inward normal pointing forward (+Z)
+        // Bounds: z >= nearDistance (in front of near plane)
+        planes[NEAR].normal = new Point3D(0, 0, 1);
+        planes[NEAR].distance = nearDistance;
+
+        // Far plane: inward normal pointing backward (-Z)
+        // Bounds: z <= farDistance (behind far plane)
+        planes[FAR].normal = new Point3D(0, 0, -1);
+        planes[FAR].distance = -farDistance;
+    }
+
+    /**
+     * Tests whether an axis-aligned bounding box intersects the frustum.
+     *
+     * <p>This is a conservative test: returns {@code true} if the box is
+     * potentially visible (inside or partially inside the frustum), and
+     * {@code false} only if the box is completely outside all frustum planes.</p>
+     *
+     * <p><b>Optimized algorithm:</b></p>
+     * <p>For each plane, we test only the AABB corner most aligned with the
+     * plane normal (the "P-vertex"). If this corner is behind the plane,
+     * the entire AABB must be outside the frustum.</p>
+     *
+     * @param box the axis-aligned bounding box to test (in view space coordinates)
+     * @return {@code true} if the box intersects or is inside the frustum,
+     *         {@code false} if completely outside
+     */
+    public boolean intersectsAABB(final Box box) {
+        // Get box min/max for each axis
+        final double minX = box.getMinX();
+        final double maxX = box.getMaxX();
+        final double minY = box.getMinY();
+        final double maxY = box.getMaxY();
+        final double minZ = box.getMinZ();
+        final double maxZ = box.getMaxZ();
+
+        for (int i = 0; i < 6; i++) {
+            final Plane plane = planes[i];
+            final Point3D n = plane.normal;
+            final double d = plane.distance;
+
+            // Find the P-vertex: the corner most aligned with the plane normal
+            // If normal component is positive, use max; if negative, use min
+            final double px = (n.x > 0) ? maxX : minX;
+            final double py = (n.y > 0) ? maxY : minY;
+            final double pz = (n.z > 0) ? maxZ : minZ;
+
+            // Test if P-vertex is outside the frustum (behind the plane)
+            // For inward-pointing normals: inside = dot(N,P) >= distance
+            // So outside = dot(N,P) < distance
+            if (n.x * px + n.y * py + n.z * pz < d) {
+                return false; // AABB entirely outside this plane
+            }
+        }
+
+        return true; // AABB intersects or inside all planes
+    }
+
+    /**
+     * Returns the near clipping plane distance.
+     *
+     * @return the near distance in world units
+     */
+    public double getNearDistance() {
+        return nearDistance;
+    }
+
+    /**
+     * Returns the far clipping plane distance.
+     *
+     * @return the far distance in world units
+     */
+    public double getFarDistance() {
+        return farDistance;
+    }
+
+    /**
+     * Sets the near and far clipping distances.
+     *
+     * @param near the near plane distance (objects closer are culled)
+     * @param far  the far plane distance (objects farther are culled)
+     */
+    public void setClipDistances(final double near, final double far) {
+        this.nearDistance = near;
+        this.farDistance = far;
+    }
+
+    /**
+     * Returns a specific frustum plane for debugging or advanced usage.
+     *
+     * @param planeIndex one of LEFT, RIGHT, TOP, BOTTOM, NEAR, FAR
+     * @return the plane at the specified index
+     */
+    public Plane getPlane(final int planeIndex) {
+        return planes[planeIndex];
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java
new file mode 100644 (file)
index 0000000..e04e579
--- /dev/null
@@ -0,0 +1,198 @@
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hierarchical depth pyramid for whole-block occlusion culling
+ * (Hi-Z). Built from the just-painted frame's depth buffer; queried
+ * during the NEXT frame's transform to skip blocks that are fully
+ * hidden behind what was drawn last frame.
+ *
+ * <p>Semantics: each tile stores the MINIMUM w (= 1/z, i.e. the
+ * FARTHEST written depth) over its pixels. A block whose nearest
+ * possible point (max w over its AABB corners) is farther than a
+ * tile's farthest written depth is behind something at every written
+ * pixel of that tile — and tiles with any unwritten (sky) pixel hold
+ * -infinity and never occlude. That makes the test conservative: it
+ * may keep a hidden block, it never culls a visible one — under a
+ * static camera. (The first version max-pooled, storing the NEAREST
+ * depth per tile; that culled houses visible BETWEEN nearer tree
+ * trunks — per-pixel gaps inside a tile are invisible to a max.)
+ * Camera motion reuses the stale pyramid, which can cull a
+ * newly-visible block wrongly; the block's pixels then contain far
+ * background depth, so the next frame's pyramid no longer occludes
+ * it — wrong culls self-heal in one frame (sanctioned design
+ * concession), and the query margin absorbs small-motion
+ * parallax.</p>
+ *
+ * <p>Empty tiles (never depth-written) store -infinity and never
+ * occlude, so the first frame after startup (or after any pass that
+ * leaves the pyramid unbuilt) culls nothing.</p>
+ *
+ * <p>Knobs: {@code -Daukio.hiz=false} disables culling (build still
+ * happens — it is cheap — so flipping the flag needs no warm-up);
+ * {@code -Daukio.hiz.margin=0.02} sets the relative w-space safety
+ * margin against parallax between frames.</p>
+ */
+public final class HiZPyramid {
+
+    /** Level-0 tile edge in pixels; level i tiles cover TILE*2^i px. */
+    private static final int TILE = 8;
+
+    private static final boolean ENABLED = Boolean.parseBoolean(
+            System.getProperty("aukio.hiz", "true"));
+    private static final double MARGIN = Double.parseDouble(
+            System.getProperty("aukio.hiz.margin", "0.02"));
+
+    /** Blocks occlusion-tested this process (telemetry). */
+    public final AtomicLong blocksTested = new AtomicLong();
+    /** Blocks culled as fully occluded (telemetry). */
+    public final AtomicLong blocksCulled = new AtomicLong();
+
+    private float[] tiles = new float[0];
+    private int[] levelOff = new int[0];
+    private int[] levelW = new int[0];
+    private int[] levelH = new int[0];
+    private int levels;
+    private int bufW = -1, bufH = -1;
+
+    /** Rebuilds the pyramid from a freshly painted depth buffer. */
+    public synchronized void buildFrom(final float[] depth,
+                                       final int width, final int height) {
+        if (width != bufW || height != bufH) {
+            allocate(width, height);
+        }
+
+        // Level 0: min-pool depth into TILE x TILE tiles. Depth
+        // outside the painted area is -infinity (cleared), so a tile
+        // with any unpainted pixel never occludes.
+        final int w0 = levelW[0], h0 = levelH[0];
+        final int off0 = levelOff[0];
+        for (int ty = 0; ty < h0; ty++) {
+            final int yEnd = Math.min((ty + 1) * TILE, height);
+            for (int tx = 0; tx < w0; tx++) {
+                final int xEnd = Math.min((tx + 1) * TILE, width);
+                float m = Float.POSITIVE_INFINITY;
+                for (int y = ty * TILE; y < yEnd; y++) {
+                    final int row = y * width;
+                    for (int x = tx * TILE; x < xEnd; x++)
+                        if (depth[row + x] < m)
+                            m = depth[row + x];
+                }
+                tiles[off0 + ty * w0 + tx] = m;
+            }
+        }
+
+        // Higher levels: min of 2x2 children.
+        for (int l = 1; l < levels; l++) {
+            final int pw = levelW[l - 1], ph = levelH[l - 1];
+            final int poff = levelOff[l - 1];
+            final int cw = levelW[l], ch = levelH[l];
+            final int coff = levelOff[l];
+            for (int ty = 0; ty < ch; ty++)
+                for (int tx = 0; tx < cw; tx++) {
+                    float m = Float.POSITIVE_INFINITY;
+                    for (int dy = 0; dy < 2; dy++)
+                        for (int dx = 0; dx < 2; dx++) {
+                            final int sx = tx * 2 + dx, sy = ty * 2 + dy;
+                            if (sx < pw && sy < ph) {
+                                final float v = tiles[poff + sy * pw + sx];
+                                if (v < m)
+                                    m = v;
+                            }
+                        }
+                    tiles[coff + ty * cw + tx] = m;
+                }
+        }
+    }
+
+    /**
+     * Conservative whole-block occlusion test.
+     *
+     * @param x1..y2    screen-space AABB of the block (will be clamped
+     *                  to the buffer; a fully off-screen box returns
+     *                  false)
+     * @param nearestW  the block's nearest possible depth = MAX 1/z
+     *                  over its corners
+     * @return true when the block is certainly hidden behind last
+     *         frame's occluders (within the parallax margin)
+     */
+    public synchronized boolean occluded(final double x1, final double y1,
+                            final double x2, final double y2,
+                            final double nearestW) {
+        if (!ENABLED || levels == 0)
+            return false;
+
+        int bx1 = (int) Math.floor(x1), by1 = (int) Math.floor(y1);
+        int bx2 = (int) Math.ceil(x2), by2 = (int) Math.ceil(y2);
+        if (bx1 < 0) bx1 = 0;
+        if (by1 < 0) by1 = 0;
+        if (bx2 >= bufW) bx2 = bufW - 1;
+        if (by2 >= bufH) by2 = bufH - 1;
+        if (bx1 > bx2 || by1 > by2)
+            return false;
+
+        // Coarsest level where the box still covers <= 2 tiles per axis.
+        int level = 0;
+        while (level + 1 < levels) {
+            final int s = TILE << (level + 1);
+            final int tw = (bx2 / s) - (bx1 / s) + 1;
+            final int th = (by2 / s) - (by1 / s) + 1;
+            if (tw > 2 || th > 2)
+                break;
+            level++;
+        }
+
+        final int s = TILE << level;
+        final int tx1 = bx1 / s, ty1 = by1 / s;
+        final int tx2 = bx2 / s, ty2 = by2 / s;
+        final int w = levelW[level], off = levelOff[level];
+
+        float minStored = Float.POSITIVE_INFINITY;
+        for (int ty = ty1; ty <= ty2; ty++)
+            for (int tx = tx1; tx <= tx2; tx++) {
+                final float v = tiles[off + ty * w + tx];
+                if (v < minStored)
+                    minStored = v;
+            }
+
+        // Occluded only when the block's nearest point is clearly
+        // behind the farthest written depth in the range; the
+        // relative margin absorbs parallax between frames.
+        return nearestW < minStored * (1.0 - MARGIN);
+    }
+
+    private void allocate(final int width, final int height) {
+        bufW = width;
+        bufH = height;
+        int lw = (width + TILE - 1) / TILE;
+        int lh = (height + TILE - 1) / TILE;
+        int count = 0;
+        levels = 0;
+        while (true) {
+            levels++;
+            count += lw * lh;
+            if (lw == 1 && lh == 1)
+                break;
+            lw = Math.max(1, (lw + 1) / 2);
+            lh = Math.max(1, (lh + 1) / 2);
+        }
+        tiles = new float[count];
+        levelOff = new int[levels];
+        levelW = new int[levels];
+        levelH = new int[levels];
+        lw = (width + TILE - 1) / TILE;
+        lh = (height + TILE - 1) / TILE;
+        int off = 0;
+        for (int l = 0; l < levels; l++) {
+            levelOff[l] = off;
+            levelW[l] = lw;
+            levelH[l] = lh;
+            off += lw * lh;
+            lw = Math.max(1, (lw + 1) / 2);
+            lh = Math.max(1, (lh + 1) / 2);
+        }
+        Arrays.fill(tiles, Float.NEGATIVE_INFINITY);
+    }
+}
index 2a48f08..54f544f 100644 (file)
@@ -95,7 +95,7 @@ public final class ParallelTransformCoordinator {
      * Frame parity captured at construction, stamped onto recorded
      * timeline intervals so overlapping frames keep distinct colors.
      */
-    private final int traceParity = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.frameParity();
+    private final int traceParity = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity();
 
     /**
      * Frame-wide cap on chunk tasks. Deep hierarchies of heavy composites
@@ -133,15 +133,15 @@ public final class ParallelTransformCoordinator {
      * @param task transforms a chunk of children into a private aggregator
      */
     public void submit(final Callable<RenderAggregator> task) {
-        if (eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled()) {
+        if (eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled()) {
             final int parity = traceParity;
             futures.add(executor.submit(() -> {
                 final long t0 = System.nanoTime();
                 try {
                     return task.call();
                 } finally {
-                    eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
-                            eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_TRANSFORM + parity,
+                    eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+                            eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_TRANSFORM + parity,
                             t0, System.nanoTime());
                 }
             }));
@@ -173,7 +173,7 @@ public final class ParallelTransformCoordinator {
      * @param target the root aggregator to merge results into
      */
     public void drainAndMergeInto(final RenderAggregator target) {
-        final boolean trace = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
+        final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
         final java.util.List<RenderAggregator> parts = new java.util.ArrayList<>();
         Future<RenderAggregator> future;
         while ((future = futures.poll()) != null) {
@@ -181,8 +181,8 @@ public final class ParallelTransformCoordinator {
                 final long t0 = trace ? System.nanoTime() : 0;
                 parts.add(future.get());
                 if (trace) {
-                    eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
-                            eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_AWAIT,
+                    eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+                            eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_AWAIT,
                             t0, System.nanoTime());
                 }
             } catch (final InterruptedException e) {
index ed0f794..f8701ba 100644 (file)
@@ -194,14 +194,14 @@ final class RadixLongSort {
             final int to = Math.min(n, from + chunk);
             futures.add(executor.submit(() -> {
                 final boolean trace =
-                        eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
+                        eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
                 final long t0 = trace ? System.nanoTime() : 0;
                 try {
                     work.run(ti, f, to);
                 } finally {
                     if (trace)
-                        eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
-                                eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_SORT,
+                        eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
+                                eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_SORT,
                                 t0, System.nanoTime());
                 }
             }));
index 698a51b..1e9ab17 100644 (file)
@@ -4,7 +4,6 @@
  */
 package eu.svjatoslav.aukio.e3d.renderer.raster;
 
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
 
 import java.io.Serializable;
@@ -546,9 +545,9 @@ public class RenderAggregator {
                           final boolean parallel,
                           final ExecutorService executor) {
         final int size = sortedCount;
-        final boolean trace = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.isEnabled();
-        final int traceKind = eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.KIND_BIN
-                + eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.frameParity();
+        final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled();
+        final int traceKind = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_BIN
+                + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity();
         final java.util.List<Future<?>> futures =
                 parallel ? new java.util.ArrayList<>(chunkCount) : null;
         for (int c = 0; c < chunkCount; c++) {
@@ -563,7 +562,7 @@ public class RenderAggregator {
                             chunk * tileCount, countMode);
                 } finally {
                     if (trace)
-                        eu.svjatoslav.aukio.e3d.gui.ThreadActivityRecorder.record(
+                        eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record(
                                 traceKind, t0, System.nanoTime());
                 }
             };
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java
new file mode 100644 (file)
index 0000000..3ba085b
--- /dev/null
@@ -0,0 +1,709 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer;
+import eu.svjatoslav.aukio.e3d.gui.DeveloperTools;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point2D;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.WritableRaster;
+import java.util.concurrent.ExecutorService;
+import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
+import java.util.function.Consumer;
+
+/**
+ * Contains all state needed to render a single frame: the pixel buffer, graphics context,
+ * screen dimensions, and mouse event tracking.
+ *
+ * <p>A new {@code RenderingContext} is created whenever the view panel is resized.
+ * During rendering, shapes use this context to:</p>
+ * <ul>
+ *   <li>Access the raw pixel array ({@link #pixels}) for direct pixel manipulation</li>
+ *   <li>Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing</li>
+ *   <li>Read screen dimensions ({@link #width}, {@link #height}) and the
+ *       {@link #centerCoordinate} for coordinate projection</li>
+ *   <li>Use the {@link #projectionScale} factor for perspective projection</li>
+ * </ul>
+ *
+ * <p>The context also manages mouse interaction detection: as shapes are painted
+ * back-to-front, each shape can report itself as the object under the mouse cursor.
+ * After painting completes, the topmost shape receives the mouse event.</p>
+ *
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel the panel that creates and manages this context
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext)
+ */
+public class RenderingContext {
+
+    /**
+     * The {@link BufferedImage} pixel format used for the rendering buffer.
+     * TYPE_INT_RGB provides optimal performance for Java2D blitting.
+     */
+    public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB;
+
+    /**
+     * Number of horizontal segments (bands) for parallel rendering.
+     * Bands are finer than the paint thread count: paint threads steal
+     * bands off a shared ticket until all bands are done, so a thread
+     * that finishes a cheap band immediately picks up more work.
+     * Derived from the render thread count via
+     * {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel#setNumRenderThreads(int)}.
+     *
+     * <p>Equals {@code tilesX * tilesY * viewportCount}: the tile grid
+     * covers one viewport, and in stereo mode a second grid covers the
+     * other eye (segment indices for the right eye start at
+     * {@code tilesX * tilesY}).</p>
+     */
+    public final int numRenderSegments;
+
+    /** Tile columns per viewport (1 = horizontal bands only). */
+    public final int tilesX;
+
+    /** Tile rows per viewport. */
+    public final int tilesY;
+
+    /** Number of side-by-side viewports (2 in stereo mode, else 1). */
+    public final int viewportCount;
+
+    /**
+     * Java2D graphics context for drawing text, anti-aliased shapes, and other
+     * high-level graphics operations onto the render buffer.
+     */
+    public final Graphics2D graphics;
+
+    /**
+     * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band.
+     * Used for thread-safe text and shape rendering without synchronization.
+     * Only initialized in the main RenderingContext; null in segment views.
+     */
+    private Graphics2D[] segmentGraphics;
+
+    /**
+     * Pixels of the rendering area.
+     * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}.
+     */
+    public final int[] pixels;
+
+    /**
+     * Per-pixel depth (biased 1/z, larger = nearer), always allocated —
+     * the painter path was deleted 2026-09-17 and the z-buffer is the
+     * only visibility mechanism. Shared with segment/pass copies like
+     * {@link #pixels}. Cleared per tile by the paint workers.
+     */
+    public float[] depth;
+
+    /**
+     * Active paint pass, set internally by
+     * {@code RenderAggregator.paintSorted}: 0 = not painting, 1 =
+     * opaque pass (opaque-class triangles only, depth test + write),
+     * 2 = alpha pass (alpha-carrying
+     * triangles only, depth test, no depth write). Shapes read it to
+     * decide whether they belong to the current pass.
+     */
+    public int depthPass;
+
+    /**
+     * Depth tolerance in world units for the z-buffer test, in the form
+     * {@code zw > stored - DEPTH_MARGIN_DZ * zw * zw} (tolerance behind
+     * stored, per-pixel at fragment depth). Default 0 = strict depth: any
+     * nonzero window exports per-triangle painter-sort errors into
+     * per-pixel occlusion errors (dirt whose triangles sort late beats
+     * road pavement that strictly wins at margin 0 — user bugreport
+     * 2026-09-16, road pose). Tunable via -Daukio.zbuffer.margin.
+     */
+    public static final double DEPTH_MARGIN_DZ =
+            Double.parseDouble(System.getProperty("aukio.zbuffer.margin", "0"));
+
+    /**
+     * Width of the rendering area in pixels.
+     */
+    public final int width;
+
+    /**
+     * Height of the rendering area in pixels.
+     */
+    public final int height;
+
+    /**
+     * Center of the screen in screen space (pixels).
+     * This is the point where (0,0) coordinate of the world space is rendered.
+     */
+    public final Point2D centerCoordinate;
+
+    /**
+     * Scale factor for perspective projection, derived from screen width.
+     * Used to convert normalized device coordinates to screen pixels.
+     * This is mutable to support stereo rendering where each eye has a different viewport width.
+     */
+    public double projectionScale;
+
+    /**
+     * Minimum Y coordinate (inclusive) to render. Used for multi-threaded rendering
+     * where each thread renders a horizontal segment.
+     */
+    public final int renderMinY;
+
+    /**
+     * Maximum Y coordinate (exclusive) to render. Used for multi-threaded rendering
+     * where each thread renders a horizontal segment.
+     */
+    public final int renderMaxY;
+
+    /** The backing image (public: the AWT shell in {@code gui} blits it directly). */
+    public final BufferedImage bufferedImage;
+    /**
+     * Unique id of the current transform cycle, assigned by
+     * {@code ShapeCollection.transformShapes()} from a global counter.
+     * Unlike {@link #frameNumber} (per-context, can repeat across context
+     * instances), this never collides, so per-cycle memoization such as
+     * composite subtree weights can safely key on it.
+     */
+    public long transformCycleId;
+
+    /**
+     * Which projection buffer slot this context writes/reads: 0, 1 or 2.
+     * Cycles per render pass (per eye in stereo) when the
+     * triple-buffered pipeline is active, so the transform phase of a
+     * pass never overwrites the vertex state either of the two previous
+     * passes' paints may still be reading. Always 0 when the pipeline is
+     * off (tests, single-pass rendering).
+     */
+    public int vertexSlot = 0;
+
+    /**
+     * Near-plane distance in camera-space Z units. Polygons whose vertices
+     * straddle this plane are clipped against it (new intersection vertices
+     * are generated with interpolated UVs); polygons fully behind it are
+     * culled. Must be &gt; 0 so the perspective divide stays safe.
+     */
+    public double nearPlaneDistance = 1.0;
+
+    /**
+     * Number of frame that is currently being rendered.
+     * Every frame has its own number.
+     */
+    public int frameNumber = 0;
+
+    /**
+     * Projected-size cull threshold in screen pixels: shapes whose screen
+     * bounds span less than this in both axes are not queued for
+     * rendering. 0 (the default) disables the cull. Set globally with
+     * {@code -Daukio.cull.subpixel=<px>}.
+     */
+    public double subpixelCullingThreshold = Double.parseDouble(
+            System.getProperty("aukio.cull.subpixel", "0"));
+
+    /**
+     * Epoch of the subpixel-culling verdict cache, stamped per frame by
+     * {@code ShapeCollection.transformShapesBegin}: the epoch advances
+     * when the camera moves significantly (or after a bounded number of
+     * frames), which invalidates all cached skip verdicts and forces one
+     * re-evaluation pass. Meaningless when the cull is off.
+     */
+    public int subpixelCullingEpoch;
+
+    /**
+     * UI component that mouse is currently hovering over.
+     */
+    private MouseInteractionController objectPreviouslyUnderMouseCursor;
+    /**
+     * Mouse click event that needs to be processed.
+     * This event is processed only once per frame.
+     * If there are multiple objects under the mouse cursor, the top-most object will receive the event.
+     * If there are no objects under the mouse cursor, the event will be ignored.
+     * If there is no event, this field will be null.
+     * This field is set to null after the event is processed.
+     */
+    private MouseEvent mouseEvent;
+    /**
+     * UI component that mouse is currently hovering over.
+     */
+    private MouseInteractionController currentObjectUnderMouseCursor;
+    /**
+     * Texture coordinates of the mouse cursor on the hit shape (primary
+     * texture pixels), or NaN when the hit shape has no texture.
+     */
+    private double currentMouseTextureU = Double.NaN;
+    private double currentMouseTextureV = Double.NaN;
+    /**
+     * Developer tools for this rendering context.
+     * Controls diagnostic features like logging and visualization.
+     */
+    public DeveloperTools developerTools;
+
+    /**
+     * Debug log buffer for capturing diagnostic output.
+     * Shapes can log messages here that appear in the Developer Tools panel.
+     */
+    public DebugLogBuffer debugLogBuffer;
+
+    /**
+     * Global lighting manager for the scene.
+     * All shaded polygons use this to calculate lighting. Contains all light sources
+     * and ambient light settings for the world.
+     */
+    public LightingManager lightingManager;
+
+    /**
+     * Which eye is being rendered in stereo mode. NONE for normal single-view rendering.
+     */
+    public StereoEye stereoEye = StereoEye.NONE;
+
+    /**
+     * Width of the viewport for the current eye in stereo mode.
+     * Equals {@link #width} when not in stereo mode.
+     */
+    public int stereoViewportWidth;
+
+    /**
+     * X offset of the current eye's viewport within the full buffer.
+     * 0 for left eye, width/2 for right eye, 0 in normal mode.
+     */
+    public int stereoViewportOffsetX;
+
+    /**
+     * Minimum X coordinate (inclusive) for rendering.
+     * In stereo mode, this is {@link #stereoViewportOffsetX}.
+     * In normal mode, this is 0.
+     */
+    public int renderMinX;
+
+    /**
+     * Maximum X coordinate (exclusive) for rendering.
+     * In stereo mode, this is {@link #stereoViewportOffsetX} + {@link #stereoViewportWidth}.
+     * In normal mode, this is {@link #width}.
+     */
+    public int renderMaxX;
+
+    /**
+     * View frustum for frustum culling.
+     * Updated each frame from camera state and screen dimensions.
+     * Shapes can test their bounding boxes against this frustum to determine
+     * if they are potentially visible before expensive vertex transformations.
+     */
+    public Frustum frustum;
+
+    /**
+     * World-space position of the viewer for this pass, copied from the
+     * camera in {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection#transformShapesBegin}.
+     * Used by BSP painter ordering (viewpoint for tree traversal).
+     * Fresh instance per context, including per-pass copies, so overlapping
+     * pipeline passes each see their own viewpoint.
+     */
+    public final eu.svjatoslav.aukio.e3d.geometry.Point3D viewerPosition =
+            new eu.svjatoslav.aukio.e3d.geometry.Point3D();
+
+    /**
+     * Statistics for frustum culling performance tracking.
+     * Updated each frame: total shapes counted at start, visible shapes
+     * incremented during rendering, culled composites tracked during transform.
+     */
+    public CullingStatistics cullingStatistics;
+
+    /**
+     * Hi-Z occlusion pyramid, rebuilt from the depth buffer after every
+     * painted frame (live {@code ViewPanel} path only — headless
+     * {@code Snapshot} renders leave it empty so golden renders never
+     * cull). Read during the next frame's transform by
+     * {@code TriangleMeshBlock} to skip fully occluded blocks. Shared
+     * with pass/segment copies like {@link #depth}.
+     */
+    public HiZPyramid occlusionPyramid;
+
+    /**
+     * Executor for the parallel transform phase. When non-null, composites
+     * with enough children split their render lists into chunks transformed
+     * concurrently. When null, the transform phase runs serially on the
+     * render thread.
+     */
+    public ExecutorService transformExecutor;
+
+    /**
+     * Per-frame coordinator for the non-blocking parallel transform fork.
+     * Set by {@code ShapeCollection.transformShapes()} for the duration of
+     * the root transform when {@link #transformExecutor} is available;
+     * composites at any nesting level submit chunk tasks to it. Null outside
+     * the transform phase and when transforming serially.
+     */
+    public ParallelTransformCoordinator transformCoordinator;
+
+    /**
+     * Present gate for this framebuffer: fires when the frame currently
+     * held in this buffer has been presented to the display (or dropped
+     * from the presentation mailbox). The render thread installs a fresh
+     * gate at the start of each frame that reuses the buffer, and the
+     * frame's paint continuation awaits the PREVIOUS gate before writing
+     * pixels — without it, painting frame F+3 would overwrite the buffer
+     * while the present thread is still blitting frame F from it.
+     */
+    public volatile java.util.concurrent.CountDownLatch presentGate = new java.util.concurrent.CountDownLatch(0);
+
+    /**
+     * Chunk tasks submitted during the last transform phase, across all
+     * nesting levels. Diagnostics: proves nested composites forked.
+     */
+    public int lastTransformTaskCount;
+
+    /**
+     * Creates a new rendering context for full-screen rendering.
+     *
+     * <p>Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.</p>
+     *
+     * @param width             the rendering area width in pixels
+     * @param height            the rendering area height in pixels
+     * @param numRenderSegments number of parallel render segments (threads)
+     */
+    public RenderingContext(final int width, final int height, final int numRenderSegments) {
+        this(width, height, 0, height, 1, numRenderSegments, 1);
+    }
+
+    /**
+     * Creates a new rendering context with a rectangular tile grid.
+     *
+     * <p>Equivalent to the band-only constructors when {@code tilesX == 1}.
+     * In stereo mode ({@code viewportCount == 2}) each viewport gets its own
+     * tile grid; segment indices for viewport v start at
+     * {@code v * tilesX * tilesY}.</p>
+     *
+     * @param width         the rendering area width in pixels
+     * @param height        the rendering area height in pixels
+     * @param tilesX        tile columns per viewport (1 = bands only)
+     * @param tilesY        tile rows per viewport
+     * @param viewportCount number of side-by-side viewports (2 = stereo)
+     */
+    public RenderingContext(final int width, final int height,
+                            final int tilesX, final int tilesY,
+                            final int viewportCount) {
+        this(width, height, 0, height, tilesX, tilesY, viewportCount);
+    }
+
+    private RenderingContext(final int width, final int height,
+                             final int renderMinY, final int renderMaxY,
+                             final int tilesX, final int tilesY,
+                             final int viewportCount) {
+        this.width = width;
+        this.height = height;
+        this.renderMinY = renderMinY;
+        this.renderMaxY = renderMaxY;
+        this.tilesX = tilesX;
+        this.tilesY = tilesY;
+        this.viewportCount = viewportCount;
+        this.numRenderSegments = tilesX * tilesY * viewportCount;
+        this.centerCoordinate = new Point2D(width / 2d, height / 2d);
+        this.projectionScale = width / 3d;
+        this.stereoViewportWidth = width;
+        this.stereoViewportOffsetX = 0;
+        this.renderMinX = 0;
+        this.renderMaxX = width;
+
+        // Eagerly allocated so the developer-tools panel always finds it:
+        // transformPass() hands the pipeline a per-pass COPY of this context,
+        // and the copy constructor shares this reference. Lazy creation in
+        // ShapeCollection.transformShapesBegin() would only ever populate the
+        // throwaway pass copy, leaving this frame context null forever
+        // (the culling display then showed "-" permanently).
+        this.cullingStatistics = new CullingStatistics();
+        this.occlusionPyramid = new HiZPyramid();
+
+        bufferedImage = new BufferedImage(width, height, bufferedImageType);
+
+        final WritableRaster raster = bufferedImage.getRaster();
+        final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer();
+        pixels = dbi.getData();
+
+        // Z-buffer: one w-depth (biased 1/z) value per pixel, cleared per
+        // tile in the paint workers. Depth turns the queue order into a
+        // performance heuristic only; correctness comes from the per-pixel
+        // test. (The queue itself stays painter back-to-front — Z
+        // descending, see RenderAggregator.) Always allocated: the
+        // z-buffer path is the only renderer.
+        depth = new float[width * height];
+
+        graphics = (Graphics2D) bufferedImage.getGraphics();
+        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+        segmentGraphics = createSegmentGraphics();
+    }
+
+    /**
+     * Protected constructor for creating segment views.
+     * Shares the pixel buffer and graphics context with the parent.
+     *
+     * @param parent     the parent rendering context
+     * @param renderMinY minimum Y coordinate (inclusive) for this segment
+     * @param renderMaxY maximum Y coordinate (exclusive) for this segment
+     */
+    protected RenderingContext(final RenderingContext parent,
+                               final int renderMinY, final int renderMaxY) {
+        this.width = parent.width;
+        this.height = parent.height;
+        this.renderMinY = renderMinY;
+        this.renderMaxY = renderMaxY;
+        this.tilesX = parent.tilesX;
+        this.tilesY = parent.tilesY;
+        this.viewportCount = parent.viewportCount;
+        this.numRenderSegments = parent.numRenderSegments;
+        this.centerCoordinate = parent.centerCoordinate;
+        this.projectionScale = parent.projectionScale;
+        this.stereoViewportWidth = parent.stereoViewportWidth;
+        this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
+        this.stereoEye = parent.stereoEye;
+        this.renderMinX = parent.renderMinX;
+        this.renderMaxX = parent.renderMaxX;
+        this.bufferedImage = parent.bufferedImage;
+        this.pixels = parent.pixels;
+        this.depth = parent.depth;
+        this.graphics = parent.graphics;
+        this.vertexSlot = parent.vertexSlot;
+        this.nearPlaneDistance = parent.nearPlaneDistance;
+        this.developerTools = parent.developerTools;
+        this.debugLogBuffer = parent.debugLogBuffer;
+        this.lightingManager = parent.lightingManager;
+        this.occlusionPyramid = parent.occlusionPyramid;
+        this.segmentGraphics = null;
+    }
+
+    /**
+     * Creates an independent pass context for one pipeline pass (one eye
+     * in stereo): shares the frame's pixel buffer, graphics and services,
+     * but owns the per-pass projection fields (center, scale, stereo
+     * viewport, slot, frame/cycle stamps). The next pass's setup writes
+     * to its own copy, so it cannot disturb this pass's in-flight
+     * transform chunks or its asynchronous sort/bin/paint continuation.
+     *
+     * @param parent the frame rendering context to copy from
+     */
+    public RenderingContext(final RenderingContext parent) {
+        this.width = parent.width;
+        this.height = parent.height;
+        this.renderMinY = parent.renderMinY;
+        this.renderMaxY = parent.renderMaxY;
+        this.tilesX = parent.tilesX;
+        this.tilesY = parent.tilesY;
+        this.viewportCount = parent.viewportCount;
+        this.numRenderSegments = parent.numRenderSegments;
+        this.centerCoordinate = new Point2D(parent.centerCoordinate.x, parent.centerCoordinate.y);
+        this.projectionScale = parent.projectionScale;
+        this.stereoViewportWidth = parent.stereoViewportWidth;
+        this.stereoViewportOffsetX = parent.stereoViewportOffsetX;
+        this.stereoEye = parent.stereoEye;
+        this.renderMinX = parent.renderMinX;
+        this.renderMaxX = parent.renderMaxX;
+        this.bufferedImage = parent.bufferedImage;
+        this.pixels = parent.pixels;
+        this.depth = parent.depth;
+        this.graphics = parent.graphics;
+        this.vertexSlot = parent.vertexSlot;
+        this.nearPlaneDistance = parent.nearPlaneDistance;
+        this.frameNumber = parent.frameNumber;
+        this.transformCycleId = parent.transformCycleId;
+        this.transformExecutor = parent.transformExecutor;
+        this.developerTools = parent.developerTools;
+        this.debugLogBuffer = parent.debugLogBuffer;
+        this.lightingManager = parent.lightingManager;
+        this.cullingStatistics = parent.cullingStatistics;
+        this.occlusionPyramid = parent.occlusionPyramid;
+        this.subpixelCullingThreshold = parent.subpixelCullingThreshold;
+        this.subpixelCullingEpoch = parent.subpixelCullingEpoch;
+        this.setMouseEvent(parent.getMouseEvent());
+        // Share the pre-clipped per-tile graphics: glyph rendering
+        // (user-facing text) draws through them by segment index. Null
+        // here made every glyph paint die with an NPE mid-tile (broken
+        // tiles whenever text faced the reader).
+        this.segmentGraphics = parent.segmentGraphics;
+        // frustum stays null: created fresh per pass in transformShapesBegin
+    }
+
+    /**
+     * Resets per-frame state in preparation for rendering a new frame.
+     * Increments the frame number and clears the mouse event state.
+     */
+    public void prepareForNewFrameRendering() {
+        frameNumber++;
+        mouseEvent = null;
+        currentObjectUnderMouseCursor = null;
+    }
+
+    /**
+     * Creates Graphics2D contexts for each render segment, pre-clipped to
+     * its tile rectangle. Segment index layout: viewport v, tile row ty,
+     * tile column tx -> v * tilesX * tilesY + ty * tilesX + tx.
+     *
+     * @return array of Graphics2D objects, one per segment
+     */
+    private Graphics2D[] createSegmentGraphics() {
+        final Graphics2D[] contexts = new Graphics2D[numRenderSegments];
+        final int viewportWidth = width / viewportCount;
+        final int tileW = viewportWidth / tilesX;
+        final int tileH = height / tilesY;
+
+        for (int v = 0; v < viewportCount; v++) {
+            final int viewportX = v * viewportWidth;
+            for (int ty = 0; ty < tilesY; ty++) {
+                final int minY = ty * tileH;
+                final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH;
+                for (int tx = 0; tx < tilesX; tx++) {
+                    final int minX = viewportX + tx * tileW;
+                    final int maxX = (tx == tilesX - 1)
+                            ? viewportX + viewportWidth : minX + tileW;
+
+                    final Graphics2D g = bufferedImage.createGraphics();
+                    g.setClip(minX, minY, maxX - minX, maxY - minY);
+                    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+                    g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+                    contexts[v * tilesX * tilesY + ty * tilesX + tx] = g;
+                }
+            }
+        }
+
+        return contexts;
+    }
+
+    /**
+     * Returns the backing image whose pixel buffer the rasterizer paints into.
+     *
+     * <p>Exposed for headless rendering: after a transform/sort/paint pass the
+     * image holds the finished frame and can be saved or compared directly.</p>
+     *
+     * @return the backing buffered image
+     */
+    public BufferedImage getImage() {
+        return bufferedImage;
+    }
+
+    /**
+     * Returns the Graphics2D context for a specific render segment.
+     * Each segment's Graphics2D is pre-clipped to its Y bounds.
+     *
+     * @param segmentIndex the segment index (0 to numRenderSegments-1)
+     * @return the Graphics2D for that segment
+     * @throws NullPointerException if called on a segment view (not the main context)
+     */
+    public Graphics2D getSegmentGraphics(final int segmentIndex) {
+        return segmentGraphics[segmentIndex];
+    }
+
+    /**
+     * Disposes all Graphics2D resources associated with this context.
+     * Should be called when the context is no longer needed (e.g., on resize).
+     */
+    public void dispose() {
+        if (segmentGraphics != null) {
+            for (final Graphics2D g : segmentGraphics) {
+                if (g != null) {
+                    g.dispose();
+                }
+            }
+        }
+        if (graphics != null) {
+            graphics.dispose();
+        }
+    }
+
+    /**
+     * Executes a graphics operation in a thread-safe manner.
+     * This must be used for all Graphics2D operations (text, lines, etc.)
+     * during multi-threaded rendering.
+     *
+     * @param operation the graphics operation to execute
+     */
+    public void executeWithGraphics(final Consumer<Graphics2D> operation) {
+        synchronized (graphics) {
+            operation.accept(graphics);
+        }
+    }
+
+    /**
+     * Returns the pending mouse event for this frame, or {@code null} if none.
+     *
+     * @return the mouse event to process, or {@code null}
+     */
+    public MouseEvent getMouseEvent() {
+        return mouseEvent;
+    }
+
+    /**
+     * Sets the mouse event to be processed during this frame's rendering.
+     *
+     * @param mouseEvent the mouse event with position and button information
+     */
+    public void setMouseEvent(MouseEvent mouseEvent) {
+        this.mouseEvent = mouseEvent;
+    }
+
+    /**
+     * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}.
+     * Because objects are rendered back to front. The last method caller will set the top-most object, if
+     * there are multiple objects under mouse cursor.
+     *
+     * @param currentObjectUnderMouseCursor the object that is currently under the mouse cursor
+     */
+    public synchronized void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) {
+        setCurrentObjectUnderMouseCursor(currentObjectUnderMouseCursor,
+                Double.NaN, Double.NaN);
+    }
+
+    /**
+     * Called when given object was detected under mouse cursor, with the
+     * texture coordinates of the hit point (for textured shapes).
+     *
+     * @param currentObjectUnderMouseCursor the object under the mouse cursor
+     * @param textureU texture-space X of the hit point in primary-texture pixels
+     * @param textureV texture-space Y of the hit point in primary-texture pixels
+     */
+    public synchronized void setCurrentObjectUnderMouseCursor(
+            final MouseInteractionController currentObjectUnderMouseCursor,
+            final double textureU, final double textureV) {
+        this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
+        this.currentMouseTextureU = textureU;
+        this.currentMouseTextureV = textureV;
+    }
+
+    /**
+     * Returns the current object under the mouse cursor.
+     * Used by segment rendering to collect mouse results.
+     *
+     * @return the current object under mouse cursor, or null
+     */
+    public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
+        return currentObjectUnderMouseCursor;
+    }
+
+    /**
+     * Handles mouse events for components and returns whether a view repaint is needed.
+     *
+     * @return {@code true} if view update is needed as a consequence of this mouse event
+     */
+    public boolean handlePossibleComponentMouseEvent() {
+        if (mouseEvent == null) return false;
+
+        boolean viewRepaintNeeded = false;
+
+        if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) {
+            // Mouse cursor has just entered or left component.
+            viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited();
+            viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered();
+            objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor;
+        }
+
+        if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
+            // Mouse button was clicked on some component.
+            viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(
+                    mouseEvent.button, currentMouseTextureU, currentMouseTextureV);
+        } else if (currentObjectUnderMouseCursor != null)
+            // hover: let the component track the pointer position
+            viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover(
+                    currentMouseTextureU, currentMouseTextureV);
+
+        return viewRepaintNeeded;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java
new file mode 100644 (file)
index 0000000..567bdcb
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent;
+import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
+
+import java.awt.*;
+import java.util.function.Consumer;
+
+/**
+ * A view of a RenderingContext for rendering a horizontal screen segment.
+ *
+ * <p>This class wraps a parent RenderingContext and provides its own Y-bounds
+ * for multi-threaded rendering. All operations delegate to the parent context,
+ * but with segment-specific Y bounds for pixel operations.</p>
+ *
+ * <p>Mouse tracking is local to each segment and must be combined after all
+ * segments complete rendering.</p>
+ *
+ * @see RenderingContext
+ */
+public class SegmentRenderingContext extends RenderingContext {
+
+    private final RenderingContext parent;
+    private final int segmentIndex;
+    private MouseInteractionController segmentMouseHit;
+    private double segmentMouseHitU = Double.NaN;
+    private double segmentMouseHitV = Double.NaN;
+
+    /**
+     * Creates a segment view of a parent rendering context.
+     *
+     * @param parent       the parent rendering context to delegate to
+     * @param renderMinY   minimum Y coordinate (inclusive) for this segment
+     * @param renderMaxY   maximum Y coordinate (exclusive) for this segment
+     * @param segmentIndex the index of this segment (0 to numRenderSegments-1)
+     */
+    public SegmentRenderingContext(final RenderingContext parent,
+                                    final int renderMinY, final int renderMaxY,
+                                    final int segmentIndex) {
+        super(parent, renderMinY, renderMaxY);
+        this.parent = parent;
+        this.segmentIndex = segmentIndex;
+    }
+
+    @Override
+    public void executeWithGraphics(final Consumer<Graphics2D> operation) {
+        operation.accept(parent.getSegmentGraphics(segmentIndex));
+    }
+
+    @Override
+    public MouseEvent getMouseEvent() {
+        return parent.getMouseEvent();
+    }
+
+    @Override
+    public void setMouseEvent(final MouseEvent mouseEvent) {
+        parent.setMouseEvent(mouseEvent);
+    }
+
+    @Override
+    public synchronized void setCurrentObjectUnderMouseCursor(final MouseInteractionController controller) {
+        setCurrentObjectUnderMouseCursor(controller, Double.NaN, Double.NaN);
+    }
+
+    @Override
+    public synchronized void setCurrentObjectUnderMouseCursor(
+            final MouseInteractionController controller,
+            final double textureU, final double textureV) {
+        this.segmentMouseHit = controller;
+        this.segmentMouseHitU = textureU;
+        this.segmentMouseHitV = textureV;
+    }
+
+    /**
+     * Returns the mouse hit detected in this segment.
+     *
+     * @return the MouseInteractionController that was under the mouse in this segment, or null
+     */
+    public MouseInteractionController getSegmentMouseHit() {
+        return segmentMouseHit;
+    }
+
+    /**
+     * Texture-space X of the hit point (primary texture pixels), NaN if none.
+     */
+    public double getSegmentMouseHitU() {
+        return segmentMouseHitU;
+    }
+
+    /**
+     * Texture-space Y of the hit point (primary texture pixels), NaN if none.
+     */
+    public double getSegmentMouseHitV() {
+        return segmentMouseHitV;
+    }
+
+    @Override
+    public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() {
+        return segmentMouseHit;
+    }
+}
\ No newline at end of file
index 5856d6b..da63431 100755 (executable)
@@ -4,12 +4,8 @@
  */
 package eu.svjatoslav.aukio.e3d.renderer.raster;
 
-import eu.svjatoslav.aukio.e3d.geometry.Frustum;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.CullingStatistics;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.math.Quaternion;
 import eu.svjatoslav.aukio.e3d.math.Transform;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
@@ -64,7 +60,7 @@ import java.util.concurrent.ExecutorService;
  * <p>The {@link #addShape} method is synchronized, making it safe to add shapes from
  * any thread while the rendering loop is active.</p>
  *
- * @see ViewPanel#getRootShapeCollection()
+ * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getRootShapeCollection()
  * @see AbstractShape the base class for all shapes
  * @see AbstractCompositeShape the root composite that stores and processes all shapes
  * @see RenderAggregator handles depth sorting and painting
@@ -267,15 +263,9 @@ public class ShapeCollection {
      * @param viewPanel        the view panel providing the camera state
      * @param renderingContext the rendering context with frame metadata
      */
-    public synchronized void transformShapes(final ViewPanel viewPanel,
-                                             final RenderingContext renderingContext) {
-        transformShapesBegin(viewPanel, renderingContext);
-        drainTransformShapes(renderingContext);
-    }
-
     /**
-     * Camera-based variant of {@link #transformShapes(ViewPanel, RenderingContext)}
-     * for headless rendering without a {@link ViewPanel} (off-screen snapshots,
+     * Camera-based variant of {@link #transformShapes(Camera, RenderingContext)}
+     * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} (off-screen snapshots,
      * golden-image tests, GI scene setup).
      *
      * @param camera           the camera providing position and orientation
@@ -288,25 +278,8 @@ public class ShapeCollection {
     }
 
     /**
-     * First half of {@link #transformShapes}: resets the frame's
-     * aggregator, computes the frustum and walks the scene tree,
-     * forking heavy composites into chunk tasks on the frame's
-     * coordinator. Returns WITHOUT waiting for the chunk tasks; the
-     * caller hands the coordinator to {@link #drainTransformShapes}
-     * later (the pipeline drains it inside the asynchronous
-     * sort/bin/paint continuation on a worker thread).
-     *
-     * @param viewPanel        the view panel providing the camera state
-     * @param renderingContext the pass rendering context
-     */
-    public synchronized void transformShapesBegin(final ViewPanel viewPanel,
-                                                  final RenderingContext renderingContext) {
-        transformShapesBegin(viewPanel.getCamera(), renderingContext);
-    }
-
-    /**
-     * Camera-based variant of {@link #transformShapesBegin(ViewPanel, RenderingContext)}
-     * for headless rendering without a {@link ViewPanel}.
+     * Camera-based variant of {@link #transformShapesBegin(Camera, RenderingContext)}
+     * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel}.
      *
      * @param camera           the camera providing position and orientation
      * @param renderingContext the pass rendering context
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java
new file mode 100644 (file)
index 0000000..39ceafb
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+
+/**
+ * Identifies which eye is being rendered in stereoscopic mode.
+ *
+ * @see RenderingContext#stereoEye
+ */
+public enum StereoEye {
+    /** Normal single-view rendering (no stereo). */
+    NONE,
+    /** Left eye view in side-by-side stereo mode. */
+    LEFT,
+    /** Right eye view in side-by-side stereo mode. */
+    RIGHT
+}
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java
new file mode 100644 (file)
index 0000000..9ee819d
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster;
+import eu.svjatoslav.aukio.e3d.math.TransformStack;
+
+import eu.svjatoslav.aukio.e3d.geometry.Point2D;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+
+/**
+ * A vertex in 3D space with transformation and screen projection support.
+ *
+ * <p>A vertex represents a corner point of a polygon or polyhedron. In addition to
+ * the 3D coordinate, it stores the transformed position (relative to viewer) and
+ * the projected screen coordinates for rendering.</p>
+ *
+ * <p><b>Coordinate spaces:</b></p>
+ * <ul>
+ *   <li>{@link #coordinate} - Original position in local/model space</li>
+ *   <li>{@link #transformedCoordinate} - Position relative to viewer (camera space)</li>
+ *   <li>{@link #onScreenCoordinate} - 2D screen position after perspective projection</li>
+ * </ul>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>{@code
+ * Vertex v = new Vertex(new Point3D(10, 20, 30));
+ * v.calculateLocationRelativeToViewer(transformStack, renderContext);
+ * if (v.transformedCoordinate.z > 0) {
+ *     // Vertex is in front of the camera
+ * }
+ * }</pre>
+ *
+ * @see Point3D
+ * @see TransformStack
+ */
+public class Vertex {
+
+    /**
+     * Vertex coordinate in local/model 3D space.
+     */
+    public Point3D coordinate;
+
+    /**
+     * Vertex coordinate relative to the viewer after transformation (camera
+     * space), per buffer slot. Slot parity lets the transform phase of the
+     * NEXT frame write slot B while the paint phase of the current frame
+     * still reads slot A (double-buffered pipeline). Never access directly:
+     * use {@link #transformedCoordinate(RenderingContext)}.
+     */
+    private final Point3D transformedCoordinate0 = new Point3D();
+    private final Point3D transformedCoordinate1 = new Point3D();
+    private final Point3D transformedCoordinate2 = new Point3D();
+
+    /**
+     * Vertex position on screen in pixels, per buffer slot.
+     * Use {@link #onScreenCoordinate(RenderingContext)}.
+     */
+    private final Point2D onScreenCoordinate0 = new Point2D();
+    private final Point2D onScreenCoordinate1 = new Point2D();
+    private final Point2D onScreenCoordinate2 = new Point2D();
+
+    /**
+     * Texture coordinate for UV mapping (optional).
+     */
+    public Point2D textureCoordinate;
+
+    /**
+     * Normal vector for this vertex (optional).
+     * Used by CSG operations for smooth interpolation during polygon splitting.
+     * Null for non-CSG usage; existing rendering code ignores this field.
+     */
+    public Point3D normal;
+
+
+    /**
+     * The transform cycle when each slot was last transformed (for
+     * caching). Keyed by {@link RenderingContext#transformCycleId}, NOT
+     * by frameNumber: in stereo mode both eye passes share the same
+     * parity context and therefore the same frameNumber, while using
+     * different slots — a frameNumber-based key lets one eye's pass
+     * falsely hit the cache entry the other eye wrote for the same slot
+     * an odd number of passes earlier, leaving opposite-eye (wrong
+     * viewport-offset) screen coordinates in the slot: the affected eye
+     * paints fully off-viewport, i.e. a black half-frame (observed as
+     * violent left/right flashing, 2026-09-09). transformCycleId is
+     * unique per pass, so a hit always means "already transformed within
+     * THIS pass" — the only correct dedupe semantics.
+     */
+    private long lastTransformCycle0 = -1;
+    private long lastTransformCycle1 = -1;
+    private long lastTransformCycle2 = -1;
+
+    /**
+     * Creates a vertex at the origin (0, 0, 0) with no texture coordinate.
+     */
+    public Vertex() {
+        this(new Point3D());
+    }
+
+    /**
+     * Creates a vertex at the specified position with no texture coordinate.
+     *
+     * @param coordinate the 3D position of this vertex
+     */
+    public Vertex(final Point3D coordinate) {
+        this(coordinate, null);
+    }
+
+    /**
+     * Creates a vertex at the specified position with an optional texture coordinate.
+     *
+     * @param coordinate        the 3D position of this vertex
+     * @param textureCoordinate the UV texture coordinate, or {@code null} for none
+     */
+    public Vertex(final Point3D coordinate, final Point2D textureCoordinate) {
+        this.coordinate = coordinate;
+        this.textureCoordinate = textureCoordinate;
+    }
+
+    /**
+     * Returns the camera-space coordinate for the rendering context's buffer
+     * slot. Valid only after this vertex was transformed for that slot's
+     * current frame.
+     *
+     * @param renderContext the rendering context (selects the buffer slot)
+     * @return the transformed coordinate (camera space) for the active slot
+     */
+    public Point3D transformedCoordinate(final RenderingContext renderContext) {
+        // Dual fields, not a slot array: measured 2026-09-05 (400-sphere
+        // scene) that array indexing adds a second dependent load per access
+        // and cost ~60% of transform phase time; a perfectly-predicted
+        // branch + direct field load is free.
+        final int slot = renderContext.vertexSlot;
+        return slot == 0 ? transformedCoordinate0
+                : slot == 1 ? transformedCoordinate1 : transformedCoordinate2;
+    }
+
+    /**
+     * Returns the screen-space position for the rendering context's buffer
+     * slot.
+     *
+     * @param renderContext the rendering context (selects the buffer slot)
+     * @return the on-screen coordinate (pixels) for the active slot
+     */
+    public Point2D onScreenCoordinate(final RenderingContext renderContext) {
+        final int slot = renderContext.vertexSlot;
+        return slot == 0 ? onScreenCoordinate0
+                : slot == 1 ? onScreenCoordinate1 : onScreenCoordinate2;
+    }
+
+
+    /**
+     * Transforms this vertex from model space to screen space.
+     *
+     * <p>This method applies the transform stack to compute the vertex position
+     * relative to the viewer, then projects it to 2D screen coordinates.
+     * Results are cached per-frame per-slot to avoid redundant calculations.</p>
+     *
+     * @param transforms    the transform stack to apply (world-to-camera transforms)
+     * @param renderContext the rendering context providing projection parameters
+     */
+    public void calculateLocationRelativeToViewer(final TransformStack transforms,
+                                                  final RenderingContext renderContext) {
+
+        final Point3D transformedCoordinate;
+        final Point2D onScreenCoordinate;
+        switch (renderContext.vertexSlot) {
+            case 0:
+                if (lastTransformCycle0 == renderContext.transformCycleId)
+                    return;
+                lastTransformCycle0 = renderContext.transformCycleId;
+                transformedCoordinate = transformedCoordinate0;
+                onScreenCoordinate = onScreenCoordinate0;
+                break;
+            case 1:
+                if (lastTransformCycle1 == renderContext.transformCycleId)
+                    return;
+                lastTransformCycle1 = renderContext.transformCycleId;
+                transformedCoordinate = transformedCoordinate1;
+                onScreenCoordinate = onScreenCoordinate1;
+                break;
+            default:
+                if (lastTransformCycle2 == renderContext.transformCycleId)
+                    return;
+                lastTransformCycle2 = renderContext.transformCycleId;
+                transformedCoordinate = transformedCoordinate2;
+                onScreenCoordinate = onScreenCoordinate2;
+                break;
+        }
+        transforms.transform(coordinate, transformedCoordinate);
+        onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale);
+        onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
+        onScreenCoordinate.add(renderContext.centerCoordinate);
+        onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
+    }
+
+    /**
+     * Writes a camera-space position directly into this vertex's slot state
+     * and projects it to screen coordinates, bypassing the transform stack.
+     *
+     * <p>Used by near-plane clipping ({@code AbstractCoordinateShape}), which
+     * creates intersection vertices that exist ONLY in camera space — there
+     * is no model-space coordinate to transform. The given z must be &gt; 0
+     * (clip against the near plane guarantees z == nearPlaneDistance).</p>
+     *
+     * @param x             camera-space X
+     * @param y             camera-space Y
+     * @param z             camera-space Z (depth in front of viewer, &gt; 0)
+     * @param renderContext the rendering context (selects the buffer slot and
+     *                      provides projection parameters)
+     */
+    public void setCameraSpaceCoordinate(final double x, final double y, final double z,
+                                         final RenderingContext renderContext) {
+        final Point3D transformedCoordinate;
+        final Point2D onScreenCoordinate;
+        switch (renderContext.vertexSlot) {
+            case 0:
+                transformedCoordinate = transformedCoordinate0;
+                onScreenCoordinate = onScreenCoordinate0;
+                break;
+            case 1:
+                transformedCoordinate = transformedCoordinate1;
+                onScreenCoordinate = onScreenCoordinate1;
+                break;
+            default:
+                transformedCoordinate = transformedCoordinate2;
+                onScreenCoordinate = onScreenCoordinate2;
+                break;
+        }
+        transformedCoordinate.x = x;
+        transformedCoordinate.y = y;
+        transformedCoordinate.z = z;
+        onScreenCoordinate.x = ((x / z) * renderContext.projectionScale);
+        onScreenCoordinate.y = ((y / z) * renderContext.projectionScale);
+        onScreenCoordinate.add(renderContext.centerCoordinate);
+        onScreenCoordinate.x += renderContext.stereoViewportOffsetX;
+    }
+
+    // ========== CSG support methods ==========
+
+    /**
+     * Creates a deep copy of this vertex.
+     * Clones the coordinate, normal (if present), and texture coordinate (if present).
+     * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame).
+     *
+     * @return a new Vertex with cloned data
+     */
+    public Vertex clone() {
+        final Vertex result = new Vertex(new Point3D(coordinate),
+                textureCoordinate != null ? new Point2D(textureCoordinate) : null);
+        if (normal != null) {
+            result.normal = new Point3D(normal);
+        }
+        return result;
+    }
+
+    /**
+     * Flips the orientation of this vertex by negating the normal vector.
+     * Called when the orientation of a polygon is flipped during CSG operations.
+     * If normal is null, this method does nothing.
+     */
+    public void flip() {
+        if (normal != null) {
+            normal = normal.withNegated();
+        }
+    }
+
+    /**
+     * Creates a new vertex between this vertex and another by linearly interpolating
+     * all properties using parameter t.
+     *
+     * <p>Interpolates: position, normal (if present), and texture coordinate (if present).</p>
+     *
+     * @param other the other vertex to interpolate towards
+     * @param t     the interpolation parameter (0 = this vertex, 1 = other vertex)
+     * @return a new Vertex representing the interpolated position
+     */
+    public Vertex interpolate(final Vertex other, final double t) {
+        final Vertex result = new Vertex(
+                coordinate.interpolate(other.coordinate, t),
+                (textureCoordinate != null && other.textureCoordinate != null)
+                        ? new Point2D(
+                        textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
+                        textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t)
+                        : null
+        );
+        if (normal != null && other.normal != null) {
+            result.normal = normal.interpolate(other.normal, t);
+        }
+        return result;
+    }
+}
index e285b97..0df0d60 100644 (file)
@@ -6,7 +6,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.gi;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
 
index 94fafb0..5d18d0a 100644 (file)
@@ -7,9 +7,9 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes;
 import eu.svjatoslav.aukio.e3d.geometry.Box;
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
 
 import java.util.ArrayList;
index f6e81a3..633a3bd 100644 (file)
@@ -6,7 +6,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes;
 
 import eu.svjatoslav.aukio.e3d.geometry.Box;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
index 1924793..77f08f5 100644 (file)
@@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
index d327c38..7b1e7a4 100644 (file)
@@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
 
index 6dd3b19..313b66b 100644 (file)
@@ -4,13 +4,13 @@
  */
 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon;
 
-import eu.svjatoslav.aukio.e3d.geometry.Plane;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane;
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
index 9e040eb..391b17d 100644 (file)
@@ -1,7 +1,7 @@
 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
 
index 648baec..76f4b29 100644 (file)
@@ -5,8 +5,8 @@
 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
index 94d8195..5220870 100644 (file)
@@ -3,9 +3,9 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
 import eu.svjatoslav.aukio.e3d.geometry.Box;
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.HiZPyramid;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.StereoEye;
+import eu.svjatoslav.aukio.e3d.renderer.raster.HiZPyramid;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye;
 import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
index 89e8d79..a6b5db3 100644 (file)
@@ -7,7 +7,7 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite;
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
 import eu.svjatoslav.aukio.e3d.math.Transform;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture;
index 46724bb..fa0ca38 100644 (file)
@@ -5,15 +5,14 @@
 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
 
 import eu.svjatoslav.aukio.e3d.geometry.Box;
-import eu.svjatoslav.aukio.e3d.geometry.BspTree;
-import eu.svjatoslav.aukio.e3d.geometry.Frustum;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Frustum;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.gui.ViewSpaceTracker;
 import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController;
 import eu.svjatoslav.aukio.e3d.math.Transform;
 import eu.svjatoslav.aukio.e3d.math.TransformStack;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
 import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java
new file mode 100644 (file)
index 0000000..6b92eca
--- /dev/null
@@ -0,0 +1,230 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
+
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A Binary Space Partitioning (BSP) tree for CSG operations.
+ *
+ * <p>BSP trees are the data structure that makes CSG boolean operations possible.
+ * Each node divides 3D space into two half-spaces using a plane, enabling
+ * efficient spatial queries and polygon clipping.</p>
+ *
+ * <p><b>BSP Tree Structure:</b></p>
+ * <pre>
+ *                 [Node: plane P]
+ *                /               \
+ *        [Front subtree]     [Back subtree]
+ *     (same side as P's     (opposite side
+ *        normal)             of P's normal)
+ * </pre>
+ *
+ * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
+ * @see Plane the plane type used for spatial partitioning
+ * @see SolidPolygon the polygon type stored in BSP nodes
+ */
+public class BspTree {
+
+    /**
+     * Polygons that lie on this node's partitioning plane.
+     */
+    public final List<SolidPolygon> polygons = new ArrayList<>();
+
+    /**
+     * The partitioning plane for this node.
+     */
+    public Plane plane;
+
+    /**
+     * The front child subtree.
+     */
+    public BspTree front;
+
+    /**
+     * The back child subtree.
+     */
+    public BspTree back;
+
+    /**
+     * Creates an empty BSP tree with no plane or children.
+     */
+    public BspTree() {
+    }
+
+    /**
+     * Creates a BSP tree from a list of polygons.
+     *
+     * @param polygons the polygons to partition into a BSP tree
+     */
+    public BspTree(final List<SolidPolygon> polygons) {
+        addPolygons(polygons);
+    }
+
+    /**
+     * Creates a deep clone of this BSP tree.
+     *
+     * @return a new BspTree with cloned data
+     */
+    public BspTree clone() {
+        final BspTree tree = new BspTree();
+
+        tree.plane = plane != null ? plane.clone() : null;
+        tree.front = front != null ? front.clone() : null;
+        tree.back = back != null ? back.clone() : null;
+
+        for (final SolidPolygon p : polygons) {
+            tree.polygons.add(p.deepClone());
+        }
+
+        return tree;
+    }
+
+    /**
+     * Inverts this BSP tree, converting "inside" to "outside" and vice versa.
+     */
+    public void invert() {
+        for (final SolidPolygon polygon : polygons) polygon.flip();
+
+        if (plane != null) plane.flip();
+        if (front != null) front.invert();
+        if (back != null) back.invert();
+
+        final BspTree temp = front;
+        front = back;
+        back = temp;
+    }
+
+    /**
+     * Clips a list of polygons against this BSP tree, returning only the
+     * portions that lie outside the solid represented by this tree.
+     *
+     * <p>This is a core CSG operation used for boolean subtraction and
+     * intersection. The method recursively traverses the BSP tree, splitting
+     * polygons at each partitioning plane and discarding interior fragments.</p>
+     *
+     * <p><b>Algorithm:</b></p>
+     * <ol>
+     *   <li>At each node, split polygons by the partitioning plane</li>
+     *   <li>Recursively clip front fragments against the front subtree</li>
+     *   <li>Recursively clip back fragments against the back subtree</li>
+     *   <li>Combine and return all surviving fragments</li>
+     * </ol>
+     *
+     * <p><b>Leaf nodes:</b> If this node has no plane (leaf node), all polygons
+     * are considered outside and returned unchanged.</p>
+     *
+     * @param polygons the polygons to clip against this BSP tree
+     * @return a new list containing only the portions outside this solid
+     */
+    public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
+        // Leaf node: no partitioning plane means all polygons are outside
+        if (plane == null) {
+            return new ArrayList<>(polygons);
+        }
+
+        // Split polygons by this node's partitioning plane
+        final List<SolidPolygon> frontList = new ArrayList<>();
+        final List<SolidPolygon> backList = new ArrayList<>();
+
+        for (final SolidPolygon polygon : polygons)
+            // Split by plane: coplanar polygons are classified by their normal direction
+            // (same-facing normal → frontList, opposite-facing normal → backList)
+            plane.splitPolygon(polygon, frontList, backList, frontList, backList);
+
+        // Recursively clip front fragments against front subtree
+        List<SolidPolygon> resultFront = frontList;
+        if (front != null) resultFront = front.clipPolygons(frontList);
+
+        // Recursively clip back fragments against back subtree
+        List<SolidPolygon> resultBack;
+        if (back != null) resultBack = back.clipPolygons(backList);
+        else resultBack = new ArrayList<>();
+
+        // Combine surviving fragments from both subtrees
+        final List<SolidPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
+        result.addAll(resultFront);
+        result.addAll(resultBack);
+        return result;
+    }
+
+    /**
+     * Clips this BSP tree against another BSP tree.
+     *
+     * @param bsp the BSP tree to clip against
+     */
+    public void clipTo(final BspTree bsp) {
+        final List<SolidPolygon> newPolygons = bsp.clipPolygons(polygons);
+        polygons.clear();
+        polygons.addAll(newPolygons);
+
+        if (front != null) front.clipTo(bsp);
+        if (back != null) back.clipTo(bsp);
+    }
+
+    /**
+     * Collects all polygons from this BSP tree into a flat list.
+     *
+     * @return a new list containing all polygons in this tree
+     */
+    public List<SolidPolygon> allPolygons() {
+        final List<SolidPolygon> result = new ArrayList<>(polygons);
+
+        if (front != null) result.addAll(front.allPolygons());
+        if (back != null) result.addAll(back.allPolygons());
+
+        return result;
+    }
+
+    /**
+     * Adds polygons to this BSP tree, partitioning space recursively.
+     *
+     * <p>This method is the core BSP tree construction algorithm. It builds or
+     * extends the tree by choosing a partition plane and classifying each polygon:</p>
+     *
+     * <ul>
+     *   <li><b>Coplanar</b> — polygons on the partition plane are stored in this node</li>
+     *   <li><b>Front</b> — polygons in the front half-space (same side as plane normal)
+     *       go to the front child subtree</li>
+     *   <li><b>Back</b> — polygons in the back half-space (opposite to plane normal)
+     *       go to the back child subtree</li>
+     *   <li><b>Spanning</b> — polygons crossing the plane are split into front and back
+     *       fragments, each going to its respective subtree</li>
+     * </ul>
+     *
+     * <p>For an empty tree, the first polygon's plane becomes the partition plane.
+     * Child nodes are created lazily when polygons need to be stored in them.</p>
+     *
+     * <p>Can be called multiple times to incrementally extend an existing tree,
+     * though the original partition planes remain unchanged.</p>
+     *
+     * @param polygons the polygons to insert into this BSP tree
+     * @see Plane#splitPolygon the method that classifies and splits individual polygons
+     */
+    public void addPolygons(final List<SolidPolygon> polygons) {
+        if (polygons.isEmpty()) return;
+
+        if (plane == null) plane = polygons.get(0).getPlane().clone();
+
+        final List<SolidPolygon> frontList = new ArrayList<>();
+        final List<SolidPolygon> backList = new ArrayList<>();
+
+        for (final SolidPolygon polygon : polygons)
+            plane.splitPolygon(polygon, this.polygons, this.polygons, frontList, backList);
+
+        if (!frontList.isEmpty()) {
+            if (front == null) front = new BspTree();
+            front.addPolygons(frontList);
+        }
+
+        if (!backList.isEmpty()) {
+            if (back == null) back = new BspTree();
+            back.addPolygons(backList);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java
new file mode 100644 (file)
index 0000000..d405b42
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an infinite plane in 3D space using the Hesse normal form.
+ *
+ * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
+ * in CSG. They divide 3D space into two half-spaces.</p>
+ *
+ * @see SolidPolygon polygons that reference their containing plane
+ * @see BspTree BSP trees that use planes for spatial partitioning
+ */
+public class Plane {
+
+    /**
+     * Epsilon value used for floating-point comparisons in BSP operations.
+     * Smaller values provide higher precision but may cause issues with
+     * near-coplanar polygons. 1e-5 is a good balance for most 3D geometry.
+     */
+    public static final double EPSILON = 1e-12;
+
+    /**
+     * The unit normal vector perpendicular to the plane surface.
+     */
+    public Point3D normal;
+
+    /**
+     * The signed distance from the origin to the plane along the normal.
+     */
+    public double distance;
+
+    /**
+     * Creates a plane with the given normal and distance.
+     *
+     * @param normal   the unit normal vector
+     * @param distance the signed distance from origin to the plane
+     */
+    public Plane(final Point3D normal, final double distance) {
+        this.normal = normal;
+        this.distance = distance;
+    }
+
+    /**
+     * Computes the unit normal vector for a triangle defined by three points.
+     *
+     * <p>Zero-allocation method: fills the result point instead of creating a new one.
+     * This is the shared implementation used by both {@link #fromPoints} and
+     * {@link SolidPolygon} for shading calculations.</p>
+     *
+     * <p>The normal is computed as the cross product of two edge vectors (b-a and c-a),
+     * then normalized to unit length.</p>
+     *
+     * @param a     first point (base point for edge vectors)
+     * @param b     second point
+     * @param c     third point
+     * @param result Point3D to receive the unit normal vector (modified in place)
+     * @return true if normal computed successfully, false if points are collinear
+     *         (cross product magnitude less than EPSILON)
+     */
+    public static boolean computeNormal(final Point3D a, final Point3D b,
+                                        final Point3D c, final Point3D result) {
+        // Edge vectors from a to b and a to c
+        final double ax = b.x - a.x;
+        final double ay = b.y - a.y;
+        final double az = b.z - a.z;
+
+        final double bx = c.x - a.x;
+        final double by = c.y - a.y;
+        final double bz = c.z - a.z;
+
+        // Cross product: (edge1 × edge2)
+        double nx = ay * bz - az * by;
+        double ny = az * bx - ax * bz;
+        double nz = ax * by - ay * bx;
+
+        // Normalize
+        final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
+        if (length < EPSILON) {
+            result.x = result.y = result.z = 0;
+            return false;
+        }
+
+        result.x = nx / length;
+        result.y = ny / length;
+        result.z = nz / length;
+        return true;
+    }
+
+    /**
+     * Creates a plane from three non-collinear points.
+     *
+     * <p>Uses {@link #computeNormal} for the normal calculation, then computes
+     * the signed distance from origin using the dot product.</p>
+     *
+     * @param a the first point on the plane
+     * @param b the second point on the plane
+     * @param c the third point on the plane
+     * @return a new Plane passing through the three points
+     * @throws ArithmeticException if the points are collinear (cannot define a plane)
+     */
+    public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
+        final Point3D n = new Point3D();
+        if (!computeNormal(a, b, c, n)) {
+            throw new ArithmeticException(
+                    "Cannot create plane from collinear points: cross product is zero");
+        }
+        return new Plane(n, n.dot(a));
+    }
+
+    /**
+     * Creates a deep clone of this plane.
+     *
+     * @return a new Plane with the same normal and distance
+     */
+    public Plane clone() {
+        return new Plane(new Point3D(normal.x, normal.y, normal.z), distance);
+    }
+
+    /**
+     * Flips the plane orientation by negating the normal and distance.
+     */
+    public void flip() {
+        normal = normal.withNegated();
+        distance = -distance;
+    }
+
+    /**
+     * Splits a polygon by this plane, classifying and potentially dividing it.
+     *
+     * @param polygon       the polygon to classify and potentially split
+     * @param coplanarFront list to receive coplanar polygons with same-facing normals
+     * @param coplanarBack  list to receive coplanar polygons with opposite-facing normals
+     * @param front         list to receive polygons in the front half-space
+     * @param back          list to receive polygons in the back half-space
+     */
+    public void splitPolygon(final SolidPolygon polygon,
+                             final List<SolidPolygon> coplanarFront,
+                             final List<SolidPolygon> coplanarBack,
+                             final List<SolidPolygon> front,
+                             final List<SolidPolygon> back) {
+
+        PolygonType polygonType = PolygonType.COPLANAR;
+        final int vertexCount = polygon.getVertexCount();
+        final PolygonType[] types = new PolygonType[vertexCount];
+
+        for (int i = 0; i < vertexCount; i++) {
+            final Vertex v = polygon.vertices.get(i);
+            final double t = normal.dot(v.coordinate) - distance;
+            final PolygonType type = (t < -EPSILON) ? PolygonType.BACK
+                    : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR;
+            polygonType = polygonType.combine(type);
+            types[i] = type;
+        }
+
+        switch (polygonType) {
+            case COPLANAR:
+                ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
+                break;
+
+            case FRONT:
+                front.add(polygon);
+                break;
+
+            case BACK:
+                back.add(polygon);
+                break;
+
+            case SPANNING:
+                // Split spanning polygon by clipping each edge against the plane.
+                // Vertices on each side go to their respective lists.
+                // Edges crossing the plane create intersection vertices added to both lists.
+                final List<Vertex> frontVertices = new ArrayList<>();
+                final List<Vertex> backVertices = new ArrayList<>();
+
+                for (int i = 0; i < vertexCount; i++) {
+                    final int nextIndex = (i + 1) % vertexCount;
+                    final PolygonType currentType = types[i];
+                    final PolygonType nextType = types[nextIndex];
+                    final Vertex currentVertex = polygon.vertices.get(i);
+                    final Vertex nextVertex = polygon.vertices.get(nextIndex);
+
+                    // Add current vertex to the polygon on its side of the plane
+                    if (currentType.isFront()) {
+                        frontVertices.add(currentVertex.clone());
+                    }
+                    if (currentType.isBack()) {
+                        backVertices.add(currentVertex.clone());
+                    }
+
+                    // If edge crosses the plane, create intersection vertex for both polygons
+                    if (currentType != nextType
+                            && currentType != PolygonType.COPLANAR
+                            && nextType != PolygonType.COPLANAR) {
+                        // Calculate interpolation parameter t (0 = current, 1 = next)
+                        // t represents where along the edge the plane intersection occurs
+                        final double t = (distance - normal.dot(currentVertex.coordinate))
+                                / normal.dot(nextVertex.coordinate.withSubtracted(currentVertex.coordinate));
+
+                        final Vertex intersectionVertex = currentVertex.interpolate(nextVertex, t);
+                        frontVertices.add(intersectionVertex);
+                        backVertices.add(intersectionVertex.clone());
+                    }
+                }
+
+                if (frontVertices.size() >= 3) {
+                    final SolidPolygon frontPoly = SolidPolygon.fromVertices(
+                            frontVertices, polygon.getColor(), polygon.isShadingEnabled());
+                    front.add(frontPoly);
+                }
+                if (backVertices.size() >= 3) {
+                    final SolidPolygon backPoly = SolidPolygon.fromVertices(
+                            backVertices, polygon.getColor(), polygon.isShadingEnabled());
+                    back.add(backPoly);
+                }
+                break;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java
new file mode 100644 (file)
index 0000000..506c9db
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Aukio 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base;
+
+/**
+ * Classification of a polygon's position relative to a plane.
+ * Used in BSP tree operations to determine how polygons should be split.
+ */
+public enum PolygonType {
+    /** Polygon lies on the plane. */
+    COPLANAR,
+    /** Polygon is entirely in front of the plane. */
+    FRONT,
+    /** Polygon is entirely behind the plane. */
+    BACK,
+    /** Polygon straddles the plane (vertices on both sides). */
+    SPANNING;
+
+    /**
+     * Combines this type with another to compute the aggregate classification.
+     * When vertices are on both sides of a plane, the result is SPANNING.
+     *
+     * @param other the other polygon type to combine with
+     * @return the combined classification
+     */
+    public PolygonType combine(final PolygonType other) {
+        if (this == other || other == COPLANAR) {
+            return this;
+        }
+        if (this == COPLANAR) {
+            return other;
+        }
+        // FRONT + BACK = SPANNING
+        return SPANNING;
+    }
+
+    /**
+     * Checks if this type represents a vertex in front of the plane.
+     *
+     * @return true if FRONT or COPLANAR (treated as front for classification)
+     */
+    public boolean isFront() {
+        return this == FRONT || this == COPLANAR;
+    }
+
+    /**
+     * Checks if this type represents a vertex behind the plane.
+     *
+     * @return true if BACK or COPLANAR (treated as back for classification)
+     */
+    public boolean isBack() {
+        return this == BACK || this == COPLANAR;
+    }
+}
\ No newline at end of file
index 92d69b9..5792de1 100644 (file)
@@ -139,7 +139,7 @@ public class Texture {
      * Creates a new texture with the specified dimensions and upscale capacity.
      *
      * <p>The underlying {@link java.awt.image.BufferedImage} is created using
-     * {@link eu.svjatoslav.aukio.e3d.gui.RenderingContext#bufferedImageType} for
+     * {@link eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext#bufferedImageType} for
      * compatibility with the raster rendering pipeline.</p>
      *
      * @param width      the width of the primary bitmap in pixels
index c648bdd..c75d42c 100644 (file)
@@ -5,7 +5,7 @@
 package eu.svjatoslav.aukio.e3d.headless;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.Camera;
+import eu.svjatoslav.aukio.e3d.geometry.Camera;
 import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
 import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
index 0d6fea0..6eb00d5 100644 (file)
@@ -5,7 +5,7 @@
 package eu.svjatoslav.aukio.e3d.renderer.raster;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
 import eu.svjatoslav.aukio.e3d.math.Transform;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
@@ -115,7 +115,7 @@ public class ParallelTransformTest {
         // Serial run: no executor -> serial fallback path
         final RenderingContext serialCtx = new RenderingContext(W, H, 1);
         serialCtx.prepareForNewFrameRendering();
-        scene.transformShapes(panel, serialCtx);
+        scene.transformShapes(panel.getCamera(), serialCtx);
         scene.sortShapes();
         final int[] serialIds = snapshotIds(scene);
         final double[] serialZs = snapshotZs(scene);
@@ -128,7 +128,7 @@ public class ParallelTransformTest {
         parallelCtx.transformExecutor = executor;
         parallelCtx.prepareForNewFrameRendering();
         parallelCtx.prepareForNewFrameRendering(); // distinct frameNumber -> full re-transform
-        scene.transformShapes(panel, parallelCtx);
+        scene.transformShapes(panel.getCamera(), parallelCtx);
         scene.sortShapes();
         final int[] parallelIds = snapshotIds(scene);
         final double[] parallelZs = snapshotZs(scene);
@@ -183,7 +183,7 @@ public class ParallelTransformTest {
         // Serial run
         final RenderingContext serialCtx = new RenderingContext(W, H, 1);
         serialCtx.prepareForNewFrameRendering();
-        scene.transformShapes(panel, serialCtx);
+        scene.transformShapes(panel.getCamera(), serialCtx);
         scene.sortShapes();
         final int[] serialIds = snapshotIds(scene);
         final double[] serialZs = snapshotZs(scene);
@@ -194,7 +194,7 @@ public class ParallelTransformTest {
         parallelCtx.transformExecutor = executor;
         parallelCtx.prepareForNewFrameRendering();
         parallelCtx.prepareForNewFrameRendering();
-        scene.transformShapes(panel, parallelCtx);
+        scene.transformShapes(panel.getCamera(), parallelCtx);
         scene.sortShapes();
         final int[] parallelIds = snapshotIds(scene);
         final double[] parallelZs = snapshotZs(scene);
index ef7c9af..a93dec7 100644 (file)
@@ -6,11 +6,11 @@ package eu.svjatoslav.aukio.e3d.renderer.raster;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.gui.SegmentRenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext;
 import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
 import eu.svjatoslav.aukio.e3d.math.Transform;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.GlowingPoint;
 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line;
@@ -182,7 +182,7 @@ public class SegmentBinningTest {
     private void transformAndSort(final ViewPanel panel, final ShapeCollection scene,
                                   final RenderingContext context) {
         context.prepareForNewFrameRendering();
-        scene.transformShapes(panel, context);
+        scene.transformShapes(panel.getCamera(), context);
         scene.sortShapes();
     }
 
index 881e716..50b9418 100644 (file)
@@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
 import org.junit.Test;
 
index 0c04e7f..4fe9ef3 100644 (file)
@@ -6,8 +6,8 @@ package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
 
 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
-import eu.svjatoslav.aukio.e3d.math.Vertex;
+import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex;
 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
 import org.junit.Test;