feat: unify polygon type for CSG and rendering feat master
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 30 Mar 2026 16:44:01 +0000 (19:44 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Mon, 30 Mar 2026 16:44:01 +0000 (19:44 +0300)
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

44 files changed:
AGENTS.md
doc/Axis.png [deleted file]
doc/Minimal example.png [deleted file]
doc/Winding order demo.png [deleted file]
doc/index.org
doc/perspective-correct-textures/index.org
src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java
src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java
src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java

index 26fba88..5c1ee6f 100644 (file)
--- 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<Vertex>` 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 (file)
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 (file)
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 (file)
index e9f9e25..0000000
Binary files a/doc/Winding order demo.png and /dev/null differ
index 532b066..dc1de32 100644 (file)
@@ -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
-<dependencies>
-    <dependency>
-        <groupId>eu.svjatoslav</groupId>
-        <artifactId>sixth-3d</artifactId>
-        <version>1.3</version>
-    </dependency>
-</dependencies>
-
-<repositories>
-    <repository>
-        <id>svjatoslav.eu</id>
-        <name>Svjatoslav repository</name>
-        <url>https://www3.svjatoslav.eu/maven/</url>
-    </repository>
-</repositories>
-#+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)
 
index 35f2cce..3b734a0 100644 (file)
@@ -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 (file)
index 2d26a98..0000000
+++ /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.
- *
- * <p>CSG allows combining 3D shapes using boolean operations:</p>
- * <ul>
- *   <li><b>Union:</b> Combine two shapes into one</li>
- *   <li><b>Subtract:</b> Carve one shape out of another</li>
- *   <li><b>Intersect:</b> Keep only the overlapping volume</li>
- * </ul>
- *
- * <p><b>Usage example:</b></p>
- * <pre>{@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);
- * }</pre>
- *
- * @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<CSGPolygon> 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<CSGPolygon> polygonList) {
-        final CSG csg = new CSG();
-        csg.polygons.addAll(polygonList);
-        return csg;
-    }
-
-    /**
-     * Creates a CSG solid from a list of SolidPolygon triangles.
-     *
-     * <p>Each SolidPolygon is converted to a CSGPolygon (3-vertex N-gon).
-     * The color from each SolidPolygon is preserved.</p>
-     *
-     * @param solidPolygons the triangles to convert
-     * @return a new CSG solid
-     */
-    public static CSG fromSolidPolygons(final List<SolidPolygon> solidPolygons) {
-        final List<CSGPolygon> csgPolygons = new ArrayList<>(solidPolygons.size());
-
-        for (final SolidPolygon sp : solidPolygons) {
-            final List<Vertex> 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.
-     *
-     * <p>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.</p>
-     *
-     * @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<CSGPolygon> toPolygons() {
-        return polygons;
-    }
-
-    /**
-     * Performs a union operation with another CSG solid.
-     *
-     * <p>The result contains all points that are in either solid.</p>
-     *
-     * <h3>Algorithm:</h3>
-     * <pre>
-     * Union(A, B) = clip(A to outside B) + clip(B to outside A)
-     * </pre>
-     * <ol>
-     *   <li>Clip A's polygons to keep only parts outside B</li>
-     *   <li>Clip B's polygons to keep only parts outside A</li>
-     *   <li>Invert B, clip to A, invert again (keeps B's surface inside A)</li>
-     *   <li>Build final tree from all remaining polygons</li>
-     * </ol>
-     *
-     * @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.
-     *
-     * <p>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.</p>
-     *
-     * <h3>Algorithm:</h3>
-     * <pre>
-     * Subtract(A, B) = A - B = clip(inverted A to B) inverted
-     * </pre>
-     * <ol>
-     *   <li>Invert A (turning solid into cavity, cavity into solid)</li>
-     *   <li>Clip inverted A to keep only parts inside B</li>
-     *   <li>Clip B to keep only parts inside inverted A</li>
-     *   <li>Invert B twice to get B's cavity surface</li>
-     *   <li>Combine and invert final result</li>
-     * </ol>
-     *
-     * <p>The inversion trick converts "subtract B from A" into "intersect A
-     * with the inverse of B", which the BSP algorithm handles naturally.</p>
-     *
-     * @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.
-     *
-     * <p>The result contains only the points that are in both solids.</p>
-     *
-     * <h3>Algorithm:</h3>
-     * <pre>
-     * Intersect(A, B) = clip(inverted A to outside B) inverted
-     * </pre>
-     * <ol>
-     *   <li>Invert A (swap inside/outside)</li>
-     *   <li>Clip inverted-A to B, keeping parts outside B</li>
-     *   <li>Invert B, clip to A (captures B's interior surface)</li>
-     *   <li>Clip B again to ensure proper boundaries</li>
-     *   <li>Combine and invert final result</li>
-     * </ol>
-     *
-     * <p>This uses the principle: A ∩ B = ¬(¬A ∪ ¬B)</p>
-     *
-     * @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.
-     *
-     * <p>The inverse has all polygons flipped, effectively turning the solid inside-out.</p>
-     *
-     * @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.
-     *
-     * <p>All polygons are rendered with the specified color, ignoring
-     * any colors stored in the CSGPolygons.</p>
-     *
-     * @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<SolidPolygon> triangles = new ArrayList<>();
-
-        for (final CSGPolygon polygon : polygons) {
-            triangulatePolygon(polygon, color, triangles);
-        }
-
-        return new SolidPolygonMesh(triangles, location);
-    }
-
-    /**
-     * Triangulates a CSGPolygon using fan triangulation.
-     *
-     * <p>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:</p>
-     *
-     * <pre>
-     * Original N-gon:    v0-v1-v2-v3-v4...
-     * Triangles:         v0-v1-v2, v0-v2-v3, v0-v3-v4, ...
-     * </pre>
-     *
-     * <p>This method is suitable for convex polygons. For concave polygons,
-     * it may produce overlapping triangles, but CSG operations typically
-     * generate convex polygon fragments.</p>
-     *
-     * @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<SolidPolygon> 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
index 0766122..86d1490 100644 (file)
@@ -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.</p>
  *
- * <h3>BSP Tree Structure:</h3>
+ * <p><b>BSP Tree Structure:</b></p>
  * <pre>
  *                 [Node: plane P]
  *                /               \
@@ -23,73 +25,34 @@ import java.util.List;
  *        normal)             of P's normal)
  * </pre>
  *
- * <h3>Key Properties:</h3>
- * <ul>
- *   <li><b>polygons:</b> Polygons coplanar with this node's partitioning plane</li>
- *   <li><b>plane:</b> The partitioning plane that divides space</li>
- *   <li><b>front:</b> Subtree for the half-space the plane normal points toward</li>
- *   <li><b>back:</b> Subtree for the opposite half-space</li>
- * </ul>
- *
- * <h3>CSG Algorithm Overview:</h3>
- * <p>CSG boolean operations (union, subtraction, intersection) work by:</p>
- * <ol>
- *   <li>Building BSP trees from both input solids</li>
- *   <li>Clipping each tree against the other (removing overlapping geometry)</li>
- *   <li>Optionally inverting trees (for subtraction and intersection)</li>
- *   <li>Collecting the resulting polygons</li>
- * </ol>
- *
- * @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.
-     *
-     * <p>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.</p>
      */
-    public final List<CSGPolygon> polygons = new ArrayList<>();
+    public final List<SolidPolygon> polygons = new ArrayList<>();
 
     /**
      * The partitioning plane for this node.
-     *
-     * <p>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.</p>
-     *
-     * <p>Null for leaf nodes (empty subtrees).</p>
      */
     public CSGPlane plane;
 
     /**
      * The front child subtree.
-     *
-     * <p>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.</p>
      */
     public CSGNode front;
 
     /**
      * The back child subtree.
-     *
-     * <p>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.</p>
      */
     public CSGNode back;
 
     /**
      * Creates an empty BSP node with no plane or children.
-     *
-     * <p>This constructor creates a leaf node. The plane, front, and back
-     * fields will be populated when polygons are added via {@link #build(List)}.</p>
      */
     public CSGNode() {
     }
@@ -97,35 +60,26 @@ public class CSGNode {
     /**
      * Creates a BSP tree from a list of polygons.
      *
-     * <p>Delegates to {@link #build(List)} to construct the tree.</p>
-     *
      * @param polygons the polygons to partition into a BSP tree
      */
-    public CSGNode(final List<CSGPolygon> polygons) {
+    public CSGNode(final List<SolidPolygon> polygons) {
         build(polygons);
     }
 
     /**
      * Creates a deep clone of this BSP tree.
      *
-     * <p>Recursively clones all child nodes and polygons. The resulting tree
-     * is completely independent of the original.</p>
-     *
      * @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.
-     *
-     * <p>This operation is fundamental to CSG subtraction and intersection:</p>
-     * <ul>
-     *   <li>All polygon normals are flipped (reversing their facing direction)</li>
-     *   <li>All plane normals are flipped</li>
-     *   <li>Front and back subtrees are swapped</li>
-     * </ul>
-     *
-     * <p>After inversion:</p>
-     * <ul>
-     *   <li>What was solid becomes empty space</li>
-     *   <li>What was empty space becomes solid</li>
-     *   <li>Front/back relationships are reversed throughout the tree</li>
-     * </ul>
-     *
-     * <p>This is used in CSG subtraction where solid B "carves out" of solid A
-     * by inverting B, unioning, then inverting the result.</p>
      */
     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.
      *
-     * <p>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.</p>
-     *
-     * <h3>Algorithm:</h3>
-     * <ol>
-     *   <li>At each node, split input polygons by the node's plane</li>
-     *   <li>Polygons in front go to front child for further clipping</li>
-     *   <li>Polygons in back go to back child for further clipping</li>
-     *   <li>Coplanar polygons are kept (they're on the surface)</li>
-     *   <li>If no back child exists, back polygons are discarded (they're inside)</li>
-     * </ol>
-     *
-     * <p>This is used during CSG operations to remove overlapping geometry.</p>
-     *
      * @param polygons the polygons to clip against this BSP tree
      * @return a new list containing only the portions outside this solid
      */
-    public List<CSGPolygon> clipPolygons(final List<CSGPolygon> polygons) {
-        // Base case: if this is a leaf node, return copies of all polygons
+    public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
         if (plane == null) {
             return new ArrayList<>(polygons);
         }
 
-        // Split all input polygons by this node's plane
-        final List<CSGPolygon> frontList = new ArrayList<>();
-        final List<CSGPolygon> backList = new ArrayList<>();
+        final List<SolidPolygon> frontList = new ArrayList<>();
+        final List<SolidPolygon> 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<CSGPolygon> resultFront = frontList;
+        List<SolidPolygon> resultFront = frontList;
         if (front != null) {
             resultFront = front.clipPolygons(frontList);
         }
 
-        // Recursively clip back polygons against back subtree
-        List<CSGPolygon> resultBack = backList;
+        List<SolidPolygon> 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<CSGPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
+        final List<SolidPolygon> 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.
      *
-     * <p>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.</p>
-     *
-     * <p>The operation modifies this tree in place, replacing all polygons
-     * with their clipped versions.</p>
-     *
-     * @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<CSGPolygon> newPolygons = bsp.clipPolygons(polygons);
+        final List<SolidPolygon> 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.
      *
-     * <p>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.</p>
-     *
      * @return a new list containing all polygons in this tree
      */
-    public List<CSGPolygon> allPolygons() {
-        final List<CSGPolygon> result = new ArrayList<>(polygons);
+    public List<SolidPolygon> allPolygons() {
+        final List<SolidPolygon> 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.
      *
-     * <p>This is the core BSP tree construction algorithm. It partitions
-     * space by selecting a splitting plane and recursively building subtrees.</p>
-     *
-     * <h3>Algorithm:</h3>
-     * <ol>
-     *   <li>If this node has no plane, use the first polygon's plane as the partitioning plane</li>
-     *   <li>For each polygon:
-     *     <ul>
-     *       <li>Coplanar polygons are stored in this node</li>
-     *       <li>Front polygons go to the front list</li>
-     *       <li>Back polygons go to the back list</li>
-     *       <li>Spanning polygons are split into front and back parts</li>
-     *     </ul>
-     *   </li>
-     *   <li>Recursively build front subtree with front polygons</li>
-     *   <li>Recursively build back subtree with back polygons</li>
-     * </ol>
-     *
-     * <h3>Calling Conventions:</h3>
-     * <ul>
-     *   <li>Can be called multiple times to add more polygons to an existing tree</li>
-     *   <li>Empty polygon list is a no-op</li>
-     *   <li>Creates child nodes as needed</li>
-     * </ul>
-     *
      * @param polygonList the polygons to add to this BSP tree
      */
-    public void build(final List<CSGPolygon> polygonList) {
-        // Base case: no polygons to add
+    public void build(final List<SolidPolygon> 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<CSGPolygon> frontList = new ArrayList<>();
-        final List<CSGPolygon> backList = new ArrayList<>();
+        final List<SolidPolygon> frontList = new ArrayList<>();
+        final List<SolidPolygon> 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();
index 473608b..c14f671 100644 (file)
@@ -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.
  *
- * <p>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}</p>
- *
  * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
- * in CSG. They divide 3D space into two half-spaces:</p>
- * <ul>
- *   <li><b>Front half-space:</b> Points where {@code normal · point > w}</li>
- *   <li><b>Back half-space:</b> Points where {@code normal · point < w}</li>
- * </ul>
- *
- * <p>Planes are used to:</p>
- * <ul>
- *   <li>Define the surface orientation of {@link CSGPolygon} faces</li>
- *   <li>Split polygons that cross BSP partition boundaries</li>
- *   <li>Determine which side of a BSP node a polygon lies on</li>
- * </ul>
+ * in CSG. They divide 3D space into two half-spaces.</p>
  *
- * @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.
-     *
-     * <p>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.</p>
      */
     public static final double EPSILON = 0.01;
 
     /**
      * The unit normal vector perpendicular to the plane surface.
-     *
-     * <p>The direction of the normal determines which side is "front"
-     * and which is "back". The front is the side the normal points toward.</p>
      */
     public Point3D normal;
 
     /**
      * The signed distance from the origin to the plane along the normal.
-     *
-     * <p>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}</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.
      *
-     * <p>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:</p>
-     * <ul>
-     *   <li>Counter-clockwise (CCW) winding → normal points toward viewer</li>
-     *   <li>Clockwise (CW) winding → normal points away from viewer</li>
-     * </ul>
-     *
      * @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.
      *
-     * <p>The normal vector is cloned to avoid shared references.</p>
-     *
      * @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.
-     *
-     * <p>This effectively swaps the front and back half-spaces. After flipping:</p>
-     * <ul>
-     *   <li>Points that were in front are now in back</li>
-     *   <li>Points that were in back are now in front</li>
-     *   <li>Coplanar points remain coplanar</li>
-     * </ul>
-     *
-     * <p>Used during CSG operations when inverting solids (converting "inside"
-     * to "outside" and vice versa).</p>
      */
     public void flip() {
-        normal = normal.negated();
-        w = -w;
+        normal = normal.withNegated();
+        distance = -distance;
     }
 
     /**
      * Splits a polygon by this plane, classifying and potentially dividing it.
      *
-     * <p>This is the core operation for BSP tree construction. The polygon is
-     * classified based on where its vertices lie relative to the plane:</p>
-     *
-     * <h3>Classification types:</h3>
-     * <ul>
-     *   <li><b>COPLANAR (0):</b> All vertices lie on the plane (within EPSILON)</li>
-     *   <li><b>FRONT (1):</b> All vertices are in the front half-space</li>
-     *   <li><b>BACK (2):</b> All vertices are in the back half-space</li>
-     *   <li><b>SPANNING (3):</b> Vertices are on both sides (polygon crosses the plane)</li>
-     * </ul>
-     *
-     * <h3>Destination lists:</h3>
-     * <ul>
-     *   <li><b>coplanarFront:</b> Coplanar polygons with same-facing normals</li>
-     *   <li><b>coplanarBack:</b> Coplanar polygons with opposite-facing normals</li>
-     *   <li><b>front:</b> Polygons entirely in front half-space</li>
-     *   <li><b>back:</b> Polygons entirely in back half-space</li>
-     * </ul>
-     *
-     * <h3>Spanning polygon handling:</h3>
-     * <p>When a polygon spans the plane, it is split into two polygons:</p>
-     * <ol>
-     *   <li>Vertices on the front side become a new polygon (added to front list)</li>
-     *   <li>Vertices on the back side become a new polygon (added to back list)</li>
-     *   <li>Intersection points are computed and added to both polygons</li>
-     * </ol>
-     *
      * @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<CSGPolygon> coplanarFront,
-                             final List<CSGPolygon> coplanarBack,
-                             final List<CSGPolygon> front,
-                             final List<CSGPolygon> back) {
+    public void splitPolygon(final SolidPolygon polygon,
+                             final List<SolidPolygon> coplanarFront,
+                             final List<SolidPolygon> coplanarBack,
+                             final List<SolidPolygon> front,
+                             final List<SolidPolygon> back) {
 
         PolygonType polygonType = PolygonType.COPLANAR;
-        final 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<Vertex> f = new ArrayList<>();
-                final List<Vertex> b = new ArrayList<>();
+                final List<Vertex> frontVertices = new ArrayList<>();
+                final List<Vertex> 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 (file)
index 9ba8ceb..0000000
+++ /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.
- *
- * <p>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).</p>
- *
- * <p>The color is preserved through CSG operations - split polygons inherit
- * the color from their parent.</p>
- *
- * @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<Vertex> 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<Vertex> 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.
-     *
-     * <p>Clones all vertices and preserves the color.</p>
-     *
-     * @return a new CSGPolygon with cloned data
-     */
-    public CSGPolygon clone() {
-        final List<Vertex> 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.
-     *
-     * <p>Reverses the vertex order and negates vertex normals.
-     * Also flips the plane. Used during CSG operations when inverting solids.</p>
-     */
-    public void flip() {
-        Collections.reverse(vertices);
-        for (final Vertex v : vertices) {
-            v.flip();
-        }
-        plane.flip();
-    }
-}
\ No newline at end of file
index abb48b0..6558a07 100644 (file)
@@ -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();
     }
 
 }
index 6e6ac97..78a9b79 100755 (executable)
@@ -16,9 +16,24 @@ import static java.lang.Math.sqrt;
  * <p>All mutation methods return {@code this} for fluent chaining:</p>
  * <pre>{@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)
+ * }</pre>
+ *
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ *   <li><b>Imperative verbs</b> ({@code add}, {@code subtract}, {@code negate}, {@code multiply}, 
+ *       {@code divide}) mutate this point and return {@code this}</li>
+ *   <li><b>{@code with}-prefixed methods</b> ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ *       {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one</li>
+ * </ul>
+ *
+ * <p><b>Warning:</b> This class is mutable with public fields. Clone before storing
+ * references that should not be shared:</p>
+ * <pre>{@code
+ * Point2D safeCopy = original.clone();
  * }</pre>
  *
  * @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);
+    }
 }
index c4e6b52..1f51c1c 100755 (executable)
@@ -18,7 +18,7 @@ import static java.lang.Math.*;
  * <p>All mutation methods return {@code this} for fluent chaining:</p>
  * <pre>{@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.*;
  * <p><b>Common operations:</b></p>
  * <pre>{@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
  * }</pre>
  *
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ *   <li><b>Imperative verbs</b> ({@code add}, {@code subtract}, {@code negate}, {@code multiply}, 
+ *       {@code divide}) mutate this point and return {@code this}</li>
+ *   <li><b>{@code with}-prefixed methods</b> ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ *       {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one</li>
+ * </ul>
+ *
  * <p><b>Warning:</b> This class is mutable with public fields. Clone before storing
  * references that should not be shared:</p>
  * <pre>{@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);
     }
 
index 50f9dc6..71176b7 100644 (file)
@@ -82,4 +82,26 @@ public class Polygon {
 
     }
 
+    /**
+     * Tests whether a point lies inside a triangle using integer coordinates.
+     *
+     * <p>This overload creates temporary Point2D objects for the vertices,
+     * suitable when the caller has pre-computed integer coordinates.</p>
+     *
+     * @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));
+    }
+
 }
index 23c2079..95c3f92 100644 (file)
@@ -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();
     }
 
     /**
index 8737a28..d453700 100644 (file)
@@ -119,7 +119,7 @@ public class Camera implements FrameListener {
         if (currentSpeed <= SPEED_LIMIT)
             return;
 
-        movementVector.scaleDown(currentSpeed / SPEED_LIMIT);
+        movementVector.divide(currentSpeed / SPEED_LIMIT);
     }
 
     /**
index 46c628e..d3cf13e 100755 (executable)
@@ -11,6 +11,14 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D;
  *
  * <p>Transformations are applied in order: rotation first, then translation.</p>
  *
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ *   <li><b>Imperative verbs</b> ({@code set}, {@code setTranslation}, {@code transform})
+ *       mutate this transform or the input point</li>
+ *   <li><b>{@code with}-prefixed methods</b> ({@code withTransformed})
+ *       return a new instance without modifying the original</li>
+ * </ul>
+ *
  * @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;
     }
 
     /**
index ba63d6c..f1eed76 100644 (file)
@@ -153,7 +153,7 @@ public class Vertex {
      */
     public void flip() {
         if (normal != null) {
-            normal = normal.negated();
+            normal = normal.withNegated();
         }
     }
 
index 900c508..63fca47 100644 (file)
@@ -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);
     }
 
 }
index c594571..390027d 100644 (file)
@@ -15,24 +15,18 @@ package eu.svjatoslav.sixth.e3d.renderer.raster;
  * <pre>{@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();
  * }</pre>
  *
  * <p><b>Important:</b> 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.
+     *
+     * <p>Supported formats:</p>
+     * <ul>
+     *   <li>{@code RGB} - 3 hex digits, fully opaque</li>
+     *   <li>{@code RGBA} - 4 hex digits</li>
+     *   <li>{@code RRGGBB} - 6 hex digits, fully opaque</li>
+     *   <li>{@code RRGGBBAA} - 8 hex digits</li>
+     * </ul>
+     *
+     * @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.
      */
index 3112fde..c9b6fee 100644 (file)
@@ -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.
  *
  * <p>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.</p>
  *
  * <p>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
  *     }
  * }
  * }</pre>
@@ -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}.
+     *
+     * <p>Stored as a mutable list to support CSG operations that modify
+     * polygon vertices in place (splitting, flipping).</p>
      */
-    public final Vertex[] vertices;
+    public final List<Vertex> 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<Vertex> 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);
         }
     }
index 974e285..1e661a4 100644 (file)
@@ -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;
     }
 
 }
index 4b53da1..895ca2c 100644 (file)
@@ -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<LineInterpolator[]> 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);
             }
index fa14d14..629d16e 100644 (file)
@@ -129,4 +129,24 @@ public class LineInterpolator implements Comparable<LineInterpolator> {
         absoluteHeight = Math.abs(height);
     }
 
+    /**
+     * Sets the two endpoints of this edge using integer coordinates.
+     *
+     * <p>This method creates new Point2D objects to avoid storing references to shared
+     * vertex data, which is essential for thread safety during parallel rendering.</p>
+     *
+     * @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);
+    }
+
 }
index 9d03c8f..fd54e21 100644 (file)
@@ -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.
- * <p>
- * 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
- * <p>
- * The static drawPolygon method is designed for reuse by other polygon types.
+ * A solid-color convex polygon renderer supporting N vertices (N >= 3).
+ *
+ * <p>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.</p>
+ *
+ * <p><b>Rendering:</b></p>
+ * <ul>
+ *   <li>Fan triangulation for N-vertex polygons (N-2 triangles)</li>
+ *   <li>Scanline rasterization with alpha blending</li>
+ *   <li>Backface culling and flat shading support</li>
+ *   <li>Mouse interaction via point-in-polygon testing</li>
+ * </ul>
+ *
+ * <p><b>CSG Support:</b></p>
+ * <ul>
+ *   <li>Lazy-computed plane for BSP operations</li>
+ *   <li>{@link #flip()} for inverting polygon orientation</li>
+ *   <li>{@link #deepClone()} for creating independent copies</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@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);
+ * }</pre>
+ *
+ * @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.
+     *
+     * <p>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.</p>
+     */
     private static final ThreadLocal<LineInterpolator[]> 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.
+     *
+     * <p>Lazy-computed on first call to {@link #getPlane()}.</p>
+     */
+    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<Point3D> 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<Point3D> 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<Point2D[]> 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<Point3D> 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.
+     *
+     * <p>This constructor is used for CSG operations where vertices already exist.</p>
+     *
+     * @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&lt;Point3D&gt; constructor
+     * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices
+     */
+    public SolidPolygon(final List<Vertex> 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<Vertex> createVerticesFromPoints(final Point3D[] points) {
+        if (points == null || points.length < 3) {
+            return new ArrayList<>();
+        }
+        final List<Vertex> 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<Vertex> createVerticesFromPoints(final List<Point3D> points) {
+        if (points == null || points.size() < 3) {
+            return new ArrayList<>();
+        }
+        final List<Vertex> 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.
+     *
+     * <p>Computed from the first three vertices and cached for reuse.
+     * Used by CSG operations for BSP tree construction.</p>
+     *
+     * @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.
+     *
+     * <p>Reverses the vertex order and negates vertex normals.
+     * Also flips the cached plane if computed. Used during CSG operations
+     * when inverting solids.</p>
+     */
+    public void flip() {
+        Collections.reverse(vertices);
+        for (final Vertex v : vertices) {
+            v.flip();
+        }
+        if (planeComputed) {
+            plane.flip();
+        }
+    }
+
+    /**
+     * Creates a deep clone of this polygon.
+     *
+     * <p>Clones all vertices and preserves the color. Used by CSG operations
+     * to create independent copies before modification.</p>
+     *
+     * @return a new SolidPolygon with cloned data
+     */
+    public SolidPolygon deepClone() {
+        final List<Vertex> 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.
      *
      * <p>This static method handles:</p>
      * <ul>
      *   <li>Rounding vertices to integer screen coordinates</li>
-     *   <li>Mouse hover detection via point-in-polygon test</li>
+     *   <li>Mouse hover detection via point-in-triangle test</li>
      *   <li>Viewport clipping</li>
      *   <li>Scanline rasterization with alpha blending</li>
      * </ul>
@@ -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.
-     *
-     * <p>When enabled, polygons facing away from the camera (determined by
-     * screen-space winding order) are not rendered.</p>
-     *
-     * @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.
      *
-     * <p>This method performs:</p>
-     * <ul>
-     *   <li>Backface culling check (if enabled)</li>
-     *   <li>Flat shading calculation (if lighting is enabled)</li>
-     *   <li>Triangle rasterization using the static drawPolygon method</li>
-     * </ul>
+     * @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
index 79b79d5..71683a5 100644 (file)
@@ -4,15 +4,15 @@
  */
 
 /**
- * Solid-color triangle rendering with scanline rasterization.
+ * Solid-color polygon rendering with scanline rasterization.
  *
- * <p>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.</p>
+ * <p>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.</p>
  *
  * <p>Key classes:</p>
  * <ul>
- *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - The solid triangle shape</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - Unified polygon for rendering and CSG</li>
  *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines</li>
  * </ul>
  *
index ec4caeb..3081b27 100644 (file)
@@ -139,6 +139,10 @@ public class PolygonBorderInterpolator implements
     /**
      * Sets the screen and texture coordinates for this edge.
      *
+     * <p>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.</p>
+     *
      * @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 (file)
index b5ba635..0000000
+++ /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.
- *
- * <p>This class renders triangles with UV-mapped textures. For large triangles,
- * the rendering may be sliced into smaller pieces for better perspective correction.</p>
- *
- * <p><b>Perspective-correct texture rendering:</b></p>
- * <ul>
- *   <li>Small polygons are rendered without perspective correction</li>
- *   <li>Larger polygons are sliced into smaller pieces for accurate perspective</li>
- * </ul>
- *
- * @see Texture
- * @see Vertex#textureCoordinate
- */
-public class TexturedPolygon extends AbstractCoordinateShape {
-
-    private static final ThreadLocal<PolygonBorderInterpolator[]> 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.
-     *
-     * <p>This method performs:</p>
-     * <ul>
-     *   <li>Backface culling check (if enabled)</li>
-     *   <li>Mouse interaction detection</li>
-     *   <li>Mipmap level selection based on screen coverage</li>
-     *   <li>Scanline rasterization with texture sampling</li>
-     * </ul>
-     *
-     * @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 (file)
index 0000000..6a015b5
--- /dev/null
@@ -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.
+ *
+ * <p>This class renders triangles with UV-mapped textures. For large triangles,
+ * the rendering may be sliced into smaller pieces for better perspective correction.</p>
+ *
+ * <p><b>Perspective-correct texture rendering:</b></p>
+ * <ul>
+ *   <li>Small triangles are rendered without perspective correction</li>
+ *   <li>Larger triangles are sliced into smaller pieces for accurate perspective</li>
+ * </ul>
+ *
+ * @see Texture
+ * @see Vertex#textureCoordinate
+ */
+public class TexturedTriangle extends AbstractCoordinateShape {
+
+    private static final ThreadLocal<PolygonBorderInterpolator[]> 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.
+     *
+     * <p>This method performs:</p>
+     * <ul>
+     *   <li>Backface culling check (if enabled)</li>
+     *   <li>Mouse interaction detection</li>
+     *   <li>Mipmap level selection based on screen coverage</li>
+     *   <li>Scanline rasterization with texture sampling</li>
+     * </ul>
+     *
+     * @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);
+        });
+    }
+
+}
index f893beb..44489af 100644 (file)
@@ -6,16 +6,16 @@
 /**
  * Textured triangle rendering with perspective-correct UV mapping.
  *
- * <p>Textured polygons apply 2D textures to 3D triangles using UV coordinates.
- * Large polygons may be sliced into smaller pieces for accurate perspective correction.</p>
+ * <p>Textured triangles apply 2D textures to 3D triangles using UV coordinates.
+ * Large triangles may be sliced into smaller pieces for accurate perspective correction.</p>
  *
  * <p>Key classes:</p>
  * <ul>
- *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon} - The textured triangle shape</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape</li>
  *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs</li>
  * </ul>
  *
- * @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
  */
 
index c7f71d7..7cf643b 100644 (file)
@@ -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) {
 
index aa52272..f1efcaa 100644 (file)
@@ -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.
  *
  * <p>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.</p>
  *
@@ -39,7 +39,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
  * shapeCollection.addShape(rect);
  * }</pre>
  *
- * @see TexturedPolygon
+ * @see TexturedTriangle
  * @see Texture
  * @see AbstractCompositeShape
  */
@@ -129,7 +129,7 @@ public class TexturedRectangle extends AbstractCompositeShape {
      *
      * <p>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.</p>
      *
      * @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);
index d9bfe23..6752ce5 100644 (file)
@@ -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.
      *
      * <p>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}.</p>
+     * SolidPolygon instances. Used for CSG operations where polygons
+     * are needed directly without conversion.</p>
      *
-     * <p><b>Example:</b></p>
-     * <pre>{@code
-     * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
-     * List<SolidPolygon> triangles = cube.extractSolidPolygons();
-     * CSG csg = CSG.fromSolidPolygons(triangles);
-     * }</pre>
-     *
-     * @return list of all SolidPolygon sub-shapes
+     * @return list of SolidPolygon instances from this shape hierarchy
      */
     public List<SolidPolygon> extractSolidPolygons() {
         final List<SolidPolygon> 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.
+     *
+     * <p>This shape's SolidPolygon children are replaced with the union result.
+     * Non-SolidPolygon children from both shapes are preserved and combined.</p>
+     *
+     * <p><b>CSG Operation:</b> Union combines two shapes into one, keeping all
+     * geometry from both. Uses BSP tree algorithms for robust boolean operations.</p>
+     *
+     * <p><b>Child handling:</b></p>
+     * <ul>
+     *   <li>SolidPolygon children from both shapes → replaced with union result</li>
+     *   <li>Non-SolidPolygon children from this shape → preserved</li>
+     *   <li>Non-SolidPolygon children from other shape → added to this shape</li>
+     *   <li>Nested AbstractCompositeShape children → preserved unchanged (not recursively processed)</li>
+     * </ul>
+     *
+     * @param other the shape to union with
+     * @see #subtract(AbstractCompositeShape)
+     * @see #intersect(AbstractCompositeShape)
+     */
+    public void union(final AbstractCompositeShape other) {
+        final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+        final List<SolidPolygon> 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.
+     *
+     * <p>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.</p>
+     *
+     * <p><b>CSG Operation:</b> Subtract removes the volume of the second shape
+     * from the first shape. Useful for creating holes, cavities, and cutouts.</p>
+     *
+     * <p><b>Child handling:</b></p>
+     * <ul>
+     *   <li>SolidPolygon children from this shape → replaced with difference result</li>
+     *   <li>Non-SolidPolygon children from this shape → preserved</li>
+     *   <li>All children from other shape → discarded (other is just a cutter)</li>
+     *   <li>Nested AbstractCompositeShape children → preserved unchanged</li>
+     * </ul>
+     *
+     * @param other the shape to subtract (the cutter)
+     * @see #union(AbstractCompositeShape)
+     * @see #intersect(AbstractCompositeShape)
+     */
+    public void subtract(final AbstractCompositeShape other) {
+        final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+        final List<SolidPolygon> 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.
+     *
+     * <p>This shape's SolidPolygon children are replaced with the intersection result.
+     * Only the overlapping volume between the two shapes remains.</p>
+     *
+     * <p><b>CSG Operation:</b> Intersect keeps only the volume where both shapes
+     * overlap. Useful for creating shapes constrained by multiple boundaries.</p>
+     *
+     * <p><b>Child handling:</b></p>
+     * <ul>
+     *   <li>SolidPolygon children from this shape → replaced with intersection result</li>
+     *   <li>Non-SolidPolygon children from this shape → preserved</li>
+     *   <li>All children from other shape → discarded</li>
+     *   <li>Nested AbstractCompositeShape children → preserved unchanged</li>
+     * </ul>
+     *
+     * @param other the shape to intersect with
+     * @see #union(AbstractCompositeShape)
+     * @see #subtract(AbstractCompositeShape)
+     */
+    public void intersect(final AbstractCompositeShape other) {
+        final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+        final List<SolidPolygon> 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.
+     *
+     * <p>CSG operations modify polygons in-place via BSP tree operations.
+     * Cloning ensures the original polygon data is preserved.</p>
+     *
+     * @param polygons the polygons to clone
+     * @return a new list containing deep clones of all polygons
+     */
+    private List<SolidPolygon> clonePolygons(final List<SolidPolygon> polygons) {
+        final List<SolidPolygon> 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.
+     *
+     * <p>Preserves all non-SolidPolygon children (Lines, nested composites, etc.).
+     * Optionally carries over non-SolidPolygon children from another shape.</p>
+     *
+     * @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<SolidPolygon> newPolygons,
+                                       final AbstractCompositeShape other,
+                                       final boolean carryOtherNonPolygons) {
+        // Remove all direct SolidPolygon children from this shape
+        final Iterator<SubShape> 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());
index b139a2b..b3bfc81 100644 (file)
@@ -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.
      *
index d8a9236..29c2c4e 100644 (file)
@@ -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
index 05ceafc..3a4327f 100644 (file)
@@ -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);
     }
index 276e6d4..b4673f6 100644 (file)
@@ -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);
     }
index 3014ef7..7481894 100644 (file)
@@ -15,21 +15,17 @@ import java.util.List;
  * A renderable mesh composed of SolidPolygon triangles.
  *
  * <p>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.</p>
+ * It can be constructed from any source of triangles, such as procedural
+ * geometry generation or loaded mesh data.</p>
  *
  * <p><b>Usage:</b></p>
  * <pre>{@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<SolidPolygon> 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));
  * }</pre>
  *
  * @see SolidPolygon the triangle type for rendering
index e3c038e..fbf6eb6 100644 (file)
@@ -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);
     }
index fa67cfc..1293c62 100755 (executable)
@@ -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).
  *
  * <p>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);
     }
index 3a3c501..d812d4b 100644 (file)
@@ -4,7 +4,7 @@
  */
 
 /**
- * Solid composite shapes built from SolidPolygon primitives.
+ * Solid composite shapes built from SolidTriangle primitives.
  *
  * <p>These shapes render as filled surfaces with optional flat shading.
  * Useful for creating opaque 3D objects like boxes, spheres, and cylinders.</p>
index 59fdb2b..989f028 100644 (file)
@@ -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)
index c023f3f..9226e5d 100644 (file)
@@ -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.</p>
  *
  * @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<TexturedPolygon> result = new ArrayList<>();
+    private final List<TexturedTriangle> 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<TexturedPolygon> getResult() {
+    public List<TexturedTriangle> 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);
     }
 
index 689d0c3..e208058 100644 (file)
@@ -47,7 +47,7 @@ import static java.util.Arrays.fill;
  * }</pre>
  *
  * @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 {