From: Svjatoslav Agejenko
Date: Mon, 30 Mar 2026 16:44:01 +0000 (+0300)
Subject: feat: unify polygon type for CSG and rendering
X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=refs%2Fheads%2Fmaster;p=sixth-3d.git
feat: unify polygon type for CSG and rendering
Extend SolidPolygon to support N-vertex convex polygons, enabling
direct use in CSG operations without triangulation. Move CSG boolean
operations (union, subtract, intersect) from the standalone CSG class
to AbstractCompositeShape for in-place modifications with simpler API.
- SolidPolygon now handles arbitrary convex polygons via fan triangulation
- CSG operations work directly on SolidPolygon, eliminating CSGPolygon
- Add chainable setters to AbstractCompositeShape for fluent API
- Add non-mutating methods to Point2D/Point3D/Transform
- Rename TexturedPolygon to TexturedTriangle for consistency
- Fix vertex cloning, polygon validation, collinear point detection
- Use ThreadLocal fields for thread-safe rendering state
---
diff --git a/AGENTS.md b/AGENTS.md
index 26fba88..5c1ee6f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,7 +22,7 @@ sixth-3d-engine is a Java-based 3D rendering engine. It provides:
âââ octree/ â Octree volume representation and ray tracer
âââ raster/ â Rasterization pipeline
âââ shapes/
- â âââ basic/ â Primitive shapes: Line, SolidPolygon, TexturedPolygon
+ â âââ basic/ â Primitive shapes: Line, SolidPolygon, TexturedTriangle
â âââ composite/ â Composite shapes: AbstractCompositeShape, TextCanvas,
â WireframeBox, SolidPolygonRectangularBox
âââ slicer/ â Geometry slicing for level-of-detail
@@ -103,13 +103,21 @@ All Java files must start with this exact header:
Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen coordinates:
-| Axis | Positive Direction | Meaning |
-|------|-------------------|--------------------------------------|
-| X | RIGHT | Larger X = further right |
-| Y | DOWN | Smaller Y = higher visually (up) |
-| Z | AWAY from viewer | Negative Z = closer to camera |
+| Axis | Positive Direction | Meaning |
+|------|--------------------|----------------------------------|
+| X | RIGHT | Larger X = further right |
+| Y | DOWN | Smaller Y = higher visually (up) |
+| Z | AWAY from viewer | Negative Z = closer to camera |
+
+**Important positioning rules:**
+
+- To place object A **above** object B, give A a **smaller Y value** (`y - offset`)
+- To place object A **below** object B, give A a **larger Y value** (`y + offset`)
+- This is the opposite of many 3D engines (OpenGL, Unity, Blender) which use Y-up
+
+**Common mistake:** If you're used to Y-up engines, you may accidentally place elements above when you intend below (or
+vice versa). Always verify: positive Y = down in Sixth 3D.
-- To place object A "above" object B, give A a **smaller Y value**
- `Point2D` and `Point3D` are mutable value types with public fields (`x`, `y`, `z`)
- Points support fluent/chaining API â mutation methods return `this`
- `Vertex` wraps a `Point3D` and adds `transformedCoordinate` for viewer-relative positioning
@@ -123,9 +131,9 @@ Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen co
## Shape Hierarchy
- `AbstractShape` â base class with optional `MouseInteractionController`
-- `AbstractCoordinateShape` â has `Vertex[]` coordinates and `onScreenZ` for depth sorting
+- `AbstractCoordinateShape` â has `List` coordinates and `onScreenZ` for depth sorting
- `AbstractCompositeShape` â groups sub-shapes with group IDs and visibility toggles
-- Concrete shapes: `Line`, `SolidPolygon`, `TexturedPolygon`, `TextCanvas`, `WireframeBox`
+- Concrete shapes: `Line`, `SolidPolygon`, `TexturedTriangle`, `TextCanvas`, `WireframeBox`
## Rendering
@@ -155,5 +163,6 @@ Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen co
4. **Render pipeline:** Shapes must implement `transform()` and `paint()` methods
5. **Depth sorting:** Set `onScreenZ` correctly during `transform()` for proper rendering order
6. **Backface culling:** Uses signed area in screen space; `signedArea < 0` = front-facing (CCW)
-7. **Polygon winding:** CCW in screen space = front face. Vertex order: top â lower-left â lower-right (as seen from camera). See `WindingOrderDemo` in sixth-3d-demos.
+7. **Polygon winding:** CCW in screen space = front face. Vertex order: top â lower-left â lower-right (as seen from
+ camera). See `WindingOrderDemo` in sixth-3d-demos.
8. **Testing:** Write JUnit 4 tests in `src/test/java/` with matching package structure
diff --git a/doc/Axis.png b/doc/Axis.png
deleted file mode 100644
index d028e61..0000000
Binary files a/doc/Axis.png and /dev/null differ
diff --git a/doc/Minimal example.png b/doc/Minimal example.png
deleted file mode 100644
index b2ceac7..0000000
Binary files a/doc/Minimal example.png and /dev/null differ
diff --git a/doc/Winding order demo.png b/doc/Winding order demo.png
deleted file mode 100644
index e9f9e25..0000000
Binary files a/doc/Winding order demo.png and /dev/null differ
diff --git a/doc/index.org b/doc/index.org
index 532b066..dc1de32 100644
--- a/doc/index.org
+++ b/doc/index.org
@@ -104,125 +104,6 @@ of providing a platform for 3D user interfaces and interactive data
visualization. It can also be used as a standalone 3D engine in any
Java project. See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demos]] for examples of what it can do today.
-* Minimal example
-:PROPERTIES:
-:CUSTOM_ID: tutorial
-:ID: 19a0e3f9-5225-404e-a48b-584b099fccf9
-:END:
-
-*Resources to help you understand the Sixth 3D library:*
-- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]].
-- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using
- [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
-- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]].
-
-
-*Brief tutorial:*
-
-Here we guide you through creating your first 3D scene with Sixth 3D
-engine.
-
-Prerequisites:
-- Java 21 or later installed
-- Maven 3.x
-- Basic Java knowledge
-
-** Add Dependency to Your Project
-:PROPERTIES:
-:CUSTOM_ID: add-dependency-to-your-project
-:ID: 3fffc32e-ae66-40b7-ad7d-fab6093c778b
-:END:
-
-Add Sixth 3D to your pom.xml:
-
-#+BEGIN_SRC xml
-
-
- eu.svjatoslav
- sixth-3d
- 1.3
-
-
-
-
-
- svjatoslav.eu
- Svjatoslav repository
- https://www3.svjatoslav.eu/maven/
-
-
-#+END_SRC
-
-** Create Your First 3D Scene
-:PROPERTIES:
-:CUSTOM_ID: create-your-first-3d-scene
-:ID: 564fa596-9b2b-418a-9df9-baa46f0d0a66
-:END:
-
-Here is a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/MinimalExample.java;h=af755e8a159c64b3ab8a14c8e76441608ecbf8ee;hb=HEAD][minimal working example]]:
-
-#+BEGIN_SRC java
- import eu.svjatoslav.sixth.e3d.geometry.Point3D;
- import eu.svjatoslav.sixth.e3d.gui.ViewFrame;
- import eu.svjatoslav.sixth.e3d.math.Transform;
- import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
- import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
- import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox;
-
- public class MyFirstScene {
- public static void main(String[] args) {
- // Create the application window
- ViewFrame viewFrame = new ViewFrame();
-
- // Get the collection where you add 3D shapes
- ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection();
-
- // Add a red box at position (0, 0, 0)
- Transform boxTransform = new Transform(new Point3D(0, 0, 0), 0, 0);
- SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
- new Point3D(-50, -50, -50),
- new Point3D(50, 50, 50),
- Color.RED
- );
- box.setTransform(boxTransform);
- shapes.addShape(box);
-
- // Position your camera
- viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, -100, -300));
-
- // Start the render thread
- viewFrame.getViewPanel().ensureRenderThreadStarted();
- }
- }
-#+END_SRC
-
-Compile and run *MyFirstScene* class. A new window should open that will
-display 3D scene with red box.
-
-This example is available in the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] project. Run it directly:
-
-: java -cp sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.MyFirstScene
-
-You should see this:
-
-[[file:Minimal example.png]]
-
-
-*Navigating the scene:*
-
-| Input | Action |
-|---------------------+-------------------------------------|
-| Arrow Up / W | Move forward |
-| Arrow Down / S | Move backward |
-| Arrow Left | Move left (strafe) |
-| Arrow Right | Move right (strafe) |
-| Mouse drag | Look around (rotate camera) |
-| Mouse scroll wheel | Move up / down |
-
-Movement uses physics-based acceleration for smooth, natural
-motion. The faster you're moving, the more acceleration builds up,
-creating an intuitive flying experience.
-
* Understanding 3D engine
:PROPERTIES:
:CUSTOM_ID: defining-scene
@@ -234,7 +115,8 @@ creating an intuitive flying experience.
- To understand perspective-correct texture mapping, see dedicated
page: [[file:perspective-correct-textures/][Perspective-correct textures]]
-- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples.
+- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples. Start with [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#minimal-example][minimal
+ example]].
** Coordinate System (X, Y, Z)
:PROPERTIES:
@@ -281,6 +163,10 @@ graphics background.
- To place object A "above" object B, give A a **smaller Y value**
than B.
+The [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#coordinate-system][sixth-3d-demos]] project includes an interactive
+coordinate system reference showing X, Y, Z axes as colored arrows
+with a grid plane for spatial context.
+
** Vertex
:PROPERTIES:
:CUSTOM_ID: vertex
@@ -373,7 +259,7 @@ A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamenta
- Always guaranteed to be coplanar
- Quads (4 vertices) = 2 triangles
- Complex shapes = many triangles (a "mesh")
-- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] in Sixth 3D.
+- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html][SolidTriangle]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] in Sixth 3D.
** Normal Vector
:PROPERTIES:
@@ -453,6 +339,10 @@ A *mesh* is a collection of vertices, edges, and faces that together define the
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]]: base class for single shapes with vertices (triangles, lines). Use when creating one primitive.
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together.
+See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual showcase of
+all primitive shapes available in Sixth 3D, rendered in both
+wireframe and solid polygon styles with dynamic lighting.
+
** Winding Order & Backface Culling
:PROPERTIES:
:CUSTOM_ID: winding-order-backface-culling
@@ -511,45 +401,9 @@ optimization.
(in Y-down screen coordinates, negative signed area corresponds to
visually CCW winding)
-
-*Minimal Example: WindingOrderDemo*
-
-The [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][sixth-3d-demos]] project includes a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java][winding order demo]] to
-demonstrate how winding order affects backface culling:
-
-#+BEGIN_SRC java
-// WindingOrderDemo.java - validates CCW winding = front face
-public class WindingOrderDemo {
- public static void main(String[] args) {
- ViewFrame viewFrame = new ViewFrame();
- ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection();
-
- double size = 150;
-
- // CCW winding: top â lower-left â lower-right
- Point3D upperCenter = new Point3D(0, -size, 0);
- Point3D lowerLeft = new Point3D(-size, +size, 0);
- Point3D lowerRight = new Point3D(+size, +size, 0);
-
- SolidPolygon triangle = new SolidPolygon(upperCenter, lowerLeft, lowerRight, Color.GREEN);
- triangle.setBackfaceCulling(true);
-
- shapes.addShape(triangle);
-
- viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, 0, -500));
- viewFrame.getViewPanel().ensureRenderThreadStarted();
- }
-}
-#+END_SRC
-
-Run this demo: if the green triangle is visible, the winding order is
-correct (CCW = front face)
-
-[[file:Winding order demo.png]]
-
In Sixth 3D, backface culling is *optional* and disabled by default. Enable it per-shape:
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]]
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html#setBackfaceCulling(boolean)][TexturedPolygon.setBackfaceCulling(true)]]
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html#setBackfaceCulling(boolean)][SolidTriangle.setBackfaceCulling(true)]]
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]]
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all
sub-shapes)
diff --git a/doc/perspective-correct-textures/index.org b/doc/perspective-correct-textures/index.org
index 35f2cce..3b734a0 100644
--- a/doc/perspective-correct-textures/index.org
+++ b/doc/perspective-correct-textures/index.org
@@ -78,7 +78,7 @@ negligible.
The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] class recursively splits triangles:
#+BEGIN_SRC java
-void slice(TexturedPolygon polygon) {
+void slice(TexturedTriangle polygon) {
// Find the longest edge
BorderLine longest = findLongestEdge(polygon);
@@ -214,7 +214,7 @@ This visualization helps you:
| Class | Purpose |
|-----------------+--------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] | Textured triangle shape |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle shape |
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] | Recursive triangle subdivision |
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container with Graphics2D |
| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level |
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java
deleted file mode 100644
index 2d26a98..0000000
--- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.csg;
-
-import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-import eu.svjatoslav.sixth.e3d.math.Vertex;
-import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonMesh;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Represents a solid for Constructive Solid Geometry (CSG) operations.
- *
- * CSG allows combining 3D shapes using boolean operations:
- *
- * - Union: Combine two shapes into one
- * - Subtract: Carve one shape out of another
- * - Intersect: Keep only the overlapping volume
- *
- *
- * Usage example:
- * {@code
- * // Create shapes from existing composite shapes
- * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 80, Color.RED);
- * SolidPolygonSphere sphere = new SolidPolygonSphere(new Point3D(0, 0, 0), 96, 12, Color.BLUE);
- *
- * // Convert to CSG solids
- * CSG cubeCSG = CSG.fromCompositeShape(cube);
- * CSG sphereCSG = CSG.fromCompositeShape(sphere);
- *
- * // Perform boolean operation
- * CSG result = cubeCSG.subtract(sphereCSG);
- *
- * // Render the result
- * SolidPolygonMesh mesh = result.toMesh(new Color(255, 100, 100), new Point3D(0, 0, 0));
- * shapes.addShape(mesh);
- * }
- *
- * @see CSGNode the BSP tree node used internally
- * @see CSGPolygon the N-gon polygon type used for BSP operations
- * @see SolidPolygonMesh the renderable mesh created from CSG results
- */
-public class CSG {
-
- /**
- * The list of polygons that make up this solid.
- */
- public final List polygons = new ArrayList<>();
-
- /**
- * Creates an empty CSG solid.
- */
- public CSG() {
- }
-
- /**
- * Creates a CSG solid from a list of CSG polygons.
- *
- * @param polygonList the polygons to include
- * @return a new CSG solid
- */
- public static CSG fromPolygons(final List polygonList) {
- final CSG csg = new CSG();
- csg.polygons.addAll(polygonList);
- return csg;
- }
-
- /**
- * Creates a CSG solid from a list of SolidPolygon triangles.
- *
- * Each SolidPolygon is converted to a CSGPolygon (3-vertex N-gon).
- * The color from each SolidPolygon is preserved.
- *
- * @param solidPolygons the triangles to convert
- * @return a new CSG solid
- */
- public static CSG fromSolidPolygons(final List solidPolygons) {
- final List csgPolygons = new ArrayList<>(solidPolygons.size());
-
- for (final SolidPolygon sp : solidPolygons) {
- final List vertices = new ArrayList<>(3);
- for (int i = 0; i < 3; i++) {
- final Vertex v = new Vertex(sp.vertices[i].coordinate);
- v.normal = sp.vertices[i].normal;
- vertices.add(v);
- }
-
- final CSGPolygon csgPoly = new CSGPolygon(vertices, sp.getColor());
- csgPolygons.add(csgPoly);
- }
-
- return fromPolygons(csgPolygons);
- }
-
- /**
- * Creates a CSG solid from a composite shape.
- *
- * Extracts all SolidPolygon triangles from the composite shape
- * and converts them to CSGPolygons. This allows using shapes like
- * {@code SolidPolygonCube}, {@code SolidPolygonSphere}, etc. with CSG operations.
- *
- * @param shape the composite shape to convert
- * @return a new CSG solid containing all triangles from the shape
- */
- public static CSG fromCompositeShape(final AbstractCompositeShape shape) {
- return fromSolidPolygons(shape.extractSolidPolygons());
- }
-
- /**
- * Creates a deep clone of this CSG solid.
- *
- * @return a new CSG solid with cloned polygons
- */
- public CSG clone() {
- final CSG csg = new CSG();
- for (final CSGPolygon p : polygons) {
- csg.polygons.add(p.clone());
- }
- return csg;
- }
-
- /**
- * Returns the list of polygons in this solid.
- *
- * @return the polygon list
- */
- public List toPolygons() {
- return polygons;
- }
-
- /**
- * Performs a union operation with another CSG solid.
- *
- * The result contains all points that are in either solid.
- *
- * Algorithm:
- *
- * Union(A, B) = clip(A to outside B) + clip(B to outside A)
- *
- *
- * - Clip A's polygons to keep only parts outside B
- * - Clip B's polygons to keep only parts outside A
- * - Invert B, clip to A, invert again (keeps B's surface inside A)
- * - Build final tree from all remaining polygons
- *
- *
- * @param csg the other solid to union with
- * @return a new CSG solid representing the union
- */
- public CSG union(final CSG csg) {
- // Create BSP trees from both solids
- final CSGNode a = new CSGNode(clone().polygons);
- final CSGNode b = new CSGNode(csg.clone().polygons);
-
- // Remove from A any parts that are inside B
- a.clipTo(b);
-
- // Remove from B any parts that are inside A
- b.clipTo(a);
-
- // Invert B temporarily to capture B's interior surface that touches A
- b.invert();
- b.clipTo(a);
- b.invert();
-
- // Combine all polygons into A's tree
- a.build(b.allPolygons());
-
- return CSG.fromPolygons(a.allPolygons());
- }
-
- /**
- * Performs a subtraction operation with another CSG solid.
- *
- * The result contains all points that are in this solid but not in the other.
- * This effectively carves the other solid out of this one.
- *
- * Algorithm:
- *
- * Subtract(A, B) = A - B = clip(inverted A to B) inverted
- *
- *
- * - Invert A (turning solid into cavity, cavity into solid)
- * - Clip inverted A to keep only parts inside B
- * - Clip B to keep only parts inside inverted A
- * - Invert B twice to get B's cavity surface
- * - Combine and invert final result
- *
- *
- * The inversion trick converts "subtract B from A" into "intersect A
- * with the inverse of B", which the BSP algorithm handles naturally.
- *
- * @param csg the solid to subtract
- * @return a new CSG solid representing the difference
- */
- public CSG subtract(final CSG csg) {
- // Create BSP trees from both solids
- final CSGNode a = new CSGNode(clone().polygons);
- final CSGNode b = new CSGNode(csg.clone().polygons);
-
- // Invert A: what was solid becomes empty, what was empty becomes solid
- // This transforms the problem into finding the intersection of inverted-A and B
- a.invert();
-
- // Remove from inverted-A any parts outside B (keep intersection)
- a.clipTo(b);
-
- // Remove from B any parts outside inverted-A (keep intersection)
- b.clipTo(a);
-
- // Capture B's interior surface
- b.invert();
- b.clipTo(a);
- b.invert();
-
- // Combine B's interior surface with A
- a.build(b.allPolygons());
-
- // Invert result to convert back from "intersection with inverse" to "subtraction"
- a.invert();
-
- return CSG.fromPolygons(a.allPolygons());
- }
-
- /**
- * Performs an intersection operation with another CSG solid.
- *
- * The result contains only the points that are in both solids.
- *
- * Algorithm:
- *
- * Intersect(A, B) = clip(inverted A to outside B) inverted
- *
- *
- * - Invert A (swap inside/outside)
- * - Clip inverted-A to B, keeping parts outside B
- * - Invert B, clip to A (captures B's interior surface)
- * - Clip B again to ensure proper boundaries
- * - Combine and invert final result
- *
- *
- * This uses the principle: A ⩠B = ¬(¬A ⪠¬B)
- *
- * @param csg the other solid to intersect with
- * @return a new CSG solid representing the intersection
- */
- public CSG intersect(final CSG csg) {
- // Create BSP trees from both solids
- final CSGNode a = new CSGNode(clone().polygons);
- final CSGNode b = new CSGNode(csg.clone().polygons);
-
- // Invert A to transform intersection into a union-like operation
- a.invert();
-
- // Clip B to keep only parts inside inverted-A (outside original A)
- b.clipTo(a);
-
- // Invert B to capture its interior surface
- b.invert();
-
- // Clip A to keep only parts inside inverted-B (outside original B)
- a.clipTo(b);
-
- // Clip B again to ensure proper boundary handling
- b.clipTo(a);
-
- // Combine B's interior surface with A
- a.build(b.allPolygons());
-
- // Invert result to get the actual intersection
- a.invert();
-
- return CSG.fromPolygons(a.allPolygons());
- }
-
- /**
- * Returns the inverse of this solid.
- *
- * The inverse has all polygons flipped, effectively turning the solid inside-out.
- *
- * @return a new CSG solid representing the inverse
- */
- public CSG inverse() {
- final CSG csg = clone();
- for (final CSGPolygon p : csg.polygons) {
- p.flip();
- }
- return csg;
- }
-
- /**
- * Converts this CSG solid to a renderable mesh with a uniform color.
- *
- * All polygons are rendered with the specified color, ignoring
- * any colors stored in the CSGPolygons.
- *
- * @param color the color to apply to all triangles
- * @param location the position in 3D space for the mesh
- * @return a renderable mesh containing triangles
- */
- public SolidPolygonMesh toMesh(final Color color, final Point3D location) {
- final List triangles = new ArrayList<>();
-
- for (final CSGPolygon polygon : polygons) {
- triangulatePolygon(polygon, color, triangles);
- }
-
- return new SolidPolygonMesh(triangles, location);
- }
-
- /**
- * Triangulates a CSGPolygon using fan triangulation.
- *
- * Fan triangulation works by selecting the first vertex as a central point
- * and connecting it to each pair of consecutive vertices. For an N-gon,
- * this produces (N-2) triangles:
- *
- *
- * Original N-gon: v0-v1-v2-v3-v4...
- * Triangles: v0-v1-v2, v0-v2-v3, v0-v3-v4, ...
- *
- *
- * This method is suitable for convex polygons. For concave polygons,
- * it may produce overlapping triangles, but CSG operations typically
- * generate convex polygon fragments.
- *
- * @param polygon the polygon to triangulate (may have 3+ vertices)
- * @param color the color to apply to all resulting triangles
- * @param triangles the list to add the resulting SolidPolygon triangles to
- */
- private void triangulatePolygon(final CSGPolygon polygon, final Color color,
- final List triangles) {
- final int vertexCount = polygon.vertices.size();
-
- // Skip degenerate polygons (less than 3 vertices cannot form a triangle)
- if (vertexCount < 3) {
- return;
- }
-
- // Use the first vertex as the "pivot" of the fan
- final Point3D v0 = polygon.vertices.get(0).coordinate;
-
- // Create triangles by connecting v0 to each consecutive pair of vertices
- // For a polygon with vertices [v0, v1, v2, v3], we create:
- // - Triangle 1: v0, v1, v2 (i=1)
- // - Triangle 2: v0, v2, v3 (i=2)
- for (int i = 1; i < vertexCount - 1; i++) {
- final Point3D v1 = polygon.vertices.get(i).coordinate;
- final Point3D v2 = polygon.vertices.get(i + 1).coordinate;
-
- // Clone the points to avoid sharing references with the original polygon
- final SolidPolygon triangle = new SolidPolygon(
- new Point3D(v0),
- new Point3D(v1),
- new Point3D(v2),
- color
- );
-
- triangles.add(triangle);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java
index 0766122..86d1490 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java
@@ -4,6 +4,8 @@
*/
package eu.svjatoslav.sixth.e3d.csg;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
import java.util.ArrayList;
import java.util.List;
@@ -14,7 +16,7 @@ import java.util.List;
* Each node divides 3D space into two half-spaces using a plane, enabling
* efficient spatial queries and polygon clipping.
*
- * BSP Tree Structure:
+ * BSP Tree Structure:
*
* [Node: plane P]
* / \
@@ -23,73 +25,34 @@ import java.util.List;
* normal) of P's normal)
*
*
- * Key Properties:
- *
- * - polygons: Polygons coplanar with this node's partitioning plane
- * - plane: The partitioning plane that divides space
- * - front: Subtree for the half-space the plane normal points toward
- * - back: Subtree for the opposite half-space
- *
- *
- * CSG Algorithm Overview:
- * CSG boolean operations (union, subtraction, intersection) work by:
- *
- * - Building BSP trees from both input solids
- * - Clipping each tree against the other (removing overlapping geometry)
- * - Optionally inverting trees (for subtraction and intersection)
- * - Collecting the resulting polygons
- *
- *
- * @see CSG the main CSG class that provides the boolean operation API
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
* @see CSGPlane the plane type used for spatial partitioning
- * @see CSGPolygon the polygon type stored in BSP nodes
+ * @see SolidPolygon the polygon type stored in BSP nodes
*/
public class CSGNode {
/**
* Polygons that lie on this node's partitioning plane.
- *
- * These polygons are coplanar with the plane and are stored directly
- * in this node rather than being pushed down to child nodes. This includes
- * both polygons originally on this plane and polygons split by planes above
- * that ended up coplanar here.
*/
- public final List polygons = new ArrayList<>();
+ public final List polygons = new ArrayList<>();
/**
* The partitioning plane for this node.
- *
- * This plane divides 3D space into two half-spaces: front (where the
- * normal points) and back. All polygons in this node are coplanar with
- * this plane. Child nodes contain polygons on their respective sides.
- *
- * Null for leaf nodes (empty subtrees).
*/
public CSGPlane plane;
/**
* The front child subtree.
- *
- * Contains polygons that lie in the front half-space of this node's plane
- * (the side the normal points toward). May be null if no polygons exist
- * in the front half-space.
*/
public CSGNode front;
/**
* The back child subtree.
- *
- * Contains polygons that lie in the back half-space of this node's plane
- * (the side opposite the normal direction). May be null if no polygons exist
- * in the back half-space.
*/
public CSGNode back;
/**
* Creates an empty BSP node with no plane or children.
- *
- * This constructor creates a leaf node. The plane, front, and back
- * fields will be populated when polygons are added via {@link #build(List)}.
*/
public CSGNode() {
}
@@ -97,35 +60,26 @@ public class CSGNode {
/**
* Creates a BSP tree from a list of polygons.
*
- * Delegates to {@link #build(List)} to construct the tree.
- *
* @param polygons the polygons to partition into a BSP tree
*/
- public CSGNode(final List polygons) {
+ public CSGNode(final List polygons) {
build(polygons);
}
/**
* Creates a deep clone of this BSP tree.
*
- * Recursively clones all child nodes and polygons. The resulting tree
- * is completely independent of the original.
- *
* @return a new CSGNode tree with cloned data
*/
public CSGNode clone() {
final CSGNode node = new CSGNode();
- // Clone the plane if present
node.plane = plane != null ? plane.clone() : null;
-
- // Recursively clone child subtrees
node.front = front != null ? front.clone() : null;
node.back = back != null ? back.clone() : null;
- // Clone each polygon in this node
- for (final CSGPolygon p : polygons) {
- node.polygons.add(p.clone());
+ for (final SolidPolygon p : polygons) {
+ node.polygons.add(p.deepClone());
}
return node;
@@ -133,36 +87,16 @@ public class CSGNode {
/**
* Inverts this BSP tree, converting "inside" to "outside" and vice versa.
- *
- * This operation is fundamental to CSG subtraction and intersection:
- *
- * - All polygon normals are flipped (reversing their facing direction)
- * - All plane normals are flipped
- * - Front and back subtrees are swapped
- *
- *
- * After inversion:
- *
- * - What was solid becomes empty space
- * - What was empty space becomes solid
- * - Front/back relationships are reversed throughout the tree
- *
- *
- * This is used in CSG subtraction where solid B "carves out" of solid A
- * by inverting B, unioning, then inverting the result.
*/
public void invert() {
- // Flip all polygons at this node
- for (final CSGPolygon polygon : polygons) {
+ for (final SolidPolygon polygon : polygons) {
polygon.flip();
}
- // Flip the partitioning plane
if (plane != null) {
plane.flip();
}
- // Recursively invert child subtrees
if (front != null) {
front.invert();
}
@@ -170,7 +104,6 @@ public class CSGNode {
back.invert();
}
- // Swap front and back children since the half-spaces are now reversed
final CSGNode temp = front;
front = back;
back = temp;
@@ -179,58 +112,34 @@ public class CSGNode {
/**
* Clips a list of polygons against this BSP tree.
*
- * This recursively removes the portions of the input polygons that lie
- * inside the solid represented by this BSP tree. The result contains only
- * the portions that are outside this solid.
- *
- * Algorithm:
- *
- * - At each node, split input polygons by the node's plane
- * - Polygons in front go to front child for further clipping
- * - Polygons in back go to back child for further clipping
- * - Coplanar polygons are kept (they're on the surface)
- * - If no back child exists, back polygons are discarded (they're inside)
- *
- *
- * This is used during CSG operations to remove overlapping geometry.
- *
* @param polygons the polygons to clip against this BSP tree
* @return a new list containing only the portions outside this solid
*/
- public List clipPolygons(final List polygons) {
- // Base case: if this is a leaf node, return copies of all polygons
+ public List clipPolygons(final List polygons) {
if (plane == null) {
return new ArrayList<>(polygons);
}
- // Split all input polygons by this node's plane
- final List frontList = new ArrayList<>();
- final List backList = new ArrayList<>();
+ final List frontList = new ArrayList<>();
+ final List backList = new ArrayList<>();
- for (final CSGPolygon polygon : polygons) {
- // Split polygon into front/back/coplanar parts
- // Note: coplanar polygons go into both front and back lists
+ for (final SolidPolygon polygon : polygons) {
plane.splitPolygon(polygon, frontList, backList, frontList, backList);
}
- // Recursively clip front polygons against front subtree
- List resultFront = frontList;
+ List resultFront = frontList;
if (front != null) {
resultFront = front.clipPolygons(frontList);
}
- // Recursively clip back polygons against back subtree
- List resultBack = backList;
+ List resultBack = backList;
if (back != null) {
resultBack = back.clipPolygons(backList);
} else {
- // No back child means this is a boundary - discard back polygons
- // (they would be inside the solid we're clipping against)
resultBack = new ArrayList<>();
}
- // Combine the clipped results
- final List result = new ArrayList<>(resultFront.size() + resultBack.size());
+ final List result = new ArrayList<>(resultFront.size() + resultBack.size());
result.addAll(resultFront);
result.addAll(resultBack);
return result;
@@ -239,22 +148,13 @@ public class CSGNode {
/**
* Clips this BSP tree against another BSP tree.
*
- * This removes from this tree all polygons that lie inside the solid
- * represented by the other BSP tree. Used during CSG operations to
- * eliminate overlapping geometry.
- *
- * The operation modifies this tree in place, replacing all polygons
- * with their clipped versions.
- *
- * @param bsp the BSP tree to clip against (the "cutter")
+ * @param bsp the BSP tree to clip against
*/
public void clipTo(final CSGNode bsp) {
- // Clip all polygons at this node against the other BSP tree
- final List newPolygons = bsp.clipPolygons(polygons);
+ final List newPolygons = bsp.clipPolygons(polygons);
polygons.clear();
polygons.addAll(newPolygons);
- // Recursively clip child subtrees
if (front != null) {
front.clipTo(bsp);
}
@@ -266,16 +166,11 @@ public class CSGNode {
/**
* Collects all polygons from this BSP tree into a flat list.
*
- * Recursively traverses the entire tree and collects all polygons
- * from all nodes. This is used after CSG operations to extract the
- * final result as a simple polygon list.
- *
* @return a new list containing all polygons in this tree
*/
- public List allPolygons() {
- final List result = new ArrayList<>(polygons);
+ public List allPolygons() {
+ final List result = new ArrayList<>(polygons);
- // Recursively collect polygons from child subtrees
if (front != null) {
result.addAll(front.allPolygons());
}
@@ -289,58 +184,24 @@ public class CSGNode {
/**
* Builds or extends this BSP tree from a list of polygons.
*
- * This is the core BSP tree construction algorithm. It partitions
- * space by selecting a splitting plane and recursively building subtrees.
- *
- * Algorithm:
- *
- * - If this node has no plane, use the first polygon's plane as the partitioning plane
- * - For each polygon:
- *
- * - Coplanar polygons are stored in this node
- * - Front polygons go to the front list
- * - Back polygons go to the back list
- * - Spanning polygons are split into front and back parts
- *
- *
- * - Recursively build front subtree with front polygons
- * - Recursively build back subtree with back polygons
- *
- *
- * Calling Conventions:
- *
- * - Can be called multiple times to add more polygons to an existing tree
- * - Empty polygon list is a no-op
- * - Creates child nodes as needed
- *
- *
* @param polygonList the polygons to add to this BSP tree
*/
- public void build(final List polygonList) {
- // Base case: no polygons to add
+ public void build(final List polygonList) {
if (polygonList.isEmpty()) {
return;
}
- // Initialize the partitioning plane if this is a new node
- // Use the first polygon's plane as the splitting plane
if (plane == null) {
- plane = polygonList.get(0).plane.clone();
+ plane = polygonList.get(0).getPlane().clone();
}
- // Classify each polygon relative to this node's plane
- final List frontList = new ArrayList<>();
- final List backList = new ArrayList<>();
+ final List frontList = new ArrayList<>();
+ final List backList = new ArrayList<>();
- for (final CSGPolygon polygon : polygonList) {
- // Split the polygon and distribute to appropriate lists:
- // - coplanarFront/coplanarBack â this node's polygons list
- // - front â frontList (for front subtree)
- // - back â backList (for back subtree)
+ for (final SolidPolygon polygon : polygonList) {
plane.splitPolygon(polygon, polygons, polygons, frontList, backList);
}
- // Recursively build front subtree
if (!frontList.isEmpty()) {
if (front == null) {
front = new CSGNode();
@@ -348,7 +209,6 @@ public class CSGNode {
front.build(frontList);
}
- // Recursively build back subtree
if (!backList.isEmpty()) {
if (back == null) {
back = new CSGNode();
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java
index 473608b..c14f671 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java
@@ -6,6 +6,7 @@ package eu.svjatoslav.sixth.e3d.csg;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
import java.util.ArrayList;
import java.util.List;
@@ -13,173 +14,103 @@ import java.util.List;
/**
* Represents an infinite plane in 3D space using the Hesse normal form.
*
- * A plane is defined by a normal vector (perpendicular to the plane surface)
- * and a scalar value 'w' representing the signed distance from the origin.
- * The plane equation is: {@code normal.x * x + normal.y * y + normal.z * z = w}
- *
* Planes are fundamental to BSP (Binary Space Partitioning) tree operations
- * in CSG. They divide 3D space into two half-spaces:
- *
- * - Front half-space: Points where {@code normal · point > w}
- * - Back half-space: Points where {@code normal · point < w}
- *
- *
- * Planes are used to:
- *
- * - Define the surface orientation of {@link CSGPolygon} faces
- * - Split polygons that cross BSP partition boundaries
- * - Determine which side of a BSP node a polygon lies on
- *
+ * in CSG. They divide 3D space into two half-spaces.
*
- * @see CSGPolygon polygons that reference their containing plane
+ * @see SolidPolygon polygons that reference their containing plane
* @see CSGNode BSP tree nodes that use planes for spatial partitioning
*/
public class CSGPlane {
/**
* Epsilon value used for floating-point comparisons.
- *
- * When determining which side of a plane a point lies on, values within
- * this threshold are considered coplanar (on the plane). This prevents
- * numerical instability from causing infinite recursion or degenerate
- * polygons during BSP operations.
*/
public static final double EPSILON = 0.01;
/**
* The unit normal vector perpendicular to the plane surface.
- *
- * The direction of the normal determines which side is "front"
- * and which is "back". The front is the side the normal points toward.
*/
public Point3D normal;
/**
* The signed distance from the origin to the plane along the normal.
- *
- * This is equivalent to the dot product of the normal with any point
- * on the plane. For a plane defined by point P and normal N:
- * {@code w = N · P}
*/
- public double w;
+ public double distance;
/**
* Creates a plane with the given normal and distance.
*
- * @param normal the unit normal vector (caller must ensure it's normalized)
- * @param w the signed distance from origin to the plane
+ * @param normal the unit normal vector
+ * @param distance the signed distance from origin to the plane
*/
- public CSGPlane(final Point3D normal, final double w) {
+ public CSGPlane(final Point3D normal, final double distance) {
this.normal = normal;
- this.w = w;
+ this.distance = distance;
}
/**
* Creates a plane from three non-collinear points.
*
- * The plane passes through all three points. The normal is computed
- * using the cross product of vectors (b-a) and (c-a), then normalized.
- * The winding order of the points determines the normal direction:
- *
- * - Counter-clockwise (CCW) winding â normal points toward viewer
- * - Clockwise (CW) winding â normal points away from viewer
- *
- *
* @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 CSGPlane passing through the three points
- * @throws ArithmeticException if the points are collinear (cross product is zero)
*/
public static CSGPlane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
- // Compute two edge vectors from point a
- final Point3D edge1 = b.minus(a);
- final Point3D edge2 = c.minus(a);
+ final Point3D edge1 = b.withSubtracted(a);
+ final Point3D edge2 = c.withSubtracted(a);
+
+ final Point3D cross = edge1.cross(edge2);
+
+ if (cross.getVectorLength() < EPSILON) {
+ throw new ArithmeticException(
+ "Cannot create plane from collinear points: cross product is zero");
+ }
- // Cross product gives the normal direction (perpendicular to both edges)
- final Point3D n = edge1.cross(edge2).unit();
+ final Point3D n = cross.unit();
- // Distance from origin is the projection of any point on the plane onto the normal
return new CSGPlane(n, n.dot(a));
}
/**
* Creates a deep clone of this plane.
*
- * The normal vector is cloned to avoid shared references.
- *
* @return a new CSGPlane with the same normal and distance
*/
public CSGPlane clone() {
- return new CSGPlane(new Point3D(normal.x, normal.y, normal.z), w);
+ return new CSGPlane(new Point3D(normal.x, normal.y, normal.z), distance);
}
/**
* Flips the plane orientation by negating the normal and distance.
- *
- * This effectively swaps the front and back half-spaces. After flipping:
- *
- * - Points that were in front are now in back
- * - Points that were in back are now in front
- * - Coplanar points remain coplanar
- *
- *
- * Used during CSG operations when inverting solids (converting "inside"
- * to "outside" and vice versa).
*/
public void flip() {
- normal = normal.negated();
- w = -w;
+ normal = normal.withNegated();
+ distance = -distance;
}
/**
* Splits a polygon by this plane, classifying and potentially dividing it.
*
- * This is the core operation for BSP tree construction. The polygon is
- * classified based on where its vertices lie relative to the plane:
- *
- * Classification types:
- *
- * - COPLANAR (0): All vertices lie on the plane (within EPSILON)
- * - FRONT (1): All vertices are in the front half-space
- * - BACK (2): All vertices are in the back half-space
- * - SPANNING (3): Vertices are on both sides (polygon crosses the plane)
- *
- *
- * Destination lists:
- *
- * - coplanarFront: Coplanar polygons with same-facing normals
- * - coplanarBack: Coplanar polygons with opposite-facing normals
- * - front: Polygons entirely in front half-space
- * - back: Polygons entirely in back half-space
- *
- *
- * Spanning polygon handling:
- * When a polygon spans the plane, it is split into two polygons:
- *
- * - Vertices on the front side become a new polygon (added to front list)
- * - Vertices on the back side become a new polygon (added to back list)
- * - Intersection points are computed and added to both polygons
- *
- *
* @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 CSGPolygon polygon,
- final List coplanarFront,
- final List coplanarBack,
- final List front,
- final List back) {
+ public void splitPolygon(final SolidPolygon polygon,
+ final List coplanarFront,
+ final List coplanarBack,
+ final List front,
+ final List back) {
PolygonType polygonType = PolygonType.COPLANAR;
- final PolygonType[] types = new PolygonType[polygon.vertices.size()];
+ final int vertexCount = polygon.getVertexCount();
+ final PolygonType[] types = new PolygonType[vertexCount];
- for (int i = 0; i < polygon.vertices.size(); i++) {
+ for (int i = 0; i < vertexCount; i++) {
final Vertex v = polygon.vertices.get(i);
- final double t = normal.dot(v.coordinate) - w;
+ 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);
@@ -188,7 +119,7 @@ public class CSGPlane {
switch (polygonType) {
case COPLANAR:
- ((normal.dot(polygon.plane.normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
+ ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
break;
case FRONT:
@@ -200,37 +131,37 @@ public class CSGPlane {
break;
case SPANNING:
- final List f = new ArrayList<>();
- final List b = new ArrayList<>();
+ final List frontVertices = new ArrayList<>();
+ final List backVertices = new ArrayList<>();
- for (int i = 0; i < polygon.vertices.size(); i++) {
- final int j = (i + 1) % polygon.vertices.size();
+ for (int i = 0; i < vertexCount; i++) {
+ final int j = (i + 1) % vertexCount;
final PolygonType ti = types[i];
final PolygonType tj = types[j];
final Vertex vi = polygon.vertices.get(i);
final Vertex vj = polygon.vertices.get(j);
if (ti.isFront()) {
- f.add(vi);
+ frontVertices.add(vi.clone());
}
if (ti.isBack()) {
- b.add(ti == PolygonType.COPLANAR ? vi.clone() : vi);
+ backVertices.add(vi.clone());
}
if (ti != tj && ti != PolygonType.COPLANAR && tj != PolygonType.COPLANAR) {
- final double t = (w - normal.dot(vi.coordinate))
- / normal.dot(vj.coordinate.minus(vi.coordinate));
+ final double t = (distance - normal.dot(vi.coordinate))
+ / normal.dot(vj.coordinate.withSubtracted(vi.coordinate));
final Vertex v = vi.interpolate(vj, t);
- f.add(v);
- b.add(v.clone());
+ frontVertices.add(v);
+ backVertices.add(v.clone());
}
}
- if (f.size() >= 3) {
- final CSGPolygon frontPoly = new CSGPolygon(f, polygon.color);
+ if (frontVertices.size() >= 3) {
+ final SolidPolygon frontPoly = new SolidPolygon(frontVertices, polygon.getColor(), true);
front.add(frontPoly);
}
- if (b.size() >= 3) {
- final CSGPolygon backPoly = new CSGPolygon(b, polygon.color);
+ if (backVertices.size() >= 3) {
+ final SolidPolygon backPoly = new SolidPolygon(backVertices, polygon.getColor(), true);
back.add(backPoly);
}
break;
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java
deleted file mode 100644
index 9ba8ceb..0000000
--- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.csg;
-
-import eu.svjatoslav.sixth.e3d.math.Vertex;
-import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * An N-gon polygon used for CSG BSP tree operations.
- *
- * During BSP tree traversal, polygons may be split by planes, resulting
- * in polygons with varying vertex counts (3 or more). The polygon stores
- * its vertices, the plane it lies on, and material properties (color).
- *
- * The color is preserved through CSG operations - split polygons inherit
- * the color from their parent.
- *
- * @see CSG the main CSG solid class
- * @see CSGPlane used for splitting polygons
- */
-public class CSGPolygon {
-
- /**
- * The vertices defining this polygon's geometry.
- * For CSG operations, this can be 3 or more vertices (N-gon).
- */
- public final List vertices;
-
- /**
- * The plane that contains this polygon.
- * Cached for efficient BSP operations.
- */
- public final CSGPlane plane;
-
- /**
- * The color of this polygon.
- * Preserved through CSG operations; split polygons inherit this color.
- */
- public Color color;
-
- /**
- * Creates a polygon with vertices and a color.
- *
- * @param vertices the vertices defining this polygon (must be at least 3)
- * @param color the color of this polygon
- */
- public CSGPolygon(final List vertices, final Color color) {
- this.vertices = vertices;
- this.color = color;
- this.plane = CSGPlane.fromPoints(
- vertices.get(0).coordinate,
- vertices.get(1).coordinate,
- vertices.get(2).coordinate
- );
- }
-
- /**
- * Creates a deep clone of this polygon.
- *
- * Clones all vertices and preserves the color.
- *
- * @return a new CSGPolygon with cloned data
- */
- public CSGPolygon clone() {
- final List clonedVertices = new ArrayList<>(vertices.size());
- for (final Vertex v : vertices) {
- clonedVertices.add(v.clone());
- }
- return new CSGPolygon(clonedVertices, this.color);
- }
-
- /**
- * Flips the orientation of this polygon.
- *
- * Reverses the vertex order and negates vertex normals.
- * Also flips the plane. Used during CSG operations when inverting solids.
- */
- public void flip() {
- Collections.reverse(vertices);
- for (final Vertex v : vertices) {
- v.flip();
- }
- plane.flip();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java
index abb48b0..6558a07 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java
@@ -136,8 +136,8 @@ public class Box implements Cloneable {
* @param size {@link Point3D} specifies box size in x, y and z axis.
*/
public void setBoxSize(final Point3D size) {
- p2.clone(size).scaleDown(2);
- p1.clone(p2).invert();
+ p2.clone(size).divide(2);
+ p1.clone(p2).negate();
}
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java
index 6e6ac97..78a9b79 100755
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java
@@ -16,9 +16,24 @@ import static java.lang.Math.sqrt;
* All mutation methods return {@code this} for fluent chaining:
* {@code
* Point2D p = new Point2D(10, 20)
+ * .multiply(2.0)
* .add(new Point2D(5, 5))
- * .invert();
- * // p is now (-15, -25)
+ * .negate();
+ * // p is now (-25, -45)
+ * }
+ *
+ * Mutability convention:
+ *
+ * - Imperative verbs ({@code add}, {@code subtract}, {@code negate}, {@code multiply},
+ * {@code divide}) mutate this point and return {@code this}
+ * - {@code with}-prefixed methods ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one
+ *
+ *
+ * Warning: This class is mutable with public fields. Clone before storing
+ * references that should not be shared:
+ * {@code
+ * Point2D safeCopy = original.clone();
* }
*
* @see Point3D the 3D equivalent
@@ -59,10 +74,12 @@ public class Point2D implements Cloneable {
/**
- * Adds another point to this point. The other point is not modified.
+ * Adds another point to this point in place.
+ * This point is modified, the other point is not.
*
* @param otherPoint the point to add
* @return this point (for chaining)
+ * @see #withAdded(Point2D) for the non-mutating version that returns a new point
*/
public Point2D add(final Point2D otherPoint) {
x += otherPoint.x;
@@ -145,11 +162,13 @@ public class Point2D implements Cloneable {
}
/**
- * Inverts this point's coordinates (negates both x and y).
+ * Negates this point's coordinates in place.
+ * This point is modified.
*
* @return this point (for chaining)
+ * @see #withNegated() for the non-mutating version that returns a new point
*/
- public Point2D invert() {
+ public Point2D negate() {
x = -x;
y = -y;
return this;
@@ -164,10 +183,12 @@ public class Point2D implements Cloneable {
}
/**
- * Subtracts another point from this point. The other point is not modified.
+ * Subtracts another point from this point in place.
+ * This point is modified, the other point is not.
*
* @param otherPoint the point to subtract
* @return this point (for chaining)
+ * @see #withSubtracted(Point2D) for the non-mutating version that returns a new point
*/
public Point2D subtract(final Point2D otherPoint) {
x -= otherPoint.x;
@@ -175,6 +196,34 @@ public class Point2D implements Cloneable {
return this;
}
+ /**
+ * Multiplies both coordinates by a factor.
+ * This point is modified.
+ *
+ * @param factor the multiplier
+ * @return this point (for chaining)
+ * @see #withMultiplied(double) for the non-mutating version that returns a new point
+ */
+ public Point2D multiply(final double factor) {
+ x *= factor;
+ y *= factor;
+ return this;
+ }
+
+ /**
+ * Divides both coordinates by a factor.
+ * This point is modified.
+ *
+ * @param factor the divisor
+ * @return this point (for chaining)
+ * @see #withDivided(double) for the non-mutating version that returns a new point
+ */
+ public Point2D divide(final double factor) {
+ x /= factor;
+ y /= factor;
+ return this;
+ }
+
/**
* Converts this 2D point to a 3D point with z = 0.
*
@@ -202,4 +251,63 @@ public class Point2D implements Cloneable {
", y=" + y +
'}';
}
+
+ /**
+ * Returns a new point that is the sum of this point and another.
+ * This point is not modified.
+ *
+ * @param other the point to add
+ * @return a new Point2D representing the sum
+ * @see #add(Point2D) for the mutating version
+ */
+ public Point2D withAdded(final Point2D other) {
+ return new Point2D(x + other.x, y + other.y);
+ }
+
+ /**
+ * Returns a new point that is this point minus another.
+ * This point is not modified.
+ *
+ * @param other the point to subtract
+ * @return a new Point2D representing the difference
+ * @see #subtract(Point2D) for the mutating version
+ */
+ public Point2D withSubtracted(final Point2D other) {
+ return new Point2D(x - other.x, y - other.y);
+ }
+
+ /**
+ * Returns a new point with negated coordinates.
+ * This point is not modified.
+ *
+ * @return a new Point2D with negated coordinates
+ * @see #negate() for the mutating version
+ */
+ public Point2D withNegated() {
+ return new Point2D(-x, -y);
+ }
+
+ /**
+ * Returns a new point with coordinates multiplied by a factor.
+ * This point is not modified.
+ *
+ * @param factor the multiplier
+ * @return a new Point2D with multiplied coordinates
+ * @see #multiply(double) for the mutating version
+ */
+ public Point2D withMultiplied(final double factor) {
+ return new Point2D(x * factor, y * factor);
+ }
+
+ /**
+ * Returns a new point with coordinates divided by a factor.
+ * This point is not modified.
+ *
+ * @param factor the divisor
+ * @return a new Point2D with divided coordinates
+ * @see #divide(double) for the mutating version
+ */
+ public Point2D withDivided(final double factor) {
+ return new Point2D(x / factor, y / factor);
+ }
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java
index c4e6b52..1f51c1c 100755
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java
@@ -18,7 +18,7 @@ import static java.lang.Math.*;
* All mutation methods return {@code this} for fluent chaining:
* {@code
* Point3D p = new Point3D(10, 20, 30)
- * .scaleUp(2.0)
+ * .multiply(2.0)
* .translateX(5)
* .add(new Point3D(1, 1, 1));
* // p is now (25, 41, 61)
@@ -27,9 +27,9 @@ import static java.lang.Math.*;
* Common operations:
* {@code
* // Create points
- * Point3D origin = new Point3D(); // (0, 0, 0)
- * Point3D pos = new Point3D(100, 200, 300);
- * Point3D copy = new Point3D(pos); // clone
+ * Point3D origin = Point3D.origin(); // (0, 0, 0)
+ * Point3D pos = Point3D.point(100, 200, 300);
+ * Point3D copy = new Point3D(pos); // clone
*
* // Measure distance
* double dist = pos.getDistanceTo(origin);
@@ -38,10 +38,18 @@ import static java.lang.Math.*;
* pos.rotate(origin, Math.PI / 4, 0); // rotate 45 degrees on XZ plane
*
* // Scale
- * pos.scaleUp(2.0); // double all coordinates
- * pos.scaleDown(2.0); // halve all coordinates
+ * pos.multiply(2.0); // double all coordinates
+ * pos.divide(2.0); // halve all coordinates
* }
*
+ * Mutability convention:
+ *
+ * - Imperative verbs ({@code add}, {@code subtract}, {@code negate}, {@code multiply},
+ * {@code divide}) mutate this point and return {@code this}
+ * - {@code with}-prefixed methods ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one
+ *
+ *
* Warning: This class is mutable with public fields. Clone before storing
* references that should not be shared:
* {@code
@@ -129,10 +137,33 @@ public class Point3D implements Cloneable {
}
/**
- * Add other point to current point. Value of other point will not be changed.
+ * Returns a new point at the origin (0, 0, 0).
*
- * @param otherPoint point to add.
- * @return current point.
+ * @return a new Point3D at the origin
+ */
+ public static Point3D origin() {
+ return new Point3D();
+ }
+
+ /**
+ * Returns a new point with the specified coordinates.
+ *
+ * @param x the X coordinate
+ * @param y the Y coordinate
+ * @param z the Z coordinate
+ * @return a new Point3D with the given coordinates
+ */
+ public static Point3D point(final double x, final double y, final double z) {
+ return new Point3D(x, y, z);
+ }
+
+ /**
+ * Adds another point to this point in place.
+ * This point is modified, the other point is not.
+ *
+ * @param otherPoint the point to add
+ * @return this point (for chaining)
+ * @see #withAdded(Point3D) for the non-mutating version that returns a new point
*/
public Point3D add(final Point3D otherPoint) {
x += otherPoint.x;
@@ -252,11 +283,13 @@ public class Point3D implements Cloneable {
}
/**
- * Invert current point coordinates.
+ * Negates this point's coordinates in place.
+ * This point is modified.
*
- * @return current point.
+ * @return this point (for chaining)
+ * @see #withNegated() for the non-mutating version that returns a new point
*/
- public Point3D invert() {
+ public Point3D negate() {
x = -x;
y = -y;
z = -z;
@@ -319,13 +352,14 @@ public class Point3D implements Cloneable {
}
/**
- * Scale down current point by factor.
- * All coordinates will be divided by factor.
+ * Divides all coordinates by a factor.
+ * This point is modified.
*
- * @param factor factor to scale by.
- * @return current point.
+ * @param factor the divisor
+ * @return this point (for chaining)
+ * @see #withDivided(double) for the non-mutating version that returns a new point
*/
- public Point3D scaleDown(final double factor) {
+ public Point3D divide(final double factor) {
x /= factor;
y /= factor;
z /= factor;
@@ -333,13 +367,14 @@ public class Point3D implements Cloneable {
}
/**
- * Scale up current point by factor.
- * All coordinates will be multiplied by factor.
+ * Multiplies all coordinates by a factor.
+ * This point is modified.
*
- * @param factor factor to scale by.
- * @return current point.
+ * @param factor the multiplier
+ * @return this point (for chaining)
+ * @see #withMultiplied(double) for the non-mutating version that returns a new point
*/
- public Point3D scaleUp(final double factor) {
+ public Point3D multiply(final double factor) {
x *= factor;
y *= factor;
z *= factor;
@@ -360,10 +395,12 @@ public class Point3D implements Cloneable {
}
/**
- * Subtracts another point from this point.
+ * Subtracts another point from this point in place.
+ * This point is modified, the other point is not.
*
* @param otherPoint the point to subtract
* @return this point (for chaining)
+ * @see #withSubtracted(Point3D) for the non-mutating version that returns a new point
*/
public Point3D subtract(final Point3D otherPoint) {
x -= otherPoint.x;
@@ -432,8 +469,6 @@ public class Point3D implements Cloneable {
return this;
}
- // ========== Non-mutating vector operations (return new Point3D) ==========
-
/**
* Computes the dot product of this vector with another.
*
@@ -445,11 +480,11 @@ public class Point3D implements Cloneable {
}
/**
- * Computes the cross product of this vector with another.
+ * Computes the cross-product of this vector with another.
* Returns a new vector perpendicular to both input vectors.
*
* @param other the other vector
- * @return a new Point3D representing the cross product
+ * @return a new Point3D representing the cross-product
*/
public Point3D cross(final Point3D other) {
return new Point3D(
@@ -461,23 +496,25 @@ public class Point3D implements Cloneable {
/**
* Returns a new point that is the sum of this point and another.
- * Neither point is modified.
+ * This point is not modified.
*
* @param other the point to add
* @return a new Point3D representing the sum
+ * @see #add(Point3D) for the mutating version
*/
- public Point3D plus(final Point3D other) {
+ public Point3D withAdded(final Point3D other) {
return new Point3D(x + other.x, y + other.y, z + other.z);
}
/**
* Returns a new point that is this point minus another.
- * Neither point is modified.
+ * This point is not modified.
*
* @param other the point to subtract
* @return a new Point3D representing the difference
+ * @see #subtract(Point3D) for the mutating version
*/
- public Point3D minus(final Point3D other) {
+ public Point3D withSubtracted(final Point3D other) {
return new Point3D(x - other.x, y - other.y, z - other.z);
}
@@ -486,8 +523,9 @@ public class Point3D implements Cloneable {
* This point is not modified.
*
* @return a new Point3D with negated coordinates
+ * @see #negate() for the mutating version
*/
- public Point3D negated() {
+ public Point3D withNegated() {
return new Point3D(-x, -y, -z);
}
@@ -523,23 +561,25 @@ public class Point3D implements Cloneable {
/**
* Returns a new point with coordinates multiplied by a factor.
- * This point is not modified. Unlike {@link #scaleUp}, this returns a new instance.
+ * This point is not modified.
*
- * @param factor the scaling factor
- * @return a new scaled Point3D
+ * @param factor the multiplier
+ * @return a new Point3D with multiplied coordinates
+ * @see #multiply(double) for the mutating version
*/
- public Point3D times(final double factor) {
+ public Point3D withMultiplied(final double factor) {
return new Point3D(x * factor, y * factor, z * factor);
}
/**
* Returns a new point with coordinates divided by a factor.
- * This point is not modified. Unlike {@link #scaleDown}, this returns a new instance.
+ * This point is not modified.
*
* @param factor the divisor
- * @return a new scaled Point3D
+ * @return a new Point3D with divided coordinates
+ * @see #divide(double) for the mutating version
*/
- public Point3D dividedBy(final double factor) {
+ public Point3D withDivided(final double factor) {
return new Point3D(x / factor, y / factor, z / factor);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
index 50f9dc6..71176b7 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
@@ -82,4 +82,26 @@ public class Polygon {
}
+ /**
+ * Tests whether a point lies inside a triangle using integer coordinates.
+ *
+ * This overload creates temporary Point2D objects for the vertices,
+ * suitable when the caller has pre-computed integer coordinates.
+ *
+ * @param point the point to test
+ * @param x1 the x coordinate of the first vertex
+ * @param y1 the y coordinate of the first vertex
+ * @param x2 the x coordinate of the second vertex
+ * @param y2 the y coordinate of the second vertex
+ * @param x3 the x coordinate of the third vertex
+ * @param y3 the y coordinate of the third vertex
+ * @return {@code true} if the point is inside the triangle
+ */
+ public static boolean pointWithinPolygon(final Point2D point,
+ final int x1, final int y1,
+ final int x2, final int y2,
+ final int x3, final int y3) {
+ return pointWithinPolygon(point, new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(x3, y3));
+ }
+
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java
index 23c2079..95c3f92 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java
@@ -30,7 +30,7 @@ public class Rectangle {
*/
public Rectangle(final double size) {
p2 = new Point2D(size / 2, size / 2);
- p1 = p2.clone().invert();
+ p1 = p2.clone().negate();
}
/**
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
index 8737a28..d453700 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
@@ -119,7 +119,7 @@ public class Camera implements FrameListener {
if (currentSpeed <= SPEED_LIMIT)
return;
- movementVector.scaleDown(currentSpeed / SPEED_LIMIT);
+ movementVector.divide(currentSpeed / SPEED_LIMIT);
}
/**
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
index 46c628e..d3cf13e 100755
--- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
@@ -11,6 +11,14 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D;
*
* Transformations are applied in order: rotation first, then translation.
*
+ * Mutability convention:
+ *
+ * - Imperative verbs ({@code set}, {@code setTranslation}, {@code transform})
+ * mutate this transform or the input point
+ * - {@code with}-prefixed methods ({@code withTransformed})
+ * return a new instance without modifying the original
+ *
+ *
* @see Quaternion
* @see Point3D
*/
@@ -120,21 +128,38 @@ public class Transform implements Cloneable {
* Applies this transform to a point: rotation followed by translation.
*
* @param point the point to transform (modified in place)
+ * @see #withTransformed(Point3D) for the non-mutating version that returns a new point
*/
public void transform(final Point3D point) {
rotation.toMatrix().transform(point, point);
point.add(translation);
}
+ /**
+ * Returns a new point with this transform applied.
+ * The original point is not modified.
+ *
+ * @param point the point to transform
+ * @return a new Point3D with the transform applied
+ * @see #transform(Point3D) for the mutating version
+ */
+ public Point3D withTransformed(final Point3D point) {
+ final Point3D result = new Point3D(point);
+ transform(result);
+ return result;
+ }
+
/**
* Sets the translation for this transform by copying the values from the given point.
*
* @param translation the translation values to copy
+ * @return this transform (for chaining)
*/
- public void setTranslation(final Point3D translation) {
+ public Transform setTranslation(final Point3D translation) {
this.translation.x = translation.x;
this.translation.y = translation.y;
this.translation.z = translation.z;
+ return this;
}
/**
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
index ba63d6c..f1eed76 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
@@ -153,7 +153,7 @@ public class Vertex {
*/
public void flip() {
if (normal != null) {
- normal = normal.negated();
+ normal = normal.withNegated();
}
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
index 900c508..63fca47 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
@@ -49,7 +49,7 @@ public class CameraView {
temp.clone(bottomRight);
m.transform(temp, bottomRight);
- camera.getTransform().getTranslation().clone().scaleDown(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight);
+ camera.getTransform().getTranslation().clone().divide(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight);
}
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
index c594571..390027d 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
@@ -15,24 +15,18 @@ package eu.svjatoslav.sixth.e3d.renderer.raster;
* {@code
* // Use predefined color constants
* Color red = Color.RED;
- * Color semiTransparent = new Color(255, 0, 0, 128);
+ * Color semiTransparent = Color.hex("FF000080");
+ *
+ * // Create from hex string (recommended)
+ * Color hex6 = Color.hex("FF8800"); // RGB, fully opaque
+ * Color hex8 = Color.hex("FF880080"); // RGBA with alpha
+ * Color hex3 = Color.hex("F80"); // Short RGB format
*
* // Create from integer RGBA components (0-255)
* Color custom = new Color(100, 200, 50, 255);
*
- * // Create from floating-point components (0.0-1.0)
- * Color half = new Color(0.5, 0.5, 0.5, 1.0);
- *
- * // Create from hex string
- * Color hex6 = new Color("FF8800"); // RGB, fully opaque
- * Color hex8 = new Color("FF880080"); // RGBA with alpha
- * Color hex3 = new Color("F80"); // Short RGB format
- *
* // Create from packed RGB integer
* Color packed = new Color(0xFF8800);
- *
- * // Convert to AWT for interop with Java Swing
- * java.awt.Color awtColor = custom.toAwtColor();
* }
*
* Important: Always use this class instead of {@link java.awt.Color} when
@@ -64,6 +58,24 @@ public final class Color {
/** Fully transparent (alpha = 0). */
public static final Color TRANSPARENT = new Color(0, 0, 0, 0);
+ /**
+ * Creates a color from a hexadecimal string.
+ *
+ *
Supported formats:
+ *
+ * - {@code RGB} - 3 hex digits, fully opaque
+ * - {@code RGBA} - 4 hex digits
+ * - {@code RRGGBB} - 6 hex digits, fully opaque
+ * - {@code RRGGBBAA} - 8 hex digits
+ *
+ *
+ * @param hex hex color code
+ * @return a new Color instance
+ */
+ public static Color hex(final String hex) {
+ return new Color(hex);
+ }
+
/**
* Red component. 0-255.
*/
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
index 3112fde..c9b6fee 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
@@ -9,13 +9,16 @@ import eu.svjatoslav.sixth.e3d.math.TransformStack;
import eu.svjatoslav.sixth.e3d.math.Vertex;
import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
- * Base class for shapes defined by an array of vertex coordinates.
+ * Base class for shapes defined by a list of vertex coordinates.
*
* This is the foundation for all primitive renderable shapes such as lines,
- * solid polygons, and textured polygons. Each shape has a fixed number of vertices
+ * solid polygons, and textured polygons. Each shape has a list of vertices
* ({@link Vertex} objects) that define its geometry in 3D space.
*
* During each render frame, the {@link #transform} method projects all vertices
@@ -35,7 +38,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*
* public void paint(RenderingContext ctx) {
* // Custom painting logic using ctx.graphics and
- * // coordinates[i].transformedCoordinate for screen positions
+ * // vertices.get(i).transformedCoordinate for screen positions
* }
* }
* }
@@ -62,8 +65,11 @@ public abstract class AbstractCoordinateShape extends AbstractShape {
* The vertex coordinates that define this shape's geometry.
* Each vertex contains both the original world-space coordinate and
* a transformed screen-space coordinate computed during {@link #transform}.
+ *
+ *
Stored as a mutable list to support CSG operations that modify
+ * polygon vertices in place (splitting, flipping).
*/
- public final Vertex[] vertices;
+ public final List vertices;
/**
* Average Z-depth of this shape in screen space after transformation.
@@ -79,10 +85,10 @@ public abstract class AbstractCoordinateShape extends AbstractShape {
* @param vertexCount the number of vertices in this shape
*/
public AbstractCoordinateShape(final int vertexCount) {
- vertices = new Vertex[vertexCount];
- for (int i = 0; i < vertexCount; i++)
- vertices[i] = new Vertex();
-
+ vertices = new ArrayList<>(vertexCount);
+ for (int i = 0; i < vertexCount; i++) {
+ vertices.add(new Vertex());
+ }
shapeId = lastShapeId.getAndIncrement();
}
@@ -92,8 +98,17 @@ public abstract class AbstractCoordinateShape extends AbstractShape {
* @param vertices the vertices defining this shape's geometry
*/
public AbstractCoordinateShape(final Vertex... vertices) {
- this.vertices = vertices;
+ this.vertices = new ArrayList<>(Arrays.asList(vertices));
+ shapeId = lastShapeId.getAndIncrement();
+ }
+ /**
+ * Creates a shape from a list of vertices.
+ *
+ * @param vertices the list of vertices defining this shape's geometry
+ */
+ public AbstractCoordinateShape(final List vertices) {
+ this.vertices = vertices;
shapeId = lastShapeId.getAndIncrement();
}
@@ -137,12 +152,13 @@ public abstract class AbstractCoordinateShape extends AbstractShape {
accumulatedZ += geometryPoint.transformedCoordinate.z;
- if (!geometryPoint.transformedCoordinate.isVisible())
+ if (!geometryPoint.transformedCoordinate.isVisible()) {
paint = false;
+ }
}
if (paint) {
- onScreenZ = accumulatedZ / vertices.length;
+ onScreenZ = accumulatedZ / vertices.size();
aggregator.queueShapeForRendering(this);
}
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java
index 974e285..1e661a4 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java
@@ -77,7 +77,7 @@ public class Billboard extends AbstractCoordinateShape {
public void paint(final RenderingContext targetRenderingArea) {
// distance from camera/viewer to center of the texture
- final double z = vertices[0].transformedCoordinate.z;
+ final double z = vertices.get(0).transformedCoordinate.z;
// compute forward oriented texture visible distance from center
final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width
@@ -92,7 +92,7 @@ public class Billboard extends AbstractCoordinateShape {
final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom);
- final Point2D onScreenCoordinate = vertices[0].onScreenCoordinate;
+ final Point2D onScreenCoordinate = vertices.get(0).onScreenCoordinate;
// compute Y
final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter);
@@ -219,7 +219,7 @@ public class Billboard extends AbstractCoordinateShape {
* @return the center position in world coordinates
*/
public Point3D getLocation() {
- return vertices[0].coordinate;
+ return vertices.get(0).coordinate;
}
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
index 4b53da1..895ca2c 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
@@ -35,11 +35,23 @@ public class Line extends AbstractCoordinateShape {
private static final double LINE_WIDTH_MULTIPLIER = 0.2d;
+ /**
+ * Thread-local interpolators for line rendering.
+ * Each rendering thread gets its own array to avoid race conditions.
+ */
+ private static final ThreadLocal LINE_INTERPOLATORS =
+ ThreadLocal.withInitial(() -> {
+ final LineInterpolator[] arr = new LineInterpolator[4];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = new LineInterpolator();
+ }
+ return arr;
+ });
+
/**
* width of the line.
*/
public final double width;
- final LineInterpolator[] lineInterpolators = new LineInterpolator[4];
/**
* Color of the line.
@@ -52,8 +64,8 @@ public class Line extends AbstractCoordinateShape {
* @param parentLine the line to copy
*/
public Line(final Line parentLine) {
- this(parentLine.vertices[0].coordinate.clone(),
- parentLine.vertices[1].coordinate.clone(),
+ this(parentLine.vertices.get(0).coordinate.clone(),
+ parentLine.vertices.get(1).coordinate.clone(),
new Color(parentLine.color), parentLine.width);
}
@@ -75,10 +87,6 @@ public class Line extends AbstractCoordinateShape {
this.color = color;
this.width = width;
-
- for (int i = 0; i < lineInterpolators.length; i++)
- lineInterpolators[i] = new LineInterpolator();
-
}
/**
@@ -164,8 +172,8 @@ public class Line extends AbstractCoordinateShape {
private void drawSinglePixelHorizontalLine(final RenderingContext buffer,
final int alpha) {
- final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate;
int xStart = (int) onScreenPoint1.x;
int xEnd = (int) onScreenPoint2.x;
@@ -232,8 +240,8 @@ public class Line extends AbstractCoordinateShape {
private void drawSinglePixelVerticalLine(final RenderingContext buffer,
final int alpha) {
- final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate;
int yStart = (int) onScreenPoint1.y;
int yEnd = (int) onScreenPoint2.y;
@@ -292,11 +300,13 @@ public class Line extends AbstractCoordinateShape {
/**
* Finds the index of the first interpolator (starting from startPointer) that contains the given Y coordinate.
*
- * @param startPointer the index to start searching from
- * @param y the Y coordinate to search for
+ * @param lineInterpolators the interpolators array
+ * @param startPointer the index to start searching from
+ * @param y the Y coordinate to search for
* @return the index of the interpolator, or -1 if not found
*/
- private int getLineInterpolator(final int startPointer, final int y) {
+ private int getLineInterpolator(final LineInterpolator[] lineInterpolators,
+ final int startPointer, final int y) {
for (int i = startPointer; i < lineInterpolators.length; i++)
if (lineInterpolators[i].containsY(y))
@@ -320,16 +330,16 @@ public class Line extends AbstractCoordinateShape {
@Override
public void paint(final RenderingContext buffer) {
- final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate;
final double xp = onScreenPoint2.x - onScreenPoint1.x;
final double yp = onScreenPoint2.y - onScreenPoint1.y;
final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width)
- / vertices[0].transformedCoordinate.z;
+ / vertices.get(0).transformedCoordinate.z;
final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width)
- / vertices[1].transformedCoordinate.z;
+ / vertices.get(1).transformedCoordinate.z;
if ((point1radius < MINIMUM_WIDTH_THRESHOLD)
|| (point2radius < MINIMUM_WIDTH_THRESHOLD)) {
@@ -370,6 +380,9 @@ public class Line extends AbstractCoordinateShape {
final double p2x2 = onScreenPoint2.x + xdec2;
final double p2y2 = onScreenPoint2.y - yinc2;
+ // Get thread-local interpolators
+ final LineInterpolator[] lineInterpolators = LINE_INTERPOLATORS.get();
+
lineInterpolators[0].setPoints(p1x1, p1y1, 1d, p2x1, p2y1, 1d);
lineInterpolators[1].setPoints(p1x2, p1y2, -1d, p2x2, p2y2, -1d);
@@ -403,9 +416,9 @@ public class Line extends AbstractCoordinateShape {
return;
for (int y = (int) ymin; y <= ymax; y++) {
- final int li1 = getLineInterpolator(0, y);
+ final int li1 = getLineInterpolator(lineInterpolators, 0, y);
if (li1 != -1) {
- final int li2 = getLineInterpolator(li1 + 1, y);
+ final int li2 = getLineInterpolator(lineInterpolators, li1 + 1, y);
if (li2 != -1)
drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, buffer);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java
index fa14d14..629d16e 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java
@@ -129,4 +129,24 @@ public class LineInterpolator implements Comparable {
absoluteHeight = Math.abs(height);
}
+ /**
+ * Sets the two endpoints of this edge using integer coordinates.
+ *
+ * This method creates new Point2D objects to avoid storing references to shared
+ * vertex data, which is essential for thread safety during parallel rendering.
+ *
+ * @param x1 the x coordinate of the first endpoint
+ * @param y1 the y coordinate of the first endpoint
+ * @param x2 the x coordinate of the second endpoint
+ * @param y2 the y coordinate of the second endpoint
+ */
+ public void setPoints(final int x1, final int y1, final int x2, final int y2) {
+ this.p1 = new Point2D(x1, y1);
+ this.p2 = new Point2D(x2, y2);
+ height = y2 - y1;
+ width = x2 - x1;
+
+ absoluteHeight = Math.abs(height);
+ }
+
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
index 9d03c8f..fd54e21 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
@@ -4,6 +4,7 @@
*/
package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon;
+import eu.svjatoslav.sixth.e3d.csg.CSGPlane;
import eu.svjatoslav.sixth.e3d.geometry.Point2D;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
@@ -12,52 +13,375 @@ import eu.svjatoslav.sixth.e3d.math.Vertex;
import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
/**
- * A solid-color triangle renderer with mouse interaction support.
- *
- * This class implements a high-performance triangle rasterizer using scanline
- * algorithms. It handles:
- * - Perspective-correct edge interpolation
- * - Alpha blending with background pixels
- * - Viewport clipping
- * - Mouse hover detection via point-in-polygon tests
- * - Optional flat shading based on light sources
- *
- * The static drawPolygon method is designed for reuse by other polygon types.
+ * A solid-color convex polygon renderer supporting N vertices (N >= 3).
+ *
+ *
This class serves as the unified polygon type for both rendering and CSG operations.
+ * It renders convex polygons by decomposing them into triangles using fan triangulation,
+ * and supports CSG operations directly without conversion to intermediate types.
+ *
+ * Rendering:
+ *
+ * - Fan triangulation for N-vertex polygons (N-2 triangles)
+ * - Scanline rasterization with alpha blending
+ * - Backface culling and flat shading support
+ * - Mouse interaction via point-in-polygon testing
+ *
+ *
+ * CSG Support:
+ *
+ * - Lazy-computed plane for BSP operations
+ * - {@link #flip()} for inverting polygon orientation
+ * - {@link #deepClone()} for creating independent copies
+ *
+ *
+ * Usage examples:
+ * {@code
+ * // Create a triangle
+ * SolidPolygon triangle = new SolidPolygon(
+ * new Point3D(0, 0, 0),
+ * new Point3D(50, 0, 0),
+ * new Point3D(25, 50, 0),
+ * Color.RED
+ * );
+ *
+ * // Create a quad
+ * SolidPolygon quad = SolidPolygon.quad(
+ * new Point3D(-50, -50, 0),
+ * new Point3D(50, -50, 0),
+ * new Point3D(50, 50, 0),
+ * new Point3D(-50, 50, 0),
+ * Color.BLUE
+ * );
+ *
+ * // Use with CSG (via AbstractCompositeShape)
+ * SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(...);
+ * box.subtract(sphere);
+ * }
+ *
+ * @see CSGPlane for BSP plane operations
+ * @see LineInterpolator for scanline edge interpolation
*/
public class SolidPolygon extends AbstractCoordinateShape {
+ /**
+ * Thread-local storage for line interpolators used during scanline rasterization.
+ *
+ * Contains three interpolators representing the three edges of a triangle.
+ * ThreadLocal ensures thread safety when multiple threads render triangles
+ * concurrently, avoiding allocation during rendering by reusing these objects.
+ */
private static final ThreadLocal INTERPOLATORS =
ThreadLocal.withInitial(() -> new LineInterpolator[]{
new LineInterpolator(), new LineInterpolator(), new LineInterpolator()
});
- private final Point3D cachedNormal = new Point3D();
- private final Point3D cachedCenter = new Point3D();
+ /**
+ * Cached plane containing this polygon, used for CSG operations.
+ *
+ * Lazy-computed on first call to {@link #getPlane()}.
+ */
+ private CSGPlane plane;
+
+ /**
+ * Flag indicating whether the plane has been computed.
+ */
+ private boolean planeComputed = false;
+
+ /**
+ * Thread-local cached normal vector for shading calculations.
+ * Each rendering thread gets its own instance to avoid race conditions.
+ */
+ private static final ThreadLocal CACHED_NORMAL =
+ ThreadLocal.withInitial(Point3D::new);
+
+ /**
+ * Thread-local cached centroid for lighting calculations.
+ * Each rendering thread gets its own instance to avoid race conditions.
+ */
+ private static final ThreadLocal CACHED_CENTER =
+ ThreadLocal.withInitial(Point3D::new);
+
+ /**
+ * Thread-local storage for screen coordinates during rendering.
+ * Each rendering thread gets its own array to avoid race conditions.
+ */
+ private static final ThreadLocal SCREEN_POINTS = new ThreadLocal<>();
+
+ /**
+ * The fill color of this polygon.
+ */
private Color color;
+
+ /**
+ * Whether flat shading is enabled for this polygon.
+ */
private boolean shadingEnabled = false;
+
+ /**
+ * Whether backface culling is enabled for this polygon.
+ */
private boolean backfaceCulling = false;
+ // ==================== CONSTRUCTORS ====================
+
+ /**
+ * Creates a solid polygon with the specified vertices and color.
+ *
+ * @param vertices the vertices defining the polygon (must have at least 3)
+ * @param color the fill color of the polygon
+ * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices
+ */
+ public SolidPolygon(final Point3D[] vertices, final Color color) {
+ super(createVerticesFromPoints(vertices));
+ if (vertices == null || vertices.length < 3) {
+ throw new IllegalArgumentException(
+ "Polygon must have at least 3 vertices, but got "
+ + (vertices == null ? "null" : vertices.length));
+ }
+ this.color = color;
+ }
+
+ /**
+ * Creates a solid polygon from a list of points and color.
+ *
+ * @param points the list of points defining the polygon (must have at least 3)
+ * @param color the fill color of the polygon
+ * @throws IllegalArgumentException if points is null or has fewer than 3 points
+ */
+ public SolidPolygon(final List points, final Color color) {
+ super(createVerticesFromPoints(points));
+ if (points == null || points.size() < 3) {
+ throw new IllegalArgumentException(
+ "Polygon must have at least 3 vertices, but got "
+ + (points == null ? "null" : points.size()));
+ }
+ this.color = color;
+ }
+
+ /**
+ * Creates a solid polygon from a vertex list and color.
+ *
+ * This constructor is used for CSG operations where vertices already exist.
+ *
+ * @param vertices the list of Vertex objects (will be used directly, not copied)
+ * @param color the fill color of the polygon
+ * @param dummy dummy parameter to distinguish from List<Point3D> constructor
+ * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices
+ */
+ public SolidPolygon(final List vertices, final Color color, final boolean dummy) {
+ super(vertices);
+ if (vertices == null || vertices.size() < 3) {
+ throw new IllegalArgumentException(
+ "Polygon must have at least 3 vertices, but got "
+ + (vertices == null ? "null" : vertices.size()));
+ }
+ this.color = color;
+ }
+
/**
* Creates a solid triangle with the specified vertices and color.
*
* @param point1 the first vertex position
* @param point2 the second vertex position
* @param point3 the third vertex position
- * @param color the fill color of the triangle
+ * @param color the fill color
*/
public SolidPolygon(final Point3D point1, final Point3D point2,
final Point3D point3, final Color color) {
- super(
- new Vertex(point1),
- new Vertex(point2),
- new Vertex(point3)
- );
+ super(new Vertex(point1), new Vertex(point2), new Vertex(point3));
this.color = color;
}
+ // ==================== STATIC FACTORY METHODS ====================
+
+ /**
+ * Creates a triangle (3-vertex polygon).
+ *
+ * @param p1 the first vertex
+ * @param p2 the second vertex
+ * @param p3 the third vertex
+ * @param color the fill color
+ * @return a new SolidPolygon with 3 vertices
+ */
+ public static SolidPolygon triangle(final Point3D p1, final Point3D p2,
+ final Point3D p3, final Color color) {
+ return new SolidPolygon(p1, p2, p3, color);
+ }
+
+ /**
+ * Creates a quad (4-vertex polygon).
+ *
+ * @param p1 the first vertex
+ * @param p2 the second vertex
+ * @param p3 the third vertex
+ * @param p4 the fourth vertex
+ * @param color the fill color
+ * @return a new SolidPolygon with 4 vertices
+ */
+ public static SolidPolygon quad(final Point3D p1, final Point3D p2,
+ final Point3D p3, final Point3D p4, final Color color) {
+ return new SolidPolygon(new Point3D[]{p1, p2, p3, p4}, color);
+ }
+
+ // ==================== VERTEX HELPER METHODS ====================
+
+ /**
+ * Helper method to create Vertex list from Point3D array.
+ */
+ private static List createVerticesFromPoints(final Point3D[] points) {
+ if (points == null || points.length < 3) {
+ return new ArrayList<>();
+ }
+ final List verts = new ArrayList<>(points.length);
+ for (final Point3D point : points) {
+ verts.add(new Vertex(point));
+ }
+ return verts;
+ }
+
+ /**
+ * Helper method to create Vertex list from Point3D list.
+ */
+ private static List createVerticesFromPoints(final List points) {
+ if (points == null || points.size() < 3) {
+ return new ArrayList<>();
+ }
+ final List verts = new ArrayList<>(points.size());
+ for (final Point3D point : points) {
+ verts.add(new Vertex(point));
+ }
+ return verts;
+ }
+
+ /**
+ * Returns the number of vertices in this polygon.
+ *
+ * @return the vertex count
+ */
+ public int getVertexCount() {
+ return vertices.size();
+ }
+
+ // ==================== PROPERTIES ====================
+
+ /**
+ * Returns the fill color of this polygon.
+ *
+ * @return the polygon color
+ */
+ public Color getColor() {
+ return color;
+ }
+
+ /**
+ * Sets the fill color of this polygon.
+ *
+ * @param color the new color
+ */
+ public void setColor(final Color color) {
+ this.color = color;
+ }
+
+ /**
+ * Checks if shading is enabled for this polygon.
+ *
+ * @return true if shading is enabled, false otherwise
+ */
+ public boolean isShadingEnabled() {
+ return shadingEnabled;
+ }
+
+ /**
+ * Enables or disables shading for this polygon.
+ *
+ * @param shadingEnabled true to enable shading, false to disable
+ */
+ public void setShadingEnabled(final boolean shadingEnabled) {
+ this.shadingEnabled = shadingEnabled;
+ }
+
+ /**
+ * Checks if backface culling is enabled for this polygon.
+ *
+ * @return {@code true} if backface culling is enabled
+ */
+ public boolean isBackfaceCullingEnabled() {
+ return backfaceCulling;
+ }
+
+ /**
+ * Enables or disables backface culling for this polygon.
+ *
+ * @param backfaceCulling {@code true} to enable backface culling
+ */
+ public void setBackfaceCulling(final boolean backfaceCulling) {
+ this.backfaceCulling = backfaceCulling;
+ }
+
+ // ==================== CSG SUPPORT ====================
+
+ /**
+ * Returns the plane containing this polygon.
+ *
+ * Computed from the first three vertices and cached for reuse.
+ * Used by CSG operations for BSP tree construction.
+ *
+ * @return the CSGPlane containing this polygon
+ */
+ public CSGPlane getPlane() {
+ if (!planeComputed) {
+ plane = CSGPlane.fromPoints(
+ vertices.get(0).coordinate,
+ vertices.get(1).coordinate,
+ vertices.get(2).coordinate
+ );
+ planeComputed = true;
+ }
+ return plane;
+ }
+
+ /**
+ * Flips the orientation of this polygon.
+ *
+ * Reverses the vertex order and negates vertex normals.
+ * Also flips the cached plane if computed. Used during CSG operations
+ * when inverting solids.
+ */
+ public void flip() {
+ Collections.reverse(vertices);
+ for (final Vertex v : vertices) {
+ v.flip();
+ }
+ if (planeComputed) {
+ plane.flip();
+ }
+ }
+
+ /**
+ * Creates a deep clone of this polygon.
+ *
+ * Clones all vertices and preserves the color. Used by CSG operations
+ * to create independent copies before modification.
+ *
+ * @return a new SolidPolygon with cloned data
+ */
+ public SolidPolygon deepClone() {
+ final List clonedVertices = new ArrayList<>(vertices.size());
+ for (final Vertex v : vertices) {
+ clonedVertices.add(v.clone());
+ }
+ return new SolidPolygon(clonedVertices, color, true);
+ }
+
+ // ==================== RENDERING ====================
+
/**
* Draws a horizontal scanline between two edge interpolators with alpha blending.
*
@@ -68,8 +392,8 @@ public class SolidPolygon extends AbstractCoordinateShape {
* @param color the color to draw with
*/
public static void drawHorizontalLine(final LineInterpolator line1,
- final LineInterpolator line2, final int y,
- final RenderingContext renderBuffer, final Color color) {
+ final LineInterpolator line2, final int y,
+ final RenderingContext renderBuffer, final Color color) {
int x1 = line1.getX(y);
int x2 = line2.getX(y);
@@ -80,11 +404,13 @@ public class SolidPolygon extends AbstractCoordinateShape {
x2 = tmp;
}
- if (x1 < 0)
+ if (x1 < 0) {
x1 = 0;
+ }
- if (x2 >= renderBuffer.width)
+ if (x2 >= renderBuffer.width) {
x2 = renderBuffer.width - 1;
+ }
final int width = x2 - x1;
@@ -121,16 +447,15 @@ public class SolidPolygon extends AbstractCoordinateShape {
pixels[offset++] = (newR << 16) | (newG << 8) | newB;
}
}
-
}
/**
- * Renders a triangle with mouse interaction support and optional backface culling.
+ * Renders a triangle using scanline rasterization.
*
* This static method handles:
*
* - Rounding vertices to integer screen coordinates
- * - Mouse hover detection via point-in-polygon test
+ * - Mouse hover detection via point-in-triangle test
* - Viewport clipping
* - Scanline rasterization with alpha blending
*
@@ -142,64 +467,76 @@ public class SolidPolygon extends AbstractCoordinateShape {
* @param mouseInteractionController optional controller for mouse events, or null
* @param color the fill color
*/
- public static void drawPolygon(final RenderingContext context,
- final Point2D onScreenPoint1, final Point2D onScreenPoint2,
- final Point2D onScreenPoint3,
- final MouseInteractionController mouseInteractionController,
- final Color color) {
-
- onScreenPoint1.roundToInteger();
- onScreenPoint2.roundToInteger();
- onScreenPoint3.roundToInteger();
-
- if (mouseInteractionController != null)
- if (context.getMouseEvent() != null)
+ public static void drawTriangle(final RenderingContext context,
+ final Point2D onScreenPoint1, final Point2D onScreenPoint2,
+ final Point2D onScreenPoint3,
+ final MouseInteractionController mouseInteractionController,
+ final Color color) {
+
+ // Copy and round coordinates to local variables (don't modify original Point2D)
+ // This is thread-safe: multiple threads may paint the same polygon across different
+ // Y segments, so we must not mutate shared vertex data
+ final int x1 = (int) onScreenPoint1.x;
+ final int y1 = (int) onScreenPoint1.y;
+ final int x2 = (int) onScreenPoint2.x;
+ final int y2 = (int) onScreenPoint2.y;
+ final int x3 = (int) onScreenPoint3.x;
+ final int y3 = (int) onScreenPoint3.y;
+
+ if (mouseInteractionController != null) {
+ if (context.getMouseEvent() != null) {
if (pointWithinPolygon(context.getMouseEvent().coordinate,
- onScreenPoint1, onScreenPoint2, onScreenPoint3))
+ x1, y1, x2, y2, x3, y3)) {
context.setCurrentObjectUnderMouseCursor(mouseInteractionController);
+ }
+ }
+ }
- if (color.isTransparent())
+ if (color.isTransparent()) {
return;
+ }
- // find top-most point
- int yTop = (int) onScreenPoint1.y;
-
- if (onScreenPoint2.y < yTop)
- yTop = (int) onScreenPoint2.y;
-
- if (onScreenPoint3.y < yTop)
- yTop = (int) onScreenPoint3.y;
-
- if (yTop < 0)
+ // Find top-most point
+ int yTop = y1;
+ if (y2 < yTop) {
+ yTop = y2;
+ }
+ if (y3 < yTop) {
+ yTop = y3;
+ }
+ if (yTop < 0) {
yTop = 0;
+ }
- // find bottom-most point
- int yBottom = (int) onScreenPoint1.y;
-
- if (onScreenPoint2.y > yBottom)
- yBottom = (int) onScreenPoint2.y;
-
- if (onScreenPoint3.y > yBottom)
- yBottom = (int) onScreenPoint3.y;
-
- if (yBottom >= context.height)
+ // Find bottom-most point
+ int yBottom = y1;
+ if (y2 > yBottom) {
+ yBottom = y2;
+ }
+ if (y3 > yBottom) {
+ yBottom = y3;
+ }
+ if (yBottom >= context.height) {
yBottom = context.height - 1;
+ }
- // clamp to render Y bounds
+ // Clamp to render Y bounds
yTop = Math.max(yTop, context.renderMinY);
yBottom = Math.min(yBottom, context.renderMaxY);
- if (yTop >= yBottom)
+ if (yTop >= yBottom) {
return;
+ }
- // paint
+ // Paint using line interpolators
final LineInterpolator[] interp = INTERPOLATORS.get();
final LineInterpolator polygonBoundary1 = interp[0];
final LineInterpolator polygonBoundary2 = interp[1];
final LineInterpolator polygonBoundary3 = interp[2];
- polygonBoundary1.setPoints(onScreenPoint1, onScreenPoint2);
- polygonBoundary2.setPoints(onScreenPoint1, onScreenPoint3);
- polygonBoundary3.setPoints(onScreenPoint2, onScreenPoint3);
+ // Use rounded integer coordinates for interpolation
+ polygonBoundary1.setPoints(x1, y1, x2, y2);
+ polygonBoundary2.setPoints(x1, y1, x3, y3);
+ polygonBoundary3.setPoints(x2, y2, x3, y3);
// Inline sort for 3 elements to avoid array allocation
LineInterpolator a = polygonBoundary1;
@@ -222,93 +559,43 @@ public class SolidPolygon extends AbstractCoordinateShape {
b = t;
}
- for (int y = yTop; y < yBottom; y++)
+ for (int y = yTop; y < yBottom; y++) {
if (a.containsY(y)) {
- if (b.containsY(y))
+ if (b.containsY(y)) {
drawHorizontalLine(a, b, y, context, color);
- else if (c.containsY(y))
+ } else if (c.containsY(y)) {
drawHorizontalLine(a, c, y, context, color);
- } else if (b.containsY(y))
- if (c.containsY(y))
+ }
+ } else if (b.containsY(y)) {
+ if (c.containsY(y)) {
drawHorizontalLine(b, c, y, context, color);
+ }
+ }
+ }
}
/**
- * Returns the fill color of this polygon.
- *
- * @return the polygon color
- */
- public Color getColor() {
- return color;
- }
-
- /**
- * Sets the fill color of this polygon.
- *
- * @param color the new color
- */
- public void setColor(final Color color) {
- this.color = color;
- }
-
- /**
- * Checks if shading is enabled for this polygon.
- *
- * @return true if shading is enabled, false otherwise
- */
- public boolean isShadingEnabled() {
- return shadingEnabled;
- }
-
- /**
- * Enables or disables shading for this polygon.
- * When enabled, the polygon uses the global lighting manager from the
- * rendering context to calculate flat shading based on light sources.
- *
- * @param shadingEnabled true to enable shading, false to disable
- */
- public void setShadingEnabled(final boolean shadingEnabled) {
- this.shadingEnabled = shadingEnabled;
- }
-
- /**
- * Checks if backface culling is enabled for this polygon.
- *
- * @return {@code true} if backface culling is enabled
- */
- public boolean isBackfaceCullingEnabled() {
- return backfaceCulling;
- }
-
- /**
- * Enables or disables backface culling for this polygon.
- *
- * When enabled, polygons facing away from the camera (determined by
- * screen-space winding order) are not rendered.
- *
- * @param backfaceCulling {@code true} to enable backface culling
- */
- public void setBackfaceCulling(final boolean backfaceCulling) {
- this.backfaceCulling = backfaceCulling;
- }
-
- /**
- * Calculates the unit normal vector of this triangle.
+ * Calculates the unit normal vector of this polygon.
*
* @param result the point to store the normal vector in
*/
private void calculateNormal(final Point3D result) {
- final Point3D v1 = vertices[0].coordinate;
- final Point3D v2 = vertices[1].coordinate;
- final Point3D v3 = vertices[2].coordinate;
+ if (vertices.size() < 3) {
+ result.x = result.y = result.z = 0;
+ return;
+ }
- final double ax = v2.x - v1.x;
- final double ay = v2.y - v1.y;
- final double az = v2.z - v1.z;
+ final Point3D v0 = vertices.get(0).coordinate;
+ final Point3D v1 = vertices.get(1).coordinate;
+ final Point3D v2 = vertices.get(2).coordinate;
- final double bx = v3.x - v1.x;
- final double by = v3.y - v1.y;
- final double bz = v3.z - v1.z;
+ final double ax = v1.x - v0.x;
+ final double ay = v1.y - v0.y;
+ final double az = v1.z - v0.z;
+
+ final double bx = v2.x - v0.x;
+ final double by = v2.y - v0.y;
+ final double bz = v2.z - v0.z;
double nx = ay * bz - az * by;
double ny = az * bx - ax * bz;
@@ -327,59 +614,169 @@ public class SolidPolygon extends AbstractCoordinateShape {
}
/**
- * Calculates the centroid (geometric center) of this triangle.
+ * Calculates the centroid (geometric center) of this polygon.
*
* @param result the point to store the center in
*/
private void calculateCenter(final Point3D result) {
- final Point3D v1 = vertices[0].coordinate;
- final Point3D v2 = vertices[1].coordinate;
- final Point3D v3 = vertices[2].coordinate;
+ if (vertices.isEmpty()) {
+ result.x = result.y = result.z = 0;
+ return;
+ }
+
+ double sumX = 0, sumY = 0, sumZ = 0;
+ for (final Vertex v : vertices) {
+ sumX += v.coordinate.x;
+ sumY += v.coordinate.y;
+ sumZ += v.coordinate.z;
+ }
- result.x = (v1.x + v2.x + v3.x) / 3.0;
- result.y = (v1.y + v2.y + v3.y) / 3.0;
- result.z = (v1.z + v2.z + v3.z) / 3.0;
+ result.x = sumX / vertices.size();
+ result.y = sumY / vertices.size();
+ result.z = sumZ / vertices.size();
}
/**
- * Renders this triangle to the screen.
+ * Calculates the signed area of this polygon in screen space.
*
- * This method performs:
- *
- * - Backface culling check (if enabled)
- * - Flat shading calculation (if lighting is enabled)
- * - Triangle rasterization using the static drawPolygon method
- *
+ * @param screenPoints the screen coordinates of this polygon's vertices
+ * @param vertexCount the number of vertices in the polygon
+ * @return the signed area (negative = front-facing in Y-down coordinate system)
+ */
+ private double calculateSignedArea(final Point2D[] screenPoints, final int vertexCount) {
+ double area = 0;
+ final int n = vertexCount;
+ for (int i = 0; i < n; i++) {
+ final Point2D curr = screenPoints[i];
+ final Point2D next = screenPoints[(i + 1) % n];
+ area += curr.x * next.y - next.x * curr.y;
+ }
+ return area / 2.0;
+ }
+
+ /**
+ * Tests whether a point lies inside this polygon using ray-casting.
+ *
+ * @param point the point to test
+ * @param screenPoints the screen coordinates of this polygon's vertices
+ * @param vertexCount the number of vertices in the polygon
+ * @return {@code true} if the point is inside the polygon
+ */
+ private boolean isPointInsidePolygon(final Point2D point, final Point2D[] screenPoints,
+ final int vertexCount) {
+ int intersectionCount = 0;
+ final int n = vertexCount;
+
+ for (int i = 0; i < n; i++) {
+ final Point2D p1 = screenPoints[i];
+ final Point2D p2 = screenPoints[(i + 1) % n];
+
+ if (intersectsRay(point, p1, p2)) {
+ intersectionCount++;
+ }
+ }
+
+ return (intersectionCount % 2) == 1;
+ }
+
+ /**
+ * Tests if a horizontal ray from the point intersects the edge.
+ */
+ private boolean intersectsRay(final Point2D point, Point2D edgeP1, Point2D edgeP2) {
+ if (edgeP1.y > edgeP2.y) {
+ final Point2D tmp = edgeP1;
+ edgeP1 = edgeP2;
+ edgeP2 = tmp;
+ }
+
+ if (point.y < edgeP1.y || point.y > edgeP2.y) {
+ return false;
+ }
+
+ final double dy = edgeP2.y - edgeP1.y;
+ if (Math.abs(dy) < 0.0001) {
+ return false;
+ }
+
+ final double t = (point.y - edgeP1.y) / dy;
+ final double intersectX = edgeP1.x + t * (edgeP2.x - edgeP1.x);
+
+ return point.x >= intersectX;
+ }
+
+ /**
+ * Renders this polygon to the screen.
*
* @param renderBuffer the rendering context containing the pixel buffer
*/
@Override
public void paint(final RenderingContext renderBuffer) {
+ if (vertices.size() < 3 || color.isTransparent()) {
+ return;
+ }
- final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
- final Point2D onScreenPoint3 = vertices[2].onScreenCoordinate;
+ // Get thread-local screen points array
+ final Point2D[] screenPoints = getScreenPoints(vertices.size());
+ // Get screen coordinates
+ for (int i = 0; i < vertices.size(); i++) {
+ screenPoints[i] = vertices.get(i).onScreenCoordinate;
+ }
+
+ // Backface culling check
if (backfaceCulling) {
- final double signedArea = (onScreenPoint2.x - onScreenPoint1.x)
- * (onScreenPoint3.y - onScreenPoint1.y)
- - (onScreenPoint3.x - onScreenPoint1.x)
- * (onScreenPoint2.y - onScreenPoint1.y);
- if (signedArea >= 0)
+ final double signedArea = calculateSignedArea(screenPoints, vertices.size());
+ if (signedArea >= 0) {
return;
+ }
}
+ // Determine paint color (with optional shading)
Color paintColor = color;
-
if (shadingEnabled && renderBuffer.lightingManager != null) {
+ final Point3D cachedCenter = CACHED_CENTER.get();
+ final Point3D cachedNormal = CACHED_NORMAL.get();
calculateCenter(cachedCenter);
calculateNormal(cachedNormal);
paintColor = renderBuffer.lightingManager.calculateLighting(cachedCenter, cachedNormal, color);
}
- drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2,
- onScreenPoint3, mouseInteractionController, paintColor);
+ // Mouse interaction
+ if (mouseInteractionController != null && renderBuffer.getMouseEvent() != null) {
+ if (isPointInsidePolygon(renderBuffer.getMouseEvent().coordinate, screenPoints, vertices.size())) {
+ renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
+ }
+ }
+
+ // For triangles, use direct triangle rendering
+ if (vertices.size() == 3) {
+ drawTriangle(renderBuffer, screenPoints[0], screenPoints[1], screenPoints[2],
+ mouseInteractionController, paintColor);
+ return;
+ }
+
+ // Fan triangulation for N-vertex polygons
+ final Point2D v0 = screenPoints[0];
+ for (int i = 1; i < vertices.size() - 1; i++) {
+ final Point2D v1 = screenPoints[i];
+ final Point2D v2 = screenPoints[i + 1];
+ drawTriangle(renderBuffer, v0, v1, v2, null, paintColor);
+ }
}
-}
+ /**
+ * Gets a thread-local screen points array sized for the given number of vertices.
+ *
+ * @param size the required array size
+ * @return a thread-local Point2D array
+ */
+ private Point2D[] getScreenPoints(final int size) {
+ Point2D[] screenPoints = SCREEN_POINTS.get();
+ if (screenPoints == null || screenPoints.length < size) {
+ screenPoints = new Point2D[size];
+ SCREEN_POINTS.set(screenPoints);
+ }
+ return screenPoints;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java
index 79b79d5..71683a5 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java
@@ -4,15 +4,15 @@
*/
/**
- * Solid-color triangle rendering with scanline rasterization.
+ * Solid-color polygon rendering with scanline rasterization.
*
- * Solid polygons are the primary building blocks for opaque 3D surfaces.
- * The rasterizer handles perspective-correct interpolation, alpha blending,
- * viewport clipping, and optional flat shading.
+ * SolidPolygon is the unified polygon type for both rendering and CSG operations.
+ * It supports N vertices (N >= 3) and handles perspective-correct interpolation,
+ * alpha blending, viewport clipping, backface culling, and optional flat shading.
*
* Key classes:
*
- * - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - The solid triangle shape
+ * - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - Unified polygon for rendering and CSG
* - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines
*
*
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java
index ec4caeb..3081b27 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java
@@ -139,6 +139,10 @@ public class PolygonBorderInterpolator implements
/**
* Sets the screen and texture coordinates for this edge.
*
+ * Screen coordinates are copied to new Point2D objects to avoid
+ * storing references to shared vertex data, which is essential for
+ * thread safety during parallel rendering.
+ *
* @param onScreenPoint1 the first screen-space endpoint
* @param onScreenPoint2 the second screen-space endpoint
* @param texturePoint1 the texture coordinate for the first endpoint
@@ -147,13 +151,14 @@ public class PolygonBorderInterpolator implements
public void setPoints(final Point2D onScreenPoint1, final Point2D onScreenPoint2,
final Point2D texturePoint1, final Point2D texturePoint2) {
- this.onScreenPoint1 = onScreenPoint1;
- this.onScreenPoint2 = onScreenPoint2;
+ // Copy screen coordinates to avoid race conditions with shared vertex data
+ this.onScreenPoint1 = new Point2D(onScreenPoint1.x, onScreenPoint1.y);
+ this.onScreenPoint2 = new Point2D(onScreenPoint2.x, onScreenPoint2.y);
this.texturePoint1 = texturePoint1;
this.texturePoint2 = texturePoint2;
- onScreenHeight = (int) (onScreenPoint2.y - onScreenPoint1.y);
- onScreenWidth = (int) (onScreenPoint2.x - onScreenPoint1.x);
+ onScreenHeight = (int) (this.onScreenPoint2.y - this.onScreenPoint1.y);
+ onScreenWidth = (int) (this.onScreenPoint2.x - this.onScreenPoint1.x);
onscreenAbsoluteHeight = abs(onScreenHeight);
textureWidth = texturePoint2.x - texturePoint1.x;
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java
deleted file mode 100644
index b5ba635..0000000
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon;
-
-import eu.svjatoslav.sixth.e3d.geometry.Point2D;
-import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
-import eu.svjatoslav.sixth.e3d.math.Vertex;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
-import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
-import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap;
-
-import java.awt.*;
-
-import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
-
-/**
- * A textured triangle renderer with perspective-correct texture mapping.
- *
- * This class renders triangles with UV-mapped textures. For large triangles,
- * the rendering may be sliced into smaller pieces for better perspective correction.
- *
- * Perspective-correct texture rendering:
- *
- * - Small polygons are rendered without perspective correction
- * - Larger polygons are sliced into smaller pieces for accurate perspective
- *
- *
- * @see Texture
- * @see Vertex#textureCoordinate
- */
-public class TexturedPolygon extends AbstractCoordinateShape {
-
- private static final ThreadLocal INTERPOLATORS =
- ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
- new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
- });
-
- /**
- * The texture to apply to this polygon.
- */
- public final Texture texture;
-
- private boolean backfaceCulling = false;
-
- private double totalTextureDistance = -1;
-
- /**
- * Creates a textured triangle with the specified vertices and texture.
- *
- * @param p1 the first vertex (must have textureCoordinate set)
- * @param p2 the second vertex (must have textureCoordinate set)
- * @param p3 the third vertex (must have textureCoordinate set)
- * @param texture the texture to apply
- */
- public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
-
- super(p1, p2, p3);
- this.texture = texture;
- }
-
- /**
- * Computes the total UV distance between all texture coordinate pairs.
- * Used to determine appropriate mipmap level.
- */
- private void computeTotalTextureDistance() {
- // compute total texture distance
- totalTextureDistance = vertices[0].textureCoordinate.getDistanceTo(vertices[1].textureCoordinate);
- totalTextureDistance += vertices[0].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate);
- totalTextureDistance += vertices[1].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate);
- }
-
- /**
- * Draws a horizontal scanline between two edge interpolators with texture sampling.
- *
- * @param line1 the left edge interpolator
- * @param line2 the right edge interpolator
- * @param y the Y coordinate of the scanline
- * @param renderBuffer the rendering context to draw into
- * @param textureBitmap the texture bitmap to sample from
- */
- private void drawHorizontalLine(final PolygonBorderInterpolator line1,
- final PolygonBorderInterpolator line2, final int y,
- final RenderingContext renderBuffer,
- final TextureBitmap textureBitmap) {
-
- line1.setCurrentY(y);
- line2.setCurrentY(y);
-
- int x1 = line1.getX();
- int x2 = line2.getX();
-
- final double tx2, ty2;
- final double tx1, ty1;
-
- if (x1 <= x2) {
-
- tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
- ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
-
- tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
- ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
-
- } else {
- final int tmp = x1;
- x1 = x2;
- x2 = tmp;
-
- tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
- ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
-
- tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
- ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
- }
-
- final double realWidth = x2 - x1;
- final double realX1 = x1;
-
- if (x1 < 0)
- x1 = 0;
-
- if (x2 >= renderBuffer.width)
- x2 = renderBuffer.width - 1;
-
- int renderBufferOffset = (y * renderBuffer.width) + x1;
- final int[] renderBufferPixels = renderBuffer.pixels;
-
- final double twidth = tx2 - tx1;
- final double theight = ty2 - ty1;
-
- final double txStep = twidth / realWidth;
- final double tyStep = theight / realWidth;
-
- double tx = tx1 + txStep * (x1 - realX1);
- double ty = ty1 + tyStep * (x1 - realX1);
-
- final int[] texPixels = textureBitmap.pixels;
- final int texW = textureBitmap.width;
- final int texH = textureBitmap.height;
- final int texWMinus1 = texW - 1;
- final int texHMinus1 = texH - 1;
-
- for (int x = x1; x < x2; x++) {
-
- int itx = (int) tx;
- int ity = (int) ty;
-
- if (itx < 0) itx = 0;
- else if (itx > texWMinus1) itx = texWMinus1;
-
- if (ity < 0) ity = 0;
- else if (ity > texHMinus1) ity = texHMinus1;
-
- final int srcPixel = texPixels[ity * texW + itx];
- final int srcAlpha = (srcPixel >> 24) & 0xff;
-
- if (srcAlpha != 0) {
- if (srcAlpha == 255) {
- renderBufferPixels[renderBufferOffset] = srcPixel;
- } else {
- final int backgroundAlpha = 255 - srcAlpha;
-
- final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha;
- final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha;
- final int srcB = (srcPixel & 0xff) * srcAlpha;
-
- final int destPixel = renderBufferPixels[renderBufferOffset];
- final int destR = (destPixel >> 16) & 0xff;
- final int destG = (destPixel >> 8) & 0xff;
- final int destB = destPixel & 0xff;
-
- final int r = ((destR * backgroundAlpha) + srcR) >> 8;
- final int g = ((destG * backgroundAlpha) + srcG) >> 8;
- final int b = ((destB * backgroundAlpha) + srcB) >> 8;
-
- renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
- }
- }
-
- tx += txStep;
- ty += tyStep;
- renderBufferOffset++;
- }
-
- }
-
- /**
- * Renders this textured triangle to the screen.
- *
- * This method performs:
- *
- * - Backface culling check (if enabled)
- * - Mouse interaction detection
- * - Mipmap level selection based on screen coverage
- * - Scanline rasterization with texture sampling
- *
- *
- * @param renderBuffer the rendering context containing the pixel buffer
- */
- @Override
- public void paint(final RenderingContext renderBuffer) {
-
- final Point2D projectedPoint1 = vertices[0].onScreenCoordinate;
- final Point2D projectedPoint2 = vertices[1].onScreenCoordinate;
- final Point2D projectedPoint3 = vertices[2].onScreenCoordinate;
-
- if (backfaceCulling) {
- final double signedArea = (projectedPoint2.x - projectedPoint1.x)
- * (projectedPoint3.y - projectedPoint1.y)
- - (projectedPoint3.x - projectedPoint1.x)
- * (projectedPoint2.y - projectedPoint1.y);
- if (signedArea >= 0)
- return;
- }
-
- projectedPoint1.roundToInteger();
- projectedPoint2.roundToInteger();
- projectedPoint3.roundToInteger();
-
- if (mouseInteractionController != null)
- if (renderBuffer.getMouseEvent() != null)
- if (pointWithinPolygon(
- renderBuffer.getMouseEvent().coordinate, projectedPoint1,
- projectedPoint2, projectedPoint3))
- renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
-
- // Show polygon boundaries (for debugging)
- if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
- showBorders(renderBuffer);
-
- // find top-most point
- int yTop = (int) projectedPoint1.y;
-
- if (projectedPoint2.y < yTop)
- yTop = (int) projectedPoint2.y;
-
- if (projectedPoint3.y < yTop)
- yTop = (int) projectedPoint3.y;
-
- if (yTop < 0)
- yTop = 0;
-
- // find bottom-most point
- int yBottom = (int) projectedPoint1.y;
-
- if (projectedPoint2.y > yBottom)
- yBottom = (int) projectedPoint2.y;
-
- if (projectedPoint3.y > yBottom)
- yBottom = (int) projectedPoint3.y;
-
- if (yBottom >= renderBuffer.height)
- yBottom = renderBuffer.height - 1;
-
- // clamp to render Y bounds
- yTop = Math.max(yTop, renderBuffer.renderMinY);
- yBottom = Math.min(yBottom, renderBuffer.renderMaxY);
- if (yTop >= yBottom)
- return;
-
- // paint
- double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2);
- totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3);
- totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3);
-
- if (totalTextureDistance == -1)
- computeTotalTextureDistance();
- final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
-
- final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor);
-
- final PolygonBorderInterpolator[] interp = INTERPOLATORS.get();
- final PolygonBorderInterpolator polygonBorder1 = interp[0];
- final PolygonBorderInterpolator polygonBorder2 = interp[1];
- final PolygonBorderInterpolator polygonBorder3 = interp[2];
-
- polygonBorder1.setPoints(projectedPoint1, projectedPoint2,
- vertices[0].textureCoordinate,
- vertices[1].textureCoordinate);
- polygonBorder2.setPoints(projectedPoint1, projectedPoint3,
- vertices[0].textureCoordinate,
- vertices[2].textureCoordinate);
- polygonBorder3.setPoints(projectedPoint2, projectedPoint3,
- vertices[1].textureCoordinate,
- vertices[2].textureCoordinate);
-
- // Inline sort for 3 elements to avoid array allocation
- PolygonBorderInterpolator a = polygonBorder1;
- PolygonBorderInterpolator b = polygonBorder2;
- PolygonBorderInterpolator c = polygonBorder3;
- PolygonBorderInterpolator t;
- if (a.compareTo(b) > 0) {
- t = a;
- a = b;
- b = t;
- }
- if (b.compareTo(c) > 0) {
- t = b;
- b = c;
- c = t;
- }
- if (a.compareTo(b) > 0) {
- t = a;
- a = b;
- b = t;
- }
-
- for (int y = yTop; y < yBottom; y++)
- if (a.containsY(y)) {
- if (b.containsY(y))
- drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap);
- else if (c.containsY(y))
- drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap);
- } else if (b.containsY(y))
- if (c.containsY(y))
- drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap);
-
- }
-
- /**
- * Checks if backface culling is enabled for this polygon.
- *
- * @return {@code true} if backface culling is enabled
- */
- public boolean isBackfaceCullingEnabled() {
- return backfaceCulling;
- }
-
- /**
- * Enables or disables backface culling for this polygon.
- *
- * @param backfaceCulling {@code true} to enable backface culling
- */
- public void setBackfaceCulling(final boolean backfaceCulling) {
- this.backfaceCulling = backfaceCulling;
- }
-
- /**
- * Draws the polygon border edges in yellow (for debugging).
- *
- * @param renderBuffer the rendering context
- */
- private void showBorders(final RenderingContext renderBuffer) {
-
- final Point2D projectedPoint1 = vertices[0].onScreenCoordinate;
- final Point2D projectedPoint2 = vertices[1].onScreenCoordinate;
- final Point2D projectedPoint3 = vertices[2].onScreenCoordinate;
-
- final int x1 = (int) projectedPoint1.x;
- final int y1 = (int) projectedPoint1.y;
- final int x2 = (int) projectedPoint2.x;
- final int y2 = (int) projectedPoint2.y;
- final int x3 = (int) projectedPoint3.x;
- final int y3 = (int) projectedPoint3.y;
-
- renderBuffer.executeWithGraphics(g -> {
- g.setColor(Color.YELLOW);
- g.drawLine(x1, y1, x2, y2);
- g.drawLine(x3, y3, x2, y2);
- g.drawLine(x1, y1, x3, y3);
- });
- }
-
-}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
new file mode 100644
index 0000000..6a015b5
--- /dev/null
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java
@@ -0,0 +1,373 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap;
+
+import java.awt.*;
+
+import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
+
+/**
+ * A textured triangle renderer with perspective-correct texture mapping.
+ *
+ * This class renders triangles with UV-mapped textures. For large triangles,
+ * the rendering may be sliced into smaller pieces for better perspective correction.
+ *
+ * Perspective-correct texture rendering:
+ *
+ * - Small triangles are rendered without perspective correction
+ * - Larger triangles are sliced into smaller pieces for accurate perspective
+ *
+ *
+ * @see Texture
+ * @see Vertex#textureCoordinate
+ */
+public class TexturedTriangle extends AbstractCoordinateShape {
+
+ private static final ThreadLocal INTERPOLATORS =
+ ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
+ new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
+ });
+
+ /**
+ * The texture to apply to this triangle.
+ */
+ public final Texture texture;
+
+ private boolean backfaceCulling = false;
+
+ /**
+ * Total UV distance between all texture coordinate pairs.
+ * Computed at construction time to determine appropriate mipmap level.
+ */
+ private double totalTextureDistance;
+
+ /**
+ * Creates a textured triangle with the specified vertices and texture.
+ *
+ * @param p1 the first vertex (must have textureCoordinate set)
+ * @param p2 the second vertex (must have textureCoordinate set)
+ * @param p3 the third vertex (must have textureCoordinate set)
+ * @param texture the texture to apply
+ */
+ public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
+
+ super(p1, p2, p3);
+ this.texture = texture;
+ computeTotalTextureDistance();
+ }
+
+ /**
+ * Computes the total UV distance between all texture coordinate pairs.
+ * Used to determine appropriate mipmap level.
+ */
+ private void computeTotalTextureDistance() {
+ totalTextureDistance = vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(1).textureCoordinate);
+ totalTextureDistance += vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
+ totalTextureDistance += vertices.get(1).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate);
+ }
+
+ /**
+ * Draws a horizontal scanline between two edge interpolators with texture sampling.
+ *
+ * @param line1 the left edge interpolator
+ * @param line2 the right edge interpolator
+ * @param y the Y coordinate of the scanline
+ * @param renderBuffer the rendering context to draw into
+ * @param textureBitmap the texture bitmap to sample from
+ */
+ private void drawHorizontalLine(final PolygonBorderInterpolator line1,
+ final PolygonBorderInterpolator line2, final int y,
+ final RenderingContext renderBuffer,
+ final TextureBitmap textureBitmap) {
+
+ line1.setCurrentY(y);
+ line2.setCurrentY(y);
+
+ int x1 = line1.getX();
+ int x2 = line2.getX();
+
+ final double tx2, ty2;
+ final double tx1, ty1;
+
+ if (x1 <= x2) {
+
+ tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
+ ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
+
+ tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
+ ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
+
+ } else {
+ final int tmp = x1;
+ x1 = x2;
+ x2 = tmp;
+
+ tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
+ ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
+
+ tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
+ ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
+ }
+
+ final double realWidth = x2 - x1;
+ final double realX1 = x1;
+
+ if (x1 < 0)
+ x1 = 0;
+
+ if (x2 >= renderBuffer.width)
+ x2 = renderBuffer.width - 1;
+
+ int renderBufferOffset = (y * renderBuffer.width) + x1;
+ final int[] renderBufferPixels = renderBuffer.pixels;
+
+ final double twidth = tx2 - tx1;
+ final double theight = ty2 - ty1;
+
+ final double txStep = twidth / realWidth;
+ final double tyStep = theight / realWidth;
+
+ double tx = tx1 + txStep * (x1 - realX1);
+ double ty = ty1 + tyStep * (x1 - realX1);
+
+ final int[] texPixels = textureBitmap.pixels;
+ final int texW = textureBitmap.width;
+ final int texH = textureBitmap.height;
+ final int texWMinus1 = texW - 1;
+ final int texHMinus1 = texH - 1;
+
+ for (int x = x1; x < x2; x++) {
+
+ int itx = (int) tx;
+ int ity = (int) ty;
+
+ if (itx < 0) itx = 0;
+ else if (itx > texWMinus1) itx = texWMinus1;
+
+ if (ity < 0) ity = 0;
+ else if (ity > texHMinus1) ity = texHMinus1;
+
+ final int srcPixel = texPixels[ity * texW + itx];
+ final int srcAlpha = (srcPixel >> 24) & 0xff;
+
+ if (srcAlpha != 0) {
+ if (srcAlpha == 255) {
+ renderBufferPixels[renderBufferOffset] = srcPixel;
+ } else {
+ final int backgroundAlpha = 255 - srcAlpha;
+
+ final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha;
+ final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha;
+ final int srcB = (srcPixel & 0xff) * srcAlpha;
+
+ final int destPixel = renderBufferPixels[renderBufferOffset];
+ final int destR = (destPixel >> 16) & 0xff;
+ final int destG = (destPixel >> 8) & 0xff;
+ final int destB = destPixel & 0xff;
+
+ final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+ final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+ final int b = ((destB * backgroundAlpha) + srcB) >> 8;
+
+ renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
+ }
+ }
+
+ tx += txStep;
+ ty += tyStep;
+ renderBufferOffset++;
+ }
+
+ }
+
+ /**
+ * Renders this textured triangle to the screen.
+ *
+ * This method performs:
+ *
+ * - Backface culling check (if enabled)
+ * - Mouse interaction detection
+ * - Mipmap level selection based on screen coverage
+ * - Scanline rasterization with texture sampling
+ *
+ *
+ * @param renderBuffer the rendering context containing the pixel buffer
+ */
+ @Override
+ public void paint(final RenderingContext renderBuffer) {
+
+ final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate;
+ final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate;
+ final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate;
+
+ if (backfaceCulling) {
+ final double signedArea = (projectedPoint2.x - projectedPoint1.x)
+ * (projectedPoint3.y - projectedPoint1.y)
+ - (projectedPoint3.x - projectedPoint1.x)
+ * (projectedPoint2.y - projectedPoint1.y);
+ if (signedArea >= 0)
+ return;
+ }
+
+ // Copy and round coordinates to local variables (don't modify original Point2D)
+ // This is thread-safe: multiple threads may paint the same polygon across different
+ // Y segments, so we must not mutate shared vertex data
+ final int x1 = (int) projectedPoint1.x;
+ final int y1 = (int) projectedPoint1.y;
+ final int x2 = (int) projectedPoint2.x;
+ final int y2 = (int) projectedPoint2.y;
+ final int x3 = (int) projectedPoint3.x;
+ final int y3 = (int) projectedPoint3.y;
+
+ if (mouseInteractionController != null)
+ if (renderBuffer.getMouseEvent() != null)
+ if (pointWithinPolygon(
+ renderBuffer.getMouseEvent().coordinate, x1, y1, x2, y2, x3, y3))
+ renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
+
+ // Show polygon boundaries (for debugging)
+ if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
+ showBorders(renderBuffer);
+
+ // find top-most point
+ int yTop = y1;
+
+ if (y2 < yTop)
+ yTop = y2;
+
+ if (y3 < yTop)
+ yTop = y3;
+
+ if (yTop < 0)
+ yTop = 0;
+
+ // find bottom-most point
+ int yBottom = y1;
+
+ if (y2 > yBottom)
+ yBottom = y2;
+
+ if (y3 > yBottom)
+ yBottom = y3;
+
+ if (yBottom >= renderBuffer.height)
+ yBottom = renderBuffer.height - 1;
+
+ // clamp to render Y bounds
+ yTop = Math.max(yTop, renderBuffer.renderMinY);
+ yBottom = Math.min(yBottom, renderBuffer.renderMaxY);
+ if (yTop >= yBottom)
+ return;
+
+ // paint
+ double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2);
+ totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3);
+ totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3);
+
+ final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
+
+ final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor);
+
+ final PolygonBorderInterpolator[] interp = INTERPOLATORS.get();
+ final PolygonBorderInterpolator polygonBorder1 = interp[0];
+ final PolygonBorderInterpolator polygonBorder2 = interp[1];
+ final PolygonBorderInterpolator polygonBorder3 = interp[2];
+
+ // Use rounded integer coordinates for screen positions
+ polygonBorder1.setPoints(new Point2D(x1, y1), new Point2D(x2, y2),
+ vertices.get(0).textureCoordinate,
+ vertices.get(1).textureCoordinate);
+ polygonBorder2.setPoints(new Point2D(x1, y1), new Point2D(x3, y3),
+ vertices.get(0).textureCoordinate,
+ vertices.get(2).textureCoordinate);
+ polygonBorder3.setPoints(new Point2D(x2, y2), new Point2D(x3, y3),
+ vertices.get(1).textureCoordinate,
+ vertices.get(2).textureCoordinate);
+
+ // Inline sort for 3 elements to avoid array allocation
+ PolygonBorderInterpolator a = polygonBorder1;
+ PolygonBorderInterpolator b = polygonBorder2;
+ PolygonBorderInterpolator c = polygonBorder3;
+ PolygonBorderInterpolator t;
+ if (a.compareTo(b) > 0) {
+ t = a;
+ a = b;
+ b = t;
+ }
+ if (b.compareTo(c) > 0) {
+ t = b;
+ b = c;
+ c = t;
+ }
+ if (a.compareTo(b) > 0) {
+ t = a;
+ a = b;
+ b = t;
+ }
+
+ for (int y = yTop; y < yBottom; y++)
+ if (a.containsY(y)) {
+ if (b.containsY(y))
+ drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap);
+ else if (c.containsY(y))
+ drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap);
+ } else if (b.containsY(y))
+ if (c.containsY(y))
+ drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap);
+
+ }
+
+ /**
+ * Checks if backface culling is enabled for this triangle.
+ *
+ * @return {@code true} if backface culling is enabled
+ */
+ public boolean isBackfaceCullingEnabled() {
+ return backfaceCulling;
+ }
+
+ /**
+ * Enables or disables backface culling for this triangle.
+ *
+ * @param backfaceCulling {@code true} to enable backface culling
+ */
+ public void setBackfaceCulling(final boolean backfaceCulling) {
+ this.backfaceCulling = backfaceCulling;
+ }
+
+ /**
+ * Draws the triangle border edges in yellow (for debugging).
+ *
+ * @param renderBuffer the rendering context
+ */
+ private void showBorders(final RenderingContext renderBuffer) {
+
+ final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate;
+ final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate;
+ final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate;
+
+ final int x1 = (int) projectedPoint1.x;
+ final int y1 = (int) projectedPoint1.y;
+ final int x2 = (int) projectedPoint2.x;
+ final int y2 = (int) projectedPoint2.y;
+ final int x3 = (int) projectedPoint3.x;
+ final int y3 = (int) projectedPoint3.y;
+
+ renderBuffer.executeWithGraphics(g -> {
+ g.setColor(Color.YELLOW);
+ g.drawLine(x1, y1, x2, y2);
+ g.drawLine(x3, y3, x2, y2);
+ g.drawLine(x1, y1, x3, y3);
+ });
+ }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java
index f893beb..44489af 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java
@@ -6,16 +6,16 @@
/**
* Textured triangle rendering with perspective-correct UV mapping.
*
- * Textured polygons apply 2D textures to 3D triangles using UV coordinates.
- * Large polygons may be sliced into smaller pieces for accurate perspective correction.
+ * Textured triangles apply 2D textures to 3D triangles using UV coordinates.
+ * Large triangles may be sliced into smaller pieces for accurate perspective correction.
*
* Key classes:
*
- * - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon} - The textured triangle shape
+ * - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape
* - {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs
*
*
- * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle
* @see eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture
*/
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java
index c7f71d7..7cf643b 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java
@@ -103,7 +103,7 @@ public class Graph extends AbstractCompositeShape {
plotData(scale, data);
final Point3D labelLocation = new Point3D(width / 2, yMax + 0.5, 0)
- .scaleUp(scale);
+ .multiply(scale);
final TextCanvas labelCanvas = new TextCanvas(new Transform(
labelLocation), label, Color.WHITE, Color.TRANSPARENT);
@@ -114,16 +114,16 @@ public class Graph extends AbstractCompositeShape {
private void addHorizontalLinesAndLabels(final double scale) {
for (double y = yMin; y <= yMax; y += verticalStep) {
- final Point3D p1 = new Point3D(0, y, 0).scaleUp(scale);
+ final Point3D p1 = new Point3D(0, y, 0).multiply(scale);
- final Point3D p2 = new Point3D(width, y, 0).scaleUp(scale);
+ final Point3D p2 = new Point3D(width, y, 0).multiply(scale);
final Line line = new Line(p1, p2, gridColor, lineWidth);
addShape(line);
final Point3D labelLocation = new Point3D(-0.5, y, 0)
- .scaleUp(scale);
+ .multiply(scale);
final TextCanvas label = new TextCanvas(
new Transform(labelLocation), String.valueOf(y),
@@ -137,8 +137,8 @@ public class Graph extends AbstractCompositeShape {
private void addVerticalLines(final double scale) {
for (double x = 0; x <= width; x += horizontalStep) {
- final Point3D p1 = new Point3D(x, yMin, 0).scaleUp(scale);
- final Point3D p2 = new Point3D(x, yMax, 0).scaleUp(scale);
+ final Point3D p1 = new Point3D(x, yMin, 0).multiply(scale);
+ final Point3D p2 = new Point3D(x, yMax, 0).multiply(scale);
final Line line = new Line(p1, p2, gridColor, lineWidth);
@@ -150,7 +150,7 @@ public class Graph extends AbstractCompositeShape {
private void addXLabels(final double scale) {
for (double x = 0; x <= width; x += horizontalStep * 2) {
final Point3D labelLocation = new Point3D(x, yMin - 0.4, 0)
- .scaleUp(scale);
+ .multiply(scale);
final TextCanvas label = new TextCanvas(
new Transform(labelLocation), String.valueOf(x),
@@ -164,7 +164,7 @@ public class Graph extends AbstractCompositeShape {
Point3D previousPoint = null;
for (final Point2D point : data) {
- final Point3D p3d = new Point3D(point.x, point.y, 0).scaleUp(scale);
+ final Point3D p3d = new Point3D(point.x, point.y, 0).multiply(scale);
if (previousPoint != null) {
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
index aa52272..f1efcaa 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
@@ -8,7 +8,7 @@ import eu.svjatoslav.sixth.e3d.geometry.Point2D;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.math.Transform;
import eu.svjatoslav.sixth.e3d.math.Vertex;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
@@ -16,7 +16,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
* A rectangular shape with texture mapping, composed of two textured triangles.
*
* This composite shape creates a textured rectangle in 3D space by splitting it into
- * two {@link TexturedPolygon} triangles that share a common {@link Texture}. The rectangle
+ * two {@link TexturedTriangle} triangles that share a common {@link Texture}. The rectangle
* is centered at the origin of its local coordinate system, with configurable world-space
* dimensions and independent texture resolution.
*
@@ -39,7 +39,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
* shapeCollection.addShape(rect);
* }
*
- * @see TexturedPolygon
+ * @see TexturedTriangle
* @see Texture
* @see AbstractCompositeShape
*/
@@ -129,7 +129,7 @@ public class TexturedRectangle extends AbstractCompositeShape {
*
* The rectangle is centered at the local origin: corners span from
* {@code (-width/2, -height/2, 0)} to {@code (width/2, height/2, 0)}.
- * Two {@link TexturedPolygon} triangles are created to cover the full rectangle,
+ * Two {@link TexturedTriangle} triangles are created to cover the full rectangle,
* sharing a single {@link Texture} instance.
*
* @param width the width of the rectangle in world units
@@ -157,7 +157,7 @@ public class TexturedRectangle extends AbstractCompositeShape {
- final TexturedPolygon texturedPolygon1 = new TexturedPolygon(
+ final TexturedTriangle texturedPolygon1 = new TexturedTriangle(
new Vertex(topLeft, textureTopLeft),
new Vertex(topRight, textureTopRight),
new Vertex(bottomRight, textureBottomRight), texture);
@@ -165,7 +165,7 @@ public class TexturedRectangle extends AbstractCompositeShape {
texturedPolygon1
.setMouseInteractionController(mouseInteractionController);
- final TexturedPolygon texturedPolygon2 = new TexturedPolygon(
+ final TexturedTriangle texturedPolygon2 = new TexturedTriangle(
new Vertex(topLeft, textureTopLeft),
new Vertex(bottomLeft, textureBottomLeft),
new Vertex(bottomRight, textureBottomRight), texture);
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
index d9bfe23..6752ce5 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
@@ -4,6 +4,7 @@
*/
package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
+import eu.svjatoslav.sixth.e3d.csg.CSGNode;
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker;
@@ -15,10 +16,11 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
import eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
/**
@@ -184,22 +186,13 @@ public class AbstractCompositeShape extends AbstractShape {
}
/**
- * Extracts all SolidPolygon triangles from this composite shape.
+ * Extracts all SolidPolygon instances from this composite shape.
*
* Recursively traverses the shape hierarchy and collects all
- * {@link SolidPolygon} instances. Useful for CSG operations where
- * you need the raw triangles from a composite shape like
- * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube}
- * or {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere}.
+ * SolidPolygon instances. Used for CSG operations where polygons
+ * are needed directly without conversion.
*
- * Example:
- * {@code
- * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
- * List triangles = cube.extractSolidPolygons();
- * CSG csg = CSG.fromSolidPolygons(triangles);
- * }
- *
- * @return list of all SolidPolygon sub-shapes
+ * @return list of SolidPolygon instances from this shape hierarchy
*/
public List extractSolidPolygons() {
final List result = new ArrayList<>();
@@ -362,18 +355,26 @@ public class AbstractCompositeShape extends AbstractShape {
*
* @param transform the new transform to apply
*/
- public void setTransform(final Transform transform) {
+ /**
+ * Sets the transform for this composite shape.
+ *
+ * @param transform the new transform
+ * @return this composite shape (for chaining)
+ */
+ public AbstractCompositeShape setTransform(final Transform transform) {
this.transform = transform;
+ return this;
}
/**
- * Enables or disables shading for all SolidPolygon sub-shapes.
- * When enabled, polygons use the global lighting manager from the rendering
+ * Enables or disables shading for all SolidTriangle and SolidPolygon sub-shapes.
+ * When enabled, shapes use the global lighting manager from the rendering
* context to calculate flat shading based on light sources.
*
* @param shadingEnabled {@code true} to enable shading, {@code false} to disable
+ * @return this composite shape (for chaining)
*/
- public void setShadingEnabled(final boolean shadingEnabled) {
+ public AbstractCompositeShape setShadingEnabled(final boolean shadingEnabled) {
for (final SubShape subShape : getOriginalSubShapes()) {
final AbstractShape shape = subShape.getShape();
if (shape instanceof SolidPolygon) {
@@ -382,22 +383,199 @@ public class AbstractCompositeShape extends AbstractShape {
// TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too
}
+ return this;
}
/**
- * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
+ * Enables or disables backface culling for all SolidPolygon and TexturedTriangle sub-shapes.
*
* @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
+ * @return this composite shape (for chaining)
*/
- public void setBackfaceCulling(final boolean backfaceCulling) {
+ public AbstractCompositeShape setBackfaceCulling(final boolean backfaceCulling) {
for (final SubShape subShape : getOriginalSubShapes()) {
final AbstractShape shape = subShape.getShape();
if (shape instanceof SolidPolygon) {
((SolidPolygon) shape).setBackfaceCulling(backfaceCulling);
- } else if (shape instanceof TexturedPolygon) {
- ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling);
+ } else if (shape instanceof TexturedTriangle) {
+ ((TexturedTriangle) shape).setBackfaceCulling(backfaceCulling);
}
}
+ return this;
+ }
+
+ /**
+ * Performs an in-place union with another composite shape.
+ *
+ * This shape's SolidPolygon children are replaced with the union result.
+ * Non-SolidPolygon children from both shapes are preserved and combined.
+ *
+ * CSG Operation: Union combines two shapes into one, keeping all
+ * geometry from both. Uses BSP tree algorithms for robust boolean operations.
+ *
+ * Child handling:
+ *
+ * - SolidPolygon children from both shapes â replaced with union result
+ * - Non-SolidPolygon children from this shape â preserved
+ * - Non-SolidPolygon children from other shape â added to this shape
+ * - Nested AbstractCompositeShape children â preserved unchanged (not recursively processed)
+ *
+ *
+ * @param other the shape to union with
+ * @see #subtract(AbstractCompositeShape)
+ * @see #intersect(AbstractCompositeShape)
+ */
+ public void union(final AbstractCompositeShape other) {
+ final List selfPolygons = clonePolygons(extractSolidPolygons());
+ final List otherPolygons = clonePolygons(other.extractSolidPolygons());
+
+ final CSGNode a = new CSGNode(selfPolygons);
+ final CSGNode b = new CSGNode(otherPolygons);
+
+ a.clipTo(b);
+ b.clipTo(a);
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+ a.build(b.allPolygons());
+
+ replaceSolidPolygons(a.allPolygons(), other, true);
+ }
+
+ /**
+ * Performs an in-place subtraction with another composite shape.
+ *
+ * This shape's SolidPolygon children are replaced with the difference result.
+ * The other shape acts as a "cutter" that carves out volume from this shape.
+ *
+ * CSG Operation: Subtract removes the volume of the second shape
+ * from the first shape. Useful for creating holes, cavities, and cutouts.
+ *
+ * Child handling:
+ *
+ * - SolidPolygon children from this shape â replaced with difference result
+ * - Non-SolidPolygon children from this shape â preserved
+ * - All children from other shape â discarded (other is just a cutter)
+ * - Nested AbstractCompositeShape children â preserved unchanged
+ *
+ *
+ * @param other the shape to subtract (the cutter)
+ * @see #union(AbstractCompositeShape)
+ * @see #intersect(AbstractCompositeShape)
+ */
+ public void subtract(final AbstractCompositeShape other) {
+ final List selfPolygons = clonePolygons(extractSolidPolygons());
+ final List otherPolygons = clonePolygons(other.extractSolidPolygons());
+
+ final CSGNode a = new CSGNode(selfPolygons);
+ final CSGNode b = new CSGNode(otherPolygons);
+
+ a.invert();
+ a.clipTo(b);
+ b.clipTo(a);
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+ a.build(b.allPolygons());
+ a.invert();
+
+ replaceSolidPolygons(a.allPolygons(), other, false);
+ }
+
+ /**
+ * Performs an in-place intersection with another composite shape.
+ *
+ * This shape's SolidPolygon children are replaced with the intersection result.
+ * Only the overlapping volume between the two shapes remains.
+ *
+ * CSG Operation: Intersect keeps only the volume where both shapes
+ * overlap. Useful for creating shapes constrained by multiple boundaries.
+ *
+ * Child handling:
+ *
+ * - SolidPolygon children from this shape â replaced with intersection result
+ * - Non-SolidPolygon children from this shape â preserved
+ * - All children from other shape â discarded
+ * - Nested AbstractCompositeShape children â preserved unchanged
+ *
+ *
+ * @param other the shape to intersect with
+ * @see #union(AbstractCompositeShape)
+ * @see #subtract(AbstractCompositeShape)
+ */
+ public void intersect(final AbstractCompositeShape other) {
+ final List selfPolygons = clonePolygons(extractSolidPolygons());
+ final List otherPolygons = clonePolygons(other.extractSolidPolygons());
+
+ final CSGNode a = new CSGNode(selfPolygons);
+ final CSGNode b = new CSGNode(otherPolygons);
+
+ a.invert();
+ b.clipTo(a);
+ b.invert();
+ a.clipTo(b);
+ b.clipTo(a);
+ a.build(b.allPolygons());
+ a.invert();
+
+ replaceSolidPolygons(a.allPolygons(), other, false);
+ }
+
+ /**
+ * Creates deep clones of all polygons in the list.
+ *
+ * CSG operations modify polygons in-place via BSP tree operations.
+ * Cloning ensures the original polygon data is preserved.
+ *
+ * @param polygons the polygons to clone
+ * @return a new list containing deep clones of all polygons
+ */
+ private List clonePolygons(final List polygons) {
+ final List cloned = new ArrayList<>(polygons.size());
+ for (final SolidPolygon p : polygons) {
+ cloned.add(p.deepClone());
+ }
+ return cloned;
+ }
+
+ /**
+ * Replaces this shape's SolidPolygon children with new polygons.
+ *
+ * Preserves all non-SolidPolygon children (Lines, nested composites, etc.).
+ * Optionally carries over non-SolidPolygon children from another shape.
+ *
+ * @param newPolygons the polygons to replace with
+ * @param other the other shape (may be null)
+ * @param carryOtherNonPolygons whether to add other's non-SolidPolygon children to this shape
+ */
+ private void replaceSolidPolygons(final List newPolygons,
+ final AbstractCompositeShape other,
+ final boolean carryOtherNonPolygons) {
+ // Remove all direct SolidPolygon children from this shape
+ final Iterator iterator = originalSubShapes.iterator();
+ while (iterator.hasNext()) {
+ final SubShape subShape = iterator.next();
+ if (subShape.getShape() instanceof SolidPolygon) {
+ iterator.remove();
+ }
+ }
+
+ // Add all result polygons as new children
+ for (final SolidPolygon polygon : newPolygons) {
+ addShape(polygon);
+ }
+
+ // Optionally carry over non-SolidPolygon children from other shape
+ if (carryOtherNonPolygons && other != null) {
+ for (final SubShape otherSubShape : other.originalSubShapes) {
+ final AbstractShape otherShape = otherSubShape.getShape();
+ if (!(otherShape instanceof SolidPolygon)) {
+ addShape(otherShape, otherSubShape.getGroupIdentifier());
+ }
+ }
+ }
+
+ slicingOutdated = true;
}
/**
@@ -434,8 +612,8 @@ public class AbstractCompositeShape extends AbstractShape {
for (int i = 0; i < originalSubShapes.size(); i++) {
final SubShape subShape = originalSubShapes.get(i);
if (subShape.isVisible()) {
- if (subShape.getShape() instanceof TexturedPolygon) {
- slicer.slice((TexturedPolygon) subShape.getShape());
+ if (subShape.getShape() instanceof TexturedTriangle) {
+ slicer.slice((TexturedTriangle) subShape.getShape());
texturedPolygonCount++;
} else {
result.add(subShape.getShape());
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
index b139a2b..b3bfc81 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
@@ -81,6 +81,15 @@ public class SubShape {
return Objects.equals(this.groupIdentifier, groupIdentifier);
}
+ /**
+ * Returns the group identifier for this sub-shape.
+ *
+ * @return the group identifier, or {@code null} if this shape is ungrouped
+ */
+ public String getGroupIdentifier() {
+ return groupIdentifier;
+ }
+
/**
* Assigns this sub-shape to a group.
*
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java
index d8a9236..29c2c4e 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java
@@ -216,37 +216,33 @@ public class SolidPolygonArrow extends AbstractCompositeShape {
startSideRing[i] = startSideLocal;
}
- // Create cylinder side faces (two triangles per segment)
- // Winding: tipSide â startSide â tipSide+next, then tipSide+next â startSide â startSide+next
- // This creates CCW winding when viewed from outside the cylinder
+ // Create cylinder side faces (one quad per segment)
+ // Winding: tipSide[i] â startSide[i] â startSide[next] â tipSide[next]
+ // creates CCW winding when viewed from outside the cylinder
for (int i = 0; i < segments; i++) {
final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
- new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
- new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z),
- color));
-
- addShape(new SolidPolygon(
- new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z),
- new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
- new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
+ addShape(SolidPolygon.quad(
+ tipSideRing[i],
+ startSideRing[i],
+ startSideRing[next],
+ tipSideRing[next],
color));
}
// Add back cap at the start point.
+ // Single N-vertex polygon that closes the loop to create segments triangles
+ // (segments+2 vertices â segments triangles via fan triangulation)
// The cap faces backward (away from arrow tip), opposite to arrow direction.
- // Winding: center â next â current creates CCW winding when viewed from behind.
- // (Ring vertices are ordered CCW when viewed from the tip; reversing gives CCW from behind)
+ // Winding: center â ring[segments-1] â ... â ring[1] â ring[0] â ring[segments-1]
+ // (reverse order from ring array direction)
+ final Point3D[] backCapVertices = new Point3D[segments + 2];
+ backCapVertices[0] = startPoint;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(startPoint.x, startPoint.y, startPoint.z),
- new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
- new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
- color));
+ backCapVertices[i + 1] = startSideRing[segments - 1 - i];
}
+ backCapVertices[segments + 1] = startSideRing[segments - 1]; // close the loop
+ addShape(new SolidPolygon(backCapVertices, color));
}
/**
@@ -312,15 +308,17 @@ public class SolidPolygonArrow extends AbstractCompositeShape {
}
// Create base cap of the cone tip (fills the gap between cone and cylinder body)
+ // Single N-vertex polygon that closes the loop to create segments triangles
+ // (segments+2 vertices â segments triangles via fan triangulation)
// The base cap faces toward the arrow body/start, opposite to the cone's pointing direction.
- // Winding: center â next â current gives CCW when viewed from the body side.
+ // Winding: center â ring[segments-1] â ... â ring[1] â ring[0] â ring[segments-1]
+ final Point3D baseCenter = new Point3D(baseCenterX, baseCenterY, baseCenterZ);
+ final Point3D[] tipBaseCapVertices = new Point3D[segments + 2];
+ tipBaseCapVertices[0] = baseCenter;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(baseCenterX, baseCenterY, baseCenterZ),
- new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
- new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
- color));
+ tipBaseCapVertices[i + 1] = baseRing[segments - 1 - i];
}
+ tipBaseCapVertices[segments + 1] = baseRing[segments - 1]; // close the loop
+ addShape(new SolidPolygon(tipBaseCapVertices, color));
}
}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java
index 05ceafc..3a4327f 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java
@@ -139,17 +139,17 @@ public class SolidPolygonCone extends AbstractCompositeShape {
}
// Create base cap (circular bottom face)
+ // Single N-vertex polygon that closes the loop to create segments triangles
+ // (segments+2 vertices â segments triangles via fan triangulation)
// The cap faces away from the apex (in the direction the cone points).
- // Winding: center â current â next creates CCW winding when viewed from
- // outside (away from apex).
+ // Winding: center â ring[0] â ring[1] â ... â ring[segments-1] â ring[0]
+ final Point3D[] baseCapVertices = new Point3D[segments + 2];
+ baseCapVertices[0] = baseCenterPoint;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(baseCenterPoint.x, baseCenterPoint.y, baseCenterPoint.z),
- new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
- new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
- color));
+ baseCapVertices[i + 1] = baseRing[i];
}
+ baseCapVertices[segments + 1] = baseRing[0]; // close the loop
+ addShape(new SolidPolygon(baseCapVertices, color));
setBackfaceCulling(true);
}
@@ -207,17 +207,17 @@ public class SolidPolygonCone extends AbstractCompositeShape {
}
// Create base cap (circular bottom face)
+ // Single N-vertex polygon that closes the loop to create segments triangles
+ // (segments+2 vertices â segments triangles via fan triangulation)
// The base cap faces in +Y direction (downward, away from apex).
- // Base ring vertices go CCW when viewed from above (+Y), so center â current â next
- // maintains CCW for the cap when viewed from +Y (the correct direction).
+ // Winding: center â ring[0] â ring[1] â ... â ring[segments-1] â ring[0]
+ final Point3D[] baseCapVertices = new Point3D[segments + 2];
+ baseCapVertices[0] = baseCenter;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
- new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
- new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
- color));
+ baseCapVertices[i + 1] = baseRing[i];
}
+ baseCapVertices[segments + 1] = baseRing[0]; // close the loop
+ addShape(new SolidPolygon(baseCapVertices, color));
setBackfaceCulling(true);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java
index 276e6d4..b4673f6 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java
@@ -118,47 +118,42 @@ public class SolidPolygonCylinder extends AbstractCompositeShape {
endSideRing[i] = endLocal;
}
- // Create side faces (two triangles per segment)
- // Winding: startSide â endSide â startSide+next, then startSide+next â endSide â endSide+next
- // This creates CCW winding when viewed from outside the cylinder
+ // Create side faces (one quad per segment)
+ // Winding: startSide[i] â endSide[i] â endSide[next] â startSide[next]
+ // creates CCW winding when viewed from outside the cylinder
for (int i = 0; i < segments; i++) {
final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
- new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
- new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
- color));
-
- addShape(new SolidPolygon(
- new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
- new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
- new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
+ addShape(SolidPolygon.quad(
+ startSideRing[i],
+ endSideRing[i],
+ endSideRing[next],
+ startSideRing[next],
color));
}
// Create start cap (at startPoint, faces outward from cylinder)
- // Winding: center â current â next creates CCW winding when viewed from outside
+ // Single N-vertex polygon that closes the loop to create segments triangles
+ // (segments+2 vertices â segments triangles via fan triangulation)
+ // Winding: center â ring[0] â ring[1] â ... â ring[segments-1] â ring[0]
+ final Point3D[] startCapVertices = new Point3D[segments + 2];
+ startCapVertices[0] = startPoint;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(startPoint.x, startPoint.y, startPoint.z),
- new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
- new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
- color));
+ startCapVertices[i + 1] = startSideRing[i];
}
+ startCapVertices[segments + 1] = startSideRing[0]; // close the loop
+ addShape(new SolidPolygon(startCapVertices, color));
// Create end cap (at endPoint, faces outward from cylinder)
- // Winding: center â next â current creates CCW winding when viewed from outside
- // (opposite to start cap because end cap faces the opposite direction)
+ // Reverse winding for opposite-facing cap
+ // Winding: center â ring[segments-1] â ... â ring[1] â ring[0] â ring[segments-1]
+ final Point3D[] endCapVertices = new Point3D[segments + 2];
+ endCapVertices[0] = endPoint;
for (int i = 0; i < segments; i++) {
- final int next = (i + 1) % segments;
- addShape(new SolidPolygon(
- new Point3D(endPoint.x, endPoint.y, endPoint.z),
- new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
- new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
- color));
+ endCapVertices[i + 1] = endSideRing[segments - 1 - i];
}
+ endCapVertices[segments + 1] = endSideRing[segments - 1]; // close the loop
+ addShape(new SolidPolygon(endCapVertices, color));
setBackfaceCulling(true);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java
index 3014ef7..7481894 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java
@@ -15,21 +15,17 @@ import java.util.List;
* A renderable mesh composed of SolidPolygon triangles.
*
* This is a generic composite shape that holds a collection of triangles.
- * It can be constructed from any source of triangles, such as CSG operation
- * results or procedural geometry generation.
+ * It can be constructed from any source of triangles, such as procedural
+ * geometry generation or loaded mesh data.
*
* Usage:
* {@code
- * // From CSG result
- * CSG result = cubeCSG.subtract(sphereCSG);
- * SolidPolygonMesh mesh = result.toMesh(new Point3D(0, 0, 0));
- * mesh.setShadingEnabled(true);
- * mesh.setBackfaceCulling(true);
- * shapes.addShape(mesh);
- *
* // From list of triangles
* List triangles = ...;
- * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, new Point3D(0, 0, 0));
+ * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, location);
+ *
+ * // With fluent configuration
+ * shapes.addShape(mesh.setShadingEnabled(true).setBackfaceCulling(true));
* }
*
* @see SolidPolygon the triangle type for rendering
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java
index e3c038e..fbf6eb6 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java
@@ -141,32 +141,20 @@ public class SolidPolygonPyramid extends AbstractCompositeShape {
color));
}
- // Create base cap (square bottom face)
+ // Create base cap (square bottom face with center)
+ // Single N-vertex polygon that closes the loop to create 4 triangles
+ // (6 vertices â 4 triangles via fan triangulation)
// The cap faces away from the apex (in the direction the pyramid points).
- // Base corners go CW when viewed from apex, so CW when viewed from apex means
- // CCW when viewed from outside (base side). Use CCW ordering for outward normal.
- // Triangulate the square base: (center, 3, 0) and (center, 0, 1) and
- // (center, 1, 2) and (center, 2, 3)
- addShape(new SolidPolygon(
- new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
- new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
- new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
- color));
- addShape(new SolidPolygon(
- new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
- new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
- new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
- color));
- addShape(new SolidPolygon(
- new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
- new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
- new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
- color));
- addShape(new SolidPolygon(
- new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
- new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
- new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
- color));
+ // Winding: center â corner[3] â corner[0] â corner[1] â corner[2] â corner[3]
+ // (CW when viewed from apex, CCW when viewed from base side)
+ final Point3D[] baseCapVertices = new Point3D[6];
+ baseCapVertices[0] = baseCenter;
+ baseCapVertices[1] = baseCorners[3];
+ baseCapVertices[2] = baseCorners[0];
+ baseCapVertices[3] = baseCorners[1];
+ baseCapVertices[4] = baseCorners[2];
+ baseCapVertices[5] = baseCorners[3]; // close the loop
+ addShape(new SolidPolygon(baseCapVertices, color));
setBackfaceCulling(true);
}
@@ -215,12 +203,11 @@ public class SolidPolygonPyramid extends AbstractCompositeShape {
addShape(new SolidPolygon(negXposZ, negXnegZ, apex, color));
// Base cap (square bottom face)
+ // Single quad using the 4 corner vertices
// Cap faces +Y (downward, away from apex). The base is at higher Y than apex.
- // Base corners go CW when viewed from apex (looking in +Y direction).
// For outward normal (+Y direction), we need CCW ordering when viewed from +Y.
- // CCW from +Y is: 3 â 2 â 1 â 0, so triangles: (3, 2, 1) and (3, 1, 0)
- addShape(new SolidPolygon(negXposZ, posXposZ, posXnegZ, color));
- addShape(new SolidPolygon(negXposZ, posXnegZ, negXnegZ, color));
+ // Quad order: negXposZ â posXposZ â posXnegZ â negXnegZ (CCW from +Y)
+ addShape(SolidPolygon.quad(negXposZ, posXposZ, posXnegZ, negXnegZ, color));
setBackfaceCulling(true);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java
index fa67cfc..1293c62 100755
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java
@@ -10,7 +10,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPo
import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
/**
- * A solid (filled) rectangular box composed of 12 triangular polygons (2 per face,
+ * A solid (filled) rectangular box composed of 6 quadrilateral polygons (1 per face,
* covering all 6 faces).
*
* The box is defined by two diagonally opposite corner points in 3D space.
@@ -73,7 +73,7 @@ public class SolidPolygonRectangularBox extends AbstractCompositeShape {
*
* @param cornerA the first corner point (any of the 8 corners)
* @param cornerB the diagonally opposite corner point
- * @param color the fill color applied to all 12 triangular polygons
+ * @param color the fill color applied to all 6 quadrilateral polygons
*/
public SolidPolygonRectangularBox(final Point3D cornerA, final Point3D cornerB, final Color color) {
super();
@@ -99,29 +99,23 @@ public class SolidPolygonRectangularBox extends AbstractCompositeShape {
final Point3D minMaxMax = new Point3D(minX, maxY, maxZ);
final Point3D maxMaxMax = new Point3D(maxX, maxY, maxZ);
- // Bottom face (y = minY)
- addShape(new SolidPolygon(minMinMin, maxMinMin, maxMinMax, color));
- addShape(new SolidPolygon(minMinMin, maxMinMax, minMinMax, color));
+ // Bottom face (y = minY) - CCW when viewed from below
+ addShape(new SolidPolygon(new Point3D[]{minMinMin, maxMinMin, maxMinMax, minMinMax}, color));
- // Top face (y = maxY)
- addShape(new SolidPolygon(minMaxMin, minMaxMax, maxMaxMax, color));
- addShape(new SolidPolygon(minMaxMin, maxMaxMax, maxMaxMin, color));
+ // Top face (y = maxY) - CCW when viewed from above
+ addShape(new SolidPolygon(new Point3D[]{minMaxMin, minMaxMax, maxMaxMax, maxMaxMin}, color));
- // Front face (z = minZ)
- addShape(new SolidPolygon(minMinMin, minMaxMin, maxMaxMin, color));
- addShape(new SolidPolygon(minMinMin, maxMaxMin, maxMinMin, color));
+ // Front face (z = minZ) - CCW when viewed from front
+ addShape(new SolidPolygon(new Point3D[]{minMinMin, minMaxMin, maxMaxMin, maxMinMin}, color));
- // Back face (z = maxZ)
- addShape(new SolidPolygon(maxMinMax, maxMaxMax, minMaxMax, color));
- addShape(new SolidPolygon(maxMinMax, minMaxMax, minMinMax, color));
+ // Back face (z = maxZ) - CCW when viewed from behind
+ addShape(new SolidPolygon(new Point3D[]{maxMinMax, maxMaxMax, minMaxMax, minMinMax}, color));
- // Left face (x = minX)
- addShape(new SolidPolygon(minMinMin, minMinMax, minMaxMax, color));
- addShape(new SolidPolygon(minMinMin, minMaxMax, minMaxMin, color));
+ // Left face (x = minX) - CCW when viewed from left
+ addShape(new SolidPolygon(new Point3D[]{minMinMin, minMinMax, minMaxMax, minMaxMin}, color));
- // Right face (x = maxX)
- addShape(new SolidPolygon(maxMinMin, maxMaxMin, maxMaxMax, color));
- addShape(new SolidPolygon(maxMinMin, maxMaxMax, maxMinMax, color));
+ // Right face (x = maxX) - CCW when viewed from right
+ addShape(new SolidPolygon(new Point3D[]{maxMinMin, maxMaxMin, maxMaxMax, maxMinMax}, color));
setBackfaceCulling(true);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java
index 3a3c501..d812d4b 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java
@@ -4,7 +4,7 @@
*/
/**
- * Solid composite shapes built from SolidPolygon primitives.
+ * Solid composite shapes built from SolidTriangle primitives.
*
*
These shapes render as filled surfaces with optional flat shading.
* Useful for creating opaque 3D objects like boxes, spheres, and cylinders.
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java
index 59fdb2b..989f028 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java
@@ -11,7 +11,8 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
import java.awt.*;
-import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawTriangle;
import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_HEIGHT;
import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_WIDTH;
import static java.lang.String.valueOf;
@@ -63,25 +64,25 @@ public class CanvasCharacter extends AbstractCoordinateShape {
this.backgroundColor = backgroundColor;
- vertices[0].coordinate = centerLocation;
+ vertices.get(0).coordinate = centerLocation;
final double halfWidth = FONT_CHAR_WIDTH / 2d;
final double halfHeight = FONT_CHAR_HEIGHT / 2d;
// upper left
- vertices[1].coordinate = centerLocation.clone().translateX(-halfWidth)
+ vertices.get(1).coordinate = centerLocation.clone().translateX(-halfWidth)
.translateY(-halfHeight);
// upper right
- vertices[2].coordinate = centerLocation.clone().translateX(halfWidth)
+ vertices.get(2).coordinate = centerLocation.clone().translateX(halfWidth)
.translateY(-halfHeight);
// lower right
- vertices[3].coordinate = centerLocation.clone().translateX(halfWidth)
+ vertices.get(3).coordinate = centerLocation.clone().translateX(halfWidth)
.translateY(halfHeight);
// lower left
- vertices[4].coordinate = centerLocation.clone().translateX(-halfWidth)
+ vertices.get(4).coordinate = centerLocation.clone().translateX(-halfWidth)
.translateY(halfHeight);
}
@@ -150,17 +151,17 @@ public class CanvasCharacter extends AbstractCoordinateShape {
public void paint(final RenderingContext renderingContext) {
// Draw background rectangle first. It is composed of two triangles.
- drawPolygon(renderingContext,
- vertices[1].onScreenCoordinate,
- vertices[2].onScreenCoordinate,
- vertices[3].onScreenCoordinate,
+ drawTriangle(renderingContext,
+ vertices.get(1).onScreenCoordinate,
+ vertices.get(2).onScreenCoordinate,
+ vertices.get(3).onScreenCoordinate,
mouseInteractionController,
backgroundColor);
- drawPolygon(renderingContext,
- vertices[1].onScreenCoordinate,
- vertices[3].onScreenCoordinate,
- vertices[4].onScreenCoordinate,
+ drawTriangle(renderingContext,
+ vertices.get(1).onScreenCoordinate,
+ vertices.get(3).onScreenCoordinate,
+ vertices.get(4).onScreenCoordinate,
mouseInteractionController,
backgroundColor);
@@ -170,7 +171,7 @@ public class CanvasCharacter extends AbstractCoordinateShape {
if (desiredFontSize >= MAX_FONT_SIZE)
return;
- final Point2D onScreenLocation = vertices[0].onScreenCoordinate;
+ final Point2D onScreenLocation = vertices.get(0).onScreenCoordinate;
// screen borders check
if (onScreenLocation.x < 0)
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java
index c023f3f..9226e5d 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java
@@ -5,7 +5,7 @@
package eu.svjatoslav.sixth.e3d.renderer.raster.slicer;
import eu.svjatoslav.sixth.e3d.math.Vertex;
-import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +33,7 @@ import java.util.List;
* to break large composite shapes into appropriately-sized sub-polygons.
*
* @see BorderLine
- * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle
*/
public class Slicer {
@@ -51,7 +51,7 @@ public class Slicer {
/**
* Result of slicing.
*/
- private final List result = new ArrayList<>();
+ private final List result = new ArrayList<>();
/**
* Creates a new slicer with the specified maximum edge length.
@@ -66,7 +66,7 @@ public class Slicer {
private void considerSlicing(final Vertex c1,
final Vertex c2,
final Vertex c3,
- final TexturedPolygon originalPolygon) {
+ final TexturedTriangle originalPolygon) {
line1.set(c1, c2, 1);
line2.set(c2, c3, 2);
@@ -96,7 +96,7 @@ public class Slicer {
final BorderLine longestLine = c;
if (longestLine.getLength() < maxDistance) {
- final TexturedPolygon polygon = new TexturedPolygon(c1, c2, c3,
+ final TexturedTriangle polygon = new TexturedTriangle(c1, c2, c3,
originalPolygon.texture);
polygon.setMouseInteractionController(originalPolygon.mouseInteractionController);
@@ -126,9 +126,9 @@ public class Slicer {
/**
* Returns the list of subdivided polygons produced by the slicing process.
*
- * @return an unmodifiable view of the resulting {@link TexturedPolygon} list
+ * @return an unmodifiable view of the resulting {@link TexturedTriangle} list
*/
- public List getResult() {
+ public List getResult() {
return result;
}
@@ -141,12 +141,12 @@ public class Slicer {
*
* @param originalPolygon the polygon to subdivide
*/
- public void slice(final TexturedPolygon originalPolygon) {
+ public void slice(final TexturedTriangle originalPolygon) {
considerSlicing(
- originalPolygon.vertices[0],
- originalPolygon.vertices[1],
- originalPolygon.vertices[2],
+ originalPolygon.vertices.get(0),
+ originalPolygon.vertices.get(1),
+ originalPolygon.vertices.get(2),
originalPolygon);
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
index 689d0c3..e208058 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
@@ -47,7 +47,7 @@ import static java.util.Arrays.fill;
* }
*
* @see TextureBitmap
- * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle
*/
public class Texture {