Initial commit
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 12 Mar 2026 18:07:52 +0000 (20:07 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Thu, 12 Mar 2026 18:07:52 +0000 (20:07 +0200)
100 files changed:
.gitignore [new file with mode: 0644]
AGENTS.md [new file with mode: 0644]
COPYING [new file with mode: 0644]
Tools/Open with IntelliJ IDEA [new file with mode: 0755]
Tools/Update web site [new file with mode: 0755]
doc/example.png [new file with mode: 0644]
doc/index.org [new file with mode: 0644]
pom.xml [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java [new file with mode: 0755]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java [new file with mode: 0644]
src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png [new file with mode: 0644]
src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..31378ad
--- /dev/null
@@ -0,0 +1,9 @@
+/.idea/
+/target/
+/.classpath
+/.project
+/.settings/
+/doc/graphs/
+/doc/apidocs/
+/*.iml
+*.html
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644 (file)
index 0000000..3ad2276
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,148 @@
+# Project Overview
+
+sixth-3d-engine is a Java-based 3D rendering engine. It provides:
+
+- 3D geometry primitives (points, boxes, circles, polygons)
+- A rasterization-based renderer with texture support
+- An octree-based volume renderer with ray tracing
+- A GUI framework built on Java Swing (JPanel) with camera navigation
+- Composite and primitive shape rendering (lines, solid polygons, textured polygons, wireframes)
+- A text editor component rendered in 3D space
+- Human input device (HID) tracking for mouse and keyboard
+
+# Repository Structure
+
+    src/main/java/eu/svjatoslav/sixth/e3d/
+    ├── geometry/          — Core geometry: Point2D, Point3D, Box, Circle, Polygon
+    ├── math/              — Math utilities: Rotation, Transform, TransformStack, Vertex
+    ├── gui/               — GUI framework: ViewPanel (Swing), Camera, keyboard/mouse input
+    │   ├── humaninput/    — Mouse/keyboard event handling
+    │   └── textEditorComponent/ — 3D text editor widget
+    └── renderer/
+        ├── octree/        — Octree volume representation and ray tracer
+        └── raster/        — Rasterization pipeline
+            ├── shapes/
+            │   ├── basic/         — Primitive shapes: Line, SolidPolygon, TexturedPolygon
+            │   └── composite/     — Composite shapes: AbstractCompositeShape, TextCanvas,
+            │                        WireframeBox, SolidPolygonRectangularBox
+            ├── slicer/    — Geometry slicing for level-of-detail
+            └── texture/   — Texture and TextureBitmap with mipmap support
+
+# Build & Test Commands
+
+## Build System
+
+- **Build tool:** Maven
+- **Java version:** 21
+- **Build command:** `mvn clean install`
+
+## Testing
+
+- **Test framework:** JUnit 4
+- **Run all tests:** `mvn test`
+- **Run single test class:** `mvn test -Dtest=TextLineTest`
+- **Run specific test method:** `mvn test -Dtest=TextLineTest#testAddIdent`
+
+Test files are located in `src/test/java/` following the same package structure as main code.
+
+## No Linting
+
+- No Checkstyle, PMD, or SpotBugs configured
+- No `.editorconfig` or formatting configuration files present
+- Code formatting follows manual conventions (see below)
+
+# Code Style Guidelines
+
+## License Header
+
+All Java files must start with this exact header:
+
+```java
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+```
+
+## Formatting
+
+- **Indentation:** 4 spaces (no tabs)
+- **Braces:** K&R style (opening brace on same line)
+- **Line length:** No strict limit, but keep reasonable (~120 chars preferred)
+- **Blank lines:** Separate logical blocks, methods, and fields
+- **Spacing:** Space after keywords (`if`, `for`, `while`), around operators
+
+## Types & Variables
+
+- **Use `final`** for parameters and local variables where possible
+- **Explicit typing** preferred over `var` (Java 10+ feature not used)
+- **Public fields** acceptable for performance-critical geometry classes
+- **Primitive types** used over wrappers for performance
+
+## Documentation
+
+- **Javadoc required** on all public classes and methods
+- **Include usage examples** in class-level Javadoc when helpful
+- **Document parameters** with `@param` tags
+- **Document return values** with `@return` tags
+- **Reference related classes** with `{@link ClassName}`
+
+## Architecture Patterns
+
+- **No dependency injection** — manual wiring only
+- **Mutable value types** for geometry (Point2D, Point3D, Vertex)
+- **Fluent API** — mutation methods return `this`
+- **Composite pattern** for complex shapes (AbstractCompositeShape)
+- **Strategy pattern** for rendering (RenderAggregator)
+
+# Architecture & Key Concepts
+
+## Coordinate System
+
+- `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
+
+## Transform Pipeline
+
+- `TransformStack` holds an array of `Transform` objects (translation + orientation)
+- `Rotation` stores XZ and YZ rotation angles with precomputed sin/cos
+- Shapes implement `transform(TransformStack, RenderAggregator)` to project themselves
+
+## Shape Hierarchy
+
+- `AbstractShape` — base class with optional `MouseInteractionController`
+- `AbstractCoordinateShape` — has `Vertex[]` coordinates and `onScreenZ` for depth sorting
+- `AbstractCompositeShape` — groups sub-shapes with group IDs and visibility toggles
+- Concrete shapes: `Line`, `SolidPolygon`, `TexturedPolygon`, `TextCanvas`, `WireframeBox`
+
+## Rendering
+
+- `ShapeCollection` is the root container with `RenderAggregator` and `TransformStack`
+- `RenderAggregator` collects projected shapes, sorts by Z-index, paints back-to-front
+- `ViewPanel` (extends `JPanel`) drives render loop, notifies `FrameListener` per frame
+- Backface culling uses screen-space normal Z-component
+
+## Color
+
+- Use project's `eu.svjatoslav.sixth.e3d.renderer.raster.Color` (NOT `java.awt.Color`)
+- RGBA with int components (0–255), predefined constants (RED, GREEN, BLUE, etc.)
+- Provides `toAwtColor()` for AWT interop
+
+## GUI / Input
+
+- `Camera` represents viewer position and orientation
+- `InputManager` processes mouse/keyboard events
+- `MouseInteractionController` interface allows shapes to respond to input
+- `KeyboardFocusStack` manages keyboard input focus
+
+# Tips for AI Agents
+
+1. **Creating shapes:** Extend `AbstractCoordinateShape` for simple geometry or `AbstractCompositeShape` for compounds
+2. **Always use project Color:** `eu.svjatoslav.sixth.e3d.renderer.raster.Color`, never `java.awt.Color`
+3. **Mutable geometry:** `Point3D`/`Point2D` are mutable — clone when storing references that shouldn't be shared
+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 screen-space Z-component of normal; positive = front-facing
+7. **Polygon winding:** Counter-clockwise winding for front-facing polygons (when viewed from outside)
+8. **Testing:** Write JUnit 4 tests in `src/test/java/` with matching package structure
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..0e259d4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/Tools/Open with IntelliJ IDEA b/Tools/Open with IntelliJ IDEA
new file mode 100755 (executable)
index 0000000..304bf94
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/bash
+
+# This script launches IntelliJ IDEA with the current project
+# directory. The script is designed to be run by double-clicking it in
+# the GNOME Nautilus file manager.
+
+# First, we change the current working directory to the directory of
+# the script.
+
+# "${0%/*}" gives us the path of the script itself, without the
+# script's filename.
+
+# This command basically tells the system "change the current
+# directory to the directory containing this script".
+
+cd "${0%/*}"
+
+# Then, we move up one directory level.
+# The ".." tells the system to go to the parent directory of the current directory.
+# This is done because we assume that the project directory is one level up from the script.
+cd ..
+
+# Now, we use the 'setsid' command to start a new session and run
+# IntelliJ IDEA in the background. 'setsid' is a UNIX command that
+# runs a program in a new session.
+
+# The command 'idea .' opens IntelliJ IDEA with the current directory
+# as the project directory.  The '&' at the end is a UNIX command that
+# runs the process in the background.  The '> /dev/null' part tells
+# the system to redirect all output (both stdout and stderr, denoted
+# by '&') that would normally go to the terminal to go to /dev/null
+# instead, which is a special file that discards all data written to
+# it.
+
+setsid idea . &>/dev/null &
+
+# The 'disown' command is a shell built-in that removes a shell job
+# from the shell's active list. Therefore, the shell will not send a
+# SIGHUP to this particular job when the shell session is terminated.
+
+# '-h' option specifies that if the shell receives a SIGHUP, it also
+# doesn't send a SIGHUP to the job.
+
+# '$!' is a shell special parameter that expands to the process ID of
+# the most recent background job.
+disown -h $!
+
+
+sleep 2
+
+# Finally, we use the 'exit' command to terminate the shell script.
+# This command tells the system to close the terminal window after
+# IntelliJ IDEA has been opened.
+exit
diff --git a/Tools/Update web site b/Tools/Update web site
new file mode 100755 (executable)
index 0000000..9daf5a4
--- /dev/null
@@ -0,0 +1,101 @@
+#!/bin/bash
+cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi;
+
+cd ..
+
+# Function to export org to html using emacs in batch mode
+export_org_to_html() {
+    local org_file=$1
+    local dir=$(dirname "$org_file")
+    local base=$(basename "$org_file" .org)
+    (
+        cd "$dir" || return 1
+        local html_file="${base}.html"
+        if [ -f "$html_file" ]; then
+            rm -f "$html_file"
+        fi
+        echo "Exporting: $org_file → $dir/$html_file"
+        emacs --batch -l ~/.emacs --visit="${base}.org" --funcall=org-html-export-to-html --kill
+        if [ $? -eq 0 ]; then
+            echo "✓ Successfully exported $org_file"
+        else
+            echo "✗ Failed to export $org_file"
+            return 1
+        fi
+    )
+}
+
+export_org_files_to_html() {
+    echo "🔍 Searching for .org files in doc/ ..."
+    echo "======================================="
+
+    mapfile -t ORG_FILES < <(find doc -type f -name "*.org" | sort)
+
+    if [ ${#ORG_FILES[@]} -eq 0 ]; then
+        echo "❌ No .org files found!"
+        return 1
+    fi
+
+    echo "Found ${#ORG_FILES[@]} .org file(s):"
+    printf '%s\n' "${ORG_FILES[@]}"
+    echo "======================================="
+
+    SUCCESS_COUNT=0
+    FAILED_COUNT=0
+
+    for org_file in "${ORG_FILES[@]}"; do
+        export_org_to_html "$org_file"
+        if [ $? -eq 0 ]; then
+            ((SUCCESS_COUNT++))
+        else
+            ((FAILED_COUNT++))
+        fi
+    done
+
+    echo "======================================="
+    echo "📊 SUMMARY:"
+    echo "   ✓ Successful: $SUCCESS_COUNT"
+    echo "   ✗ Failed:     $FAILED_COUNT"
+    echo "   Total:        $((SUCCESS_COUNT + FAILED_COUNT))"
+    echo ""
+}
+
+build_visualization_graphs() {
+    rm -rf doc/graphs/
+    mkdir -p doc/graphs/
+
+    javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "All classes" -t png -ho
+    javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "GUI" -t png -w "eu.svjatoslav.sixth.e3d.gui.*" -ho
+    javainspect -j target/sixth-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.sixth.e3d.renderer.raster.*" -ho
+
+    meviz index -w doc/graphs/ -t "Sixth 3D classes"
+}
+
+# Build project jar file and JavaDocs
+mvn clean package
+
+# Put generated JavaDoc HTML files to documentation directory
+rm -rf doc/apidocs/
+cp -r target/apidocs/ doc/
+
+# Publish Emacs org-mode files into HTML format
+export_org_files_to_html
+
+# Generate nice looking code visualization diagrams
+build_visualization_graphs
+
+
+## Upload assembled documentation to server
+echo "📤 Uploading to server..."
+rsync -avz --delete -e 'ssh -p 10006' doc/ \
+      n0@www3.svjatoslav.eu:/mnt/big/projects/sixth-3d/
+
+if [ $? -eq 0 ]; then
+    echo "✓ Upload completed successfully!"
+else
+    echo "✗ Upload failed!"
+fi
+
+echo ""
+echo "Press ENTER to close this window."
+read
diff --git a/doc/example.png b/doc/example.png
new file mode 100644 (file)
index 0000000..7094240
Binary files /dev/null and b/doc/example.png differ
diff --git a/doc/index.org b/doc/index.org
new file mode 100644 (file)
index 0000000..c882fd4
--- /dev/null
@@ -0,0 +1,556 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Sixth 3D - Realtime 3D engine
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+  .flex-center {
+    display: flex;            /* activate flexbox */
+    justify-content: center;  /* horizontally center anything inside   */
+  }
+
+  .flex-center video {
+    width: min(90%, 1000px); /* whichever is smaller wins */
+    height: auto;            /* preserve aspect ratio */
+  }
+
+  .responsive-img {
+    width: min(100%, 1000px);
+    height: auto;
+  }
+
+
+  .flex-center {
+    display: flex;
+    justify-content: center;
+  }
+  .flex-center video {
+    width: min(90%, 1000px);
+    height: auto;
+  }
+  .responsive-img {
+    width: min(100%, 1000px);
+    height: auto;
+  }
+
+
+  /* === SVG diagram theme === */
+  svg > rect:first-child {
+    fill:  #061018;
+  }
+
+  /* Lighten axis/helper labels that were dark-on-light */
+  svg text[fill="#666"],
+  svg text[fill="#999"] {
+    fill: #aaa !important;
+  }
+
+  /* Lighten dashed axis lines */
+  svg line[stroke="#ccc"] {
+    stroke: #445566 !important;
+  }
+
+</style>
+#+end_export
+
+
+* Introduction
+:PROPERTIES:
+:CUSTOM_ID: overview
+:ID:       a31a1f4d-5368-4fd9-aaf8-fa6d81851187
+:END:
+
+[[file:example.png]]
+
+*Sixth 3D* is a realtime 3D rendering engine written in pure Java. It
+runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no
+native libraries. Just Java.
+
+The motivation is simple: GPU-based 3D is a minefield of accidental
+complexity. Drivers are buggy or missing entirely. Features you need
+aren't supported on your target hardware. You run out of GPU RAM. You
+wrestle with platform-specific interop layers, shader compilation
+quirks, and dependency hell. Every GPU API comes with its own
+ecosystem of pain — version mismatches, incomplete implementations,
+vendor-specific workarounds. I want a library that "just works".
+
+Sixth 3D takes a different path. By rendering everything in software
+on the CPU, the entire GPU problem space simply disappears. You add a
+Maven dependency, write some Java, and you have a 3D scene. It runs
+wherever Java runs.
+
+This approach is quite practical for many use-cases. Modern systems
+ship with many CPU cores, and those with unified memory architectures
+offer high bandwidth between CPU and RAM. Software rendering that once
+seemed wasteful is now a reasonable choice where you need good-enough
+performance without the overhead of a full GPU pipeline. Java's JIT
+compiler helps too, optimizing hot rendering paths at runtime.
+
+Beyond convenience, CPU rendering gives you complete control. You own
+every pixel. You can freely experiment with custom rendering
+algorithms, optimization strategies, and visual effects without being
+constrained by what a GPU API exposes. Instead of brute-forcing
+everything through a fixed GPU pipeline, you can implement clever,
+application-specific optimizations.
+
+Sixth 3D is part of the larger [[https://www3.svjatoslav.eu/projects/sixth/][Sixth project]], with the long-term goal
+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 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().setLocation(new Point3D(0, -100, -300));
+
+          // Update the screen
+          viewFrame.getViewPanel().repaintDuringNextViewUpdate();
+      }
+  }
+#+END_SRC
+
+Compile and run *MyFirstScene* class. New window should open that will
+display 3D scene with red box.
+
+*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.
+
+* In-depth understanding
+** Vertex
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 240" width="320" height="240">
+  <rect width="320" height="240" fill="#f8f8f8"/>
+  <line x1="80" y1="180" x2="260" y2="180" stroke="#ccc" stroke-width="1"/>
+  <line x1="80" y1="180" x2="80" y2="40" stroke="#ccc" stroke-width="1"/>
+  <circle cx="190" cy="100" r="20" fill="rgba(56,140,248,0.08)" stroke="none"/>
+  <circle cx="190" cy="100" r="10" fill="rgba(56,140,248,0.15)" stroke="none"/>
+  <circle cx="190" cy="100" r="4" fill="#2070c0"/>
+  <line x1="190" y1="100" x2="190" y2="180" stroke="#2070c0" stroke-width="1" stroke-dasharray="4 3" opacity="0.4"/>
+  <line x1="190" y1="100" x2="80" y2="100" stroke="#2070c0" stroke-width="1" stroke-dasharray="4 3" opacity="0.4"/>
+  <text x="196" y="94" fill="#2070c0" font-size="13" font-weight="700" font-family="monospace">V</text>
+  <text x="200" y="108" fill="#666" font-size="10" font-family="monospace">(x, y, z)</text>
+  <text x="186" y="196" fill="#999" font-size="9" font-family="monospace">x</text>
+  <text x="64" y="100" fill="#999" font-size="9" font-family="monospace">y</text>
+</svg>
+#+END_EXPORT
+
+A *vertex* is a single point in 3D space, defined by three
+coordinates: *x*, *y*, and *z*. Every 3D object is ultimately built
+from vertices. A vertex can also carry additional data beyond
+position.
+
+- Position: =(x, y, z)=
+- Can also store: color, texture UV, normal vector
+- A triangle = 3 vertices, a cube = 8 vertices
+- Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in Sixth 3D engine.
+
+
+** Edge
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 240" width="320" height="240">
+  <rect width="320" height="240" fill="#f8f8f8"/>
+  <polygon points="160,50 80,190 240,190" fill="rgba(100,100,200,0.04)" stroke="rgba(100,100,200,0.2)" stroke-width="1"/>
+  <line x1="160" y1="50" x2="240" y2="190" stroke="#5060c0" stroke-width="3" stroke-linecap="round"/>
+  <circle cx="160" cy="50" r="5" fill="#5060c0"/>
+  <circle cx="80" cy="190" r="4" fill="rgba(80,96,192,0.5)"/>
+  <circle cx="240" cy="190" r="5" fill="#5060c0"/>
+  <text x="150" y="40" fill="#666" font-size="10" font-family="monospace">V₁</text>
+  <text x="246" y="194" fill="#666" font-size="10" font-family="monospace">V₂</text>
+  <text x="60" y="200" fill="#999" font-size="10" font-family="monospace">V₃</text>
+  <text x="210" y="110" fill="#5060c0" font-size="12" font-weight="700" font-family="monospace" transform="rotate(30 210 110)">edge</text>
+</svg>
+#+END_EXPORT
+
+An *edge* is a straight line segment connecting two vertices. Edges
+define the wireframe skeleton of a 3D model. In rendering, edges
+themselves are rarely drawn — they exist implicitly as boundaries of
+faces.
+
+- Edge = line from V₁ to V₂
+- A triangle has 3 edges
+- A cube has 12 edges
+- Wireframe mode renders edges visibly
+- Edge is related to and can be represented by the [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class in Sixth
+  3D engine.
+
+** Face (Triangle)
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 240" width="320" height="240">
+  <rect width="320" height="240" fill="#f8f8f8"/>
+  <polygon points="160,40 60,200 260,200" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+  <line x1="100" y1="140" x2="220" y2="140" stroke="rgba(200,80,140,0.1)" stroke-width="0.5"/>
+  <line x1="120" y1="160" x2="200" y2="160" stroke="rgba(200,80,140,0.08)" stroke-width="0.5"/>
+  <line x1="82" y1="180" x2="238" y2="180" stroke="rgba(200,80,140,0.06)" stroke-width="0.5"/>
+  <circle cx="160" cy="40" r="4" fill="#c05088"/>
+  <circle cx="60" cy="200" r="4" fill="#c05088"/>
+  <circle cx="260" cy="200" r="4" fill="#c05088"/>
+  <text x="148" y="30" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₁</text>
+  <text x="38" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₂</text>
+  <text x="266" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₃</text>
+  <text x="132" y="150" fill="rgba(192,80,136,0.5)" font-size="14" font-weight="700" font-family="monospace">FACE</text>
+</svg>
+#+END_EXPORT
+
+A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamental face is a *triangle* — defined by exactly 3 vertices. Triangles are preferred because they are always planar (flat) and trivially simple to rasterize.
+
+- Triangle = 3 vertices + 3 edges
+- 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.
+
+** Coordinate System (X, Y, Z)
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 260" width="320" height="260">
+  <rect width="320" height="260" fill="#f8f8f8"/>
+  <circle cx="140" cy="170" r="5" fill="rgba(0,0,0,0.1)" stroke="rgba(0,0,0,0.2)" stroke-width="1"/>
+  <line x1="140" y1="170" x2="280" y2="170" stroke="#d04040" stroke-width="2.5"/>
+  <polygon points="280,170 270,165 270,175" fill="#d04040"/>
+  <text x="284" y="174" fill="#d04040" font-size="14" font-weight="700" font-family="monospace">X</text>
+  <text x="270" y="192" fill="#999" font-size="9" font-family="monospace">right / left</text>
+  <line x1="140" y1="170" x2="140" y2="30" stroke="#30a050" stroke-width="2.5"/>
+  <polygon points="140,30 135,40 145,40" fill="#30a050"/>
+  <text x="146" y="32" fill="#30a050" font-size="14" font-weight="700" font-family="monospace">Y</text>
+  <text x="146" y="48" fill="#999" font-size="9" font-family="monospace">up / down</text>
+  <line x1="140" y1="170" x2="60" y2="230" stroke="#2070c0" stroke-width="2.5"/>
+  <polygon points="60,230 70,222 66,232" fill="#2070c0"/>
+  <text x="42" y="242" fill="#2070c0" font-size="14" font-weight="700" font-family="monospace">Z</text>
+  <text x="30" y="256" fill="#999" font-size="9" font-family="monospace">depth (forward/back)</text>
+  <text x="120" y="162" fill="#666" font-size="11" font-weight="600" font-family="monospace">Origin</text>
+  <text x="117" y="175" fill="#999" font-size="9" font-family="monospace">(0, 0, 0)</text>
+  <circle cx="230" cy="90" r="3.5" fill="#30a050"/>
+  <line x1="230" y1="90" x2="230" y2="170" stroke="#30a050" stroke-width="0.8" stroke-dasharray="3 2" opacity="0.4"/>
+  <line x1="230" y1="90" x2="140" y2="90" stroke="#30a050" stroke-width="0.8" stroke-dasharray="3 2" opacity="0.4"/>
+  <text x="236" y="88" fill="#30a050" font-size="9" font-weight="600" font-family="monospace">(3, 4, 0)</text>
+</svg>
+#+END_EXPORT
+
+Every point in 3D space is located using three perpendicular axes
+originating from the *origin (0, 0, 0)*. The *X* axis runs left–right,
+the *Y* axis runs up–down, and the *Z* axis represents depth.
+
+- Right-handed vs left-handed systems differ in which direction =+Z= points
+- Right-handed: +Z towards viewer (OpenGL)
+- Left-handed: +Z into screen (DirectX)
+
+** Normal Vector
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 260" width="320" height="260">
+  <rect width="320" height="260" fill="#f8f8f8"/>
+  <polygon points="60,200 160,180 260,200 160,220" fill="rgba(180,150,30,0.1)" stroke="rgba(180,150,30,0.4)" stroke-width="1"/>
+  <line x1="90" y1="198" x2="230" y2="198" stroke="rgba(180,150,30,0.08)" stroke-width="0.5"/>
+  <line x1="110" y1="194" x2="210" y2="194" stroke="rgba(180,150,30,0.06)" stroke-width="0.5"/>
+  <line x1="160" y1="198" x2="160" y2="60" stroke="#b09020" stroke-width="2.5"/>
+  <polygon points="160,60 155,72 165,72" fill="#b09020"/>
+  <path d="M160,198 L160,178 L170,180" fill="none" stroke="rgba(180,150,30,0.5)" stroke-width="1"/>
+  <text x="168" y="56" fill="#b09020" font-size="13" font-weight="700" font-family="monospace">N̂</text>
+  <text x="168" y="72" fill="#999" font-size="9" font-family="monospace">unit normal</text>
+  <text x="168" y="86" fill="#999" font-size="9" font-family="monospace">(perpendicular</text>
+  <text x="168" y="98" fill="#999" font-size="9" font-family="monospace"> to surface)</text>
+  <circle cx="70" cy="60" r="14" fill="rgba(180,150,30,0.08)" stroke="rgba(180,150,30,0.3)" stroke-width="1"/>
+  <circle cx="70" cy="60" r="4" fill="rgba(180,150,30,0.6)"/>
+  <text x="56" y="42" fill="#999" font-size="9" font-family="monospace">Light</text>
+  <line x1="80" y1="68" x2="150" y2="170" stroke="rgba(180,150,30,0.2)" stroke-width="1" stroke-dasharray="4 3"/>
+  <text x="82" y="142" fill="rgba(180,150,30,0.5)" font-size="9" font-family="monospace">L · N = brightness</text>
+</svg>
+#+END_EXPORT
+
+A *normal* is a vector perpendicular to a surface. It tells the
+renderer which direction a face is pointing. Normals are critical for
+*lighting* — the angle between the light direction and the normal
+determines how bright a surface appears.
+
+- *Face normal*: one normal per triangle
+- *Vertex normal*: one normal per vertex (averaged from adjacent faces for smooth shading)
+- =dot(L, N)= → surface brightness
+- Flat shading → face normals
+- Gouraud/Phong → vertex normals + interpolation
+
+** Mesh
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 240" width="320" height="240">
+  <rect width="320" height="240" fill="#f8f8f8"/>
+  <ellipse cx="160" cy="120" rx="90" ry="90" fill="none" stroke="rgba(80,96,192,0.1)" stroke-width="0.5"/>
+  <ellipse cx="160" cy="120" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.25)" stroke-width="0.8"/>
+  <ellipse cx="160" cy="90" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+  <ellipse cx="160" cy="150" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+  <ellipse cx="160" cy="60" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+  <ellipse cx="160" cy="180" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+  <ellipse cx="160" cy="120" rx="20" ry="90" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+  <ellipse cx="160" cy="120" rx="55" ry="90" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+  <polygon points="160,30 185,58 140,55" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="1"/>
+  <polygon points="185,58 205,88 160,82" fill="rgba(80,96,192,0.1)" stroke="#5060c0" stroke-width="0.8"/>
+  <polygon points="160,82 185,58 140,55" fill="rgba(80,96,192,0.07)" stroke="rgba(80,96,192,0.5)" stroke-width="0.6"/>
+  <circle cx="160" cy="30" r="2.5" fill="#5060c0"/>
+  <circle cx="185" cy="58" r="2.5" fill="#5060c0"/>
+  <circle cx="140" cy="55" r="2.5" fill="#5060c0"/>
+  <circle cx="205" cy="88" r="2.5" fill="#5060c0"/>
+  <circle cx="160" cy="82" r="2.5" fill="#5060c0"/>
+  <text x="218" y="70" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">triangulated</text>
+  <text x="218" y="82" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">section</text>
+  <line x1="206" y1="75" x2="214" y2="75" stroke="#5060c0" stroke-width="0.8"/>
+</svg>
+#+END_EXPORT
+
+A *mesh* is a collection of vertices, edges, and faces that together define the shape of a 3D object. Even curved surfaces like spheres are approximated by many small triangles — more triangles means a smoother appearance.
+
+- Mesh data = vertex array + index array
+- Index array avoids duplicating shared vertices
+- Cube: 8 vertices, 12 triangles
+- Smooth sphere: hundreds–thousands of triangles
+- =vertices[] + indices[]= → efficient storage
+- In Sixth 3D engine:
+  - [[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.
+
+** Winding Order & Backface Culling
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 240" width="320" height="240">
+  <rect width="320" height="240" fill="#f8f8f8"/>
+  <polygon points="80,50 30,180 130,180" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <path d="M95,80 C 120,80 130,130 110,155" fill="none" stroke="#30a050" stroke-width="1.5" stroke-dasharray="4 2"/>
+  <polygon points="110,155 115,143 104,148" fill="#30a050"/>
+  <text x="97" y="122" fill="#30a050" font-size="10" font-weight="700" font-family="monospace">CCW</text>
+  <circle cx="80" cy="50" r="3" fill="#30a050"/>
+  <circle cx="30" cy="180" r="3" fill="#30a050"/>
+  <circle cx="130" cy="180" r="3" fill="#30a050"/>
+  <text x="58" y="44" fill="#666" font-size="9" font-family="monospace">V₁</text>
+  <text x="14" y="198" fill="#666" font-size="9" font-family="monospace">V₂</text>
+  <text x="132" y="198" fill="#666" font-size="9" font-family="monospace">V₃</text>
+  <text x="36" y="220" fill="#30a050" font-size="11" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
+  <polygon points="240,50 290,180 190,180" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="1.5" stroke-dasharray="6 3"/>
+  <path d="M225,80 C 200,80 190,130 210,155" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="4 2"/>
+  <polygon points="210,155 205,143 216,148" fill="rgba(208,64,64,0.5)"/>
+  <text x="210" y="122" fill="rgba(208,64,64,0.6)" font-size="10" font-weight="700" font-family="monospace">CW</text>
+  <line x1="228" y1="108" x2="252" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
+  <line x1="252" y1="108" x2="228" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
+  <text x="186" y="220" fill="rgba(208,64,64,0.7)" font-size="11" font-weight="700" font-family="monospace">BACK FACE ✗</text>
+  <text x="195" y="234" fill="#999" font-size="9" font-family="monospace">(culled — not drawn)</text>
+</svg>
+#+END_EXPORT
+
+The order in which a triangle's vertices are listed determines its *winding order*. Counter-clockwise (CCW) typically means front-facing. *Backface culling* skips rendering triangles that face away from the camera — a major performance optimization.
+
+- CCW winding → front face (visible)
+- CW winding → back face (culled)
+- Saves ~50% of triangle rendering
+- Normal direction derived from winding order via =cross(V₂-V₁, V₃-V₁)=
+
+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/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all
+  sub-shapes)
+
+** Working with Colors
+:PROPERTIES:
+:CUSTOM_ID: working-with-colors
+:ID:       f2c9642a-a093-444f-8992-76c97ff28c16
+:END:
+
+Sixth 3D uses its own Color class (not java.awt.Color):
+
+#+BEGIN_SRC java
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+// Using predefined colors
+Color red = Color.RED;
+Color green = Color.GREEN;
+Color blue = Color.BLUE;
+
+// Create custom color (R, G, B, A)
+Color custom = new Color(255, 128, 64, 200); // semi-transparent orange
+
+// Or use hex string
+Color hex = new Color("FF8040CC"); // same orange with alpha
+#+END_SRC
+
+* Source code
+:PROPERTIES:
+:CUSTOM_ID: source-code
+:ID:       978b7ea2-e246-45d0-be76-4d561308e9f3
+:END:
+
+*This program is free software: released under Creative Commons Zero
+(CC0) license*
+
+*Program author:*
+- Svjatoslav Agejenko
+- Homepage: https://svjatoslav.eu
+- Email: mailto://svjatoslav@svjatoslav.eu
+- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]]
+
+*Getting the source code:*
+- [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]]
+- [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d.git;a=summary][Browse Git repository online]]
+- Clone Git repository using command:
+  : git clone https://www3.svjatoslav.eu/git/sixth-3d.git
+
+** Understanding the Sixth 3D source code
+
+- 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]].
+
+* Future ideas
+:PROPERTIES:
+:CUSTOM_ID: future-ideas
+:ID:       2258231b-007d-42d3-9ba9-a9957a0dfc56
+:END:
+
++ Read this as example, and apply improvements/fixes where applicable:
+  http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html
+
++ Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/
+
++ Partial region/frame repaint: when only one small object changed on
+  the scene, it would be faster to re-render that specific area.
+
+  + Once partial rendering works, in would be easy to add multi-core
+    rendering support. So that each core renders it's own region of
+    the screen.
+
++ Anti-aliasing. Would improve text readability. If antialiazing is
+  too expensive for every frame, it could be used only for last frame
+  before animations become still and waiting for user input starts.
+
+** Render only visible polygons
+:PROPERTIES:
+:CUSTOM_ID: render-only-visible-polygons
+:ID:       c32d839a-cfa8-4aec-a8e0-8c9e7ebb8bba
+:END:
+
+Very high-level idea description:
+
++ This would significantly reduce RAM <-> CPU traffic.
+
++ General algorithm description:
+  + For each horizontal scanline:
+    + sort polygon edges from left to right
+    + while iterating and drawing pixels over screen X axis (left to
+      right) track next appearing/disappearing polygons.
+      + For each polygon edge update Z sorted active polygons list.
+      + Only draw pixel from the top-most polygon.
+        + Only if polygon area is transparent/half-transparent add
+          colors from the polygons below.
+
++ As a bonus, this would allow to track which polygons are really
+  visible in the final scene for each frame.
+
+  + Such information allows further optimizations:
+
+    + Dynamic geometry simplification:
+      + Dynamically detect and replace invisible objects from the
+        scene with simplified bounding box.
+
+      + Dynamically replace boudnig box with actual object once it
+        becomes visible.
+
+    + Dynamically unload unused textures from RAM.
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..4ba6be5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,145 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>eu.svjatoslav</groupId>
+    <artifactId>sixth-3d</artifactId>
+    <version>1.4-SNAPSHOT</version>
+    <name>Sixth 3D</name>
+    <description>3D engine</description>
+
+    <properties>
+        <java.version>21</java.version>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <organization>
+        <name>svjatoslav.eu</name>
+        <url>https://svjatoslav.eu</url>
+    </organization>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>21</source>
+                    <target>21</target>
+                    <optimize>true</optimize>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.2.1</version>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.10.4</version>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <!-- workaround for https://bugs.openjdk.java.net/browse/JDK-8212233 -->
+                    <javaApiLinks>
+                        <property>
+                            <name>foo</name>
+                            <value>bar</value>
+                        </property>
+                    </javaApiLinks>
+                    <!-- Workaround for https://stackoverflow.com/questions/49472783/maven-is-unable-to-find-javadoc-command -->
+                    <javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <version>2.4.3</version>
+                <configuration>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-release-plugin</artifactId>
+                <version>2.5.2</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>org.apache.maven.scm</groupId>
+                        <artifactId>maven-scm-provider-gitexe</artifactId>
+                        <version>1.9.4</version>
+                    </dependency>
+                </dependencies>
+            </plugin>
+        </plugins>
+
+        <extensions>
+            <extension>
+                <groupId>org.apache.maven.wagon</groupId>
+                <artifactId>wagon-ssh-external</artifactId>
+                <version>2.6</version>
+            </extension>
+        </extensions>
+    </build>
+
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>svjatoslav.eu</id>
+            <name>svjatoslav.eu</name>
+            <url>scpexe://svjatoslav.eu:10006/srv/maven</url>
+        </snapshotRepository>
+        <repository>
+            <id>svjatoslav.eu</id>
+            <name>svjatoslav.eu</name>
+            <url>scpexe://svjatoslav.eu:10006/srv/maven</url>
+        </repository>
+    </distributionManagement>
+
+    <repositories>
+        <repository>
+            <id>svjatoslav.eu</id>
+            <name>Svjatoslav repository</name>
+            <url>https://www3.svjatoslav.eu/maven/</url>
+        </repository>
+    </repositories>
+
+    <scm>
+        <connection>scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/sixth-3d.git</connection>
+        <developerConnection>scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/sixth-3d.git</developerConnection>
+        <tag>HEAD</tag>
+    </scm>
+
+</project>
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java
new file mode 100644 (file)
index 0000000..e461531
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+import static java.lang.Math.abs;
+
+/**
+ * Same as: 3D rectangle, rectangular box, rectangular parallelopiped, cuboid,
+ * rhumboid, hexahedron, rectangular prism.
+ */
+public class Box implements Cloneable {
+
+    /**
+     * The first point of the box.
+     */
+    public final Point3D p1;
+    /**
+     * The second point of the box.
+     */
+    public final Point3D p2;
+
+    /**
+     * Creates a new box with two points at the origin.
+     */
+    public Box() {
+        p1 = new Point3D();
+        p2 = new Point3D();
+    }
+
+    /**
+     * Creates a new box with two points at the specified coordinates.
+     */
+    public Box(final Point3D p1, final Point3D p2) {
+        this.p1 = p1;
+        this.p2 = p2;
+    }
+
+
+    /**
+     * Enlarges the box by the specified border in all directions.
+     *
+     * @param border The border to enlarge the box by.
+     *               If the border is negative, the box will be shrunk.
+     * @return The current box.
+     */
+    public Box enlarge(final double border) {
+
+        if (p1.x < p2.x) {
+            p1.translateX(-border);
+            p2.translateX(border);
+        } else {
+            p1.translateX(border);
+            p2.translateX(-border);
+        }
+
+        if (p1.y < p2.y) {
+            p1.translateY(-border);
+            p2.translateY(border);
+        } else {
+            p1.translateY(border);
+            p2.translateY(-border);
+        }
+
+        if (p1.z < p2.z) {
+            p1.translateZ(-border);
+            p2.translateZ(border);
+        } else {
+            p1.translateZ(border);
+            p2.translateZ(-border);
+        }
+
+        return this;
+    }
+
+    @Override
+    public Box clone() {
+        return new Box(p1.clone(), p2.clone());
+    }
+
+    /**
+     * @return The depth of the box. The depth is the distance between the two points on the z-axis.
+     */
+    public double getDepth() {
+        return abs(p1.z - p2.z);
+    }
+
+    /**
+     * @return The height of the box. The height is the distance between the two points on the y-axis.
+     */
+    public double getHeight() {
+        return abs(p1.y - p2.y);
+    }
+
+    /**
+     * @return The width of the box. The width is the distance between the two points on the x-axis.
+     */
+    public double getWidth() {
+        return abs(p1.x - p2.x);
+    }
+
+
+    /**
+     * Sets the size of the box. The box will be centered at the origin.
+     * Previous size and position of the box will be lost.
+     *
+     * @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();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Circle.java
new file mode 100644 (file)
index 0000000..2ac177d
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+/**
+ * Circle in 2D space.
+ */
+public class Circle {
+
+    /**
+     * The center of the circle.
+     */
+    Point2D location;
+
+    /**
+     * The radius of the circle.
+     */
+    double radius;
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java
new file mode 100755 (executable)
index 0000000..a6bb6a1
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+import static java.lang.Math.sqrt;
+
+/**
+ * Used to represent point in a 2D space or vector.
+ *
+ * @see Point3D
+ */
+public class Point2D implements Cloneable {
+
+    public double x, y;
+
+    public Point2D() {
+    }
+
+    public Point2D(final double x, final double y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public Point2D(final Point2D parent) {
+        x = parent.x;
+        y = parent.y;
+    }
+
+
+    /**
+     * Add other point to current point. Value of other point will not be changed.
+     *
+     * @return current point.
+     */
+    public Point2D add(final Point2D otherPoint) {
+        x += otherPoint.x;
+        y += otherPoint.y;
+        return this;
+    }
+
+    /**
+     * @return true if current point coordinates are equal to zero.
+     */
+    public boolean isZero() {
+        return (x == 0) && (y == 0);
+    }
+
+    @Override
+    public Point2D clone() {
+        return new Point2D(this);
+    }
+
+    /**
+     * Copy coordinates from other point to current point. Value of other point will not be changed.
+     */
+    public void clone(final Point2D otherPoint) {
+        x = otherPoint.x;
+        y = otherPoint.y;
+    }
+
+    /**
+     * Set current point to middle of two other points.
+     *
+     * @param p1 first point.
+     * @param p2 second point.
+     * @return current point.
+     */
+    public Point2D setToMiddle(final Point2D p1, final Point2D p2) {
+        x = (p1.x + p2.x) / 2d;
+        y = (p1.y + p2.y) / 2d;
+        return this;
+    }
+
+    public double getAngleXY(final Point2D anotherPoint) {
+        return Math.atan2(x - anotherPoint.x, y - anotherPoint.y);
+    }
+
+    /**
+     * Compute distance to another point.
+     *
+     * @param anotherPoint point to compute distance to.
+     * @return distance from current point to another point.
+     */
+    public double getDistanceTo(final Point2D anotherPoint) {
+        final double xDiff = x - anotherPoint.x;
+        final double yDiff = y - anotherPoint.y;
+
+        return sqrt(((xDiff * xDiff) + (yDiff * yDiff)));
+    }
+
+    /**
+     * Calculate length of vector.
+     *
+     * @return length of vector.
+     */
+    public double getVectorLength() {
+        return sqrt(((x * x) + (y * y)));
+    }
+
+    /**
+     * Invert current point.
+     *
+     * @return current point.
+     */
+    public Point2D invert() {
+        x = -x;
+        y = -y;
+        return this;
+    }
+
+    /**
+     * Round current point coordinates to integer.
+     */
+    public void roundToInteger() {
+        x = (int) x;
+        y = (int) y;
+    }
+
+    /**
+     * Subtract other point from current point. Value of other point will not be changed.
+     *
+     * @return current point.
+     */
+    public Point2D subtract(final Point2D otherPoint) {
+        x -= otherPoint.x;
+        y -= otherPoint.y;
+        return this;
+    }
+
+    /**
+     * Convert current point to 3D point.
+     * Value of the z coordinate will be set to zero.
+     *
+     * @return 3D point.
+     */
+    public Point3D to3D() {
+        return new Point3D(x, y, 0);
+    }
+
+    /**
+     * Set current point to zero.
+     *
+     * @return current point.
+     */
+    public Point2D zero() {
+        x = 0;
+        y = 0;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Point2D{" +
+                "x=" + x +
+                ", y=" + y +
+                '}';
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java
new file mode 100755 (executable)
index 0000000..6f93616
--- /dev/null
@@ -0,0 +1,419 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+import eu.svjatoslav.sixth.e3d.renderer.octree.IntegerPoint;
+
+import static java.lang.Math.*;
+
+/**
+ * A mutable 3D point or vector with double-precision coordinates.
+ *
+ * <p>{@code Point3D} is the fundamental coordinate type used throughout the Sixth 3D engine.
+ * It represents either a position in 3D space or a directional vector, with public
+ * {@code x}, {@code y}, {@code z} fields for direct access.</p>
+ *
+ * <p>All mutation methods return {@code this} for fluent chaining:</p>
+ * <pre>{@code
+ * Point3D p = new Point3D(10, 20, 30)
+ *     .scaleUp(2.0)
+ *     .translateX(5)
+ *     .add(new Point3D(1, 1, 1));
+ * // p is now (25, 41, 61)
+ * }</pre>
+ *
+ * <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
+ *
+ * // Measure distance
+ * double dist = pos.getDistanceTo(origin);
+ *
+ * // Rotation
+ * 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
+ * }</pre>
+ *
+ * <p><b>Warning:</b> This class is mutable with public fields. Clone before storing
+ * references that should not be shared:</p>
+ * <pre>{@code
+ * Point3D safeCopy = original.clone();
+ * }</pre>
+ *
+ * @see Point2D the 2D equivalent
+ * @see eu.svjatoslav.sixth.e3d.math.Vertex wraps a Point3D with transform support
+ */
+public class Point3D implements Cloneable {
+
+    /** X coordinate (horizontal axis). */
+    public double x;
+    /** Y coordinate (vertical axis, positive = down in screen space). */
+    public double y;
+    /** Z coordinate (depth axis, positive = into the screen / away from viewer). */
+    public double z;
+
+    /**
+     * Creates a point at the origin (0, 0, 0).
+     */
+    public Point3D() {
+    }
+
+    /**
+     * Creates a point with the specified double-precision coordinates.
+     *
+     * @param x the X coordinate
+     * @param y the Y coordinate
+     * @param z the Z coordinate
+     */
+    public Point3D(final double x, final double y, final double z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    /**
+     * Creates a point with the specified float coordinates (widened to double).
+     *
+     * @param x the X coordinate
+     * @param y the Y coordinate
+     * @param z the Z coordinate
+     */
+    public Point3D(final float x, final float y, final float z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    /**
+     * Creates a point with the specified integer coordinates (widened to double).
+     *
+     * @param x the X coordinate
+     * @param y the Y coordinate
+     * @param z the Z coordinate
+     */
+    public Point3D(final int x, final int y, final int z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    /**
+     * Creates a point from an {@link IntegerPoint} (used by octree voxel coordinates).
+     *
+     * @param point the integer point to convert
+     */
+    public Point3D(IntegerPoint point) {
+        this.x = point.x;
+        this.y = point.y;
+        this.z = point.z;
+    }
+
+
+    /**
+     * Creates new current point by cloning coordinates from parent point.
+     */
+    public Point3D(final Point3D parent) {
+        x = parent.x;
+        y = parent.y;
+        z = parent.z;
+    }
+
+    /**
+     * Add other point to current point. Value of other point will not be changed.
+     *
+     * @param otherPoint point to add.
+     * @return current point.
+     */
+    public Point3D add(final Point3D otherPoint) {
+        x += otherPoint.x;
+        y += otherPoint.y;
+        z += otherPoint.z;
+        return this;
+    }
+
+    /**
+     * Add coordinates of current point to other point. Value of current point will not be changed.
+     *
+     * @return current point.
+     */
+    public Point3D addTo(final Point3D... otherPoints) {
+        for (final Point3D otherPoint : otherPoints) otherPoint.add(this);
+        return this;
+    }
+
+    /**
+     * Create new point by cloning position of current point.
+     *
+     * @return newly created clone.
+     */
+    public Point3D clone() {
+        return new Point3D(this);
+    }
+
+    /**
+     * Copy coordinates from other point to current point. Value of other point will not be changed.
+     */
+    public Point3D clone(final Point3D otherPoint) {
+        x = otherPoint.x;
+        y = otherPoint.y;
+        z = otherPoint.z;
+        return this;
+    }
+
+    /**
+     * Set current point coordinates to the middle point between two other points.
+     *
+     * @param p1 first point.
+     * @param p2 second point.
+     * @return current point.
+     */
+    public Point3D computeMiddlePoint(final Point3D p1, final Point3D p2) {
+        x = (p1.x + p2.x) / 2d;
+        y = (p1.y + p2.y) / 2d;
+        z = (p1.z + p2.z) / 2d;
+        return this;
+    }
+
+    /**
+     * @return true if current point coordinates are equal to zero.
+     */
+    public boolean isZero() {
+        return (x == 0) && (y == 0) && (z == 0);
+    }
+
+    /**
+     * Computes the angle on the X-Z plane between this point and another point.
+     *
+     * @param anotherPoint the other point
+     * @return the angle in radians
+     */
+    public double getAngleXZ(final Point3D anotherPoint) {
+        return Math.atan2(x - anotherPoint.x, z - anotherPoint.z);
+    }
+
+    /**
+     * Computes the angle on the Y-Z plane between this point and another point.
+     *
+     * @param anotherPoint the other point
+     * @return the angle in radians
+     */
+    public double getAngleYZ(final Point3D anotherPoint) {
+        return Math.atan2(y - anotherPoint.y, z - anotherPoint.z);
+    }
+
+    /**
+     * Computes the angle on the X-Y plane between this point and another point.
+     *
+     * @param anotherPoint the other point
+     * @return the angle in radians
+     */
+    public double getAngleXY(final Point3D anotherPoint) {
+        return Math.atan2(x - anotherPoint.x, y - anotherPoint.y);
+    }
+
+    /**
+     * Compute distance to another point.
+     *
+     * @param anotherPoint point to compute distance to.
+     * @return distance to another point.
+     */
+    public double getDistanceTo(final Point3D anotherPoint) {
+        final double xDelta = x - anotherPoint.x;
+        final double yDelta = y - anotherPoint.y;
+        final double zDelta = z - anotherPoint.z;
+
+        return sqrt(((xDelta * xDelta) + (yDelta * yDelta) + (zDelta * zDelta)));
+    }
+
+    /**
+     * @return length of current vector.
+     */
+    public double getVectorLength() {
+        return sqrt(((x * x) + (y * y) + (z * z)));
+    }
+
+    /**
+     * Invert current point coordinates.
+     *
+     * @return current point.
+     */
+    public Point3D invert() {
+        x = -x;
+        y = -y;
+        z = -z;
+        return this;
+    }
+
+    /**
+     * Rotate current point around center point by angleXZ and angleYZ.
+     * <p>
+     * See also: <a href="https://marctenbosch.com/quaternions/">Let's remove Quaternions from every 3D Engine</a>
+     *
+     * @param center  center point.
+     * @param angleXZ angle around XZ axis.
+     * @param angleYZ angle around YZ axis.
+     */
+    public Point3D rotate(final Point3D center, final double angleXZ,
+                          final double angleYZ) {
+        final double s1 = sin(angleXZ);
+        final double c1 = cos(angleXZ);
+
+        final double s2 = sin(angleYZ);
+        final double c2 = cos(angleYZ);
+
+        x -= center.x;
+        y -= center.y;
+        z -= center.z;
+
+        final double y1 = (z * s2) + (y * c2);
+        final double z1 = (z * c2) - (y * s2);
+
+        final double x1 = (z1 * s1) + (x * c1);
+        final double z2 = (z1 * c1) - (x * s1);
+
+        x = x1 + center.x;
+        y = y1 + center.y;
+        z = z2 + center.z;
+
+        return this;
+    }
+
+    /**
+     * Rotate current point around the origin by the given angles.
+     *
+     * @param angleXZ angle around the XZ plane (yaw), in radians
+     * @param angleYZ angle around the YZ plane (pitch), in radians
+     * @return this point (mutated)
+     */
+    public Point3D rotate(final double angleXZ, final double angleYZ) {
+        return rotate(new Point3D(0, 0, 0), angleXZ, angleYZ);
+    }
+
+    /**
+     * Round current point coordinates to integer values.
+     */
+    public void roundToInteger() {
+        x = (int) x;
+        y = (int) y;
+        z = (int) z;
+    }
+
+    /**
+     * Scale down current point by factor.
+     * All coordinates will be divided by factor.
+     *
+     * @param factor factor to scale by.
+     * @return current point.
+     */
+    public Point3D scaleDown(final double factor) {
+        x /= factor;
+        y /= factor;
+        z /= factor;
+        return this;
+    }
+
+    /**
+     * Scale up current point by factor.
+     * All coordinates will be multiplied by factor.
+     *
+     * @param factor factor to scale by.
+     * @return current point.
+     */
+    public Point3D scaleUp(final double factor) {
+        x *= factor;
+        y *= factor;
+        z *= factor;
+        return this;
+    }
+
+    /**
+     * Set current point coordinates to given values.
+     *
+     * @param x X coordinate.
+     * @param y Y coordinate.
+     * @param z Z coordinate.
+     */
+    public void setValues(final double x, final double y, final double z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    /**
+     * Subtract other point from current point. Value of other point will not be changed.
+     *
+     * @return current point.
+     */
+    public Point3D subtract(final Point3D otherPoint) {
+        x -= otherPoint.x;
+        y -= otherPoint.y;
+        z -= otherPoint.z;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "x:" + x + " y:" + y + " z:" + z;
+    }
+
+    /**
+     * Translate current point along X axis by given increment.
+     *
+     * @return current point.
+     */
+    public Point3D translateX(final double xIncrement) {
+        x += xIncrement;
+        return this;
+    }
+
+    /**
+     * Translate current point along Y axis by given increment.
+     *
+     * @return current point.
+     */
+    public Point3D translateY(final double yIncrement) {
+        y += yIncrement;
+        return this;
+    }
+
+    /**
+     * Translate current point along Z axis by given increment.
+     *
+     * @return current point.
+     */
+    public Point3D translateZ(final double zIncrement) {
+        z += zIncrement;
+        return this;
+    }
+
+    /**
+     * Here we assume that Z coordinate is distance to the viewer.
+     * If Z is positive, then point is in front of the viewer, and therefore it is visible.
+     *
+     * @return point visibility status.
+     */
+    public boolean isVisible() {
+        return z > 0;
+    }
+
+    /**
+     * Resets point coordinates to zero along all axes.
+     *
+     * @return current point.
+     */
+    public Point3D zero() {
+        x = 0;
+        y = 0;
+        z = 0;
+        return this;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
new file mode 100644 (file)
index 0000000..d5f558e
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+/**
+ * Utility class for polygon operations.
+ */
+public class Polygon {
+
+
+    /**
+     * Checks if point is on the right side of the line.
+     * @param point point to check
+     * @param lineP1 line start point
+     * @param lineP2 line end point
+     * @return true if point is on the right side of the line
+     */
+    private static boolean intersectsLine(final Point2D point, Point2D lineP1,
+                                          Point2D lineP2) {
+
+        // Sort line points by y coordinate.
+        if (lineP1.y > lineP2.y) {
+            final Point2D tmp = lineP1;
+            lineP1 = lineP2;
+            lineP2 = tmp;
+        }
+
+        // Check if point is within line y range.
+        if (point.y < lineP1.y || point.y > lineP2.y)
+            return false;
+
+        // Check if point is on the line.
+        final double xp = lineP2.x - lineP1.x;
+        final double yp = lineP2.y - lineP1.y;
+
+        final double crossX = lineP1.x + ((xp * (point.y - lineP1.y)) / yp);
+
+        return point.x >= crossX;
+    }
+
+    public static boolean pointWithinPolygon(final Point2D point,
+                                             final Point2D p1, final Point2D p2, final Point2D p3) {
+
+        int intersectionCount = 0;
+
+        if (intersectsLine(point, p1, p2))
+            intersectionCount++;
+
+        if (intersectsLine(point, p2, p3))
+            intersectionCount++;
+
+        if (intersectsLine(point, p3, p1))
+            intersectionCount++;
+
+        return intersectionCount == 1;
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java
new file mode 100644 (file)
index 0000000..966d366
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.geometry;
+
+import static java.lang.Math.abs;
+import static java.lang.Math.min;
+
+/**
+ * Rectangle class.
+ */
+public class Rectangle {
+
+    /**
+     * Rectangle points.
+     */
+    public Point2D p1, p2;
+
+    /**
+     * Creates new rectangle with given size.
+     * The rectangle will be centered at the origin.
+     * The rectangle will be square.
+     *
+     * @param size The size of the rectangle.
+     */
+    public Rectangle(final double size) {
+        p2 = new Point2D(size / 2, size / 2);
+        p1 = p2.clone().invert();
+    }
+
+    /**
+     * @param p1 The first point of the rectangle.
+     * @param p2 The second point of the rectangle.
+     */
+    public Rectangle(final Point2D p1, final Point2D p2) {
+        this.p1 = p1;
+        this.p2 = p2;
+    }
+
+    public double getHeight() {
+        return abs(p1.y - p2.y);
+    }
+
+    /**
+     * @return The leftmost x coordinate of the rectangle.
+     */
+    public double getLowerX() {
+        return min(p1.x, p2.x);
+    }
+
+    public double getLowerY() {
+        return min(p1.y, p2.y);
+    }
+
+    /**
+     * @return rectangle width.
+     */
+    public double getWidth() {
+        return abs(p1.x - p2.x);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/package-info.java
new file mode 100644 (file)
index 0000000..9423419
--- /dev/null
@@ -0,0 +1,5 @@
+package eu.svjatoslav.sixth.e3d.geometry;
+
+/**
+ * Goal is to provide basic geometry classes.
+ */
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
new file mode 100644 (file)
index 0000000..0baf2fa
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+
+/**
+ * Represents the viewer's camera in the 3D world, with position, orientation, and movement.
+ *
+ * <p>The camera is the user's "eyes" in the 3D scene. It has a position (location),
+ * a looking direction (defined by XZ and YZ angles), and a movement system with
+ * velocity, acceleration, and friction for smooth camera navigation.</p>
+ *
+ * <p>By default, the user can navigate using arrow keys (handled by
+ * {@link eu.svjatoslav.sixth.e3d.gui.humaninput.WorldNavigationUserInputTracker}),
+ * and the mouse controls the look direction (handled by
+ * {@link eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager}).</p>
+ *
+ * <p><b>Programmatic camera control:</b></p>
+ * <pre>{@code
+ * Camera camera = viewPanel.getCamera();
+ *
+ * // Set camera position
+ * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
+ *
+ * // Set camera orientation (radians)
+ * camera.getTransform().setRotation(0, 0);  // angleXZ, angleYZ
+ *
+ * // Copy camera state from another camera
+ * Camera snapshot = new Camera(camera);
+ * }</pre>
+ *
+ * @see ViewPanel#getCamera()
+ * @see eu.svjatoslav.sixth.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation
+ */
+public class Camera implements FrameListener {
+
+    /**
+     * Camera movement speed limit, relative to the world. When camera coordinates are
+     * updated within the world, camera orientation relative to the world is
+     * taken into account.
+     */
+    public static final double SPEED_LIMIT = 30;
+    /**
+     * Just in case we want to adjust global speed for some reason.
+     */
+    private static final double SPEED_MULTIPLIER = .02d;
+    /**
+     * Determines amount of friction user experiences every millisecond while moving around in space.
+     */
+    private static final double MILLISECOND_FRICTION = 1.005;
+    /**
+     * Camera movement speed, relative to camera itself. When camera coordinates
+     * are updated within the world, camera orientation relative to the world is
+     * taken into account.
+     */
+    private final Point3D movementVector = new Point3D();
+    private final Point3D previousLocation = new Point3D();
+    public double cameraAcceleration = 0.1;
+    /**
+     * The transform containing camera location and orientation.
+     */
+    private final Transform transform;
+
+    /**
+     * Creates a camera at the world origin with no rotation.
+     */
+    public Camera() {
+        transform = new Transform();
+    }
+
+    /**
+     * Creates a copy of an existing camera, cloning its position and orientation.
+     *
+     * @param sourceView the camera to copy
+     */
+    public Camera(final Camera sourceView) {
+        transform = sourceView.getTransform().clone();
+    }
+
+    public Camera(final Transform transform){
+        this.transform = transform;
+    }
+
+    @Override
+    public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
+
+        previousLocation.clone(transform.getTranslation());
+        translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame);
+        applyFrictionToMovement(millisecondsSinceLastFrame);
+        return isFrameRepaintNeeded();
+    }
+
+    private boolean isFrameRepaintNeeded() {
+        final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation);
+        return distanceMoved > 0.03;
+    }
+
+    /**
+     * Clamps the camera's movement speed to {@link #SPEED_LIMIT}.
+     * Called after modifying the movement vector to prevent excessive velocity.
+     */
+    public void enforceSpeedLimit() {
+        final double currentSpeed = movementVector.getVectorLength();
+
+        if (currentSpeed <= SPEED_LIMIT)
+            return;
+
+        movementVector.scaleDown(currentSpeed / SPEED_LIMIT);
+    }
+
+    /**
+     * Returns the current movement velocity vector, relative to the camera's orientation.
+     * Modify this vector to programmatically move the camera.
+     *
+     * @return the movement vector (mutable reference)
+     */
+    public Point3D getMovementVector() {
+        return movementVector;
+    }
+
+    /**
+     * Returns the current movement speed (magnitude of the movement vector).
+     *
+     * @return the scalar speed value
+     */
+    public double getMovementSpeed() {
+        return movementVector.getVectorLength();
+    }
+
+    /**
+     * Apply friction to camera movement vector.
+     *
+     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+     *                                         Therefore, we take frame rendering time into account when translating
+     *                                         camera between consecutive frames.
+     */
+    private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) {
+        for (int i = 0; i < millisecondsPassedSinceLastFrame; i++)
+            applyMillisecondFrictionToUserMovementVector();
+    }
+
+    /**
+     * Apply friction to camera movement vector.
+     */
+    private void applyMillisecondFrictionToUserMovementVector() {
+        movementVector.x /= MILLISECOND_FRICTION;
+        movementVector.y /= MILLISECOND_FRICTION;
+        movementVector.z /= MILLISECOND_FRICTION;
+    }
+
+    /**
+     * Translate coordinates based on camera movement vector and camera orientation in the world.
+     *
+     * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate.
+     *                                         Therefore, we take frame rendering time into account when translating
+     *                                         camera between consecutive frames.
+     */
+    private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) {
+        final double sinXZ = transform.getRotation().getSinXZ();
+        final double cosXZ = transform.getRotation().getCosXZ();
+
+        final Point3D location = transform.getTranslation();
+
+        location.x -= (float) sinXZ
+                * movementVector.z * SPEED_MULTIPLIER
+                * millisecondsPassedSinceLastFrame;
+        location.z += (float) cosXZ
+                * movementVector.z * SPEED_MULTIPLIER
+                * millisecondsPassedSinceLastFrame;
+
+        location.x += (float) cosXZ
+                * movementVector.x * SPEED_MULTIPLIER
+                * millisecondsPassedSinceLastFrame;
+        location.z += (float) sinXZ
+                * movementVector.x * SPEED_MULTIPLIER
+                * millisecondsPassedSinceLastFrame;
+
+        location.y += movementVector.y * SPEED_MULTIPLIER
+                * millisecondsPassedSinceLastFrame;
+    }
+
+    /**
+     * Returns the transform containing this camera's location and orientation.
+     *
+     * @return the transform (mutable reference)
+     */
+    public Transform getTransform() {
+        return transform;
+    }
+
+    /**
+     * Orients the camera to look at a target point in world coordinates.
+     *
+     * <p>Calculates the required XZ and YZ rotation angles to point the camera
+     * from its current position toward the target. Useful for programmatic
+     * camera control, cinematic sequences, and following objects.</p>
+     *
+     * <p><b>Example:</b></p>
+     * <pre>{@code
+     * Camera camera = viewPanel.getCamera();
+     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
+     * }</pre>
+     *
+     * @param target the world-space point to look at
+     */
+    public void lookAt(final Point3D target) {
+        final Point3D pos = transform.getTranslation();
+        final double dx = target.x - pos.x;
+        final double dy = target.y - pos.y;
+        final double dz = target.z - pos.z;
+
+        final double angleXZ = -Math.atan2(dx, dz);
+        final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
+        final double angleYZ = -Math.atan2(dy, horizontalDist);
+
+        transform.setRotation(angleXZ, angleYZ);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/FrameListener.java
new file mode 100644 (file)
index 0000000..dcfe7a6
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+/**
+ * Listener interface for per-frame callbacks before the 3D scene is rendered.
+ *
+ * <p>Implement this interface and register it with
+ * {@link ViewPanel#addFrameListener(FrameListener)} to receive a callback
+ * before each frame. This is the primary mechanism for implementing animations,
+ * physics updates, and other time-dependent behavior.</p>
+ *
+ * <p><b>Usage example - animating a shape:</b></p>
+ * <pre>{@code
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Rotate the shape a little each frame
+ *     double angleIncrement = deltaMs * 0.001;  // radians per millisecond
+ *     myShape.setTransform(new Transform(
+ *         myShape.getLocation(),
+ *         currentAngle += angleIncrement, 0
+ *     ));
+ *     return true;  // request repaint since we changed something
+ * });
+ * }</pre>
+ *
+ * <p>The engine uses the return values to optimize rendering: if no listener
+ * returns {@code true} and no other changes occurred, the frame is skipped
+ * to save CPU and energy.</p>
+ *
+ * @see ViewPanel#addFrameListener(FrameListener)
+ * @see ViewPanel#removeFrameListener(FrameListener)
+ */
+public interface FrameListener {
+
+    /**
+     * Called before each frame render, allowing the listener to update state
+     * and indicate whether a repaint is needed.
+     *
+     * <p>Each registered listener is called exactly once per frame tick.
+     * The frame is only rendered if at least one listener returns {@code true}
+     * (or if the view was explicitly marked for repaint).</p>
+     *
+     * @param viewPanel                  the view panel being rendered
+     * @param millisecondsSinceLastFrame time elapsed since the previous frame,
+     *                                    for frame-rate-independent updates
+     * @return {@code true} if the view should be re-rendered this frame,
+     *         {@code false} if this listener has no visual changes
+     */
+    boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame);
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/GuiComponent.java
new file mode 100644 (file)
index 0000000..cd3a357
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import eu.svjatoslav.sixth.e3d.geometry.Box;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardInputHandler;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox;
+
+import java.awt.event.KeyEvent;
+
+/**
+ * Base class for interactive GUI components rendered in 3D space.
+ *
+ * <p>{@code GuiComponent} combines a composite shape with keyboard and mouse interaction
+ * handling. When clicked, it acquires keyboard focus (via the {@link eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack}),
+ * and a red wireframe border is displayed to indicate focus. Pressing ESC releases focus.</p>
+ *
+ * <p>This class is the foundation for interactive widgets like the
+ * {@link eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextEditComponent}.</p>
+ *
+ * <p><b>Usage example - creating a custom GUI component:</b></p>
+ * <pre>{@code
+ * GuiComponent myWidget = new GuiComponent(
+ *     new Transform(new Point3D(0, 0, 300)),
+ *     viewPanel,
+ *     new Point3D(400, 300, 0)  // width, height, depth
+ * );
+ *
+ * // Add visual content to the widget
+ * myWidget.addShape(someTextCanvas);
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(myWidget);
+ * }</pre>
+ *
+ * @see eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack manages which component has keyboard focus
+ * @see eu.svjatoslav.sixth.e3d.gui.textEditorComponent.TextEditComponent a full text editor built on this class
+ */
+public class GuiComponent extends AbstractCompositeShape implements
+        KeyboardInputHandler, MouseInteractionController {
+
+    private static final String GROUP_GUI_FOCUS = "gui.focus";
+
+    /**
+     * The view panel this component is attached to.
+     */
+    public final ViewPanel viewPanel;
+    Box containingBox = new Box();
+    private WireframeBox borders = null;
+
+    private boolean borderShown = false;
+
+    /**
+     * Creates a GUI component with the specified transform, view panel, and bounding box size.
+     *
+     * @param transform the position and orientation of the component in 3D space
+     * @param viewPanel the view panel this component belongs to
+     * @param size      the bounding box dimensions (width, height, depth)
+     */
+    public GuiComponent(final Transform transform,
+                        final ViewPanel viewPanel, final Point3D size) {
+        super(transform);
+        this.viewPanel = viewPanel;
+        setDimensions(size);
+    }
+
+    private WireframeBox createBorder() {
+        final LineAppearance appearance = new LineAppearance(10,
+                new eu.svjatoslav.sixth.e3d.renderer.raster.Color(255, 0, 0, 100));
+
+        final double borderSize = 10;
+
+        final Box borderArea = containingBox.clone().enlarge(borderSize);
+
+        return new WireframeBox(borderArea, appearance);
+    }
+
+    @Override
+    public boolean focusLost(final ViewPanel viewPanel) {
+        hideBorder();
+        return true;
+    }
+
+    @Override
+    public boolean focusReceived(final ViewPanel viewPanel) {
+        showBorder();
+        return true;
+    }
+
+    public WireframeBox getBorders() {
+        if (borders == null)
+            borders = createBorder();
+        return borders;
+    }
+
+    public int getDepth() {
+        return (int) containingBox.getDepth();
+    }
+
+    public int getHeight() {
+        return (int) containingBox.getHeight();
+    }
+
+    public int getWidth() {
+        return (int) containingBox.getWidth();
+    }
+
+    public void hideBorder() {
+        if (!borderShown)
+            return;
+        borderShown = false;
+        removeGroup(GROUP_GUI_FOCUS);
+    }
+
+    @Override
+    public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) {
+        if (event.getKeyChar() == KeyboardHelper.ESC)
+            viewPanel.getKeyboardFocusStack().popFocusOwner();
+        return true;
+    }
+
+    @Override
+    public boolean keyReleased(final KeyEvent event, final ViewPanel viewPanel) {
+        return false;
+    }
+
+    @Override
+    public boolean mouseClicked(int button) {
+        return viewPanel.getKeyboardFocusStack().pushFocusOwner(this);
+    }
+
+    @Override
+    public boolean mouseEntered() {
+        return false;
+    }
+
+    @Override
+    public boolean mouseExited() {
+        return false;
+    }
+
+    private void setDimensions(final Point3D size) {
+        containingBox.setBoxSize(size);
+    }
+
+    private void showBorder() {
+        if (borderShown)
+            return;
+        borderShown = true;
+        addShape(getBorders(), GROUP_GUI_FOCUS);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java
new file mode 100644 (file)
index 0000000..d072874
--- /dev/null
@@ -0,0 +1,187 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseEvent;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.awt.image.WritableRaster;
+
+/**
+ * Contains all state needed to render a single frame: the pixel buffer, graphics context,
+ * screen dimensions, and mouse event tracking.
+ *
+ * <p>A new {@code RenderingContext} is created whenever the view panel is resized.
+ * During rendering, shapes use this context to:</p>
+ * <ul>
+ *   <li>Access the raw pixel array ({@link #pixels}) for direct pixel manipulation</li>
+ *   <li>Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing</li>
+ *   <li>Read screen dimensions ({@link #width}, {@link #height}) and the
+ *       {@link #centerCoordinate} for coordinate projection</li>
+ *   <li>Use the {@link #projectionScale} factor for perspective projection</li>
+ * </ul>
+ *
+ * <p>The context also manages mouse interaction detection: as shapes are painted
+ * back-to-front, each shape can report itself as the object under the mouse cursor.
+ * After painting completes, the topmost shape receives the mouse event.</p>
+ *
+ * @see ViewPanel the panel that creates and manages this context
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext)
+ */
+public class RenderingContext {
+
+    /**
+     * The {@link BufferedImage} pixel format used for the rendering buffer (4-byte ABGR).
+     */
+    public static final int bufferedImageType = BufferedImage.TYPE_4BYTE_ABGR;
+
+    /**
+     * Java2D graphics context for drawing text, anti-aliased shapes, and other
+     * high-level graphics operations onto the render buffer.
+     */
+    public final Graphics2D graphics;
+
+    /**
+     * Pixels of the rendering area.
+     * Each pixel is represented by 4 bytes: alpha, blue, green, red.
+     */
+    public final byte[] pixels;
+
+    /**
+     * Width of the rendering area in pixels.
+     */
+    public final int width;
+
+    /**
+     * Height of the rendering area in pixels.
+     */
+    public final int height;
+
+    /**
+     * Center of the screen in screen space (pixels).
+     * This is the point where (0,0) coordinate of the world space is rendered.
+     */
+    public final Point2D centerCoordinate;
+
+    /**
+     * Scale factor for perspective projection, derived from screen width.
+     * Used to convert normalized device coordinates to screen pixels.
+     */
+    public final double projectionScale;
+    final BufferedImage bufferedImage;
+    /**
+     * Number of frame that is currently being rendered.
+     * Every frame has its own number.
+     */
+    public int frameNumber = 0;
+
+    /**
+     * UI component that mouse is currently hovering over.
+     */
+    private MouseInteractionController objectPreviouslyUnderMouseCursor;
+    /**
+     * Mouse click event that needs to be processed.
+     * This event is processed only once per frame.
+     * If there are multiple objects under mouse cursor, the top-most object will receive the event.
+     * If there are no objects under mouse cursor, the event will be ignored.
+     * If there is no event, this field will be null.
+     * This field is set to null after the event is processed.
+     */
+    private MouseEvent mouseEvent;
+    /**
+     * UI component that mouse is currently hovering over.
+     */
+    private MouseInteractionController currentObjectUnderMouseCursor;
+
+    /**
+     * Creates a new rendering context with the specified pixel dimensions.
+     *
+     * <p>Initializes the offscreen image buffer, extracts the raw pixel byte array,
+     * and configures anti-aliasing on the Graphics2D context.</p>
+     *
+     * @param width  the rendering area width in pixels
+     * @param height the rendering area height in pixels
+     */
+    public RenderingContext(final int width, final int height) {
+        this.width = width;
+        this.height = height;
+        this.centerCoordinate = new Point2D(width / 2d, height / 2d);
+        this.projectionScale = width / 3d;
+
+        bufferedImage = new BufferedImage(width, height, bufferedImageType);
+
+        final WritableRaster raster = bufferedImage.getRaster();
+        final DataBufferByte dbi = (DataBufferByte) raster.getDataBuffer();
+        pixels = dbi.getData();
+
+        graphics = (Graphics2D) bufferedImage.getGraphics();
+        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+    }
+
+    /**
+     * Resets per-frame state in preparation for rendering a new frame.
+     * Clears the mouse event and the current object under the mouse cursor.
+     */
+    public void prepareForNewFrameRendering() {
+        mouseEvent = null;
+        currentObjectUnderMouseCursor = null;
+    }
+
+    /**
+     * Returns the pending mouse event for this frame, or {@code null} if none.
+     *
+     * @return the mouse event to process, or {@code null}
+     */
+    public MouseEvent getMouseEvent() {
+        return mouseEvent;
+    }
+
+    /**
+     * Sets the mouse event to be processed during this frame's rendering.
+     *
+     * @param mouseEvent the mouse event with position and button information
+     */
+    public void setMouseEvent(MouseEvent mouseEvent) {
+        this.mouseEvent = mouseEvent;
+    }
+
+    /**
+     * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}.
+     * Because objects are rendered back to front. The last method caller will set the top-most object, if
+     * there are multiple objects under mouse cursor.
+     */
+    public void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) {
+        this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor;
+    }
+
+    /**
+     * @return <code>true</code> if view update is needed as a consequence of this mouse event.
+     */
+    public boolean handlePossibleComponentMouseEvent() {
+        if (mouseEvent == null) return false;
+
+        boolean viewRepaintNeeded = false;
+
+        if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) {
+            // Mouse cursor has just entered or left component.
+            viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited();
+            viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered();
+            objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor;
+        }
+
+        if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) {
+            // Mouse button was clicked on some component.
+            viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked(mouseEvent.button);
+        }
+
+        return viewRepaintNeeded;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/TextPointer.java
new file mode 100755 (executable)
index 0000000..91cd6e6
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import static java.lang.Integer.compare;
+
+/**
+ * A pointer to a character in a text using row and column.
+ * <p>
+ * It can be used to represent a cursor position in a text.
+ * Also, it can be used to represent beginning and end of a selection.
+ */
+public class TextPointer implements Comparable<TextPointer> {
+
+    /**
+     * The row of the character. Starts from 0.
+     */
+    public int row;
+
+    /**
+     * The column of the character. Starts from 0.
+     */
+    public int column;
+
+    public TextPointer() {
+        this(0, 0);
+    }
+
+    public TextPointer(final int row, final int column) {
+        this.row = row;
+        this.column = column;
+    }
+
+    public TextPointer(final TextPointer parent) {
+        this(parent.row, parent.column);
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (o == null) return false;
+
+        return o instanceof TextPointer && compareTo((TextPointer) o) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = row;
+        result = 31 * result + column;
+        return result;
+    }
+
+    /**
+     * Compares this pointer to another pointer.
+     *
+     * @param textPointer The pointer to compare to.
+     * @return <ul>
+     *     <li>-1 if this pointer is smaller than the argument pointer.</li>
+     *     <li>0 if they are equal.</li>
+     *     <li>1 if this pointer is bigger than the argument pointer.</li>
+     *     </ul>
+     */
+    @Override
+    public int compareTo(final TextPointer textPointer) {
+
+        if (row < textPointer.row)
+            return -1;
+        if (row > textPointer.row)
+            return 1;
+
+        return compare(column, textPointer.column);
+    }
+
+    /**
+     * Checks if this pointer is between the argument pointers.
+     * <p>
+     * This pointer is considered to be between the pointers if it is bigger or equal to the start pointer
+     * and smaller than the end pointer.
+     *
+     * @param start The start pointer.
+     * @param end   The end pointer.
+     * @return True if this pointer is between the specified pointers.
+     */
+    public boolean isBetween(final TextPointer start, final TextPointer end) {
+
+        if (start == null)
+            return false;
+
+        if (end == null)
+            return false;
+
+        // Make sure that start is smaller than end.
+        TextPointer smaller;
+        TextPointer bigger;
+
+        if (end.compareTo(start) >= 0) {
+            smaller = start;
+            bigger = end;
+        } else {
+            smaller = end;
+            bigger = start;
+        }
+
+        // Check if this pointer is between the specified pointers.
+        return (compareTo(smaller) >= 0) && (bigger.compareTo(this) > 0);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewFrame.java
new file mode 100755 (executable)
index 0000000..e4c084a
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+/**
+ * Convenience window (JFrame) that creates and hosts a {@link ViewPanel} for 3D rendering.
+ *
+ * <p>This is the simplest way to get a 3D view up and running. The frame starts
+ * maximized, enforces a minimum size of 400x400, and handles window lifecycle
+ * events (minimizing, restoring, closing) automatically.</p>
+ *
+ * <p><b>Quick start:</b></p>
+ * <pre>{@code
+ * // Create a window with a 3D view
+ * ViewFrame frame = new ViewFrame();
+ *
+ * // Access the view panel to add shapes and configure the scene
+ * ViewPanel viewPanel = frame.getViewPanel();
+ * viewPanel.getRootShapeCollection().addShape(
+ *     new WireframeCube(new Point3D(0, 0, 200), 50,
+ *         new LineAppearance(5, Color.GREEN))
+ * );
+ *
+ * // To close programmatically:
+ * frame.exit();
+ * }</pre>
+ *
+ * @see ViewPanel the embedded 3D rendering panel
+ */
+public class ViewFrame extends JFrame implements WindowListener {
+
+    private static final long serialVersionUID = -7037635097739548470L;
+
+    private final ViewPanel viewPanel;
+
+    /**
+     * Creates a new maximized window with a 3D view.
+     */
+    public ViewFrame() {
+        this(-1, -1, true);
+    }
+
+    /**
+     * Creates a new window with a 3D view at the specified size.
+     *
+     * @param width  window width in pixels, or -1 for default
+     * @param height window height in pixels, or -1 for default
+     */
+    public ViewFrame(final int width, final int height) {
+        this(width, height, false);
+    }
+
+    private ViewFrame(final int width, final int height, final boolean maximize) {
+        setTitle("3D engine");
+
+        addWindowListener(new java.awt.event.WindowAdapter() {
+            @Override
+            public void windowClosing(final java.awt.event.WindowEvent e) {
+                exit();
+            }
+        });
+
+        viewPanel = new ViewPanel();
+
+        add(getViewPanel());
+
+        if (width > 0 && height > 0) {
+            setSize(width, height);
+        } else {
+            setSize(800, 600);
+        }
+
+        if (maximize) {
+            setExtendedState(JFrame.MAXIMIZED_BOTH);
+        }
+        setVisible(true);
+
+        addResizeListener();
+        addWindowListener(this);
+    }
+
+    private void addResizeListener() {
+        addComponentListener(new ComponentListener() {
+            // This method is called after the component's size changes
+            @Override
+            public void componentHidden(final ComponentEvent e) {
+            }
+
+            @Override
+            public void componentMoved(final ComponentEvent e) {
+            }
+
+            @Override
+            public void componentResized(final ComponentEvent evt) {
+
+                final Component c = (Component) evt.getSource();
+
+                // Get new size
+                final Dimension newSize = c.getSize();
+
+                boolean sizeFixed = false;
+
+                if (newSize.width < 400) {
+                    newSize.width = 400;
+                    sizeFixed = true;
+                }
+
+                if (newSize.height < 400) {
+                    newSize.height = 400;
+                    sizeFixed = true;
+                }
+
+                if (sizeFixed)
+                    setSize(newSize);
+
+            }
+
+            @Override
+            public void componentShown(final ComponentEvent e) {
+                viewPanel.repaintDuringNextViewUpdate();
+            }
+
+        });
+    }
+
+    /**
+     * Exit the application.
+     */
+    public void exit() {
+        if (getViewPanel() != null) {
+            getViewPanel().stop();
+            getViewPanel().setEnabled(false);
+            getViewPanel().setVisible(false);
+        }
+        dispose();
+    }
+
+    @Override
+    public java.awt.Dimension getPreferredSize() {
+        return new java.awt.Dimension(640, 480);
+    }
+
+    /**
+     * Returns the embedded {@link ViewPanel} for adding shapes and configuring the scene.
+     *
+     * @return the view panel contained in this frame
+     */
+    public ViewPanel getViewPanel() {
+        return viewPanel;
+    }
+
+    @Override
+    public void windowActivated(final WindowEvent e) {
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+    @Override
+    public void windowClosed(final WindowEvent e) {
+    }
+
+    @Override
+    public void windowClosing(final WindowEvent e) {
+    }
+
+    @Override
+    public void windowDeactivated(final WindowEvent e) {
+    }
+
+    /**
+     * Repaint the view when the window is deiconified.
+     *
+     * Deiconified means that the window is restored from minimized state.
+     */
+    @Override
+    public void windowDeiconified(final WindowEvent e) {
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+    /**
+     * Do nothing when the window is iconified.
+     *
+     * Iconified means that the window is minimized.
+     * @param e the event to be processed
+     */
+    @Override
+    public void windowIconified(final WindowEvent e) {
+    }
+
+    @Override
+    public void windowOpened(final WindowEvent e) {
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java
new file mode 100755 (executable)
index 0000000..25a73f5
--- /dev/null
@@ -0,0 +1,407 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import eu.svjatoslav.sixth.e3d.gui.humaninput.InputManager;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack;
+import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Java Swing panel that provides a 3D rendering canvas with built-in camera navigation.
+ *
+ * <p>{@code ViewPanel} is the primary entry point for embedding the Sixth 3D engine into
+ * a Java Swing application. It manages the render loop, maintains a scene graph
+ * ({@link ShapeCollection}), and handles user input for camera navigation.</p>
+ *
+ * <p><b>Quick start - creating a 3D view in a window:</b></p>
+ * <pre>{@code
+ * // Option 1: Use ViewFrame (creates a maximized JFrame for you)
+ * ViewFrame frame = new ViewFrame();
+ * ViewPanel viewPanel = frame.getViewPanel();
+ *
+ * // Option 2: Embed ViewPanel in your own Swing layout
+ * JFrame frame = new JFrame("My 3D App");
+ * ViewPanel viewPanel = new ViewPanel();
+ * frame.add(viewPanel);
+ * frame.setSize(800, 600);
+ * frame.setVisible(true);
+ *
+ * // Add shapes to the scene
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ * scene.addShape(new WireframeCube(
+ *     new Point3D(0, 0, 200), 50,
+ *     new LineAppearance(5, Color.GREEN)
+ * ));
+ *
+ * // Position the camera
+ * viewPanel.getCamera().setLocation(new Point3D(0, 0, -100));
+ *
+ * // Listen for frame updates (e.g., for animations)
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Called before each frame. Return true to force repaint.
+ *     return false;
+ * });
+ * }</pre>
+ *
+ * <p><b>Architecture:</b></p>
+ * <ul>
+ *   <li>A background render thread continuously generates frames at the target FPS</li>
+ *   <li>The engine intelligently skips rendering when no visual changes are detected</li>
+ *   <li>{@link FrameListener}s are notified before each potential frame, enabling animations</li>
+ *   <li>Mouse/keyboard input is managed by {@link InputManager}</li>
+ *   <li>Keyboard focus is managed by {@link KeyboardFocusStack}</li>
+ * </ul>
+ *
+ * @see ViewFrame convenience window wrapper
+ * @see ShapeCollection the scene graph
+ * @see Camera the camera/viewer
+ * @see FrameListener for per-frame callbacks
+ */
+public class ViewPanel extends JPanel implements ComponentListener {
+    private static final long serialVersionUID = 1683277888885045387L;
+    private final InputManager inputManager = new InputManager(this);
+    private final KeyboardFocusStack keyboardFocusStack;
+    private final Camera camera = new Camera();
+    private final ShapeCollection rootShapeCollection = new ShapeCollection();
+    private final Set<FrameListener> frameListeners = ConcurrentHashMap.newKeySet();
+    public Color backgroundColor = Color.BLACK;
+
+    /**
+     * Stores milliseconds when the last frame was updated. This is needed to calculate the time delta between frames.
+     * Time delta is used to calculate smooth animation.
+     */
+    private long lastUpdateMillis = 0;
+
+    private RenderingContext renderingContext = null;
+
+    /**
+     * Currently target frames per second rate for this view. Target FPS can be changed at runtime.
+     * 3D engine tries to be smart and only repaints screen when there are visible changes.
+     */
+    private int targetFPS = 60;
+
+    /**
+     * Set to true if it is known than next frame reeds to be painted. Flag is cleared
+     * immediately after frame got updated.
+     */
+    private boolean viewRepaintNeeded = true;
+
+    /**
+     * Render thread that runs the continuous frame generation loop.
+     */
+    private Thread renderThread;
+
+    /**
+     * Flag to control whether the render thread should keep running.
+     */
+    private volatile boolean renderThreadRunning = false;
+
+    private long nextFrameTime;
+
+    public ViewPanel() {
+        frameListeners.add(camera);
+        frameListeners.add(inputManager);
+
+        keyboardFocusStack = new KeyboardFocusStack(this);
+
+        initializePanelLayout();
+
+        startRenderThread();
+
+        addComponentListener(this);
+    }
+
+    /**
+     * Returns the camera that represents the viewer's position and
+     * orientation in the 3D world. Use this to programmatically move the camera.
+     *
+     * @return the camera for this view
+     */
+    public Camera getCamera() {
+        return camera;
+    }
+
+    /**
+     * Returns the keyboard focus stack, which manages which component receives
+     * keyboard input.
+     *
+     * @return the keyboard focus stack
+     */
+    public KeyboardFocusStack getKeyboardFocusStack() {
+        return keyboardFocusStack;
+    }
+
+    /**
+     * Returns the root shape collection (scene graph). Add your 3D shapes here
+     * to make them visible in the view.
+     *
+     * <pre>{@code
+     * viewPanel.getRootShapeCollection().addShape(myShape);
+     * }</pre>
+     *
+     * @return the root shape collection
+     */
+    public ShapeCollection getRootShapeCollection() {
+        return rootShapeCollection;
+    }
+
+    /**
+     * Returns the human input device (mouse/keyboard) event tracker.
+     *
+     * @return the HID event tracker
+     */
+    /**
+     * Returns the input manager handling mouse and keyboard events for this view.
+     *
+     * @return the input manager
+     */
+    public InputManager getInputManager() {
+        return inputManager;
+    }
+
+    /**
+     * Registers a listener that will be notified before each frame render.
+     * Listeners can trigger repaints by returning {@code true} from
+     * {@link FrameListener#onFrame}.
+     *
+     * @param listener the listener to add
+     * @see #removeFrameListener(FrameListener)
+     */
+    public void addFrameListener(final FrameListener listener) {
+        frameListeners.add(listener);
+    }
+
+    @Override
+    public void componentHidden(final ComponentEvent e) {
+
+    }
+
+    @Override
+    public void componentMoved(final ComponentEvent e) {
+
+    }
+
+    @Override
+    public void componentResized(final ComponentEvent e) {
+        viewRepaintNeeded = true;
+    }
+
+    @Override
+    public void componentShown(final ComponentEvent e) {
+        viewRepaintNeeded = true;
+    }
+
+    @Override
+    public Dimension getMaximumSize() {
+        return getPreferredSize();
+    }
+
+    @Override
+    public Dimension getMinimumSize() {
+        return getPreferredSize();
+    }
+
+    @Override
+    public java.awt.Dimension getPreferredSize() {
+        return new java.awt.Dimension(640, 480);
+    }
+
+    public RenderingContext getRenderingContext() {
+        return renderingContext;
+    }
+
+    private void initializePanelLayout() {
+        setFocusCycleRoot(true);
+        setOpaque(true);
+        setFocusable(true);
+        setDoubleBuffered(false);
+        setVisible(true);
+        requestFocusInWindow();
+    }
+
+    private void renderFrame() {
+        // paint root geometry collection to the offscreen render buffer
+        clearCanvas();
+        rootShapeCollection.paint(this, renderingContext);
+
+        // draw rendered offscreen buffer to visible screen
+        final Graphics graphics = getGraphics();
+        if (graphics != null)
+            graphics.drawImage(renderingContext.bufferedImage, 0, 0, null);
+    }
+
+    private void clearCanvas() {
+        renderingContext.graphics.setColor(backgroundColor);
+        renderingContext.graphics.fillRect(0, 0, getWidth(), getHeight());
+    }
+
+    /**
+     * Calling these methods tells 3D engine that current 3D view needs to be
+     * repainted on first opportunity.
+     */
+    public void repaintDuringNextViewUpdate() {
+        viewRepaintNeeded = true;
+    }
+
+    /**
+     * Set target frames per second rate for this view. Target FPS can be changed at runtime.
+     * Use 0 or negative value for unlimited FPS (max performance mode for benchmarking).
+     *
+     * @param frameRate target frames per second rate for this view.
+     */
+    public void setFrameRate(final int frameRate) {
+        targetFPS = frameRate;
+    }
+
+    /**
+     * Stops rendering of this view.
+     */
+    public void stop() {
+        renderThreadRunning = false;
+        if (renderThread != null) {
+            try {
+                renderThread.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            renderThread = null;
+        }
+    }
+
+    /**
+     * Starts the render thread that continuously generates frames.
+     */
+    private void startRenderThread() {
+        renderThreadRunning = true;
+        renderThread = new Thread(this::renderLoop, "RenderThread");
+        renderThread.setDaemon(true);
+        renderThread.start();
+    }
+
+    /**
+     * Main render loop that generates frames continuously.
+     * Supports both unlimited FPS and fixed FPS modes with dynamic sleep adjustment.
+     */
+    private void renderLoop() {
+        nextFrameTime = System.currentTimeMillis();
+
+        while (renderThreadRunning) {
+            ensureThatViewIsUpToDate();
+
+            if (maintainTargetFps()) break;
+        }
+    }
+
+    /**
+     * Ensures that the rendering process maintains the target frames per second (FPS)
+     * by dynamically adjusting the thread sleep duration.
+     *
+     * @return {@code true} if the thread was interrupted while sleeping, otherwise {@code false}.
+     */
+    private boolean maintainTargetFps() {
+        if (targetFPS <= 0) return false;
+
+        long now = System.currentTimeMillis();
+
+        nextFrameTime += 1000L / targetFPS;
+
+        // If we've fallen behind, reset to now instead of trying to catch up
+        if (nextFrameTime < now)
+            nextFrameTime = now;
+
+        long sleepTime = nextFrameTime - now;
+        if (sleepTime > 0) {
+            try {
+                Thread.sleep(sleepTime);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This method is executed by periodic timer task, in frequency according to
+     * defined frame rate.
+     * <p>
+     * It tells view to update itself. View can decide if actual re-rendering of
+     * graphics is needed.
+     */
+    void ensureThatViewIsUpToDate() {
+        maintainRenderingContext();
+
+        final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate();
+
+        boolean renderFrame = notifyFrameListeners(millisecondsPassedSinceLastUpdate);
+
+        if (viewRepaintNeeded) {
+            viewRepaintNeeded = false;
+            renderFrame = true;
+        }
+
+        // abort rendering if window size is invalid
+        if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) {
+            renderFrame();
+            viewRepaintNeeded = renderingContext.handlePossibleComponentMouseEvent();
+        }
+
+    }
+
+    private void maintainRenderingContext() {
+        int panelWidth = getWidth();
+        int panelHeight = getHeight();
+
+        if (panelWidth <= 0 || panelHeight <= 0) {
+            renderingContext = null;
+            return;
+        }
+
+        // create new rendering context if window size has changed
+        if ((renderingContext == null)
+                || (renderingContext.width != panelWidth)
+                || (renderingContext.height != panelHeight)) {
+            renderingContext = new RenderingContext(panelWidth, panelHeight);
+        }
+
+        renderingContext.prepareForNewFrameRendering();
+    }
+
+    private boolean notifyFrameListeners(int millisecondsPassedSinceLastUpdate) {
+        boolean reRenderFrame = false;
+        for (final FrameListener listener : frameListeners)
+            if (listener.onFrame(this, millisecondsPassedSinceLastUpdate))
+                reRenderFrame = true;
+        return reRenderFrame;
+    }
+
+    private int getMillisecondsPassedSinceLastUpdate() {
+        final long currentTime = System.currentTimeMillis();
+
+        if (lastUpdateMillis == 0)
+            lastUpdateMillis = currentTime;
+
+        final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis);
+        lastUpdateMillis = currentTime;
+        return millisecondsPassedSinceLastUpdate;
+    }
+
+    /**
+     * Removes a previously registered frame listener.
+     *
+     * @param frameListener the listener to remove
+     */
+    public void removeFrameListener(FrameListener frameListener) {
+        frameListeners.remove(frameListener);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewSpaceTracker.java
new file mode 100644 (file)
index 0000000..36cb220
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.TransformStack;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+
+/**
+ * Tracks an object's position in view/camera space for distance and angle calculations.
+ *
+ * <p>Used primarily for level-of-detail (LOD) decisions based on how far and at what
+ * angle the viewer is from an object. The tracker maintains the object's center point
+ * transformed into view space, and optionally orientation axes for angle calculations.</p>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape
+ */
+public class ViewSpaceTracker {
+
+    private final static int minimalSliceFactor = 5;
+
+    /**
+     * The object's center point (0,0,0 in object space) transformed to view space.
+     */
+    public Vertex center = new Vertex();
+
+    /**
+     * Point at (10,0,0) in object space, used for XZ angle calculation.
+     * Only initialized if orientation tracking is enabled.
+     */
+    public Vertex right;
+
+    /**
+     * Point at (0,10,0) in object space, used for YZ angle calculation.
+     * Only initialized if orientation tracking is enabled.
+     */
+    public Vertex down;
+
+    public ViewSpaceTracker() {
+    }
+
+    /**
+     * Transforms the tracked points from object space to view space.
+     *
+     * @param transformPipe     the current transform stack
+     * @param renderingContext  the rendering context for frame info
+     */
+    public void analyze(final TransformStack transformPipe,
+                        final RenderingContext renderingContext) {
+
+        center.calculateLocationRelativeToViewer(transformPipe, renderingContext);
+
+        if (right != null) {
+            right.calculateLocationRelativeToViewer(transformPipe, renderingContext);
+            down.calculateLocationRelativeToViewer(transformPipe, renderingContext);
+        }
+    }
+
+    /**
+     * Enables tracking of orientation axes for angle calculations.
+     * Disabled by default to save computation when angles are not needed.
+     */
+    public void enableOrientationTracking() {
+        right = new Vertex(new Point3D(10, 0, 0));
+        down = new Vertex(new Point3D(0, 10, 0));
+    }
+
+    /**
+     * Returns the angle between the viewer and object in the XY plane.
+     *
+     * @return the XY angle in radians
+     */
+    public double getAngleXY() {
+        return center.transformedCoordinate
+                .getAngleXY(down.transformedCoordinate);
+    }
+
+    /**
+     * Returns the angle between the viewer and object in the XZ plane.
+     *
+     * @return the XZ angle in radians
+     */
+    public double getAngleXZ() {
+        return center.transformedCoordinate
+                .getAngleXZ(right.transformedCoordinate);
+    }
+
+    /**
+     * Returns the angle between the viewer and object in the YZ plane.
+     *
+     * @return the YZ angle in radians
+     */
+    public double getAngleYZ() {
+        return center.transformedCoordinate
+                .getAngleYZ(down.transformedCoordinate);
+    }
+
+    /**
+     * Returns the distance from the camera to the object's center.
+     * Used for level-of-detail calculations.
+     *
+     * @return the distance in world units
+     */
+    public double getDistanceToCamera() {
+        return center.transformedCoordinate.getVectorLength();
+    }
+
+    /**
+     * Proposes a slice factor for texture LOD based on distance to camera.
+     *
+     * @return the proposed slice factor
+     */
+    public double proposeSliceFactor() {
+        final double distanceToCamera = getDistanceToCamera();
+
+        double proposedSliceFactor = distanceToCamera / 5;
+
+        if (proposedSliceFactor < minimalSliceFactor)
+            proposedSliceFactor = minimalSliceFactor;
+
+        return proposedSliceFactor;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewUpdateTimerTask.java
new file mode 100755 (executable)
index 0000000..8594a2c
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui;
+
+/**
+ * Timer task that updates view.
+ *
+ * Tries to keep constant FPS.
+ */
+public class ViewUpdateTimerTask extends java.util.TimerTask {
+
+    public ViewPanel viewPanel;
+
+    public ViewUpdateTimerTask(final ViewPanel viewPanel) {
+        this.viewPanel = viewPanel;
+    }
+
+    @Override
+    public void run() {
+        viewPanel.ensureThatViewIsUpToDate();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/Connexion3D.java
new file mode 100644 (file)
index 0000000..027dbf0
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * I have Space Mouse Compact 3D Connexion mouse: https://3dconnexion.com/us/product/spacemouse-compact/
+ *
+ * I discovered that it is possible to read raw data from it by reading /dev/hidraw4 file.
+ *
+ * TODO: reverse engineer the data format and implement a driver for it.
+ */
+
+public class Connexion3D {
+
+    public static void main(final String[] args) throws IOException {
+
+        final BufferedReader in = new BufferedReader(new FileReader(
+                "/dev/hidraw4"));
+
+
+        // for testing purposes
+        while (true) {
+            System.out.print(in.read() + " ");
+            System.out.println("\n");
+        }
+
+        // in.close();
+
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java
new file mode 100644 (file)
index 0000000..9fb4e16
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.gui.FrameListener;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.math.Rotation;
+
+import javax.swing.*;
+import java.awt.event.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages mouse and keyboard input for the 3D view.
+ *
+ * <p>Handles Swing mouse/keyboard events, tracks pressed keys and mouse state,
+ * and forwards events to the appropriate handlers. Also provides default camera
+ * control via mouse dragging (look around) and mouse wheel (vertical movement).</p>
+ *
+ * @see ViewPanel#getInputManager()
+ */
+public class InputManager implements
+        MouseMotionListener, KeyListener, MouseListener, MouseWheelListener, FrameListener {
+
+    private final Map<Integer, Long> pressedKeysToPressedTimeMap = new HashMap<>();
+    private final List<MouseEvent> detectedMouseEvents = new ArrayList<>();
+    private final List<KeyEvent> detectedKeyEvents = new ArrayList<>();
+    private final Point2D mouseDelta = new Point2D();
+    private final ViewPanel viewPanel;
+    private int wheelMovedDirection = 0;
+    private Point2D oldMouseCoordinatesWhenDragging;
+    private Point2D currentMouseLocation;
+    private boolean mouseMoved;
+    private boolean mouseWithinWindow = false;
+
+    public InputManager(final ViewPanel viewPanel) {
+        this.viewPanel = viewPanel;
+        bind(viewPanel);
+    }
+
+    @Override
+    public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) {
+        boolean viewUpdateNeeded = handleKeyboardEvents();
+        viewUpdateNeeded |= handleMouseClicksAndHover(viewPanel);
+        viewUpdateNeeded |= handleMouseDragging();
+        viewUpdateNeeded |= handleMouseVerticalScrolling();
+        return viewUpdateNeeded;
+    }
+
+    private void bind(final JPanel panel) {
+        panel.addMouseMotionListener(this);
+        panel.addKeyListener(this);
+        panel.addMouseListener(this);
+        panel.addMouseWheelListener(this);
+    }
+
+    private boolean handleKeyboardEvents() {
+        final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner();
+        ArrayList<KeyEvent> unprocessedKeyboardEvents = getUnprocessedKeyboardEvents();
+
+        return currentFocusOwner != null
+                && forwardKeyboardEventsToFocusOwner(currentFocusOwner, unprocessedKeyboardEvents);
+    }
+
+    private ArrayList<KeyEvent> getUnprocessedKeyboardEvents() {
+        synchronized (detectedKeyEvents) {
+            ArrayList<KeyEvent> result = new ArrayList<>(detectedKeyEvents);
+            detectedKeyEvents.clear();
+            return result;
+        }
+    }
+
+    private boolean forwardKeyboardEventsToFocusOwner(
+            KeyboardInputHandler currentFocusOwner, ArrayList<KeyEvent> keyEvents) {
+        boolean viewUpdateNeeded = false;
+
+        for (KeyEvent keyEvent : keyEvents)
+            viewUpdateNeeded |= processKeyEvent(currentFocusOwner, keyEvent);
+
+        return viewUpdateNeeded;
+    }
+
+    private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) {
+        switch (keyEvent.getID()) {
+            case KeyEvent.KEY_PRESSED:
+                return currentFocusOwner.keyPressed(keyEvent, viewPanel);
+
+            case KeyEvent.KEY_RELEASED:
+                return currentFocusOwner.keyReleased(keyEvent, viewPanel);
+        }
+        return false;
+    }
+
+    private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) {
+        boolean rerenderNeeded = false;
+        MouseEvent event = findClickLocationToTrace();
+        if (event != null) {
+            rerenderNeeded = true;
+        } else {
+            if (mouseMoved) {
+                mouseMoved = false;
+                rerenderNeeded = true;
+            }
+
+            if (currentMouseLocation != null) {
+                event = new MouseEvent(currentMouseLocation, 0);
+            }
+        }
+
+        if (viewPanel.getRenderingContext() != null)
+            viewPanel.getRenderingContext().setMouseEvent(event);
+
+        return rerenderNeeded;
+    }
+
+    private MouseEvent findClickLocationToTrace() {
+        synchronized (detectedMouseEvents) {
+            if (detectedMouseEvents.isEmpty())
+                return null;
+
+            return detectedMouseEvents.remove(0);
+        }
+    }
+
+    /**
+     * Returns whether the specified key is currently pressed.
+     *
+     * @param keyCode the key code (from {@link java.awt.event.KeyEvent})
+     * @return {@code true} if the key is currently pressed
+     */
+    public boolean isKeyPressed(final int keyCode) {
+        return pressedKeysToPressedTimeMap.containsKey(keyCode);
+    }
+
+    @Override
+    public void keyPressed(final KeyEvent evt) {
+        synchronized (detectedKeyEvents) {
+            pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis());
+            detectedKeyEvents.add(evt);
+        }
+    }
+
+    @Override
+    public void keyReleased(final KeyEvent evt) {
+        synchronized (detectedKeyEvents) {
+            pressedKeysToPressedTimeMap.remove(evt.getKeyCode());
+            detectedKeyEvents.add(evt);
+        }
+    }
+
+    @Override
+    public void keyTyped(final KeyEvent e) {
+    }
+
+    @Override
+    public void mouseClicked(final java.awt.event.MouseEvent e) {
+        synchronized (detectedMouseEvents) {
+            detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton()));
+        }
+    }
+
+    @Override
+    public void mouseDragged(final java.awt.event.MouseEvent evt) {
+        final Point2D mouseLocation = new Point2D(evt.getX(), evt.getY());
+
+        if (oldMouseCoordinatesWhenDragging == null) {
+            oldMouseCoordinatesWhenDragging = mouseLocation;
+            return;
+        }
+
+        mouseDelta.add(mouseLocation.clone().subtract(oldMouseCoordinatesWhenDragging));
+
+        oldMouseCoordinatesWhenDragging = mouseLocation;
+    }
+
+    @Override
+    public void mouseEntered(final java.awt.event.MouseEvent e) {
+        mouseWithinWindow = true;
+    }
+
+    @Override
+    public synchronized void mouseExited(final java.awt.event.MouseEvent e) {
+        mouseWithinWindow = false;
+        currentMouseLocation = null;
+    }
+
+    @Override
+    public synchronized void mouseMoved(final java.awt.event.MouseEvent e) {
+        currentMouseLocation = new Point2D(e.getX(), e.getY());
+        mouseMoved = true;
+    }
+
+    @Override
+    public void mousePressed(final java.awt.event.MouseEvent e) {
+    }
+
+    @Override
+    public void mouseReleased(final java.awt.event.MouseEvent evt) {
+        oldMouseCoordinatesWhenDragging = null;
+    }
+
+    @Override
+    public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) {
+        wheelMovedDirection += evt.getWheelRotation();
+    }
+
+    private boolean handleMouseVerticalScrolling() {
+        final Camera camera = viewPanel.getCamera();
+        final double actualAcceleration = 50 * camera.cameraAcceleration * (1 + (camera.getMovementSpeed() / 10));
+        camera.getMovementVector().y += (wheelMovedDirection * actualAcceleration);
+        camera.enforceSpeedLimit();
+        boolean repaintNeeded = wheelMovedDirection != 0;
+        wheelMovedDirection = 0;
+        return repaintNeeded;
+    }
+
+    private boolean handleMouseDragging() {
+        final Camera camera = viewPanel.getCamera();
+        Rotation rotation = camera.getTransform().getRotation();
+        final double newXZ = rotation.getAngleXZ() - ((float) mouseDelta.x / 50);
+        final double newYZ = rotation.getAngleYZ() - ((float) mouseDelta.y / 50);
+        camera.getTransform().setRotation(newXZ, newYZ);
+
+        boolean viewUpdateNeeded = !mouseDelta.isZero();
+        mouseDelta.zero();
+        return viewUpdateNeeded;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardFocusStack.java
new file mode 100644 (file)
index 0000000..683dac3
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+
+import java.util.Stack;
+
+/**
+ * Manages a stack-based keyboard focus system for interactive 3D components.
+ *
+ * <p>The focus stack determines which {@link KeyboardInputHandler} currently receives
+ * keyboard events. When a component gains focus (e.g., by being clicked), it is pushed
+ * onto the stack and the previous focus owner is notified. When the component releases
+ * focus (e.g., pressing ESC), the previous handler is restored.</p>
+ *
+ * <p>The default handler at the bottom of the stack is a
+ * {@link WorldNavigationUserInputTracker}, which handles WASD/arrow-key camera movement
+ * when no other component has focus.</p>
+ *
+ * <p><b>Focus flow example:</b></p>
+ * <pre>{@code
+ * // Initial state: WorldNavigationUserInputTracker has focus (camera movement)
+ * // User clicks on a text editor:
+ * focusStack.pushFocusOwner(textEditor);
+ * // Now textEditor receives keyboard events
+ *
+ * // User presses ESC:
+ * focusStack.popFocusOwner();
+ * // Camera movement is restored
+ * }</pre>
+ *
+ * @see KeyboardInputHandler the interface that focus owners must implement
+ * @see WorldNavigationUserInputTracker default handler for camera navigation
+ */
+public class KeyboardFocusStack {
+
+    private final ViewPanel viewPanel;
+    private final WorldNavigationUserInputTracker defaultInputHandler = new WorldNavigationUserInputTracker();
+    private final Stack<KeyboardInputHandler> inputHandlers = new Stack<>();
+    private KeyboardInputHandler currentUserInputHandler;
+
+    /**
+     * Creates a new focus stack for the given view panel, with
+     * {@link WorldNavigationUserInputTracker} as the default focus owner.
+     *
+     * @param viewPanel the view panel this focus stack belongs to
+     */
+    public KeyboardFocusStack(final ViewPanel viewPanel) {
+        this.viewPanel = viewPanel;
+        pushFocusOwner(defaultInputHandler);
+    }
+
+    /**
+     * Returns the handler that currently has keyboard focus.
+     *
+     * @return the current focus owner
+     */
+    public KeyboardInputHandler getCurrentFocusOwner() {
+        return currentUserInputHandler;
+    }
+
+    /**
+     * Removes the current focus owner from the stack and restores focus to the
+     * previous handler. If the stack is empty, no action is taken.
+     */
+    public void popFocusOwner() {
+        if (currentUserInputHandler != null)
+            currentUserInputHandler.focusLost(viewPanel);
+
+        if (inputHandlers.isEmpty())
+            return;
+
+        currentUserInputHandler = inputHandlers.pop();
+        currentUserInputHandler.focusReceived(viewPanel);
+    }
+
+    /**
+     * Pushes a new handler onto the focus stack, making it the current focus owner.
+     * The previous focus owner is notified via {@link KeyboardInputHandler#focusLost}
+     * and preserved on the stack for later restoration.
+     *
+     * <p>If the given handler is already the current focus owner, this method does nothing
+     * and returns {@code false}.</p>
+     *
+     * @param newInputHandler the handler to receive keyboard focus
+     * @return {@code true} if the view needs to be repainted as a result
+     */
+    public boolean pushFocusOwner(final KeyboardInputHandler newInputHandler) {
+        boolean updateNeeded = false;
+
+        if (currentUserInputHandler == newInputHandler)
+            return false;
+
+        if (currentUserInputHandler != null) {
+            updateNeeded = currentUserInputHandler.focusLost(viewPanel);
+            inputHandlers.push(currentUserInputHandler);
+        }
+
+        currentUserInputHandler = newInputHandler;
+        updateNeeded |= currentUserInputHandler.focusReceived(viewPanel);
+
+        return updateNeeded;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardHelper.java
new file mode 100644 (file)
index 0000000..fd4d5cf
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import java.awt.event.InputEvent;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility class providing keyboard key code constants and modifier detection methods.
+ *
+ * <p>Provides named constants for common key codes and static helper methods
+ * to check whether modifier keys (Ctrl, Alt, Shift) are pressed in a given
+ * event modifier mask.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * public boolean keyPressed(KeyEvent event, ViewPanel viewPanel) {
+ *     if (event.getKeyCode() == KeyboardHelper.ENTER) {
+ *         // Handle Enter key
+ *     }
+ *     if (KeyboardHelper.isCtrlPressed(event.getModifiersEx())) {
+ *         // Handle Ctrl+key combination
+ *     }
+ *     return true;
+ * }
+ * }</pre>
+ *
+ * @see KeyboardInputHandler the interface for receiving keyboard events
+ */
+public class KeyboardHelper {
+
+    /** Key code for the Tab key. */
+    public static final int TAB = 9;
+    /** Key code for the Down arrow key. */
+    public static final int DOWN = 40;
+    /** Key code for the Up arrow key. */
+    public static final int UP = 38;
+    /** Key code for the Right arrow key. */
+    public static final int RIGHT = 39;
+    /** Key code for the Left arrow key. */
+    public static final int LEFT = 37;
+    /** Key code for the Page Down key. */
+    public static final int PGDOWN = 34;
+    /** Key code for the Page Up key. */
+    public static final int PGUP = 33;
+    /** Key code for the Home key. */
+    public static final int HOME = 36;
+    /** Key code for the End key. */
+    public static final int END = 35;
+    /** Key code for the Delete key. */
+    public static final int DEL = 127;
+    /** Key code for the Enter/Return key. */
+    public static final int ENTER = 10;
+    /** Key code for the Backspace key. */
+    public static final int BACKSPACE = 8;
+    /** Key code for the Escape key. */
+    public static final int ESC = 27;
+    /** Key code for the Shift key. */
+    public static final int SHIFT = 16;
+
+    private static final Set<Integer> nonText;
+
+    static {
+        nonText = new HashSet<>();
+        nonText.add(DOWN);
+        nonText.add(UP);
+        nonText.add(LEFT);
+        nonText.add(RIGHT);
+
+        nonText.add(SHIFT);
+        nonText.add(ESC);
+    }
+
+    /**
+     * Checks if the Alt key is pressed in the given modifier mask.
+     *
+     * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()}
+     * @return {@code true} if Alt is pressed
+     */
+    public static boolean isAltPressed(final int modifiersEx) {
+        return (modifiersEx | InputEvent.ALT_DOWN_MASK) == modifiersEx;
+    }
+
+    /**
+     * Checks if the Ctrl key is pressed in the given modifier mask.
+     *
+     * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()}
+     * @return {@code true} if Ctrl is pressed
+     */
+    public static boolean isCtrlPressed(final int modifiersEx) {
+        return (modifiersEx | InputEvent.CTRL_DOWN_MASK) == modifiersEx;
+    }
+
+    /**
+     * Checks if the Shift key is pressed in the given modifier mask.
+     *
+     * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()}
+     * @return {@code true} if Shift is pressed
+     */
+    public static boolean isShiftPressed(final int modifiersEx) {
+        return (modifiersEx | InputEvent.SHIFT_DOWN_MASK) == modifiersEx;
+    }
+
+    /**
+     * Determines whether the given key code represents a text-producing key
+     * (as opposed to navigation or modifier keys like arrows, Shift, Escape).
+     *
+     * @param keyCode the key code to check
+     * @return {@code true} if the key produces text input
+     */
+    public static boolean isText(final int keyCode) {
+        return !nonText.contains(keyCode);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/KeyboardInputHandler.java
new file mode 100644 (file)
index 0000000..ccdb962
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+
+import java.awt.event.KeyEvent;
+
+/**
+ * This is the process:
+ * <p>
+ * 1. Component receives focus, perhaps because user clicked on it with the mouse.
+ * 2. Now component will receive user key press and release events from the keyboard.
+ * 3. Component loses focus. Perhaps user chose another component to interact with.
+ */
+public interface KeyboardInputHandler {
+
+    /**
+     * @return <code>true</code> if view needs to be re-rendered.
+     */
+    boolean focusLost(ViewPanel viewPanel);
+
+    /**
+     * @return <code>true</code> if view needs to be re-rendered.
+     */
+    boolean focusReceived(ViewPanel viewPanel);
+
+    /**
+     * @return <code>true</code> if view needs to be re-rendered.
+     */
+    boolean keyPressed(KeyEvent event, ViewPanel viewPanel);
+
+    /**
+     * @return <code>true</code> if view needs to be re-rendered.
+     */
+    boolean keyReleased(KeyEvent event, ViewPanel viewPanel);
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseEvent.java
new file mode 100644 (file)
index 0000000..459d0d2
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+
+/**
+ * Represents mouse event.
+ */
+public class MouseEvent {
+
+    /**
+     * Mouse coordinate in screen space (pixels) relative to top left corner of the screen
+     * when mouse button was clicked.
+     */
+    public Point2D coordinate;
+
+    /**
+     * <pre>
+     * 0 - mouse over (no button pressed)
+     * 1 - left mouse button
+     * 2 - middle mouse button
+     * 3 - right mouse button
+     * </pre>
+     */
+    public int button;
+
+    MouseEvent(final int x, final int y, final int button) {
+        this(new Point2D(x, y), button);
+    }
+
+    MouseEvent(final Point2D coordinate, final int button) {
+        this.coordinate = coordinate;
+        this.button = button;
+    }
+
+    @Override
+    public String toString() {
+        return "MouseEvent{" +
+                "coordinate=" + coordinate +
+                ", button=" + button +
+                '}';
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/MouseInteractionController.java
new file mode 100644 (file)
index 0000000..5b6d470
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+/**
+ * Interface that allows to handle mouse events.
+ */
+public interface MouseInteractionController {
+
+    /**
+     * Called when mouse is clicked on component.
+     *
+     * @return <code>true</code> if view update is needed as a consequence of this mouse click.
+     */
+    boolean mouseClicked(int button);
+
+    /**
+     * Called when mouse gets over given component.
+     *
+     * @return <code>true</code> if view update is needed as a consequence of this mouse enter.
+     */
+    boolean mouseEntered();
+
+    /**
+     * Called when mouse leaves screen area occupied by component.
+     *
+     * @return <code>true</code> if view update is needed as a consequence of this mouse exit.
+     */
+    boolean mouseExited();
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/WorldNavigationUserInputTracker.java
new file mode 100644 (file)
index 0000000..f479a88
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.gui.FrameListener;
+
+import java.awt.event.KeyEvent;
+
+/**
+ * Default keyboard input handler that translates arrow key presses into camera (avatar)
+ * movement through the 3D world.
+ *
+ * <p>This handler is automatically registered as the default focus owner in the
+ * {@link KeyboardFocusStack}. It listens for arrow key presses on each frame and
+ * applies acceleration to the avatar's movement vector accordingly:</p>
+ * <ul>
+ *   <li><b>Up arrow</b> - move forward (positive Z)</li>
+ *   <li><b>Down arrow</b> - move backward (negative Z)</li>
+ *   <li><b>Right arrow</b> - move right (positive X)</li>
+ *   <li><b>Left arrow</b> - move left (negative X)</li>
+ * </ul>
+ *
+ * <p>Movement acceleration scales with the time delta between frames for smooth,
+ * frame-rate-independent navigation. It also scales with current speed for a natural
+ * acceleration curve.</p>
+ *
+ * @see KeyboardFocusStack the focus system that manages this handler
+ * @see Camera the camera/viewer that this handler moves
+ */
+public class WorldNavigationUserInputTracker implements KeyboardInputHandler, FrameListener {
+
+    @Override
+    public boolean onFrame(final ViewPanel viewPanel,
+                                final int millisecondsSinceLastFrame) {
+
+        final InputManager inputManager = viewPanel.getInputManager();
+
+        final Camera camera = viewPanel.getCamera();
+
+        final double actualAcceleration = (long) millisecondsSinceLastFrame
+                * camera.cameraAcceleration
+                * (1 + (camera.getMovementSpeed() / 10));
+
+        if (inputManager.isKeyPressed(KeyboardHelper.UP))
+            camera.getMovementVector().z += actualAcceleration;
+
+        if (inputManager.isKeyPressed(KeyboardHelper.DOWN))
+            camera.getMovementVector().z -= actualAcceleration;
+
+        if (inputManager.isKeyPressed(KeyboardHelper.RIGHT))
+            camera.getMovementVector().x += actualAcceleration;
+
+        if (inputManager.isKeyPressed(KeyboardHelper.LEFT))
+            camera.getMovementVector().x -= actualAcceleration;
+
+        camera.enforceSpeedLimit();
+
+        return false;
+    }
+
+    @Override
+    public boolean focusLost(final ViewPanel viewPanel) {
+        viewPanel.removeFrameListener(this);
+        return false;
+    }
+
+    @Override
+    public boolean focusReceived(final ViewPanel viewPanel) {
+        viewPanel.addFrameListener(this);
+        return false;
+    }
+
+    @Override
+    public boolean keyPressed(final KeyEvent event, final ViewPanel viewContext) {
+        return false;
+    }
+
+    @Override
+    public boolean keyReleased(final KeyEvent event, final ViewPanel viewContext) {
+        return false;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/package-info.java
new file mode 100644 (file)
index 0000000..dfbbd22
--- /dev/null
@@ -0,0 +1,6 @@
+package eu.svjatoslav.sixth.e3d.gui.humaninput;
+
+/**
+ * This package is responsible for tracking human input devices (keyboard, mouse, etc.) and
+ * forwarding those inputs to subsequent virtual components.
+ */
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Character.java
new file mode 100644 (file)
index 0000000..27113b0
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+/**
+ * A character in a text editor.
+ */
+public class Character {
+
+    /**
+     * The character value.
+     */
+    char value;
+
+    public Character(final char value) {
+        this.value = value;
+    }
+
+    boolean hasValue() {
+        return value != ' ';
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/LookAndFeel.java
new file mode 100644 (file)
index 0000000..b792779
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+/**
+ * A look and feel of a text editor.
+ */
+public class LookAndFeel {
+
+    public Color foreground = new Color(255, 255, 255);
+    public Color background = new Color(20, 20, 20, 255);
+
+    public Color tabStopBackground = new Color(25, 25, 25, 255);
+
+    public Color cursorForeground = new Color(255, 255, 255);
+    public Color cursorBackground = new Color(255, 0, 0);
+
+    public Color selectionForeground = new Color(255, 255, 255);
+    public Color selectionBackground = new Color(0, 80, 80);
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/Page.java
new file mode 100644 (file)
index 0000000..47c898f
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A page in a text editor.
+ */
+public class Page {
+
+    /**
+     * The text lines.
+     */
+    public List<TextLine> rows = new ArrayList<>();
+
+    public void ensureMaxTextLine(final int row) {
+        while (rows.size() <= row)
+            rows.add(new TextLine());
+    }
+
+    /**
+     * Returns the character at the specified location.
+     * If the location is out of bounds, returns a space.
+     *
+     * @return The character at the specified location.
+     */
+    public char getChar(final int row, final int column) {
+        if (rows.size() <= row)
+            return ' ';
+        return rows.get(row).getCharForLocation(column);
+    }
+
+    /**
+     * Returns the specified line.
+     *
+     * @param row The line number.
+     * @return The line.
+     */
+    public TextLine getLine(final int row) {
+        ensureMaxTextLine(row);
+        return rows.get(row);
+    }
+
+    /**
+     * Returns the length of the specified line.
+     *
+     * @param row The line number.
+     * @return The length of the line.
+     */
+    public int getLineLength(final int row) {
+        if (rows.size() <= row)
+            return 0;
+        return rows.get(row).getLength();
+    }
+
+    /**
+     * Returns the number of lines in the page.
+     *
+     * @return The number of lines in the page.
+     */
+    public int getLinesCount() {
+        pack();
+        return rows.size();
+    }
+
+    /**
+     * Returns the text of the page.
+     *
+     * @return The text of the page.
+     */
+    public String getText() {
+        pack();
+
+        final StringBuilder result = new StringBuilder();
+        for (final TextLine textLine : rows) {
+            if (result.length() > 0)
+                result.append("\n");
+            result.append(textLine.toString());
+        }
+        return result.toString();
+    }
+
+    public void insertCharacter(final int row, final int col, final char value) {
+        getLine(row).insertCharacter(col, value);
+    }
+
+    public void insertLine(final int row, final TextLine textLine) {
+        rows.add(row, textLine);
+    }
+
+    /**
+     * Removes empty lines from the end of the page.
+     */
+    private void pack() {
+        int newLength = 0;
+
+        for (int i = rows.size() - 1; i >= 0; i--)
+            if (!rows.get(i).isEmpty()) {
+                newLength = i + 1;
+                break;
+            }
+
+        if (newLength == rows.size())
+            return;
+
+        rows = rows.subList(0, newLength);
+    }
+
+    /**
+     * Removes the specified character from the page.
+     *
+     * @param row The line number.
+     * @param col The character number.
+     */
+    public void removeCharacter(final int row, final int col) {
+        if (rows.size() <= row)
+            return;
+        getLine(row).removeCharacter(col);
+    }
+
+    /**
+     * Removes the specified line from the page.
+     *
+     * @param row The line number.
+     */
+    public void removeLine(final int row) {
+        if (rows.size() <= row)
+            return;
+        rows.remove(row);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextEditComponent.java
new file mode 100755 (executable)
index 0000000..5ed7a76
--- /dev/null
@@ -0,0 +1,911 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.gui.GuiComponent;
+import eu.svjatoslav.sixth.e3d.gui.TextPointer;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardHelper;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
+
+import java.awt.*;
+import java.awt.datatransfer.*;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A full-featured text editor component rendered in 3D space.
+ *
+ * <p>Extends {@link GuiComponent} to integrate keyboard focus management and mouse
+ * interaction with a multi-line text editing surface. The editor is backed by a
+ * {@link Page} model containing {@link TextLine} instances and rendered via a
+ * {@link TextCanvas}.</p>
+ *
+ * <p><b>Supported editing features:</b></p>
+ * <ul>
+ *   <li>Cursor navigation with arrow keys, Home, End, Page Up, and Page Down</li>
+ *   <li>Text selection via Shift + arrow keys</li>
+ *   <li>Clipboard operations: Ctrl+C (copy), Ctrl+X (cut), Ctrl+V (paste), Ctrl+A (select all)</li>
+ *   <li>Word-level cursor movement with Ctrl+Left and Ctrl+Right</li>
+ *   <li>Tab indentation and Shift+Tab dedentation for single lines and block selections</li>
+ *   <li>Backspace dedentation of selected blocks (removes 4 spaces of indentation)</li>
+ *   <li>Automatic scrolling when the cursor moves beyond the visible area</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a look and feel (or use defaults)
+ * LookAndFeel lookAndFeel = new LookAndFeel();
+ *
+ * // Create the text editor at a position in 3D space
+ * TextEditComponent editor = new TextEditComponent(
+ *     new Transform(new Point3D(0, 0, 500)),  // position in world
+ *     viewPanel,                                // the active ViewPanel
+ *     new Point2D(800, 600),                    // size in world coordinates
+ *     lookAndFeel
+ * );
+ *
+ * // Set initial content
+ * editor.setText("Hello, World!\nSecond line of text.");
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(editor);
+ * }</pre>
+ *
+ * @see GuiComponent the base class providing keyboard focus and mouse click handling
+ * @see Page the underlying text model holding all lines
+ * @see TextCanvas the rendering surface for character-based output
+ * @see LookAndFeel configurable colors for the editor's visual appearance
+ * @see TextPointer row/column pointer used for cursor and selection positions
+ */
+public class TextEditComponent extends GuiComponent implements ClipboardOwner {
+
+    private static final long serialVersionUID = -7118833957783600630L;
+
+    /**
+     * Text rows that need to be repainted.
+     */
+    private final Set<Integer> dirtyRows = new HashSet<>();
+
+
+    /**
+     * The text canvas used to render characters on screen.
+     */
+    private final TextCanvas textCanvas;
+
+    /**
+     * The number of characters the view is scrolled horizontally.
+     */
+    public int scrolledCharacters = 0;
+
+    /**
+     * The number of lines the view is scrolled vertically.
+     */
+    public int scrolledLines = 0;
+
+    /**
+     * Whether the user is currently in selection mode (Shift key held during navigation).
+     */
+    public boolean selecting = false;
+
+    /**
+     * Selection start and end pointers.
+     */
+    public TextPointer selectionStart = new TextPointer(0, 0);
+    public TextPointer selectionEnd = new TextPointer(0, 0);
+
+    /**
+     * The current cursor position in the text (row and column).
+     */
+    public TextPointer cursorLocation = new TextPointer(0, 0);
+
+    /**
+     * The page model holding all text lines.
+     */
+    Page page = new Page();
+
+    /**
+     * The look and feel configuration controlling editor colors.
+     */
+    LookAndFeel lookAndFeel;
+
+    /**
+     * If true, the page will be repainted on the next update.
+     */
+    boolean repaintPage = false;
+
+    /**
+     * Creates a new text editor component positioned in 3D space.
+     *
+     * <p>The editor dimensions in rows and columns are computed from the given world-coordinate
+     * size and the font character dimensions defined in {@link TextCanvas}. A {@link TextCanvas}
+     * is created internally and added as a child shape.</p>
+     *
+     * @param transform              the position and orientation of the editor in 3D space
+     * @param viewPanel              the view panel this editor belongs to
+     * @param sizeInWorldCoordinates the editor size in world coordinates (width, height);
+     *                               determines the number of visible columns and rows
+     * @param lookAndFeel            the color configuration for the editor's visual appearance
+     */
+    public TextEditComponent(final Transform transform,
+                             final ViewPanel viewPanel,
+                             final Point2D sizeInWorldCoordinates,
+                             LookAndFeel lookAndFeel) {
+        super(transform, viewPanel, sizeInWorldCoordinates.to3D());
+
+        this.lookAndFeel = lookAndFeel;
+        final int columns = (int) (sizeInWorldCoordinates.x / TextCanvas.FONT_CHAR_WIDTH);
+        final int rows = (int) (sizeInWorldCoordinates.y / TextCanvas.FONT_CHAR_HEIGHT);
+
+        textCanvas = new TextCanvas(
+                new Transform(),
+                new TextPointer(rows, columns),
+                lookAndFeel.foreground, lookAndFeel.background);
+
+        textCanvas.setMouseInteractionController(this);
+
+        repaintPage();
+        addShape(textCanvas);
+    }
+
+    /**
+     * Ensures the cursor stays within the visible editor area by adjusting
+     * scroll offsets when the cursor moves beyond the visible boundaries.
+     * Also clamps the cursor position so that row and column are never negative.
+     */
+    private void checkCursorBoundaries() {
+        if (cursorLocation.column < 0)
+            cursorLocation.column = 0;
+        if (cursorLocation.row < 0)
+            cursorLocation.row = 0;
+
+        // ensure chat cursor stays within vertical editor boundaries by
+        // vertical scrolling
+        if ((cursorLocation.row - scrolledLines) < 0)
+            scroll(0, cursorLocation.row - scrolledLines);
+
+        if ((((cursorLocation.row - scrolledLines) + 1)) > textCanvas.getSize().row)
+            scroll(0,
+                    ((((((cursorLocation.row - scrolledLines) + 1) - textCanvas
+                            .getSize().row)))));
+
+        // ensure chat cursor stays within horizontal editor boundaries by
+        // horizontal scrolling
+        if ((cursorLocation.column - scrolledCharacters) < 0)
+            scroll(cursorLocation.column - scrolledCharacters, 0);
+
+        if ((((cursorLocation.column - scrolledCharacters) + 1)) > textCanvas
+                .getSize().column)
+            scroll((((((cursorLocation.column - scrolledCharacters) + 1) - textCanvas
+                    .getSize().column))), 0);
+    }
+
+    /**
+     * Clears the current text selection by setting the selection end to match
+     * the selection start, effectively making the selection empty.
+     *
+     * <p>A full page repaint is scheduled to remove the visual selection highlight.</p>
+     */
+    public void clearSelection() {
+        selectionEnd = new TextPointer(selectionStart);
+        repaintPage = true;
+    }
+
+    /**
+     * Copies the currently selected text to the system clipboard.
+     *
+     * <p>If no text is selected (i.e., selection start equals selection end),
+     * this method does nothing. Multi-line selections are joined with newline
+     * characters.</p>
+     *
+     * @see #setClipboardContents(String)
+     * @see #cutToClipboard()
+     */
+    public void copyToClipboard() {
+        if (selectionStart.compareTo(selectionEnd) == 0)
+            return;
+        // System.out.println("Copy action.");
+        final StringBuilder msg = new StringBuilder();
+
+        ensureSelectionOrder();
+
+        for (int row = selectionStart.row; row <= selectionEnd.row; row++) {
+            final TextLine textLine = page.getLine(row);
+
+            if (row == selectionStart.row) {
+                if (row == selectionEnd.row)
+                    msg.append(textLine.getSubString(selectionStart.column,
+                            selectionEnd.column + 1));
+                else
+                    msg.append(textLine.getSubString(selectionStart.column,
+                            textLine.getLength()));
+            } else {
+                msg.append('\n');
+                if (row == selectionEnd.row)
+                    msg.append(textLine
+                            .getSubString(0, selectionEnd.column + 1));
+                else
+                    msg.append(textLine.toString());
+            }
+        }
+
+        setClipboardContents(msg.toString());
+    }
+
+    /**
+     * Cuts the currently selected text to the system clipboard.
+     *
+     * <p>This copies the selected text to the clipboard via {@link #copyToClipboard()},
+     * then deletes the selection from the page and triggers a full repaint.</p>
+     *
+     * @see #copyToClipboard()
+     * @see #deleteSelection()
+     */
+    public void cutToClipboard() {
+        copyToClipboard();
+        deleteSelection();
+        repaintPage();
+    }
+
+    /**
+     * Deletes the currently selected text from the page.
+     *
+     * <p>After deletion, the selection is cleared and the cursor is moved to
+     * the position where the selection started.</p>
+     *
+     * @see #ensureSelectionOrder()
+     */
+    public void deleteSelection() {
+        ensureSelectionOrder();
+        int ym = 0;
+
+        for (int line = selectionStart.row; line <= selectionEnd.row; line++) {
+            final TextLine currentLine = page.getLine(line - ym);
+
+            if (line == selectionStart.row) {
+                if (line == selectionEnd.row)
+
+                    currentLine.cutSubString(selectionStart.column,
+                            selectionEnd.column);
+                else if (selectionStart.column == 0) {
+                    page.removeLine(line - ym);
+                    ym++;
+                } else
+                    currentLine.cutSubString(selectionStart.column,
+                            currentLine.getLength() + 1);
+            } else if (line == selectionEnd.row)
+                currentLine.cutSubString(0, selectionEnd.column);
+            else {
+                page.removeLine(line - ym);
+                ym++;
+            }
+        }
+
+        clearSelection();
+        cursorLocation = new TextPointer(selectionStart);
+    }
+
+    /**
+     * Ensures that {@link #selectionStart} is smaller than
+     * {@link #selectionEnd}.
+     *
+     * <p>If the start pointer is after the end pointer (e.g., when the user
+     * selected text backwards), the two pointers are swapped so that
+     * subsequent operations can iterate from start to end.</p>
+     */
+    public void ensureSelectionOrder() {
+        if (selectionStart.compareTo(selectionEnd) > 0) {
+            final TextPointer temp = selectionEnd;
+            selectionEnd = selectionStart;
+            selectionStart = temp;
+        }
+    }
+
+    /**
+     * Retrieves the current text contents of the system clipboard.
+     *
+     * @return the clipboard text content, or an empty string if the clipboard
+     *         is empty or does not contain text
+     */
+    public String getClipboardContents() {
+        String result = "";
+        final Clipboard clipboard = Toolkit.getDefaultToolkit()
+                .getSystemClipboard();
+        // odd: the Object param of getContents is not currently used
+        final Transferable contents = clipboard.getContents(null);
+        final boolean hasTransferableText = (contents != null)
+                && contents.isDataFlavorSupported(DataFlavor.stringFlavor);
+        if (hasTransferableText)
+            try {
+                result = (String) contents
+                        .getTransferData(DataFlavor.stringFlavor);
+            } catch (final UnsupportedFlavorException | IOException ex) {
+                // highly unlikely since we are using a standard DataFlavor
+                System.out.println(ex);
+            }
+        // System.out.println(result);
+        return result;
+    }
+
+    /**
+     * Places the given string into the system clipboard so that it can be
+     * pasted into other applications.
+     *
+     * @param contents the text to place on the clipboard
+     * @see #getClipboardContents()
+     * @see #copyToClipboard()
+     */
+    public void setClipboardContents(final String contents) {
+        final StringSelection stringSelection = new StringSelection(contents);
+        final Clipboard clipboard = Toolkit.getDefaultToolkit()
+                .getSystemClipboard();
+        clipboard.setContents(stringSelection, stringSelection);
+    }
+
+    /**
+     * Scrolls to and positions the cursor at the beginning of the specified line.
+     *
+     * <p>The view is scrolled so the target line is visible, the cursor is placed
+     * at the start of that line (column 0), and a full repaint is triggered.</p>
+     *
+     * @param Line the zero-based line number to navigate to
+     */
+    public void goToLine(final int Line) {
+        // markNavigationLocation(Line);
+        scrolledLines = Line + 1;
+        cursorLocation.row = Line + 1;
+        cursorLocation.column = 0;
+        repaintPage();
+    }
+
+    /**
+     * Inserts the given text string at the current cursor position.
+     *
+     * <p>The text is processed character by character. Special characters are
+     * handled as editing operations:</p>
+     * <ul>
+     *   <li>{@code DEL} -- deletes the character at the cursor</li>
+     *   <li>{@code ENTER} -- splits the current line at the cursor</li>
+     *   <li>{@code BACKSPACE} -- deletes the character before the cursor</li>
+     * </ul>
+     * <p>All other printable characters are inserted at the cursor position,
+     * advancing the cursor column by one for each character.</p>
+     *
+     * @param txt the text to insert; {@code null} values are silently ignored
+     */
+    public void insertText(final String txt) {
+        if (txt == null)
+            return;
+
+        for (final char c : txt.toCharArray()) {
+
+            if (c == KeyboardHelper.DEL) {
+                processDel();
+                continue;
+            }
+
+            if (c == KeyboardHelper.ENTER) {
+                processEnter();
+                continue;
+            }
+
+            if (c == KeyboardHelper.BACKSPACE) {
+                processBackspace();
+                continue;
+            }
+
+            // type character
+            if (KeyboardHelper.isText(c)) {
+                page.insertCharacter(cursorLocation.row, cursorLocation.column,
+                        c);
+                cursorLocation.column++;
+            }
+        }
+    }
+
+    /**
+     * Handles a key press event by routing it through the editor's input processing
+     * pipeline.
+     *
+     * <p>This method delegates to the parent {@link GuiComponent#keyPressed(KeyEvent, ViewPanel)}
+     * (which handles ESC for focus release), then processes the key event for text editing,
+     * marks the affected row as dirty, adjusts scroll boundaries, and repaints as needed.</p>
+     *
+     * @param event     the keyboard event
+     * @param viewPanel the view panel that dispatched this event
+     * @return always {@code true}, indicating the event was consumed
+     */
+    @Override
+    public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) {
+        super.keyPressed(event, viewPanel);
+
+        processKeyEvent(event);
+
+        markRowDirty();
+
+        checkCursorBoundaries();
+
+        repaintWhatNeeded();
+        return true;
+    }
+
+    /**
+     * Called when this editor loses ownership of the system clipboard.
+     *
+     * <p>This is an empty implementation of the {@link ClipboardOwner} interface;
+     * no action is taken when clipboard ownership is lost.</p>
+     *
+     * @param aClipboard the clipboard that this editor previously owned
+     * @param aContents  the contents that were previously placed on the clipboard
+     */
+    @Override
+    public void lostOwnership(final Clipboard aClipboard,
+                              final Transferable aContents) {
+        // do nothing
+    }
+
+    /**
+     * Marks the current cursor row as dirty, scheduling it for repaint on the
+     * next rendering cycle.
+     */
+    public void markRowDirty() {
+        dirtyRows.add(cursorLocation.row);
+    }
+
+    /**
+     * Pastes text from the system clipboard at the current cursor position.
+     *
+     * @see #getClipboardContents()
+     * @see #insertText(String)
+     */
+    public void pasteFromClipboard() {
+        insertText(getClipboardContents());
+    }
+
+    /**
+     * Processes the backspace key action.
+     *
+     * <p>If there is no active selection, deletes the character before the cursor.
+     * If the cursor is at the beginning of a line, merges the current line with the
+     * previous one. If there is an active selection, dedents the selected lines by
+     * removing up to 4 leading spaces (block dedentation).</p>
+     */
+    private void processBackspace() {
+        if (selectionStart.compareTo(selectionEnd) == 0) {
+            // erase single character
+            if (cursorLocation.column > 0) {
+                cursorLocation.column--;
+                page.removeCharacter(cursorLocation.row, cursorLocation.column);
+                // System.out.println(lines.get(currentCursor.line).toString());
+            } else if (cursorLocation.row > 0) {
+                cursorLocation.row--;
+                final int currentLineLength = page
+                        .getLineLength(cursorLocation.row);
+                cursorLocation.column = currentLineLength;
+                page.getLine(cursorLocation.row)
+                        .insertTextLine(currentLineLength,
+                                page.getLine(cursorLocation.row + 1));
+                page.removeLine(cursorLocation.row + 1);
+                repaintPage = true;
+            }
+        } else {
+            // dedent multiple lines
+            ensureSelectionOrder();
+            // scan if enough space exists
+            for (int y = selectionStart.row; y < selectionEnd.row; y++)
+                if (page.getLine(y).getIdent() < 4)
+                    return;
+
+            for (int y = selectionStart.row; y < selectionEnd.row; y++)
+                page.getLine(y).cutFromBeginning(4);
+
+            repaintPage = true;
+        }
+    }
+
+    /**
+     * Processes keyboard shortcuts involving the Ctrl modifier key.
+     *
+     * <p>Supported combinations:</p>
+     * <ul>
+     *   <li>Ctrl+A -- select all text</li>
+     *   <li>Ctrl+X -- cut selected text to clipboard</li>
+     *   <li>Ctrl+C -- copy selected text to clipboard</li>
+     *   <li>Ctrl+V -- paste from clipboard</li>
+     *   <li>Ctrl+Right -- skip to the beginning of the next word</li>
+     *   <li>Ctrl+Left -- skip to the beginning of the previous word</li>
+     * </ul>
+     *
+     * @param keyCode the key code of the pressed key (combined with Ctrl)
+     */
+    private void processCtrlCombinations(final int keyCode) {
+
+        if ((char) keyCode == 'A') { // CTRL + A -- select all
+            final int lastLineIndex = page.getLinesCount() - 1;
+            selectionStart = new TextPointer(0, 0);
+            selectionEnd = new TextPointer(lastLineIndex,
+                    page.getLineLength(lastLineIndex));
+            repaintPage();
+        }
+
+        // CTRL + X -- cut
+        if ((char) keyCode == 'X')
+            cutToClipboard();
+
+        // CTRL + C -- copy
+        if ((char) keyCode == 'C')
+            copyToClipboard();
+
+        // CTRL + V -- paste
+        if ((char) keyCode == 'V')
+            pasteFromClipboard();
+
+        if (keyCode == 39) { // RIGHT
+            // skip to the beginning of the next word
+
+            for (int x = cursorLocation.column; x < (page
+                    .getLineLength(cursorLocation.row) - 1); x++)
+                if ((page.getChar(cursorLocation.row, x) == ' ')
+                        && (page.getChar(cursorLocation.row, x + 1) != ' ')) {
+                    // beginning of the next word is found
+                    cursorLocation.column = x + 1;
+                    return;
+                }
+
+            cursorLocation.column = page.getLineLength(cursorLocation.row);
+            return;
+        }
+
+        if (keyCode == 37) { // Left
+
+            // skip to the beginning of the previous word
+            for (int x = cursorLocation.column - 2; x >= 0; x--)
+                if ((page.getChar(cursorLocation.row, x) == ' ')
+                        & (page.getChar(cursorLocation.row, x + 1) != ' ')) {
+                    cursorLocation.column = x + 1;
+                    return;
+                }
+
+            cursorLocation.column = 0;
+        }
+    }
+
+    /**
+     * Processes the Delete key action.
+     *
+     * <p>If there is no active selection, deletes the character at the cursor position.
+     * If the cursor is at the end of the line, the next line is merged into the current one.
+     * If there is an active selection, the entire selection is deleted.</p>
+     */
+    public void processDel() {
+        if (selectionStart.compareTo(selectionEnd) == 0) {
+            // is there still some text right to the cursor ?
+            if (cursorLocation.column < page.getLineLength(cursorLocation.row))
+                page.removeCharacter(cursorLocation.row, cursorLocation.column);
+            else {
+                page.getLine(cursorLocation.row).insertTextLine(
+                        cursorLocation.column,
+                        page.getLine(cursorLocation.row + 1));
+                page.removeLine(cursorLocation.row + 1);
+                repaintPage = true;
+            }
+        } else {
+            deleteSelection();
+            repaintPage = true;
+        }
+    }
+
+    /**
+     * Processes the Enter key action by splitting the current line at the cursor position.
+     *
+     * <p>Everything to the right of the cursor is moved to a new line inserted
+     * below. The cursor moves to the beginning of the new line.</p>
+     */
+    private void processEnter() {
+        final TextLine currentLine = page.getLine(cursorLocation.row);
+        // move everything right to the cursor into new line
+        final TextLine newLine = currentLine.getSubLine(cursorLocation.column,
+                currentLine.getLength());
+        page.insertLine(cursorLocation.row + 1, newLine);
+
+        // trim existing line
+        page.getLine(cursorLocation.row).cutUntilEnd(cursorLocation.column);
+        repaintPage = true;
+
+        cursorLocation.row++;
+        cursorLocation.column = 0;
+    }
+
+    /**
+     * Routes a keyboard event to the appropriate handler based on modifier keys
+     * and key codes.
+     *
+     * <p>Handles Ctrl combinations, Tab/Shift+Tab, text input, Shift-based selection,
+     * and cursor navigation keys (Home, End, arrows, Page Up/Down). Alt key events
+     * are ignored.</p>
+     *
+     * @param event the keyboard event to process
+     */
+    private void processKeyEvent(final KeyEvent event) {
+        final int modifiers = event.getModifiersEx();
+        final int keyCode = event.getKeyCode();
+        final char keyChar = event.getKeyChar();
+
+        // System.out.println("Keycode:" + keyCode s+ ", keychar:" + keyChar);
+
+        if (KeyboardHelper.isAltPressed(modifiers))
+            return;
+
+        if (KeyboardHelper.isCtrlPressed(modifiers)) {
+            processCtrlCombinations(keyCode);
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.TAB) {
+            processTab(modifiers);
+            return;
+        }
+
+        clearSelection();
+
+        if (KeyboardHelper.isText(keyCode)) {
+            insertText(String.valueOf(keyChar));
+            return;
+        }
+
+        if (KeyboardHelper.isShiftPressed(modifiers)) {
+            if (!selecting)
+                attemptSelectionStart:{
+
+                    if (keyChar == 65535)
+                        if (keyCode == 16)
+                            break attemptSelectionStart;
+                    if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10)
+                            | (keyChar == 8) | (keyChar == 9))
+                        break attemptSelectionStart;
+
+                    selectionStart = new TextPointer(cursorLocation);
+                    selectionEnd = selectionStart;
+                    selecting = true;
+                    repaintPage();
+                }
+        } else
+            selecting = false;
+
+        if (keyCode == KeyboardHelper.HOME) {
+            cursorLocation.column = 0;
+            return;
+        }
+        if (keyCode == KeyboardHelper.END) {
+            cursorLocation.column = page.getLineLength(cursorLocation.row);
+            return;
+        }
+
+        // process cursor keys
+        if (keyCode == KeyboardHelper.DOWN) {
+            markRowDirty();
+            cursorLocation.row++;
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.UP) {
+            markRowDirty();
+            cursorLocation.row--;
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.RIGHT) {
+            cursorLocation.column++;
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.LEFT) {
+            cursorLocation.column--;
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.PGDOWN) {
+            cursorLocation.row += textCanvas.getSize().row;
+            repaintPage();
+            return;
+        }
+
+        if (keyCode == KeyboardHelper.PGUP) {
+            cursorLocation.row -= textCanvas.getSize().row;
+            repaintPage = true;
+        }
+
+    }
+
+    /**
+     * Processes the Tab key action for indentation and dedentation.
+     *
+     * <p>Behavior depends on modifiers and selection state:</p>
+     * <ul>
+     *   <li><strong>Shift+Tab with selection:</strong> dedents all selected lines by
+     *       removing up to 4 leading spaces, if all lines have sufficient indentation</li>
+     *   <li><strong>Shift+Tab without selection:</strong> dedents the current line by
+     *       removing 4 leading spaces and moving the cursor back</li>
+     *   <li><strong>Tab with selection:</strong> indents all selected lines by adding
+     *       4 leading spaces</li>
+     * </ul>
+     *
+     * @param modifiers the keyboard modifier flags from the key event
+     */
+    private void processTab(final int modifiers) {
+        if (KeyboardHelper.isShiftPressed(modifiers)) {
+            if (selectionStart.compareTo(selectionEnd) != 0) {
+                // dedent multiple lines
+                ensureSelectionOrder();
+
+                identSelection:
+                {
+                    // check that indentation is possible
+                    for (int y = selectionStart.row; y < selectionEnd.row; y++) {
+                        final TextLine textLine = page.getLine(y);
+
+                        if (!textLine.isEmpty())
+                            if (textLine.getIdent() < 4)
+                                break identSelection;
+                    }
+
+                    for (int y = selectionStart.row; y < selectionEnd.row; y++)
+                        page.getLine(y).cutFromBeginning(4);
+                }
+            } else {
+                // dedent current line
+                final TextLine textLine = page.getLine(cursorLocation.row);
+
+                if (cursorLocation.column >= 4)
+                    if (textLine.isEmpty())
+                        cursorLocation.column -= 4;
+                    else if (textLine.getIdent() >= 4) {
+                        cursorLocation.column -= 4;
+                        textLine.cutFromBeginning(4);
+                    }
+
+            }
+
+            repaintPage();
+
+        } else if (selectionStart.compareTo(selectionEnd) != 0) {
+            // ident multiple lines
+            ensureSelectionOrder();
+            for (int y = selectionStart.row; y < selectionEnd.row; y++)
+                page.getLine(y).addIdent(4);
+
+            repaintPage();
+        }
+    }
+
+    /**
+     * Repaints the entire visible page area onto the text canvas.
+     *
+     * <p>Iterates over every visible cell (row and column), applying the appropriate
+     * foreground and background colors based on whether the cell is the cursor position,
+     * part of a selection, or a tab stop margin. Characters are read from the underlying
+     * {@link Page} model with scroll offsets applied.</p>
+     */
+    public void repaintPage() {
+
+        final int columnCount = textCanvas.getSize().column + 2;
+        final int rowCount = textCanvas.getSize().row + 2;
+
+        for (int row = 0; row < rowCount; row++)
+            for (int column = 0; column < columnCount; column++) {
+                final boolean isTabMargin = ((column + scrolledCharacters) % 4) == 0;
+
+                if ((column == (cursorLocation.column - scrolledCharacters))
+                        & (row == (cursorLocation.row - scrolledLines))) {
+                    // cursor
+                    textCanvas.setBackgroundColor(lookAndFeel.cursorBackground);
+                    textCanvas.setForegroundColor(lookAndFeel.cursorForeground);
+                } else if (new TextPointer(row + scrolledLines, column).isBetween(
+                        selectionStart, selectionEnd)) {
+                    // selected text
+                    textCanvas.setBackgroundColor(lookAndFeel.selectionBackground);
+                    textCanvas.setForegroundColor(lookAndFeel.selectionForeground);
+                } else {
+                    // normal text
+                    textCanvas.setBackgroundColor(lookAndFeel.background);
+                    textCanvas.setForegroundColor(lookAndFeel.foreground);
+
+                    if (isTabMargin)
+                        textCanvas
+                                .setBackgroundColor(lookAndFeel.tabStopBackground);
+
+                }
+
+                final char charUnderCursor = page.getChar(row + scrolledLines,
+                        column + scrolledCharacters);
+
+                textCanvas.putChar(row, column, charUnderCursor);
+            }
+
+    }
+
+    /**
+     * Repaints a single row of the editor.
+     *
+     * <p><strong>Note:</strong> the current implementation delegates to
+     * {@link #repaintPage()} and repaints the entire page. This is a candidate
+     * for optimization.</p>
+     *
+     * @param rowNumber the zero-based row index to repaint
+     */
+    public void repaintRow(final int rowNumber) {
+        // TODO: Optimize this. No need to repaint entire page.
+        repaintPage();
+    }
+
+    /**
+     * Repaints only the portions of the editor that have been marked as dirty.
+     *
+     * <p>If {@link #repaintPage} is set, the entire page is repainted and all
+     * dirty row tracking is cleared. Otherwise, only the individually dirty rows
+     * are repainted.</p>
+     */
+    private void repaintWhatNeeded() {
+        if (repaintPage) {
+            dirtyRows.clear();
+            repaintPage();
+            return;
+        }
+
+        dirtyRows.forEach(this::repaintRow);
+        dirtyRows.clear();
+    }
+
+    /**
+     * Scrolls the visible editor area by the specified number of characters and lines.
+     *
+     * <p>Scroll offsets are clamped so they never go below zero. A full page
+     * repaint is scheduled after scrolling.</p>
+     *
+     * @param charactersToScroll the number of characters to scroll horizontally
+     *                           (positive = right, negative = left)
+     * @param linesToScroll      the number of lines to scroll vertically
+     *                           (positive = down, negative = up)
+     */
+    public void scroll(final int charactersToScroll, final int linesToScroll) {
+        scrolledLines += linesToScroll;
+        scrolledCharacters += charactersToScroll;
+
+        if (scrolledLines < 0)
+            scrolledLines = 0;
+
+        if (scrolledCharacters < 0)
+            scrolledCharacters = 0;
+
+        repaintPage = true;
+    }
+
+    /**
+     * Replaces the entire editor content with the given text.
+     *
+     * <p>Resets the cursor to position (0, 0), clears all scroll offsets and
+     * selections, creates a fresh {@link Page}, inserts the text, and triggers
+     * a full repaint.</p>
+     *
+     * @param text the new text content for the editor; may contain newline
+     *             characters to create multiple lines
+     */
+    public void setText(final String text) {
+        // System.out.println("Set text:" + text);
+        cursorLocation = new TextPointer(0, 0);
+        scrolledCharacters = 0;
+        scrolledLines = 0;
+        selectionStart = new TextPointer(0, 0);
+        selectionEnd = new TextPointer(0, 0);
+        page = new Page();
+        insertText(text);
+        repaintPage();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLine.java
new file mode 100755 (executable)
index 0000000..1b83320
--- /dev/null
@@ -0,0 +1,410 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a single line of text in the text editor.
+ *
+ * <p>Internally stores a mutable list of {@link Character} objects, one per character in
+ * the line. Provides operations for inserting, cutting, and copying substrings, as well
+ * as indentation manipulation (adding or removing leading spaces).</p>
+ *
+ * <p>Lines automatically trim trailing whitespace via the internal {@code pack()} method,
+ * which is invoked after most mutating operations. This ensures that lines never store
+ * unnecessary trailing space characters.</p>
+ *
+ * @see Character the wrapper for individual character values in a line
+ * @see Page the container that holds multiple {@code TextLine} instances
+ * @see TextEditComponent the text editor component that uses lines for editing
+ */
+public class TextLine {
+
+    private List<Character> chars = new ArrayList<>();
+
+    /**
+     * Creates an empty text line with no characters.
+     */
+    public TextLine() {
+    }
+
+    /**
+     * Creates a text line from an existing list of {@link Character} objects.
+     *
+     * <p>Trailing whitespace is automatically trimmed via {@code pack()}.</p>
+     *
+     * @param value the list of characters to initialize this line with
+     */
+    public TextLine(final List<Character> value) {
+        chars = value;
+        pack();
+    }
+
+    /**
+     * Creates a text line initialized with the given string.
+     *
+     * <p>Each character in the string is converted to a {@link Character} object.
+     * Trailing whitespace is automatically trimmed.</p>
+     *
+     * @param value the string to initialize this line with
+     */
+    public TextLine(final String value) {
+        setValue(value);
+    }
+
+    /**
+     * Adds indentation (leading spaces) to the beginning of this line.
+     *
+     * <p>If the line is empty, no indentation is added. Otherwise, the specified
+     * number of space characters are prepended to the line.</p>
+     *
+     * @param amount the number of space characters to prepend
+     */
+    public void addIdent(final int amount) {
+        if (isEmpty())
+            return;
+
+        for (int i = 0; i < amount; i++)
+            chars.add(0, new Character(' '));
+    }
+
+    /**
+     * Removes characters from the specified range and returns them as a string.
+     *
+     * <p>This is a destructive operation: the characters in the range
+     * [{@code from}, {@code until}) are removed from this line. If the line is
+     * shorter than {@code until}, it is padded with spaces before extraction.
+     * Trailing whitespace is trimmed after removal.</p>
+     *
+     * @param from  the start index (inclusive) of the range to extract
+     * @param until the end index (exclusive) of the range to extract
+     * @return the extracted characters as a string
+     */
+    public String copySubString(final int from, final int until) {
+        final StringBuilder result = new StringBuilder();
+
+        ensureLength(until);
+
+        for (int i = from; i < until; i++)
+            result.append(chars.remove(from).value);
+
+        pack();
+        return result.toString();
+    }
+
+
+    /**
+     * Removes the specified number of characters from the beginning of this line.
+     *
+     * <p>If {@code charactersToCut} exceeds the line length, the entire line is cleared.
+     * If {@code charactersToCut} is zero, no changes are made.</p>
+     *
+     * @param charactersToCut the number of leading characters to remove
+     */
+    public void cutFromBeginning(int charactersToCut) {
+
+        if (charactersToCut > chars.size())
+            charactersToCut = chars.size();
+
+        if (charactersToCut == 0)
+            return;
+
+        chars = chars.subList(charactersToCut, chars.size());
+    }
+
+    /**
+     * Extracts a substring from this line, removing those characters and returning them.
+     *
+     * <p>Characters in the range [{@code from}, {@code until}) are removed from this
+     * line and returned as a string. Characters outside the range are retained. If the
+     * line is shorter than {@code until}, it is padded with spaces before extraction.
+     * Trailing whitespace is trimmed after the cut.</p>
+     *
+     * @param from  the start index (inclusive) of the range to cut
+     * @param until the end index (exclusive) of the range to cut
+     * @return the cut characters as a string
+     */
+    public String cutSubString(final int from, final int until) {
+        final StringBuilder result = new StringBuilder();
+
+        final List<Character> reminder = new ArrayList<>();
+
+        ensureLength(until);
+
+        for (int i = 0; i < chars.size(); i++)
+            if ((i >= from) && (i < until))
+                result.append(chars.get(i).value);
+            else
+                reminder.add(chars.get(i));
+
+        chars = reminder;
+
+        pack();
+        return result.toString();
+    }
+
+    /**
+     * Truncates this line at the specified column, discarding all characters from
+     * that position to the end.
+     *
+     * <p>If {@code col} is greater than or equal to the current line length,
+     * no changes are made.</p>
+     *
+     * @param col the column index at which to truncate (exclusive; characters at
+     *            indices 0 through {@code col - 1} are kept)
+     */
+    public void cutUntilEnd(final int col) {
+        if (col >= chars.size())
+            return;
+
+        chars = chars.subList(0, col);
+    }
+
+    /**
+     * Ensures the internal character list is at least the given length,
+     * padding with space characters as needed.
+     */
+    private void ensureLength(final int length) {
+        while (chars.size() < length)
+            chars.add(new Character(' '));
+    }
+
+    /**
+     * Returns the character at the specified column position.
+     *
+     * <p>If the column is beyond the end of this line, a space character is returned.</p>
+     *
+     * @param col the zero-based column index
+     * @return the character at the given column, or {@code ' '} if out of bounds
+     */
+    public char getCharForLocation(final int col) {
+
+        if (col >= chars.size())
+            return ' ';
+
+        return chars.get(col).value;
+    }
+
+    /**
+     * Returns the internal list of {@link Character} objects backing this line.
+     *
+     * <p><strong>Note:</strong> the returned list is the live internal list. Modifications
+     * to the returned list will directly affect this line.</p>
+     *
+     * @return the mutable list of characters in this line
+     */
+    public List<Character> getChars() {
+        return chars;
+    }
+
+    /**
+     * Returns the indentation level of this line, measured as the number of
+     * leading space characters before the first non-space character.
+     *
+     * <p>If the line is empty, returns {@code 0}.</p>
+     *
+     * @return the number of leading space characters
+     * @throws RuntimeException if the line is non-empty but contains only spaces
+     *         (should not occur due to trailing whitespace trimming by {@code pack()})
+     */
+    public int getIdent() {
+        if (isEmpty())
+            return 0;
+
+        for (int i = 0; i < chars.size(); i++)
+            if (chars.get(i).hasValue())
+                return i;
+
+        throw new RuntimeException("This code shall never execute");
+    }
+
+    /**
+     * Returns the length of this line (number of characters, excluding trimmed
+     * trailing whitespace).
+     *
+     * @return the number of characters in this line
+     */
+    public int getLength() {
+        return chars.size();
+    }
+
+    /**
+     * Returns a new {@code TextLine} containing the characters from this line
+     * in the range [{@code from}, {@code until}).
+     *
+     * <p>If {@code until} exceeds the line length, only the available characters
+     * are included. The returned line is an independent copy.</p>
+     *
+     * @param from  the start index (inclusive)
+     * @param until the end index (exclusive)
+     * @return a new {@code TextLine} with the specified sub-range of characters
+     */
+    public TextLine getSubLine(final int from, final int until) {
+        final List<Character> result = new ArrayList<>();
+
+        for (int i = from; i < until; i++) {
+            if (i >= chars.size())
+                break;
+            result.add(chars.get(i));
+        }
+
+        return new TextLine(result);
+    }
+
+    /**
+     * Returns a substring of this line from column {@code from} (inclusive) to
+     * column {@code until} (exclusive).
+     *
+     * <p>If the requested range extends beyond the line length, space characters
+     * are used for positions past the end of the line.</p>
+     *
+     * @param from  the start column (inclusive)
+     * @param until the end column (exclusive)
+     * @return the substring in the specified range
+     */
+    public String getSubString(final int from, final int until) {
+        final StringBuilder result = new StringBuilder();
+
+        for (int i = from; i < until; i++)
+            result.append(getCharForLocation(i));
+
+        return result.toString();
+    }
+
+    /**
+     * Inserts a single character at the specified column position.
+     *
+     * <p>If the column is beyond the current line length, the line is padded
+     * with spaces up to that position. Trailing whitespace is trimmed after
+     * insertion.</p>
+     *
+     * @param col   the zero-based column at which to insert
+     * @param value the character to insert
+     */
+    public void insertCharacter(final int col, final char value) {
+        ensureLength(col);
+        chars.add(col, new Character(value));
+        pack();
+    }
+
+    /**
+     * Inserts a string at the specified column position.
+     *
+     * <p>Each character in the string is inserted sequentially starting at
+     * {@code col}. If the column is beyond the current line length, the line
+     * is padded with spaces. Trailing whitespace is trimmed after insertion.</p>
+     *
+     * @param col   the zero-based column at which to start inserting
+     * @param value the string to insert
+     */
+    public void insertString(final int col, final String value) {
+        ensureLength(col);
+        int i = 0;
+        for (final char c : value.toCharArray()) {
+            chars.add(col + i, new Character(c));
+            i++;
+        }
+        pack();
+    }
+
+    /**
+     * Inserts all characters from another {@code TextLine} at the specified column.
+     *
+     * <p>If the column is beyond the current line length, the line is padded with
+     * spaces. Trailing whitespace is trimmed after insertion.</p>
+     *
+     * @param col      the zero-based column at which to start inserting
+     * @param textLine the text line whose characters will be inserted
+     */
+    public void insertTextLine(final int col, final TextLine textLine) {
+        ensureLength(col);
+        int i = 0;
+        for (final Character c : textLine.getChars()) {
+            chars.add(col + i, c);
+            i++;
+        }
+        pack();
+    }
+
+    /**
+     * Returns whether this line contains no characters.
+     *
+     * <p>Because trailing whitespace is trimmed, an empty line means there are
+     * no visible characters on this line.</p>
+     *
+     * @return {@code true} if the line has no characters, {@code false} otherwise
+     */
+    public boolean isEmpty() {
+        return chars.isEmpty();
+    }
+
+    /**
+     * Trims trailing whitespace from this line by removing trailing space
+     * characters that have no visible content.
+     */
+    private void pack() {
+        int newLength = 0;
+
+        for (int i = chars.size() - 1; i >= 0; i--)
+            if (chars.get(i).hasValue()) {
+                newLength = i + 1;
+                break;
+            }
+
+        if (newLength == chars.size())
+            return;
+
+        chars = chars.subList(0, newLength);
+    }
+
+    /**
+     * Removes the character at the specified column position.
+     *
+     * <p>If the column is beyond the end of the line, no changes are made.</p>
+     *
+     * @param col the zero-based column of the character to remove
+     */
+    public void removeCharacter(final int col) {
+        if (col >= chars.size())
+            return;
+
+        chars.remove(col);
+    }
+
+    /**
+     * Replaces the entire contents of this line with the given string.
+     *
+     * <p>The existing characters are cleared, and each character from the string
+     * is added as a new {@link Character} object. Trailing whitespace is trimmed.</p>
+     *
+     * @param string the new text content for this line
+     */
+    public void setValue(final String string) {
+        chars.clear();
+        for (final char c : string.toCharArray())
+            chars.add(new Character(c));
+
+        pack();
+    }
+
+    /**
+     * Returns the string representation of this line by concatenating
+     * all character values.
+     *
+     * @return the text content of this line as a {@code String}
+     */
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder();
+
+        for (final Character character : chars)
+            buffer.append(character.value);
+
+        return buffer.toString();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/package-info.java
new file mode 100644 (file)
index 0000000..ff8dff5
--- /dev/null
@@ -0,0 +1,5 @@
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+/**
+ * This package contains a simple text editor component.
+ */
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java
new file mode 100644 (file)
index 0000000..d921357
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+
+/**
+ * Represents a rotation in 3D space using two Euler angles (XZ and YZ).
+ *
+ * <p>Angles are stored with precomputed sine and cosine values for efficient
+ * repeated rotation operations without recalculating trigonometric functions.</p>
+ *
+ * @see Transform
+ */
+public class Rotation implements Cloneable {
+
+    /**
+     * Precomputed sine and cosine of the rotation angles.
+     */
+    private double s1, c1, s2, c2;
+
+    /**
+     * The angle of rotation around the XZ axis (yaw).
+     */
+    private double angleXZ = 0;
+
+    /**
+     * The angle of rotation around the YZ axis (pitch).
+     */
+    private double angleYZ = 0;
+
+    public Rotation() {
+        computeMultipliers();
+    }
+
+    /**
+     * Creates a rotation with the specified Euler angles.
+     *
+     * @param angleXZ the angle around the XZ axis (yaw) in radians
+     * @param angleYZ the angle around the YZ axis (pitch) in radians
+     */
+    public Rotation(final double angleXZ, final double angleYZ) {
+        this.angleXZ = angleXZ;
+        this.angleYZ = angleYZ;
+        computeMultipliers();
+    }
+
+    @Override
+    public Rotation clone() {
+        return new Rotation(angleXZ, angleYZ);
+    }
+
+    /**
+     * Recomputes the sine and cosine values from the current angles.
+     */
+    private void computeMultipliers() {
+        s1 = sin(angleXZ);
+        c1 = cos(angleXZ);
+
+        s2 = sin(angleYZ);
+        c2 = cos(angleYZ);
+    }
+
+    /**
+     * Rotates a point around the origin using this rotation's angles.
+     *
+     * @param point3d the point to rotate (modified in place)
+     */
+    public void rotate(final Point3D point3d) {
+        // Rotate around the XZ axis
+        final double z1 = (point3d.z * c1) - (point3d.x * s1);
+        point3d.x = (point3d.z * s1) + (point3d.x * c1);
+
+        // Rotate around the YZ axis
+        point3d.z = (z1 * c2) - (point3d.y * s2);
+        point3d.y = (z1 * s2) + (point3d.y * c2);
+    }
+
+    /**
+     * Adds the specified angles to this rotation and updates the trigonometric values.
+     *
+     * @param angleXZ the angle to add around the XZ axis in radians
+     * @param angleYZ the angle to add around the YZ axis in radians
+     */
+    public void rotate(final double angleXZ, final double angleYZ) {
+        this.angleXZ += angleXZ;
+        this.angleYZ += angleYZ;
+        computeMultipliers();
+    }
+
+    /**
+     * Sets the rotation angles and recomputes the trigonometric values.
+     *
+     * @param angleXZ the angle around the XZ axis (yaw) in radians
+     * @param angleYZ the angle around the YZ axis (pitch) in radians
+     */
+    public void setAngles(final double angleXZ, final double angleYZ) {
+        this.angleXZ = angleXZ;
+        this.angleYZ = angleYZ;
+        computeMultipliers();
+    }
+
+    public void setAngles(Rotation rotation) {
+        this.angleXZ = rotation.angleXZ;
+        this.angleYZ = rotation.angleYZ;
+        computeMultipliers();
+    }
+
+    /**
+     * Returns the angle around the XZ axis (yaw) in radians.
+     *
+     * @return the XZ angle
+     */
+    public double getAngleXZ() {
+        return angleXZ;
+    }
+
+    /**
+     * Returns the angle around the YZ axis (pitch) in radians.
+     *
+     * @return the YZ angle
+     */
+    public double getAngleYZ() {
+        return angleYZ;
+    }
+
+    /**
+     * Returns the precomputed sine of the XZ angle.
+     *
+     * @return sin(angleXZ)
+     */
+    public double getSinXZ() {
+        return s1;
+    }
+
+    /**
+     * Returns the precomputed cosine of the XZ angle.
+     *
+     * @return cos(angleXZ)
+     */
+    public double getCosXZ() {
+        return c1;
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
new file mode 100755 (executable)
index 0000000..36a6202
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+/**
+ * Represents a transformation in 3D space combining translation and rotation.
+ *
+ * <p>Transformations are applied in order: rotation first, then translation.</p>
+ *
+ * @see Rotation
+ */
+public class Transform implements Cloneable {
+
+    /**
+     * The translation applied after rotation.
+     */
+    private final Point3D translation;
+
+    /**
+     * The rotation applied before translation.
+     */
+    private final Rotation rotation;
+
+    public Transform() {
+        translation = new Point3D();
+        rotation = new Rotation();
+    }
+
+    /**
+     * Creates a transform with the specified translation and no rotation.
+     *
+     * @param translation the translation
+     */
+    public Transform(final Point3D translation) {
+        this.translation = translation;
+        rotation = new Rotation();
+    }
+
+    /**
+     * Creates a transform with the specified translation and rotation angles.
+     *
+     * @param translation the translation
+     * @param angleXZ     the angle around the XZ axis (yaw) in radians
+     * @param angleYZ     the angle around the YZ axis (pitch) in radians
+     */
+    public Transform(final Point3D translation, final double angleXZ,
+                     final double angleYZ) {
+
+        this.translation = translation;
+        rotation = new Rotation(angleXZ, angleYZ);
+    }
+
+    /**
+     * Creates a transform with the specified translation and rotation.
+     *
+     * @param translation the translation
+     * @param rotation    the rotation
+     */
+    public Transform(final Point3D translation, final Rotation rotation) {
+        this.translation = translation;
+        this.rotation = rotation;
+    }
+
+    @Override
+    public Transform clone() {
+        return new Transform(translation, rotation);
+    }
+
+    public Rotation getRotation() {
+        return rotation;
+    }
+
+    public Point3D getTranslation() {
+        return translation;
+    }
+
+   /**
+     * Applies this transform to a point: rotation followed by translation.
+     *
+     * @param point the point to transform (modified in place)
+     */
+    public void transform(final Point3D point) {
+        rotation.rotate(point);
+        point.add(translation);
+    }
+
+    /**
+     * Sets the rotation angles for this transform.
+     *
+     * @param angleXZ the angle around the XZ axis (yaw) in radians
+     * @param angleYZ the angle around the YZ axis (pitch) in radians
+     */
+    public void setRotation(final double angleXZ, final double angleYZ) {
+        rotation.setAngles(angleXZ, angleYZ);
+    }
+
+    /**
+     * Sets the translation for this transform by copying the values from the given point.
+     *
+     * @param translation the translation values to copy
+     */
+    public void setTranslation(final Point3D translation) {
+        this.translation.x = translation.x;
+        this.translation.y = translation.y;
+        this.translation.z = translation.z;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/TransformStack.java
new file mode 100644 (file)
index 0000000..1088dc4
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+/**
+ * Stack of transforms applied to points during rendering.
+ *
+ * <p>Transforms are applied in reverse order (last added is applied first).
+ * This supports hierarchical scene graphs where child objects are positioned
+ * relative to their parent objects.</p>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>
+ * There is a ship in the sea. The ship moves along the sea and every object
+ * on the ship moves with it. Inside the ship there is a car. The car moves
+ * along the ship and every object on the car moves with it.
+ *
+ * To calculate the world position of an object inside the car:
+ * 1. Apply object's position relative to the car
+ * 2. Apply car's position relative to the ship
+ * 3. Apply ship's position relative to the world
+ * </pre>
+ *
+ * @see Transform
+ */
+public class TransformStack {
+
+    /**
+     * Array of transforms in the stack.
+     * Fixed size for efficiency to avoid memory allocation during rendering.
+     */
+    private final Transform[] transforms = new Transform[100];
+
+    /**
+     * Current number of transforms in the stack.
+     */
+    private int transformsCount = 0;
+
+    /**
+     * Pushes a transform onto the stack.
+     *
+     * @param transform the transform to push
+     */
+    public void addTransform(final Transform transform) {
+        transforms[transformsCount] = transform;
+        transformsCount++;
+    }
+
+    /**
+     * Clears all transforms from the stack.
+     */
+    public void clear() {
+        transformsCount = 0;
+    }
+
+    /**
+     * Pops the most recently added transform from the stack.
+     */
+    public void dropTransform() {
+        transformsCount--;
+    }
+
+    /**
+     * Transforms a point through all transforms in the stack.
+     *
+     * @param coordinate the input coordinate (not modified)
+     * @param result     the output coordinate (receives transformed result)
+     */
+    public void transform(final Point3D coordinate, final Point3D result) {
+
+        result.clone(coordinate);
+
+        // Apply transforms in reverse order (last added = first applied)
+        for (int i = transformsCount - 1; i >= 0; i--)
+            transforms[i].transform(result);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
new file mode 100644 (file)
index 0000000..4fedbb9
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.math;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+
+/**
+ * Vertex is a point where two or more lines, line segments, or rays come together.
+ * In other words, it's a corner of a polygon, polyhedron, or other geometric shape.
+ * For example, a triangle has three vertices, a square has four, and a cube has eight.
+ */
+public class Vertex {
+
+    /**
+     * Vertex coordinate in 3D space.
+     */
+    public Point3D coordinate;
+
+    /**
+     * Vertex coordinate relative to the viewer after transformation.
+     * Visible vertices have positive z coordinate.
+     * Viewer is located at (0, 0, 0).
+     * No perspective correction is applied.
+     */
+    public Point3D transformedCoordinate;
+
+    /**
+     * Vertex coordinate in pixels relative to the top left corner of the screen after transformation
+     * and perspective correction.
+     */
+    public Point2D onScreenCoordinate;
+
+
+    /**
+     * Coordinate within texture.
+     */
+    public Point2D textureCoordinate;
+
+
+    /**
+     * The frame number when this vertex was last transformed.
+     */
+    private int lastTransformedFrame;
+
+    public Vertex() {
+        this(new Point3D());
+    }
+
+    public Vertex(final Point3D location) {
+        this(location, null);
+    }
+
+    public Vertex(final Point3D location, Point2D textureCoordinate) {
+        coordinate = location;
+        transformedCoordinate = new Point3D();
+        onScreenCoordinate = new Point2D();
+        this.textureCoordinate = textureCoordinate;
+    }
+
+
+    /**
+     * Transforms vertex coordinate to calculate its location relative to the viewer.
+     * It also calculates its location on the screen.
+     *
+     * @param transforms    Transforms pipeline.
+     * @param renderContext Rendering context.
+     */
+    public void calculateLocationRelativeToViewer(final TransformStack transforms,
+                                                  final RenderingContext renderContext) {
+
+        if (lastTransformedFrame == renderContext.frameNumber)
+            return;
+
+        lastTransformedFrame = renderContext.frameNumber;
+
+        transforms.transform(coordinate, transformedCoordinate);
+
+        onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale);
+        onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
+        onScreenCoordinate.add(renderContext.centerCoordinate);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/package-info.java
new file mode 100644 (file)
index 0000000..98dfff8
--- /dev/null
@@ -0,0 +1,9 @@
+/**
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ * <p>
+ * Math that is needed for the project.
+ */
+
+package eu.svjatoslav.sixth.e3d.math;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/package-info.java
new file mode 100644 (file)
index 0000000..fc7d743
--- /dev/null
@@ -0,0 +1,7 @@
+/**
+ * This is root package for 3D engine. Since package name cannot start with a digit, it is named "e3d" instead,
+ * which stands for "Engine 3D".
+ */
+
+package eu.svjatoslav.sixth.e3d;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/IntegerPoint.java
new file mode 100644 (file)
index 0000000..7dce375
--- /dev/null
@@ -0,0 +1,20 @@
+package eu.svjatoslav.sixth.e3d.renderer.octree;
+
+/**
+ * Point in 3D space. Used for octree. All coordinates are integers.
+ */
+public class IntegerPoint
+{
+    public int x, y, z = 0;
+
+    public IntegerPoint()
+    {
+    }
+
+    public IntegerPoint(final int x, final int y, final int z)
+    {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/OctreeVolume.java
new file mode 100755 (executable)
index 0000000..a4f4382
--- /dev/null
@@ -0,0 +1,1005 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.Ray;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+import static java.lang.Integer.max;
+import static java.lang.Integer.min;
+
+/**
+ * <pre>
+ * There are 3 cell types:
+ *
+ *      UNUSED
+ *
+ *      SOLID
+ *          contains:
+ *              original color
+ *              visible color, after being illuminated by nearby light sources
+ *
+ *      CLUSTER
+ *          contains pointers to 8 sub cells
+ * </pre>
+ */
+
+public class OctreeVolume {
+
+    // cell is not hit by the ray
+    public static final int TRACE_NO_HIT = -1;
+
+    // solid cell (contains color and illumination)
+    private static final int CELL_STATE_SOLID = -2;
+
+    // unused cell
+    private static final int CELL_STATE_UNUSED = -1;
+
+    public int[] cell1;
+    public int[] cell2;
+    public int[] cell3;
+    public int[] cell4;
+    public int[] cell5;
+    public int[] cell6;
+    public int[] cell7;
+    public int[] cell8;
+
+    /**
+     * Pointer to the first unused cell.
+     */
+    public int cellAllocationPointer = 0;
+    public int usedCellsCount = 0;
+    public int masterCellSize;
+
+    public OctreeVolume() {
+        initWorld(1500000, 256 * 64);
+    }
+
+    public void breakSolidCell(final int pointer) {
+        final int color = getCellColor(pointer);
+        final int illumination = getCellIllumination(pointer);
+
+        cell1[pointer] = makeNewCell(color, illumination);
+        cell2[pointer] = makeNewCell(color, illumination);
+        cell3[pointer] = makeNewCell(color, illumination);
+        cell4[pointer] = makeNewCell(color, illumination);
+        cell5[pointer] = makeNewCell(color, illumination);
+        cell6[pointer] = makeNewCell(color, illumination);
+        cell7[pointer] = makeNewCell(color, illumination);
+        cell8[pointer] = makeNewCell(color, illumination);
+    }
+
+    /**
+     * Clears the cell.
+     * @param pointer Pointer to the cell.
+     */
+    public void clearCell(final int pointer) {
+        cell1[pointer] = 0;
+        cell2[pointer] = 0;
+        cell3[pointer] = 0;
+        cell4[pointer] = 0;
+
+        cell5[pointer] = 0;
+        cell6[pointer] = 0;
+        cell7[pointer] = 0;
+        cell8[pointer] = 0;
+    }
+
+    public void deleteCell(final int cellPointer) {
+        clearCell(cellPointer);
+        cell1[cellPointer] = CELL_STATE_UNUSED;
+        usedCellsCount--;
+    }
+
+    public int doesIntersect(final int cubeX, final int cubeY, final int cubeZ,
+                             final int cubeSize, final Ray r) {
+
+        // ray starts inside the cube
+        if ((cubeX - cubeSize) < r.origin.x)
+            if ((cubeX + cubeSize) > r.origin.x)
+                if ((cubeY - cubeSize) < r.origin.y)
+                    if ((cubeY + cubeSize) > r.origin.y)
+                        if ((cubeZ - cubeSize) < r.origin.z)
+                            if ((cubeZ + cubeSize) > r.origin.z) {
+                                r.hitPoint = r.origin.clone();
+                                return 1;
+                            }
+        // back face
+        if (r.direction.z > 0)
+            if ((cubeZ - cubeSize) > r.origin.z) {
+                final double mult = ((cubeZ - cubeSize) - r.origin.z) / r.direction.z;
+                final double hitX = (r.direction.x * mult) + r.origin.x;
+                if ((cubeX - cubeSize) < hitX)
+                    if ((cubeX + cubeSize) > hitX) {
+                        final double hitY = (r.direction.y * mult) + r.origin.y;
+                        if ((cubeY - cubeSize) < hitY)
+                            if ((cubeY + cubeSize) > hitY) {
+                                r.hitPoint = new Point3D(hitX, hitY, cubeZ
+                                        - cubeSize);
+                                return 2;
+                            }
+                    }
+            }
+
+        // up face
+        if (r.direction.y > 0)
+            if ((cubeY - cubeSize) > r.origin.y) {
+                final double mult = ((cubeY - cubeSize) - r.origin.y) / r.direction.y;
+                final double hitX = (r.direction.x * mult) + r.origin.x;
+                if ((cubeX - cubeSize) < hitX)
+                    if ((cubeX + cubeSize) > hitX) {
+                        final double hitZ = (r.direction.z * mult) + r.origin.z;
+                        if ((cubeZ - cubeSize) < hitZ)
+                            if ((cubeZ + cubeSize) > hitZ) {
+                                r.hitPoint = new Point3D(hitX, cubeY - cubeSize,
+                                        hitZ);
+                                return 3;
+                            }
+                    }
+            }
+
+        // left face
+        if (r.direction.x > 0)
+            if ((cubeX - cubeSize) > r.origin.x) {
+                final double mult = ((cubeX - cubeSize) - r.origin.x) / r.direction.x;
+                final double hitY = (r.direction.y * mult) + r.origin.y;
+                if ((cubeY - cubeSize) < hitY)
+                    if ((cubeY + cubeSize) > hitY) {
+                        final double hitZ = (r.direction.z * mult) + r.origin.z;
+                        if ((cubeZ - cubeSize) < hitZ)
+                            if ((cubeZ + cubeSize) > hitZ) {
+                                r.hitPoint = new Point3D(cubeX - cubeSize, hitY,
+                                        hitZ);
+                                return 4;
+                            }
+                    }
+            }
+
+        // front face
+        if (r.direction.z < 0)
+            if ((cubeZ + cubeSize) < r.origin.z) {
+                final double mult = ((cubeZ + cubeSize) - r.origin.z) / r.direction.z;
+                final double hitX = (r.direction.x * mult) + r.origin.x;
+                if ((cubeX - cubeSize) < hitX)
+                    if ((cubeX + cubeSize) > hitX) {
+                        final double hitY = (r.direction.y * mult) + r.origin.y;
+                        if ((cubeY - cubeSize) < hitY)
+                            if ((cubeY + cubeSize) > hitY) {
+                                r.hitPoint = new Point3D(hitX, hitY, cubeZ
+                                        + cubeSize);
+                                return 5;
+                            }
+                    }
+            }
+
+        // down face
+        if (r.direction.y < 0)
+            if ((cubeY + cubeSize) < r.origin.y) {
+                final double mult = ((cubeY + cubeSize) - r.origin.y) / r.direction.y;
+                final double hitX = (r.direction.x * mult) + r.origin.x;
+                if ((cubeX - cubeSize) < hitX)
+                    if ((cubeX + cubeSize) > hitX) {
+                        final double hitZ = (r.direction.z * mult) + r.origin.z;
+                        if ((cubeZ - cubeSize) < hitZ)
+                            if ((cubeZ + cubeSize) > hitZ) {
+                                r.hitPoint = new Point3D(hitX, cubeY + cubeSize,
+                                        hitZ);
+                                return 6;
+                            }
+                    }
+            }
+
+        // right face
+        if (r.direction.x < 0)
+            if ((cubeX + cubeSize) < r.origin.x) {
+                final double mult = ((cubeX + cubeSize) - r.origin.x) / r.direction.x;
+                final double hitY = (r.direction.y * mult) + r.origin.y;
+                if ((cubeY - cubeSize) < hitY)
+                    if ((cubeY + cubeSize) > hitY) {
+                        final double hitZ = (r.direction.z * mult) + r.origin.z;
+                        if ((cubeZ - cubeSize) < hitZ)
+                            if ((cubeZ + cubeSize) > hitZ) {
+                                r.hitPoint = new Point3D(cubeX + cubeSize, hitY,
+                                        hitZ);
+                                return 7;
+                            }
+                    }
+            }
+        return 0;
+    }
+
+    /**
+     * Fill 3D rectangle.
+     */
+    public void fillRectangle(IntegerPoint p1, IntegerPoint p2, Color color) {
+
+        int x1 = min(p1.x, p2.x);
+        int x2 = max(p1.x, p2.x);
+        int y1 = min(p1.y, p2.y);
+        int y2 = max(p1.y, p2.y);
+        int z1 = min(p1.z, p2.z);
+        int z2 = max(p1.z, p2.z);
+
+        for (int x = x1; x <= x2; x++)
+            for (int y = y1; y <= y2; y++)
+                for (int z = z1; z <= z2; z++)
+                    putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color);
+    }
+
+    public int getCellColor(final int pointer) {
+        return cell2[pointer];
+    }
+
+    public int getCellIllumination(final int pointer) {
+        return cell3[pointer];
+    }
+
+    public void initWorld(final int bufferLength, final int masterCellSize) {
+        // System.out.println("Initializing new world");
+
+        // initialize world storage buffer
+        this.masterCellSize = masterCellSize;
+
+        cell1 = new int[bufferLength];
+        cell2 = new int[bufferLength];
+        cell3 = new int[bufferLength];
+        cell4 = new int[bufferLength];
+
+        cell5 = new int[bufferLength];
+        cell6 = new int[bufferLength];
+        cell7 = new int[bufferLength];
+        cell8 = new int[bufferLength];
+
+        for (int i = 0; i < bufferLength; i++)
+            cell1[i] = CELL_STATE_UNUSED;
+
+        // initialize master cell
+        clearCell(0);
+    }
+
+    public boolean isCellSolid(final int pointer) {
+        return cell1[pointer] == CELL_STATE_SOLID;
+    }
+
+    /**
+     * Scans cells arrays and returns pointer to found unused cell.
+     * @return pointer to found unused cell
+     */
+    public int getNewCellPointer() {
+        while (true) {
+            // ensure that cell allocation pointer is in bounds
+            if (cellAllocationPointer >= cell1.length)
+                cellAllocationPointer = 0;
+
+            if (cell1[cellAllocationPointer] == CELL_STATE_UNUSED) {
+                // unused cell found
+                clearCell(cellAllocationPointer);
+
+                usedCellsCount++;
+                return cellAllocationPointer;
+            } else
+                cellAllocationPointer++;
+        }
+    }
+
+    public int makeNewCell(final int color, final int illumination) {
+        final int pointer = getNewCellPointer();
+        markCellAsSolid(pointer);
+        setCellColor(pointer, color);
+        setCellIllumination(pointer, illumination);
+        return pointer;
+    }
+
+    /**
+     * Mark cell as solid.
+     *
+     * @param pointer pointer to cell
+     */
+    public void markCellAsSolid(final int pointer) {
+        cell1[pointer] = CELL_STATE_SOLID;
+    }
+
+    public void putCell(final int x, final int y, final int z, final Color color) {
+        putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color);
+    }
+
+    private void putCell(final int x, final int y, final int z,
+                         final int cellX, final int cellY, final int cellZ,
+                         final int cellSize, final int cellPointer, final Color color) {
+
+        if (cellSize > 1) {
+
+            // if case of big cell
+            if (isCellSolid(cellPointer)) {
+
+                // if cell is already a needed color, do notheing
+                if (getCellColor(cellPointer) == color.toInt())
+                    return;
+
+                // otherwise break cell up
+                breakSolidCell(cellPointer);
+
+                // continue, as if it is cluster now
+            }
+
+            // decide witch subcube to use
+            int[] subCubeArray;
+            int subX, subY, subZ;
+
+            if (x > cellX) {
+                subX = (cellSize / 2) + cellX;
+                if (y > cellY) {
+                    subY = (cellSize / 2) + cellY;
+                    if (z > cellZ) {
+                        subZ = (cellSize / 2) + cellZ;
+                        // 7
+                        subCubeArray = cell7;
+                    } else {
+                        subZ = (-cellSize / 2) + cellZ;
+                        // 3
+                        subCubeArray = cell3;
+                    }
+                } else {
+                    subY = (-cellSize / 2) + cellY;
+                    if (z > cellZ) {
+                        subZ = (cellSize / 2) + cellZ;
+                        // 6
+                        subCubeArray = cell6;
+                    } else {
+                        subZ = (-cellSize / 2) + cellZ;
+                        // 2
+                        subCubeArray = cell2;
+                    }
+                }
+            } else {
+                subX = (-cellSize / 2) + cellX;
+                if (y > cellY) {
+                    subY = (cellSize / 2) + cellY;
+                    if (z > cellZ) {
+                        subZ = (cellSize / 2) + cellZ;
+                        // 8
+                        subCubeArray = cell8;
+                    } else {
+                        subZ = (-cellSize / 2) + cellZ;
+                        // 4
+                        subCubeArray = cell4;
+                    }
+                } else {
+                    subY = (-cellSize / 2) + cellY;
+                    if (z > cellZ) {
+                        subZ = (cellSize / 2) + cellZ;
+                        // 5
+                        subCubeArray = cell5;
+                    } else {
+                        subZ = (-cellSize / 2) + cellZ;
+                        // 1
+                        subCubeArray = cell1;
+                    }
+                }
+            }
+
+            int subCubePointer;
+            if (subCubeArray[cellPointer] == 0) {
+                // create empty cluster
+                subCubePointer = getNewCellPointer();
+                subCubeArray[cellPointer] = subCubePointer;
+            } else
+                subCubePointer = subCubeArray[cellPointer];
+
+            putCell(x, y, z, subX, subY, subZ, cellSize / 2, subCubePointer,
+                    color);
+        } else {
+            cell1[cellPointer] = CELL_STATE_SOLID;
+            cell2[cellPointer] = color.toInt();
+            cell3[cellPointer] = CELL_STATE_UNUSED;
+            // System.out.println("Cell written!");
+        }
+    }
+
+    public void setCellColor(final int pointer, final int color) {
+        cell2[pointer] = color;
+    }
+
+    public void setCellIllumination(final int pointer, final int illumination) {
+        cell3[pointer] = illumination;
+    }
+
+    /**
+     * Trace ray through the world and return pointer to intersecting cell.
+     *
+     * @return pointer to intersecting cell or TRACE_NO_HIT if no intersection.
+     */
+    public int traceCell(final int cellX, final int cellY, final int cellZ,
+                         final int cellSize, final int pointer, final Ray ray) {
+        if (isCellSolid(pointer)) {
+            // solid cell
+            if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) {
+                ray.hitCellSize = cellSize;
+                ray.hitCellX = cellX;
+                ray.hitCellY = cellY;
+                ray.hitCellZ = cellZ;
+                return pointer;
+            }
+            return TRACE_NO_HIT;
+        } else // cluster
+            if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) {
+                final int halfOfCellSize = cellSize / 2;
+                int rayIntersectionResult;
+
+                if (ray.origin.x > cellX) {
+                    if (ray.origin.y > cellY) {
+                        if (ray.origin.z > cellZ) {
+                            // 7
+                            // 6 8 3 5 2 4 1
+
+                            if (cell7[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell7[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell6[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell6[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell8[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell8[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell3[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell3[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell2[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell2[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell4[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell4[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell5[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell5[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell1[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell1[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                        } else {
+                            // 3
+                            // 2 4 7 1 6 8 5
+                            if (cell3[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell3[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell2[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell2[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell4[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell4[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell7[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell7[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell6[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                + halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell6[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+                            if (cell8[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY + halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell8[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell1[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ - halfOfCellSize, halfOfCellSize,
+                                        cell1[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                            if (cell5[pointer] != 0) {
+                                rayIntersectionResult = traceCell(cellX
+                                                - halfOfCellSize, cellY - halfOfCellSize,
+                                        cellZ + halfOfCellSize, halfOfCellSize,
+                                        cell5[pointer], ray);
+                                if (rayIntersectionResult >= 0)
+                                    return rayIntersectionResult;
+                            }
+
+                        }
+                    } else if (ray.origin.z > cellZ) {
+                        // 6
+                        // 5 2 7 8 1 3 4
+                        if (cell6[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell6[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell7[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell7[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell2[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell2[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell5[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell5[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell8[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell8[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell3[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell3[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell1[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell1[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell4[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell4[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                    } else {
+                        // 2
+                        // 1 3 6 5 4 7 8
+                        if (cell2[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell2[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell3[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell3[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell1[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell1[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell6[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell6[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell7[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell7[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell5[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell5[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell4[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell4[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell8[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell8[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                    }
+                } else if (ray.origin.y > cellY) {
+                    if (ray.origin.z > cellZ) {
+                        // 8
+                        // 5 7 4 1 6 3 2
+
+                        if (cell8[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell8[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell7[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell7[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell5[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell5[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell4[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell4[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell3[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell3[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell1[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell1[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell6[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell6[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell2[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell2[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                    } else {
+                        // 4
+                        // 1 3 8 5 7 2 6
+
+                        if (cell4[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell4[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell8[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell8[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell3[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell3[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell1[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell1[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell7[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY + halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell7[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell5[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            - halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell5[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                        if (cell2[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            - halfOfCellSize, halfOfCellSize, cell2[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+                        if (cell6[pointer] != 0) {
+                            rayIntersectionResult = traceCell(cellX
+                                            + halfOfCellSize, cellY - halfOfCellSize, cellZ
+                                            + halfOfCellSize, halfOfCellSize, cell6[pointer],
+                                    ray);
+                            if (rayIntersectionResult >= 0)
+                                return rayIntersectionResult;
+                        }
+
+                    }
+                } else if (ray.origin.z > cellZ) {
+                    // 5
+                    // 1 6 8 4 2 7 3
+
+                    if (cell5[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell5[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell1[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell1[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell6[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell6[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell8[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell8[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell4[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell4[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell7[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell7[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell2[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell2[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell3[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell3[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                } else {
+                    // 1
+                    // 5 2 4 8 6 3 7
+
+                    if (cell1[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell1[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell5[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell5[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+                    if (cell2[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell2[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell4[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell4[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell6[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY - halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell6[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell8[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX - halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell8[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+
+                    if (cell3[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ - halfOfCellSize,
+                                halfOfCellSize, cell3[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+                    if (cell7[pointer] != 0) {
+                        rayIntersectionResult = traceCell(cellX + halfOfCellSize,
+                                cellY + halfOfCellSize, cellZ + halfOfCellSize,
+                                halfOfCellSize, cell7[pointer], ray);
+                        if (rayIntersectionResult >= 0)
+                            return rayIntersectionResult;
+                    }
+                }
+            }
+        return TRACE_NO_HIT;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/package-info.java
new file mode 100755 (executable)
index 0000000..821faf2
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * Octree-based voxel volume representation and rendering for the Sixth 3D engine.
+ *
+ * <p>This package provides a volumetric data structure based on an octree, which enables
+ * efficient storage and rendering of voxel data. The octree recursively subdivides 3D space
+ * into eight octants, achieving significant data compression for sparse or repetitive volumes.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume} - the main octree data structure
+ *       for storing and querying voxel cells</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.IntegerPoint} - integer 3D coordinate used
+ *       for voxel addressing</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.raytracer ray tracing through octree volumes
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.octree;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
new file mode 100644 (file)
index 0000000..f0b214b
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.math.Rotation;
+
+import static eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE;
+
+/**
+ * Represents camera view. Used to compute direction of rays during ray tracing.
+ */
+public class CameraView {
+
+    /**
+     * Camera view coordinates.
+     */
+    Point3D cameraCenter, topLeft, topRight, bottomLeft, bottomRight;
+
+    public CameraView(final Camera camera, final double zoom) {
+        // compute camera view coordinates as if camera is at (0,0,0) and look at (0,0,1)
+        final float viewAngle = (float) .6;
+        cameraCenter = new Point3D();
+        topLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, -viewAngle);
+        topRight = new Point3D(0, 0, SIZE).rotate(viewAngle, -viewAngle);
+        bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle);
+        bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle);
+
+        Rotation rotation = camera.getTransform().getRotation();
+        topLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
+        topRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
+        bottomLeft.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
+        bottomRight.rotate(-rotation.getAngleXZ(), -rotation.getAngleYZ());
+
+        // place camera view at camera location
+        camera.getTransform().getTranslation().clone().scaleDown(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/LightSource.java
new file mode 100755 (executable)
index 0000000..c0939d7
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+/**
+ * Represents light source.
+ */
+public class LightSource {
+
+    /**
+     * Light source color.
+     */
+    public Color color;
+    /**
+     * Light source brightness.
+     */
+    public float brightness;
+    /**
+     * Light source location.
+     */
+    Point3D location;
+
+    public LightSource(final Point3D location, final Color color,
+                       final float Brightness) {
+        this.location = location;
+        this.color = color;
+        brightness = Brightness;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/Ray.java
new file mode 100755 (executable)
index 0000000..afbe4b1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+
+/**
+ * Represents a ray used for tracing through an {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume}.
+ *
+ * <p>A ray is defined by an {@link #origin} point and a {@link #direction} vector.
+ * After tracing through the octree, the intersection results are stored in the
+ * {@link #hitPoint}, {@link #hitCellSize}, and {@link #hitCellX}/{@link #hitCellY}/{@link #hitCellZ}
+ * fields, which are populated by the octree traversal algorithm.</p>
+ *
+ * @see RayTracer
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume#traceCell(int, int, int, int, int, Ray)
+ */
+public class Ray {
+
+    /**
+     * The origin point of the ray (the starting position in world space).
+     */
+    public Point3D origin;
+
+    /**
+     * The direction vector of the ray. Does not need to be normalized;
+     * the octree traversal handles arbitrary direction magnitudes.
+     */
+    public Point3D direction;
+
+    /**
+     * The point in world space where the ray intersected an octree cell.
+     * Set by the octree traversal algorithm after a successful intersection.
+     */
+    public Point3D hitPoint;
+
+    /**
+     * The size (side length) of the octree cell that was hit.
+     * A value of 1 indicates a leaf cell at the finest resolution.
+     */
+    public int hitCellSize;
+
+    /**
+     * The x coordinate of the octree cell that was hit.
+     */
+    public int hitCellX;
+
+    /**
+     * The y coordinate of the octree cell that was hit.
+     */
+    public int hitCellY;
+
+    /**
+     * The z coordinate of the octree cell that was hit.
+     */
+    public int hitCellZ;
+
+    /**
+     * Creates a new ray with the specified origin and direction.
+     *
+     * @param origin    the starting point of the ray
+     * @param direction the direction vector of the ray
+     */
+    public Ray(Point3D origin, Point3D direction) {
+        this.origin = origin;
+        this.direction = direction;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayHit.java
new file mode 100755 (executable)
index 0000000..a1c8d41
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+/**
+ * Records the result of a ray-octree intersection test.
+ *
+ * <p>A {@code RayHit} stores the 3D world-space coordinates where a {@link Ray}
+ * intersected an octree cell, along with a pointer (index) to the intersected cell
+ * within the {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume}'s internal
+ * cell arrays.</p>
+ *
+ * @see Ray
+ * @see RayTracer
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume
+ */
+public class RayHit {
+
+    /**
+     * The x coordinate of the intersection point in world space.
+     */
+    float x;
+
+    /**
+     * The y coordinate of the intersection point in world space.
+     */
+    float y;
+
+    /**
+     * The z coordinate of the intersection point in world space.
+     */
+    float z;
+
+    /**
+     * The index (pointer) into the octree's cell arrays identifying the cell that was hit.
+     */
+    int cellPointer;
+
+    /**
+     * Creates a new ray hit record.
+     *
+     * @param x           the x coordinate of the intersection point
+     * @param y           the y coordinate of the intersection point
+     * @param z           the z coordinate of the intersection point
+     * @param cellPointer the index of the intersected cell in the octree's cell arrays
+     */
+    public RayHit(final float x, final float y, final float z,
+                  final int cellPointer) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+        this.cellPointer = cellPointer;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RayTracer.java
new file mode 100755 (executable)
index 0000000..aa69a15
--- /dev/null
@@ -0,0 +1,411 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+import java.util.Vector;
+
+/**
+ * Ray tracing engine for rendering {@link OctreeVolume} scenes onto a {@link Texture}.
+ *
+ * <p>{@code RayTracer} implements {@link Runnable} and is designed to execute as a background
+ * task. It casts one ray per pixel through the camera's view frustum, tracing each ray
+ * into the octree volume to find intersections with solid cells. When a hit is found, the
+ * ray tracer computes per-pixel lighting by casting shadow rays from the hit point toward
+ * each {@link LightSource} along multiple surface-normal-offset directions (6 directions:
+ * +X, -X, +Y, -Y, +Z, -Z) to approximate diffuse illumination with soft shadows.</p>
+ *
+ * <p><b>Rendering pipeline</b></p>
+ * <ol>
+ *   <li>The camera's view frustum corners are obtained via {@link RaytracingCamera#getCameraView()}.</li>
+ *   <li>For each pixel, a primary ray is constructed from the camera center through the
+ *       interpolated position on the view plane.</li>
+ *   <li>The ray is traced through the octree using
+ *       {@link OctreeVolume#traceCell(int, int, int, int, int, Ray)}.</li>
+ *   <li>If a solid cell is hit, up to 6 shadow rays are cast toward each light source.
+ *       If no shadow ray is occluded, the light's contribution is accumulated.</li>
+ *   <li>The final pixel color is the cell's base color modulated by the accumulated light.</li>
+ *   <li>Computed lighting is cached in the octree cell data ({@code cell3}) for reuse.</li>
+ * </ol>
+ *
+ * <p>Progress is reported periodically by invalidating the texture's mipmap cache and
+ * requesting a repaint on the {@link ViewPanel}, allowing partial results to be displayed
+ * while rendering continues.</p>
+ *
+ * @see OctreeVolume
+ * @see Ray
+ * @see LightSource
+ * @see RaytracingCamera
+ */
+public class RayTracer implements Runnable {
+
+    /**
+     * Minimum interval in milliseconds between progress updates (texture refresh and repaint).
+     */
+    private static final int PROGRESS_UPDATE_FREQUENCY_MILLIS = 1000;
+
+    /**
+     * The raytracing camera defining the viewpoint and view frustum for ray generation.
+     */
+    private final RaytracingCamera raytracingCamera;
+
+    /**
+     * The target texture where rendered pixels are written.
+     */
+    private final Texture texture;
+
+    /**
+     * The view panel used for triggering display repaints during progressive rendering.
+     */
+    private final ViewPanel viewPanel;
+
+    /**
+     * The octree volume to be ray-traced.
+     */
+    private final OctreeVolume octreeVolume;
+
+    /**
+     * The list of light sources used for illumination calculations.
+     */
+    private final Vector<LightSource> lights;
+
+    /**
+     * Counter tracking the number of light computations performed during the current render pass.
+     */
+    private int computedLights;
+
+    /**
+     * Creates a new ray tracer for the given scene configuration.
+     *
+     * @param texture      the texture to render into; its primary bitmap dimensions
+     *                     determine the output resolution
+     * @param octreeVolume the octree volume containing the scene geometry
+     * @param lights       the light sources to use for illumination
+     * @param raytracingCamera the raytracing camera defining the viewpoint
+     * @param viewPanel    the view panel for triggering progress repaints
+     */
+    public RayTracer(final Texture texture, final OctreeVolume octreeVolume,
+                     final Vector<LightSource> lights, final RaytracingCamera raytracingCamera,
+                     final ViewPanel viewPanel) {
+
+        this.texture = texture;
+        this.octreeVolume = octreeVolume;
+        this.lights = lights;
+        this.raytracingCamera = raytracingCamera;
+        this.viewPanel = viewPanel;
+    }
+
+    /**
+     * Executes the ray tracing render pass.
+     *
+     * <p>Iterates over every pixel of the target texture, constructs a primary ray
+     * from the camera center through the view plane, traces it into the octree volume,
+     * and writes the resulting color. The texture is periodically refreshed to show
+     * progressive results.</p>
+     */
+    @Override
+    public void run() {
+        computedLights = 0;
+
+        // create camera
+
+        // Camera cam = new Camera(camCenter, upLeft, upRight, downLeft,
+        // downRight);
+
+        // add camera to the raytracing point
+        // Main.mainWorld.geometryCollection.addObject(cam);
+        // Main.mainWorld.compiledGeometry.compileGeometry(Main.mainWorld.geometryCollection);
+
+        final int width = texture.primaryBitmap.width;
+        final int height = texture.primaryBitmap.height;
+
+        final CameraView cameraView = raytracingCamera.getCameraView();
+
+        // calculate vertical vectors
+        final double x1p = cameraView.bottomLeft.x - cameraView.topLeft.x;
+        final double y1p = cameraView.bottomLeft.y - cameraView.topLeft.y;
+        final double z1p = cameraView.bottomLeft.z - cameraView.topLeft.z;
+
+        final double x2p = cameraView.bottomRight.x - cameraView.topRight.x;
+        final double y2p = cameraView.bottomRight.y - cameraView.topRight.y;
+        final double z2p = cameraView.bottomRight.z - cameraView.topRight.z;
+
+        long nextBitmapUpdate = System.currentTimeMillis()
+                + PROGRESS_UPDATE_FREQUENCY_MILLIS;
+
+        for (int y = 0; y < height; y++) {
+            final double cx1 = cameraView.topLeft.x + ((x1p * y) / height);
+            final double cy1 = cameraView.topLeft.y + ((y1p * y) / height);
+            final double cz1 = cameraView.topLeft.z + ((z1p * y) / height);
+
+            final double cx2 = cameraView.topRight.x + ((x2p * y) / height);
+            final double cy2 = cameraView.topRight.y + ((y2p * y) / height);
+            final double cz2 = cameraView.topRight.z + ((z2p * y) / height);
+
+            // calculate horisontal vector
+            final double x3p = cx2 - cx1;
+            final double y3p = cy2 - cy1;
+            final double z3p = cz2 - cz1;
+
+            for (int x = 0; x < width; x++) {
+                final double cx3 = cx1 + ((x3p * x) / width);
+                final double cy3 = cy1 + ((y3p * x) / width);
+                final double cz3 = cz1 + ((z3p * x) / width);
+
+                final Ray r = new Ray(
+                        new Point3D(cameraView.cameraCenter.x,
+                                cameraView.cameraCenter.y,
+                                cameraView.cameraCenter.z),
+                        new Point3D(
+                                cx3 - cameraView.cameraCenter.x, cy3
+                                - cameraView.cameraCenter.y, cz3
+                                - cameraView.cameraCenter.z)
+                );
+                final int c = traceRay(r);
+
+                final Color color = new Color(c);
+                texture.primaryBitmap.drawPixel(x, y, color);
+            }
+
+            if (System.currentTimeMillis() > nextBitmapUpdate) {
+                nextBitmapUpdate = System.currentTimeMillis()
+                        + PROGRESS_UPDATE_FREQUENCY_MILLIS;
+                texture.resetResampledBitmapCache();
+                viewPanel.repaintDuringNextViewUpdate();
+            }
+        }
+
+        texture.resetResampledBitmapCache();
+        viewPanel.repaintDuringNextViewUpdate();
+    }
+
+    /**
+     * Traces a single ray into the octree volume and computes the resulting pixel color.
+     *
+     * <p>If the ray intersects a solid cell, the method computes diffuse lighting by
+     * casting shadow rays from 6 surface-offset positions toward each light source.
+     * The lighting result is cached in the octree's {@code cell3} array to avoid
+     * redundant computation for the same cell.</p>
+     *
+     * @param ray the ray to trace (origin and direction must be set)
+     * @return the packed RGB color value (0xRRGGBB), or 0 if the ray hits nothing
+     */
+    private int traceRay(final Ray ray) {
+
+        final int intersectingCell = octreeVolume.traceCell(0, 0, 0,
+                octreeVolume.masterCellSize, 0, ray);
+
+        if (intersectingCell != -1) {
+            // if lightening not computed, compute it
+            if (octreeVolume.cell3[intersectingCell] == -1)
+                // if cell is larger than 1
+                if (ray.hitCellSize > 1) {
+                    // break it up
+                    octreeVolume.breakSolidCell(intersectingCell);
+                    return traceRay(ray);
+                } else {
+                    computedLights++;
+                    float red = 30, green = 30, blue = 30;
+
+                    for (final LightSource l : lights) {
+                        final double xDist = (l.location.x - ray.hitCellX);
+                        final double yDist = (l.location.y - ray.hitCellY);
+                        final double zDist = (l.location.z - ray.hitCellZ);
+
+                        double newRed = 0, newGreen = 0, newBlue = 0;
+                        double tempRed, tempGreen, tempBlue;
+
+                        double distance = Math.sqrt((xDist * xDist)
+                                + (yDist * yDist) + (zDist * zDist));
+                        distance = (distance / 3) + 1;
+
+                        final Ray r1 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX,
+                                        ray.hitCellY - (float) 1.5,
+                                        ray.hitCellZ),
+
+                                new Point3D((float) l.location.x - (float) ray.hitCellX, l.location.y
+                                        - (ray.hitCellY - (float) 1.5), (float) l.location.z
+                                        - (float) ray.hitCellZ)
+                        );
+
+                        final int rt1 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r1);
+
+                        if (rt1 == -1) {
+                            newRed = (l.color.r * l.brightness) / distance;
+                            newGreen = (l.color.g * l.brightness) / distance;
+                            newBlue = (l.color.b * l.brightness) / distance;
+                        }
+
+                        final Ray r2 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX - (float) 1.5,
+                                        ray.hitCellY, ray.hitCellZ),
+
+                                new Point3D(
+                                        l.location.x - (ray.hitCellX - (float) 1.5), (float) l.location.y
+                                        - (float) ray.hitCellY, (float) l.location.z
+                                        - (float) ray.hitCellZ)
+                        );
+
+                        final int rt2 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r2);
+
+                        if (rt2 == -1) {
+                            tempRed = (l.color.r * l.brightness) / distance;
+                            tempGreen = (l.color.g * l.brightness) / distance;
+                            tempBlue = (l.color.b * l.brightness) / distance;
+
+                            if (tempRed > newRed)
+                                newRed = tempRed;
+                            if (tempGreen > newGreen)
+                                newGreen = tempGreen;
+                            if (tempBlue > newBlue)
+                                newBlue = tempBlue;
+                        }
+
+                        final Ray r3 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX, ray.hitCellY,
+                                        ray.hitCellZ - (float) 1.5),
+                                new Point3D(
+                                        (float) l.location.x - (float) ray.hitCellX, (float) l.location.y
+                                        - (float) ray.hitCellY, l.location.z
+                                        - (ray.hitCellZ - (float) 1.5))
+                        );
+
+                        final int rt3 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r3);
+
+                        if (rt3 == -1) {
+                            tempRed = (l.color.r * l.brightness) / distance;
+                            tempGreen = (l.color.g * l.brightness) / distance;
+                            tempBlue = (l.color.b * l.brightness) / distance;
+                            if (tempRed > newRed)
+                                newRed = tempRed;
+                            if (tempGreen > newGreen)
+                                newGreen = tempGreen;
+                            if (tempBlue > newBlue)
+                                newBlue = tempBlue;
+                        }
+
+                        final Ray r4 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX,
+                                        ray.hitCellY + (float) 1.5,
+                                        ray.hitCellZ),
+
+                                new Point3D(
+                                        (float) l.location.x - (float) ray.hitCellX, l.location.y
+                                        - (ray.hitCellY + (float) 1.5), (float) l.location.z
+                                        - (float) ray.hitCellZ)
+                        );
+
+                        final int rt4 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r4);
+
+                        if (rt4 == -1) {
+                            tempRed = (l.color.r * l.brightness) / distance;
+                            tempGreen = (l.color.g * l.brightness) / distance;
+                            tempBlue = (l.color.b * l.brightness) / distance;
+                            if (tempRed > newRed)
+                                newRed = tempRed;
+                            if (tempGreen > newGreen)
+                                newGreen = tempGreen;
+                            if (tempBlue > newBlue)
+                                newBlue = tempBlue;
+                        }
+
+                        final Ray r5 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX + (float) 1.5,
+                                        ray.hitCellY, ray.hitCellZ),
+
+                                new Point3D(
+                                        l.location.x - (ray.hitCellX + (float) 1.5), (float) l.location.y
+                                        - (float) ray.hitCellY, (float) l.location.z
+                                        - (float) ray.hitCellZ)
+                        );
+
+                        final int rt5 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r5);
+
+                        if (rt5 == -1) {
+                            tempRed = (l.color.r * l.brightness) / distance;
+                            tempGreen = (l.color.g * l.brightness) / distance;
+                            tempBlue = (l.color.b * l.brightness) / distance;
+                            if (tempRed > newRed)
+                                newRed = tempRed;
+                            if (tempGreen > newGreen)
+                                newGreen = tempGreen;
+                            if (tempBlue > newBlue)
+                                newBlue = tempBlue;
+                        }
+
+                        final Ray r6 = new Ray(
+                                new Point3D(
+                                        ray.hitCellX, ray.hitCellY,
+                                        ray.hitCellZ + (float) 1.5),
+
+                                new Point3D(
+
+                                        (float) l.location.x - (float) ray.hitCellX, (float) l.location.y
+                                        - (float) ray.hitCellY, l.location.z
+                                        - (ray.hitCellZ + (float) 1.5)));
+
+                        final int rt6 = octreeVolume.traceCell(0, 0, 0,
+                                octreeVolume.masterCellSize, 0, r6);
+
+                        if (rt6 == -1) {
+                            tempRed = (l.color.r * l.brightness) / distance;
+                            tempGreen = (l.color.g * l.brightness) / distance;
+                            tempBlue = (l.color.b * l.brightness) / distance;
+                            if (tempRed > newRed)
+                                newRed = tempRed;
+                            if (tempGreen > newGreen)
+                                newGreen = tempGreen;
+                            if (tempBlue > newBlue)
+                                newBlue = tempBlue;
+                        }
+                        red += newRed;
+                        green += newGreen;
+                        blue += newBlue;
+
+                    }
+
+                    final int cellColor = octreeVolume.cell2[intersectingCell];
+
+                    red = (red * ((cellColor & 0xFF0000) >> 16)) / 255;
+                    green = (green * ((cellColor & 0xFF00) >> 8)) / 255;
+                    blue = (blue * (cellColor & 0xFF)) / 255;
+
+                    if (red > 255)
+                        red = 255;
+                    if (green > 255)
+                        green = 255;
+                    if (blue > 255)
+                        blue = 255;
+
+                    octreeVolume.cell3[intersectingCell] = (((int) red) << 16)
+                            + (((int) green) << 8) + ((int) blue);
+
+                }
+            if (octreeVolume.cell3[intersectingCell] == 0)
+                return octreeVolume.cell2[intersectingCell];
+            return octreeVolume.cell3[intersectingCell];
+        }
+
+        // return (200 << 16) + (200 << 8) + 255;
+        return 0;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java
new file mode 100755 (executable)
index 0000000..e8237a9
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.math.Rotation;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.TexturedRectangle;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * Raytracing camera that renders a scene to a texture.
+ * It is represented on the scene as a textured rectangle showing the raytraced view.
+ */
+public class RaytracingCamera extends TexturedRectangle {
+
+    public static final int SIZE = 100;
+    public static final int IMAGE_SIZE = 500;
+    private final CameraView cameraView;
+
+    public RaytracingCamera(final Camera camera, final double zoom) {
+        super(new Transform(camera.getTransform().getTranslation().clone()));
+        cameraView = new CameraView(camera, zoom);
+
+        computeCameraCoordinates(camera);
+
+        addWaitNotification(getTexture());
+    }
+
+    private void addWaitNotification(final Texture texture) {
+        // add hourglass icon
+        try {
+            final BufferedImage sprite = getSprite("eu/svjatoslav/sixth/e3d/examples/hourglass.png");
+            texture.graphics.drawImage(sprite, IMAGE_SIZE / 2,
+                    (IMAGE_SIZE / 2) - 30, null);
+        } catch (final Exception ignored) {
+        }
+
+        // add "Please wait..." message
+        texture.graphics.setColor(java.awt.Color.WHITE);
+        texture.graphics.setFont(new Font("Monospaced", Font.PLAIN, 10));
+        texture.graphics.drawString("Please wait...", (IMAGE_SIZE / 2) - 20,
+                (IMAGE_SIZE / 2) + 30);
+    }
+
+    private void computeCameraCoordinates(final Camera camera) {
+        initialize(SIZE, SIZE, IMAGE_SIZE, IMAGE_SIZE, 3);
+
+        Point3D cameraCenter = new Point3D();
+
+        topLeft.setValues(cameraCenter.x, cameraCenter.y, cameraCenter.z + SIZE);
+        topRight.clone(topLeft);
+        bottomLeft.clone(topLeft);
+        bottomRight.clone(topLeft);
+
+        final float viewAngle = (float) .6;
+
+        topLeft.rotate(cameraCenter, -viewAngle, -viewAngle);
+        topRight.rotate(cameraCenter, viewAngle, -viewAngle);
+        bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle);
+        bottomRight.rotate(cameraCenter, viewAngle, viewAngle);
+
+        Rotation rotation = camera.getTransform().getRotation();
+        topLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
+        topRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
+        bottomLeft.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
+        bottomRight.rotate(cameraCenter, -rotation.getAngleXZ(), -rotation.getAngleYZ());
+
+        final Color cameraColor = new Color(255, 255, 0, 255);
+        final LineAppearance appearance = new LineAppearance(2, cameraColor);
+
+        addShape(appearance.getLine(topLeft, topRight));
+        addShape(appearance.getLine(bottomLeft, bottomRight));
+        addShape(appearance.getLine(topLeft, bottomLeft));
+        addShape(appearance.getLine(topRight, bottomRight));
+
+    }
+
+    public CameraView getCameraView() {
+        return cameraView;
+    }
+
+    public BufferedImage getSprite(final String ref) throws IOException {
+        final URL url = this.getClass().getClassLoader().getResource(ref);
+        return ImageIO.read(url);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/package-info.java
new file mode 100755 (executable)
index 0000000..b82ab4f
--- /dev/null
@@ -0,0 +1,21 @@
+/**
+ * Ray tracer for rendering voxel data stored in an octree structure.
+ *
+ * <p>This package implements a ray tracing renderer that casts rays through an
+ * {@link eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume} to produce rendered images
+ * of volumetric data. The ray tracer traverses the octree hierarchy for efficient
+ * intersection testing, skipping empty regions of space.</p>
+ *
+ * <p>Key classes:</p>
+ * <ul>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RayTracer} - main ray tracing engine</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.RaytracingCamera} - camera configuration for ray generation</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.Ray} - represents a single ray cast through the volume</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.octree.raytracer.LightSource} - defines a light source for shading</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.octree.OctreeVolume the voxel data structure
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.octree.raytracer;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/package-info.java
new file mode 100755 (executable)
index 0000000..249622e
--- /dev/null
@@ -0,0 +1,11 @@
+/**
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ * <p>
+ *
+ * Various 3D renderers utilizing different rendering approaches.
+ *
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java
new file mode 100644 (file)
index 0000000..e7cb25f
--- /dev/null
@@ -0,0 +1,273 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster;
+
+/**
+ * Immutable RGBA color representation for the Sixth 3D engine.
+ *
+ * <p>This is the engine's own color class (not {@link java.awt.Color}). All color values
+ * use integer components in the range 0-255. The class provides predefined constants
+ * for common colors and several constructors for creating colors from different formats.</p>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@code
+ * // Use predefined color constants
+ * Color red = Color.RED;
+ * Color semiTransparent = new Color(255, 0, 0, 128);
+ *
+ * // 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
+ * working with the Sixth 3D engine's rendering pipeline.</p>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line
+ */
+public final class Color {
+
+    /** Fully opaque red (255, 0, 0). */
+    public static final Color RED = new Color(255, 0, 0, 255);
+    /** Fully opaque green (0, 255, 0). */
+    public static final Color GREEN = new Color(0, 255, 0, 255);
+    /** Fully opaque blue (0, 0, 255). */
+    public static final Color BLUE = new Color(0, 0, 255, 255);
+    /** Fully opaque yellow (255, 255, 0). */
+    public static final Color YELLOW = new Color(255, 255, 0, 255);
+    /** Fully opaque cyan (0, 255, 255). */
+    public static final Color CYAN = new Color(0, 255, 255, 255);
+    /** Fully opaque magenta/purple (255, 0, 255). */
+    public static final Color MAGENTA = new Color(255, 0, 255, 255);
+    /** Fully opaque white (255, 255, 255). */
+    public static final Color WHITE = new Color(255, 255, 255, 255);
+    /** Fully opaque black (0, 0, 0). */
+    public static final Color BLACK = new Color(0, 0, 0, 255);
+    /** Fully opaque purple/magenta (255, 0, 255). */
+    public static final Color PURPLE = new Color(255, 0, 255, 255);
+    /** Fully transparent (alpha = 0). */
+    public static final Color TRANSPARENT = new Color(0, 0, 0, 0);
+
+    /**
+     * Red component. 0-255.
+     */
+    public final int r;
+
+    /**
+     * Green component. 0-255.
+     */
+    public final int g;
+
+    /**
+     * Blue component. 0-255.
+     */
+    public final int b;
+
+    /**
+     * Alpha component.
+     * 0 - transparent.
+     * 255 - opaque.
+     */
+    public int a;
+
+    private java.awt.Color cachedAwtColor;
+
+    /**
+     * Creates a copy of the given color.
+     *
+     * @param parentColor the color to copy
+     */
+    public Color(final Color parentColor) {
+        r = parentColor.r;
+        g = parentColor.g;
+        b = parentColor.b;
+        a = parentColor.a;
+    }
+
+    /**
+     * Creates a color from floating-point RGBA components in the range 0.0 to 1.0.
+     * Values are internally converted to 0-255 integer range and clamped.
+     *
+     * @param r red component (0.0 = none, 1.0 = full)
+     * @param g green component (0.0 = none, 1.0 = full)
+     * @param b blue component (0.0 = none, 1.0 = full)
+     * @param a alpha component (0.0 = transparent, 1.0 = opaque)
+     */
+    public Color(final double r, final double g, final double b, final double a) {
+        this.r = ensureByteLimit((int) (r * 255d));
+        this.g = ensureByteLimit((int) (g * 255d));
+        this.b = ensureByteLimit((int) (b * 255d));
+        this.a = ensureByteLimit((int) (a * 255d));
+    }
+
+    /**
+     * @param colorHexCode color code in hex format.
+     *                     Supported formats are:
+     *                     <pre>
+     *                     RGB
+     *                     RGBA
+     *                     RRGGBB
+     *                     RRGGBBAA
+     *                     </pre>
+     */
+    public Color(String colorHexCode) {
+        switch (colorHexCode.length()) {
+            case 3:
+                r = parseHexSegment(colorHexCode, 0, 1) * 16;
+                g = parseHexSegment(colorHexCode, 1, 1) * 16;
+                b = parseHexSegment(colorHexCode, 2, 1) * 16;
+                a = 255;
+                return;
+
+            case 4:
+                r = parseHexSegment(colorHexCode, 0, 1) * 16;
+                g = parseHexSegment(colorHexCode, 1, 1) * 16;
+                b = parseHexSegment(colorHexCode, 2, 1) * 16;
+                a = parseHexSegment(colorHexCode, 3, 1) * 16;
+                return;
+
+            case 6:
+                r = parseHexSegment(colorHexCode, 0, 2);
+                g = parseHexSegment(colorHexCode, 2, 2);
+                b = parseHexSegment(colorHexCode, 4, 2);
+                a = 255;
+                return;
+
+            case 8:
+                r = parseHexSegment(colorHexCode, 0, 2);
+                g = parseHexSegment(colorHexCode, 2, 2);
+                b = parseHexSegment(colorHexCode, 4, 2);
+                a = parseHexSegment(colorHexCode, 6, 2);
+                return;
+            default:
+                throw new IllegalArgumentException("Unsupported color code: " + colorHexCode);
+        }
+    }
+
+    /**
+     * Creates a fully opaque color from a packed RGB integer.
+     *
+     * <p>The integer is interpreted as {@code 0xRRGGBB}, where the upper 8 bits
+     * are the red channel, the middle 8 bits are green, and the lower 8 bits are blue.</p>
+     *
+     * @param rgb packed RGB value (e.g. {@code 0xFF8800} for orange)
+     */
+    public Color(final int rgb) {
+        r = (rgb & 0xFF0000) >> 16;
+        g = (rgb & 0xFF00) >> 8;
+        b = rgb & 0xFF;
+        a = 255;
+    }
+
+    /**
+     * Creates a fully opaque color from RGB integer components (0-255).
+     *
+     * @param r red component (0-255)
+     * @param g green component (0-255)
+     * @param b blue component (0-255)
+     */
+    public Color(final int r, final int g, final int b) {
+        this(r, g, b, 255);
+    }
+
+    /**
+     * Creates a color from RGBA integer components (0-255).
+     * Values outside 0-255 are clamped.
+     *
+     * @param r red component (0-255)
+     * @param g green component (0-255)
+     * @param b blue component (0-255)
+     * @param a alpha component (0 = transparent, 255 = opaque)
+     */
+    public Color(final int r, final int g, final int b, final int a) {
+        this.r = ensureByteLimit(r);
+        this.g = ensureByteLimit(g);
+        this.b = ensureByteLimit(b);
+        this.a = ensureByteLimit(a);
+    }
+
+    private int parseHexSegment(String hexString, int start, int length) {
+        return Integer.parseInt(hexString.substring(start, start + length), 16);
+    }
+
+    /**
+     * Ensure that color values are within allowed limits of 0 to 255.
+     */
+    private int ensureByteLimit(final int value) {
+        if (value < 0)
+            return 0;
+
+        if (value > 255)
+            return 255;
+
+        return value;
+    }
+
+    /**
+     * Returns {@code true} if this color is fully transparent (alpha = 0).
+     *
+     * @return {@code true} if the alpha component is zero
+     */
+    public boolean isTransparent() {
+        return a == 0;
+    }
+
+    /**
+     * Converts this color to a {@link java.awt.Color} instance for use with
+     * Java AWT/Swing graphics APIs.
+     *
+     * @return the equivalent {@link java.awt.Color}
+     */
+    public java.awt.Color toAwtColor() {
+        if (cachedAwtColor == null)
+            cachedAwtColor = new java.awt.Color(r, g, b, a);
+        return cachedAwtColor;
+    }
+
+    /**
+     * Converts this color to a packed ARGB integer as used by {@link java.awt.Color#getRGB()}.
+     *
+     * @return packed ARGB integer representation
+     */
+    public int toInt() {
+        return (a << 24) | (r << 16) | (g << 8) | b;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Color color = (Color) o;
+
+        if (r != color.r) return false;
+        if (g != color.g) return false;
+        if (b != color.b) return false;
+        return a == color.a;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = r;
+        result = 31 * result + g;
+        result = 31 * result + b;
+        result = 31 * result + a;
+        return result;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/RenderAggregator.java
new file mode 100644 (file)
index 0000000..c22f37c
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster;
+
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * Collects transformed shapes during a render frame and paints them in depth-sorted order.
+ *
+ * <p>The {@code RenderAggregator} implements the painter's algorithm: shapes are sorted
+ * from back to front (highest Z-depth first) and then painted sequentially. This ensures
+ * that closer shapes correctly occlude those behind them.</p>
+ *
+ * <p>When two shapes have the same Z-depth, their unique {@link AbstractCoordinateShape#shapeId}
+ * is used as a tiebreaker to guarantee deterministic rendering order.</p>
+ *
+ * <p>This class is used internally by {@link ShapeCollection} during the render pipeline.
+ * You typically do not need to interact with it directly.</p>
+ *
+ * @see ShapeCollection#paint(eu.svjatoslav.sixth.e3d.gui.ViewPanel, RenderingContext)
+ * @see AbstractCoordinateShape#onScreenZ
+ */
+public class RenderAggregator {
+
+    private final ArrayList<AbstractCoordinateShape> shapes = new ArrayList<>();
+    private final ShapesZIndexComparator comparator = new ShapesZIndexComparator();
+
+    /**
+     * Sorts all queued shapes by Z-depth (back to front) and paints them.
+     *
+     * @param renderBuffer the rendering context to paint shapes into
+     */
+    public void paint(final RenderingContext renderBuffer) {
+        shapes.sort(comparator);
+        for (int i = 0; i < shapes.size(); i++)
+            shapes.get(i).paint(renderBuffer);
+    }
+
+    /**
+     * Adds a transformed shape to the queue for rendering in this frame.
+     *
+     * @param shape the shape to render, with its screen-space coordinates already computed
+     */
+    public void queueShapeForRendering(final AbstractCoordinateShape shape) {
+        shapes.add(shape);
+    }
+
+    /**
+     * Clears all queued shapes, preparing for a new render frame.
+     */
+    public void reset() {
+        shapes.clear();
+    }
+
+    /**
+     * Comparator that sorts shapes by Z-depth in descending order (farthest first)
+     * for the painter's algorithm. Uses shape ID as a tiebreaker.
+     */
+    static class ShapesZIndexComparator implements Comparator<AbstractCoordinateShape>, Serializable {
+
+        @Override
+        public int compare(final AbstractCoordinateShape o1, final AbstractCoordinateShape o2) {
+            if (o1.getZ() < o2.getZ())
+                return 1;
+            else if (o1.getZ() > o2.getZ())
+                return -1;
+
+            return Integer.compare(o1.shapeId, o2.shapeId);
+        }
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java
new file mode 100755 (executable)
index 0000000..ad0be61
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.Camera;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.math.TransformStack;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Root container that holds all 3D shapes in a scene and orchestrates their rendering.
+ *
+ * <p>{@code ShapeCollection} is the top-level scene graph. You add shapes to it, and during
+ * each render frame it transforms all shapes from world space to screen space (relative to the
+ * camera), sorts them by depth, and paints them back-to-front.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Get the root shape collection from the view panel
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ *
+ * // Add shapes to the scene
+ * scene.addShape(new Line(
+ *     new Point3D(0, 0, 100),
+ *     new Point3D(100, 0, 100),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * scene.addShape(new WireframeCube(
+ *     new Point3D(0, 0, 200), 50,
+ *     new LineAppearance(5, Color.GREEN)
+ * ));
+ * }</pre>
+ *
+ * <p>The {@link #addShape} method is synchronized, making it safe to add shapes from
+ * any thread while the rendering loop is active.</p>
+ *
+ * @see ViewPanel#getRootShapeCollection()
+ * @see AbstractShape the base class for all shapes
+ * @see RenderAggregator handles depth sorting and painting
+ */
+public class ShapeCollection {
+
+    private final RenderAggregator aggregator = new RenderAggregator();
+    private final TransformStack transformStack = new TransformStack();
+    private final List<AbstractShape> shapes = new ArrayList<>();
+
+    // Camera rotation. We reuse this object for every frame render to avoid garbage collections.
+    private final Transform cameraRotationTransform = new Transform();
+
+    // Camera rotation. We reuse this object for every frame render to avoid garbage collections.
+    private final Transform cameraTranslationTransform = new Transform();
+
+    /**
+     * Adds a shape to this collection. This method is thread-safe.
+     *
+     * @param shape the shape to add to the scene
+     */
+    public synchronized void addShape(final AbstractShape shape) {
+        shapes.add(shape);
+    }
+
+    /**
+     * Returns the list of all shapes currently in this collection.
+     *
+     * @return unmodifiable view would be safer, but currently returns the internal list
+     */
+    public Collection<AbstractShape> getShapes() {
+        return shapes;
+    }
+
+    /**
+     * Removes all shapes from this collection.
+     */
+    public void clear() {
+        shapes.clear();
+    }
+
+    /**
+     * Renders all shapes in this collection for the current frame.
+     *
+     * <p>This method performs the full render pipeline:</p>
+     * <ol>
+     *   <li>Resets the aggregator and transform stack</li>
+     *   <li>Applies the camera rotation (avatar's viewing direction)</li>
+     *   <li>Applies the camera translation (avatar's position in the world)</li>
+     *   <li>Transforms all shapes to screen space</li>
+     *   <li>Sorts shapes by depth and paints them back-to-front</li>
+     * </ol>
+     *
+     * @param viewPanel        the view panel providing the camera state
+     * @param renderingContext  the rendering context with pixel buffer and frame metadata
+     */
+    public synchronized void paint(final ViewPanel viewPanel,
+                                   final RenderingContext renderingContext) {
+
+        renderingContext.frameNumber++;
+
+        aggregator.reset();
+        transformStack.clear();
+
+        // Translate the scene according to camera current location.
+        final Camera camera = viewPanel.getCamera();
+
+        // Rotate the scene according to camera looking direction
+
+        cameraRotationTransform.getRotation().setAngles(camera.getTransform().getRotation());
+        transformStack.addTransform(cameraRotationTransform);
+
+        // translate scene according to camera location in the world
+        final Point3D cameraLocation = camera.getTransform().getTranslation();
+        cameraTranslationTransform.getTranslation().x = -cameraLocation.x;
+        cameraTranslationTransform.getTranslation().y = -cameraLocation.y;
+        cameraTranslationTransform.getTranslation().z = -cameraLocation.z;
+        transformStack.addTransform(cameraTranslationTransform);
+
+        for (final AbstractShape shape : shapes)
+            shape.transform(transformStack, aggregator, renderingContext);
+
+        aggregator.paint(renderingContext);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.java
new file mode 100644 (file)
index 0000000..7425ca9
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.lighting;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+/**
+ * Represents a light source in the 3D scene with position, color, and intensity.
+ *
+ * <p>Light sources emit colored light that illuminates polygons based on their
+ * orientation relative to the light. The intensity of illumination follows the
+ * Lambert cosine law - surfaces facing the light receive full intensity, while
+ * surfaces at an angle receive proportionally less light.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a yellow light source at position (100, -50, 200)
+ * LightSource light = new LightSource(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW,
+ *     1.5
+ * );
+ *
+ * // Move the light source
+ * light.setPosition(new Point3D(0, 0, 300));
+ *
+ * // Change the light color
+ * light.setColor(new Color(255, 100, 50));
+ *
+ * // Adjust intensity
+ * light.setIntensity(2.0);
+ * }</pre>
+ *
+ * @see LightingManager manages multiple light sources and calculates shading
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon
+ */
+public class LightSource {
+
+    /**
+     * Position of the light source in 3D world space.
+     */
+    private Point3D position;
+
+    /**
+     * Color of the light emitted by this source.
+     */
+    private Color color;
+
+    /**
+     * Intensity multiplier for this light source.
+     * Values greater than 1.0 make the light brighter, values less than 1.0 make it dimmer.
+     * High intensity values can cause surfaces to appear white (clamped at 255).
+     */
+    private double intensity;
+
+    /**
+     * Creates a new light source at the specified position with the given color and intensity.
+     *
+     * @param position the position of the light in world space
+     * @param color    the color of the light
+     * @param intensity the intensity multiplier (1.0 = normal brightness)
+     */
+    public LightSource(final Point3D position, final Color color, final double intensity) {
+        this.position = position;
+        this.color = color;
+        this.intensity = intensity;
+    }
+
+    /**
+     * Creates a new light source at the specified position with the given color.
+     * Default intensity is 1.0.
+     *
+     * @param position the position of the light in world space
+     * @param color    the color of the light
+     */
+    public LightSource(final Point3D position, final Color color) {
+        this(position, color, 1.0);
+    }
+
+    /**
+     * Returns the color of this light source.
+     *
+     * @return the light color
+     */
+    public Color getColor() {
+        return color;
+    }
+
+    /**
+     * Returns the intensity multiplier of this light source.
+     *
+     * @return the intensity multiplier
+     */
+    public double getIntensity() {
+        return intensity;
+    }
+
+    /**
+     * Returns the position of this light source.
+     *
+     * @return the position in world space
+     */
+    public Point3D getPosition() {
+        return position;
+    }
+
+    /**
+     * Sets the color of this light source.
+     *
+     * @param color the new light color
+     */
+    public void setColor(final Color color) {
+        this.color = color;
+    }
+
+    /**
+     * Sets the intensity multiplier of this light source.
+     *
+     * @param intensity the new intensity multiplier (1.0 = normal brightness)
+     */
+    public void setIntensity(final double intensity) {
+        this.intensity = intensity;
+    }
+
+    /**
+     * Sets the position of this light source.
+     *
+     * @param position the new position in world space
+     */
+    public void setPosition(final Point3D position) {
+        this.position = position;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.java
new file mode 100644 (file)
index 0000000..b368e6e
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.lighting;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages light sources in the scene and calculates lighting for polygons.
+ *
+ * <p>This class implements flat shading using the Lambert cosine law. For each
+ * polygon face, it calculates the surface normal and determines how much light
+ * each source contributes based on the angle between the normal and the light
+ * direction.</p>
+ *
+ * <p>The lighting calculation considers:</p>
+ * <ul>
+ * <li>Distance from polygon center to each light source</li>
+ * <li>Angle between surface normal and light direction</li>
+ * <li>Color and intensity of each light source</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LightingManager lighting = new LightingManager();
+ *
+ * // Add light sources
+ * lighting.addLight(new LightSource(new Point3D(100, -50, 200), Color.YELLOW));
+ * lighting.addLight(new LightSource(new Point3D(-100, 50, 200), Color.BLUE));
+ *
+ * // Set ambient light (base illumination)
+ * lighting.setAmbientLight(new Color(30, 30, 30));
+ *
+ * // Calculate shaded color for a polygon
+ * Color shadedColor = lighting.calculateLighting(
+ *     polygonCenter,
+ *     surfaceNormal,
+ *     baseColor
+ * );
+ * }</pre>
+ *
+ * @see LightSource represents a single light source
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon
+ */
+public class LightingManager {
+
+    private final List<LightSource> lights = new ArrayList<>();
+    private Color ambientLight = new Color(10, 10, 10);
+
+    /**
+     * Creates a new lighting manager with no light sources.
+     */
+    public LightingManager() {
+    }
+
+    /**
+     * Adds a light source to the scene.
+     *
+     * @param light the light source to add
+     */
+    public void addLight(final LightSource light) {
+        lights.add(light);
+    }
+
+    /**
+     * Calculates the shaded color for a polygon based on lighting.
+     *
+     * @param polygonCenter the center point of the polygon in world space
+     * @param normal        the surface normal vector (should be normalized)
+     * @param baseColor     the original color of the polygon
+     * @return the shaded color after applying lighting
+     */
+    public Color calculateLighting(final Point3D polygonCenter,
+                                   final Point3D normal,
+                                   final Color baseColor) {
+        int totalR = 0;
+        int totalG = 0;
+        int totalB = 0;
+
+        // Add ambient light contribution
+        totalR += ambientLight.r;
+        totalG += ambientLight.g;
+        totalB += ambientLight.b;
+
+        // Calculate contribution from each light source
+        for (final LightSource light : lights) {
+            final Point3D lightPos = light.getPosition();
+            final Color lightColor = light.getColor();
+            final double lightIntensity = light.getIntensity();
+
+            // Calculate vector from polygon to light
+            final double lightDirX = lightPos.x - polygonCenter.x;
+            final double lightDirY = lightPos.y - polygonCenter.y;
+            final double lightDirZ = lightPos.z - polygonCenter.z;
+
+            // Normalize the light direction
+            final double lightDist = Math.sqrt(
+                    lightDirX * lightDirX +
+                            lightDirY * lightDirY +
+                            lightDirZ * lightDirZ
+            );
+
+            if (lightDist < 0.0001)
+                continue;
+
+            final double invLightDist = 1.0 / lightDist;
+            final double normLightDirX = lightDirX * invLightDist;
+            final double normLightDirY = lightDirY * invLightDist;
+            final double normLightDirZ = lightDirZ * invLightDist;
+
+            // Calculate dot product (Lambert cosine law)
+            final double dotProduct = normal.x * normLightDirX +
+                    normal.y * normLightDirY +
+                    normal.z * normLightDirZ;
+
+            // Only add light if surface faces the light
+            if (dotProduct > 0) {
+                // Apply distance attenuation (inverse square law, simplified)
+                final double attenuation = 1.0 / (1.0 + 0.0001 * lightDist * lightDist);
+                final double intensity = dotProduct * attenuation * lightIntensity;
+
+                // Add light color contribution
+                totalR += (int) (lightColor.r * intensity);
+                totalG += (int) (lightColor.g * intensity);
+                totalB += (int) (lightColor.b * intensity);
+            }
+        }
+
+        // Clamp values to valid range and apply to base color
+        final int r = Math.min(255, (totalR * baseColor.r) / 255);
+        final int g = Math.min(255, (totalG * baseColor.g) / 255);
+        final int b = Math.min(255, (totalB * baseColor.b) / 255);
+
+        return new Color(r, g, b, baseColor.a);
+    }
+
+    /**
+     * Returns the ambient light color.
+     *
+     * @return the ambient light color
+     */
+    public Color getAmbientLight() {
+        return ambientLight;
+    }
+
+    /**
+     * Returns all light sources in the scene.
+     *
+     * @return list of light sources
+     */
+    public List<LightSource> getLights() {
+        return lights;
+    }
+
+    /**
+     * Removes a light source from the scene.
+     *
+     * @param light the light source to remove
+     */
+    public void removeLight(final LightSource light) {
+        lights.remove(light);
+    }
+
+    /**
+     * Sets the ambient light color for the scene.
+     *
+     * <p>Ambient light provides base illumination that affects all surfaces
+     * equally, regardless of their orientation.</p>
+     *
+     * @param ambientLight the ambient light color
+     */
+    public void setAmbientLight(final Color ambientLight) {
+        this.ambientLight = ambientLight;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/package-info.java
new file mode 100755 (executable)
index 0000000..b9700ac
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Rasterization-based real-time software renderer for the Sixth 3D engine.
+ *
+ * <p>This package provides a complete rasterization pipeline that renders 3D scenes
+ * to a 2D pixel buffer using traditional approaches:</p>
+ * <ul>
+ *   <li><b>Wireframe rendering</b> - lines and wireframe shapes</li>
+ *   <li><b>Solid polygon rendering</b> - filled polygons with flat shading</li>
+ *   <li><b>Textured polygon rendering</b> - polygons with texture mapping and mipmap support</li>
+ *   <li><b>Depth sorting</b> - back-to-front painter's algorithm using Z-index ordering</li>
+ * </ul>
+ *
+ * <p>Key classes in this package:</p>
+ * <ul>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection} - root container for all 3D shapes in a scene</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator} - collects and depth-sorts shapes for rendering</li>
+ *   <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.Color} - RGBA color representation with predefined constants</li>
+ * </ul>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic basic shape primitives (lines, polygons)
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite composite shapes (boxes, grids, text)
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.texture texture and mipmap support
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster;
+
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java
new file mode 100644 (file)
index 0000000..a75d798
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes;
+
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+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.concurrent.atomic.AtomicInteger;
+
+/**
+ * Base class for shapes defined by an array 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
+ * ({@link Vertex} objects) that define its geometry in 3D space.</p>
+ *
+ * <p>During each render frame, the {@link #transform} method projects all vertices
+ * from world space to screen space. If all vertices are visible (in front of the camera),
+ * the shape is queued in the {@link RenderAggregator} for depth-sorted painting via
+ * the {@link #paint} method.</p>
+ *
+ * <p><b>Creating a custom coordinate shape:</b></p>
+ * <pre>{@code
+ * public class Triangle extends AbstractCoordinateShape {
+ *     private final Color color;
+ *
+ *     public Triangle(Point3D p1, Point3D p2, Point3D p3, Color color) {
+ *         super(new Vertex(p1), new Vertex(p2), new Vertex(p3));
+ *         this.color = color;
+ *     }
+ *
+ *     public void paint(RenderingContext ctx) {
+ *         // Custom painting logic using ctx.graphics and
+ *         // coordinates[i].transformedCoordinate for screen positions
+ *     }
+ * }
+ * }</pre>
+ *
+ * @see AbstractShape the parent class for all shapes
+ * @see Vertex wraps a 3D coordinate with its transformed (screen-space) position
+ * @see RenderAggregator collects and depth-sorts shapes before painting
+ */
+public abstract class AbstractCoordinateShape extends AbstractShape {
+
+    /**
+     * Global counter used to assign unique IDs to shapes, ensuring deterministic
+     * rendering order for shapes at the same depth.
+     */
+    private static final AtomicInteger lastShapeId = new AtomicInteger();
+
+    /**
+     * Unique identifier for this shape instance, used as a tiebreaker when
+     * sorting shapes with identical Z-depth values.
+     */
+    public final int shapeId;
+
+    /**
+     * 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}.
+     */
+    public final Vertex[] coordinates;
+
+    /**
+     * Average Z-depth of this shape in screen space after transformation.
+     * Used by the {@link RenderAggregator} to sort shapes back-to-front
+     * for correct painter's algorithm rendering.
+     */
+    public double onScreenZ;
+
+    /**
+     * Creates a shape with the specified number of vertices, each initialized
+     * to the origin (0, 0, 0).
+     *
+     * @param pointsCount the number of vertices in this shape
+     */
+    public AbstractCoordinateShape(final int pointsCount) {
+        coordinates = new Vertex[pointsCount];
+        for (int i = 0; i < pointsCount; i++)
+            coordinates[i] = new Vertex();
+
+        shapeId = lastShapeId.getAndIncrement();
+    }
+
+    /**
+     * Creates a shape from the given vertices.
+     *
+     * @param vertexes the vertices defining this shape's geometry
+     */
+    public AbstractCoordinateShape(final Vertex... vertexes) {
+        coordinates = vertexes;
+
+        shapeId = lastShapeId.getAndIncrement();
+    }
+
+    /**
+     * Returns the average Z-depth of this shape in screen space.
+     *
+     * @return the average Z-depth value, used for depth sorting
+     */
+    public double getZ() {
+        return onScreenZ;
+    }
+
+    /**
+     * Paints this shape onto the rendering context's pixel buffer.
+     *
+     * <p>This method is called after all shapes have been transformed and sorted
+     * by depth. Implementations should use the transformed screen-space coordinates
+     * from {@link Vertex#transformedCoordinate} to draw pixels.</p>
+     *
+     * @param renderBuffer the rendering context containing the pixel buffer and graphics context
+     */
+    public abstract void paint(RenderingContext renderBuffer);
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Transforms all vertices to screen space by applying the current transform stack.
+     * Computes the average Z-depth and, if all vertices are visible (in front of the camera),
+     * queues this shape for rendering.</p>
+     */
+    @Override
+    public void transform(final TransformStack transforms,
+                          final RenderAggregator aggregator,
+                          final RenderingContext renderingContext) {
+
+        double accumulatedZ = 0;
+        boolean paint = true;
+
+        for (final Vertex geometryPoint : coordinates) {
+            geometryPoint.calculateLocationRelativeToViewer(transforms, renderingContext);
+
+            accumulatedZ += geometryPoint.transformedCoordinate.z;
+
+            if (!geometryPoint.transformedCoordinate.isVisible())
+                paint = false;
+        }
+
+        if (paint) {
+            onScreenZ = accumulatedZ / coordinates.length;
+            aggregator.queueShapeForRendering(this);
+        }
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.java
new file mode 100644 (file)
index 0000000..f0d37b1
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes;
+
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.math.TransformStack;
+import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
+
+/**
+ * Base class for all renderable shapes in the Sixth 3D engine.
+ *
+ * <p>Every shape that can be rendered must extend this class and implement the
+ * {@link #transform(TransformStack, RenderAggregator, RenderingContext)} method,
+ * which projects the shape from world space into screen space during each render frame.</p>
+ *
+ * <p>Shapes can optionally have a {@link MouseInteractionController} attached to receive
+ * mouse click and hover events when the user interacts with the shape in the 3D view.</p>
+ *
+ * <p><b>Shape hierarchy overview:</b></p>
+ * <pre>
+ * AbstractShape
+ *   +-- AbstractCoordinateShape   (shapes with vertex coordinates: lines, polygons)
+ *   +-- AbstractCompositeShape    (groups of sub-shapes: boxes, grids, text canvases)
+ * </pre>
+ *
+ * @see AbstractCoordinateShape for shapes defined by vertex coordinates
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape for compound shapes
+ * @see MouseInteractionController for handling mouse events on shapes
+ */
+public abstract class AbstractShape {
+
+    /**
+     * Optional controller that receives mouse interaction events (click, enter, exit)
+     * when the user interacts with this shape in the 3D view.
+     * Set to {@code null} if mouse interaction is not needed.
+     */
+    public MouseInteractionController mouseInteractionController;
+
+    /**
+     * Assigns a mouse interaction controller to this shape.
+     *
+     * <p>Example usage:</p>
+     * <pre>{@code
+     * shape.setMouseInteractionController(new MouseInteractionController() {
+     *     public boolean mouseClicked(int button) {
+     *         System.out.println("Shape clicked!");
+     *         return true;
+     *     }
+     *     public boolean mouseEntered() { return false; }
+     *     public boolean mouseExited() { return false; }
+     * });
+     * }</pre>
+     *
+     * @param mouseInteractionController the controller to handle mouse events,
+     *                                    or {@code null} to disable mouse interaction
+     */
+    public void setMouseInteractionController(
+            final MouseInteractionController mouseInteractionController) {
+        this.mouseInteractionController = mouseInteractionController;
+    }
+
+    /**
+     * Transforms this shape from world space to screen space and queues it for rendering.
+     *
+     * <p>This method is called once per frame for each shape in the scene. Implementations
+     * should apply the current transform stack to their vertices, compute screen-space
+     * coordinates, and if the shape is visible, add it to the {@link RenderAggregator}
+     * for depth-sorted painting.</p>
+     *
+     * @param transforms       the current stack of transforms (world-to-camera transformations)
+     * @param aggregator       collects transformed shapes for depth-sorted rendering
+     * @param renderingContext  provides frame dimensions, graphics context, and frame metadata
+     */
+    public abstract void transform(final TransformStack transforms,
+                                   final RenderAggregator aggregator,
+                                   final RenderingContext renderingContext);
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java
new file mode 100644 (file)
index 0000000..8ceefc2
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * 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;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+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;
+
+/**
+ * Base class for textures always facing the viewer.
+ * <p>
+ * This class implements the "billboard" rendering technique where the texture
+ * remains oriented towards the camera regardless of 3D position. The visible size
+ * is calculated based on distance from viewer (z-coordinate) and scale factor.
+ * <p>
+ * The texture mapping algorithm:
+ * 1. Calculates screen coverage based on perspective
+ * 2. Clips to viewport boundaries
+ * 3. Maps texture pixels to screen pixels using proportional scaling
+ */
+public class Billboard extends AbstractCoordinateShape {
+
+    private static final double SCALE_MULTIPLIER = 0.005;
+    public final Texture texture;
+
+    /**
+     * Scale of the texture object.
+     * <p>
+     * Object rendered visible size on the screen depends on underlying texture size and scale.
+     * <p>
+     * 0 means that object will be infinitely small.
+     * 1 in recommended value to maintain sharpness of the texture as seen by the viewer.
+     */
+    private double scale;
+
+    public Billboard(final Point3D point, final double scale,
+                     final Texture texture) {
+        super(new Vertex(point));
+        this.texture = texture;
+        setScale(scale);
+    }
+
+    /**
+     * Paint the texture on the screen (targetRenderingArea)
+     *
+     * @param targetRenderingArea the screen to paint on
+     */
+    @Override
+    public void paint(final RenderingContext targetRenderingArea) {
+
+        // distance from camera/viewer to center of the texture
+        final double z = coordinates[0].transformedCoordinate.z;
+
+        // compute forward oriented texture visible distance from center
+        final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width
+                * scale * texture.primaryBitmap.width) / z;
+
+        final double visibleVerticalDistanceFromCenter = (targetRenderingArea.width
+                * scale * texture.primaryBitmap.height) / z;
+
+        // compute visible pixel density, and get appropriate bitmap
+        final double zoom = (visibleHorizontalDistanceFromCenter * 2)
+                / texture.primaryBitmap.width;
+
+        final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom);
+
+        final Point2D onScreenCoordinate = coordinates[0].onScreenCoordinate;
+
+        // compute Y
+        final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter);
+        final int onScreenUncappedYEnd = (int) (onScreenCoordinate.y + visibleVerticalDistanceFromCenter);
+        final int onScreenUncappedHeight = onScreenUncappedYEnd - onScreenUncappedYStart;
+
+        int onScreenCappedYStart = onScreenUncappedYStart;
+        int onScreenCappedYEnd = onScreenUncappedYEnd;
+
+        // cap Y to upper screen border
+        if (onScreenCappedYStart < 0)
+            onScreenCappedYStart = 0;
+
+        // cap Y to lower screen border
+        if (onScreenCappedYEnd > targetRenderingArea.height)
+            onScreenCappedYEnd = targetRenderingArea.height;
+
+        // compute X
+        final int onScreenUncappedXStart = (int) (onScreenCoordinate.x - visibleHorizontalDistanceFromCenter);
+        final int onScreenUncappedXEnd = (int) (onScreenCoordinate.x + visibleHorizontalDistanceFromCenter);
+        final int onScreenUncappedWidth = onScreenUncappedXEnd - onScreenUncappedXStart;
+
+        // cap X to left screen border
+        int onScreenCappedXStart = onScreenUncappedXStart;
+        if (onScreenCappedXStart < 0)
+            onScreenCappedXStart = 0;
+
+        // cap X to right screen border
+        int onScreenCappedXEnd = onScreenUncappedXEnd;
+        if (onScreenCappedXEnd > targetRenderingArea.width)
+            onScreenCappedXEnd = targetRenderingArea.width;
+
+        final byte[] targetRenderingAreaBytes = targetRenderingArea.pixels;
+
+        final int textureWidth = textureBitmap.width;
+
+        for (int y = onScreenCappedYStart; y < onScreenCappedYEnd; y++) {
+
+            final int sourceBitmapScanlinePixel = ((textureBitmap.height * (y - onScreenUncappedYStart)) / onScreenUncappedHeight)
+                    * textureWidth;
+
+            int targetRenderingAreaOffset = ((y * targetRenderingArea.width) + onScreenCappedXStart) * 4;
+
+            for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) {
+
+                final int sourceBitmapPixelAddress = (sourceBitmapScanlinePixel + ((textureWidth * (x - onScreenUncappedXStart)) / onScreenUncappedWidth)) * 4;
+
+                textureBitmap.drawPixel(sourceBitmapPixelAddress, targetRenderingAreaBytes, targetRenderingAreaOffset);
+
+                targetRenderingAreaOffset += 4;
+            }
+        }
+    }
+
+    /**
+     * Set the scale of the texture
+     *
+     * @param scale the scale of the texture
+     */
+    public void setScale(final double scale) {
+        this.scale = scale * SCALE_MULTIPLIER;
+    }
+
+    public Point3D getLocation() {
+        return coordinates[0].coordinate;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/GlowingPoint.java
new file mode 100644 (file)
index 0000000..f770b73
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import static java.lang.Math.pow;
+import static java.lang.Math.sqrt;
+
+/**
+ * A glowing 3D point rendered with a circular gradient texture.
+ * <p>
+ * This class creates and reuses textures for glowing points of the same color.
+ * The texture is a circle with an alpha gradient from center to edge, ensuring
+ *  a consistent visual appearance regardless of viewing angle.
+ * <p>
+ * The static set of glowing points enables texture sharing and garbage
+ * collection of unused textures via WeakHashMap.
+ */
+public class GlowingPoint extends Billboard {
+
+    private static final int TEXTURE_RESOLUTION_PIXELS = 100;
+    /**
+     * A set of all existing glowing points.
+     * Used to reuse textures of glowing points of the same color.
+     */
+    private static final Set<GlowingPoint> glowingPoints = Collections.newSetFromMap(new WeakHashMap<>());
+    private final Color color;
+
+    public GlowingPoint(final Point3D point, final double pointSize,
+                        final Color color) {
+        super(point, computeScale(pointSize), getTexture(color));
+        this.color = color;
+
+        synchronized (glowingPoints) {
+            glowingPoints.add(this);
+        }
+    }
+
+
+    private static double computeScale(double pointSize) {
+        return pointSize / ((double) (TEXTURE_RESOLUTION_PIXELS / 50f));
+    }
+
+    /**
+     * Returns a texture for a glowing point of the given color.
+     * The texture is a circle with a gradient from transparent to the given color.
+     */
+    private static Texture getTexture(final Color color) {
+        // attempt to reuse texture from existing glowing point of the same color
+        synchronized (glowingPoints) {
+            for (GlowingPoint glowingPoint : glowingPoints)
+                if (color.equals(glowingPoint.color))
+                    return glowingPoint.texture;
+        }
+
+        // existing texture not found, creating new one
+        return createTexture(color);
+    }
+
+    /**
+     * Creates a texture for a glowing point of the given color.
+     * The texture is a circle with a gradient from transparent to the given color.
+     */
+    private static Texture createTexture(final Color color) {
+        final Texture texture = new Texture(TEXTURE_RESOLUTION_PIXELS, TEXTURE_RESOLUTION_PIXELS, 1);
+        int halfResolution = TEXTURE_RESOLUTION_PIXELS / 2;
+
+        for (int x = 0; x < TEXTURE_RESOLUTION_PIXELS; x++)
+            for (int y = 0; y < TEXTURE_RESOLUTION_PIXELS; y++) {
+                int address = texture.primaryBitmap.getAddress(x, y);
+
+                final int distanceFromCenter = (int) sqrt(pow(halfResolution - x, 2) + pow(halfResolution - y, 2));
+
+                int alpha = 255 - ((270 * distanceFromCenter) / halfResolution);
+                if (alpha < 0)
+                    alpha = 0;
+
+                texture.primaryBitmap.bytes[address] = (byte) alpha;
+                address++;
+                texture.primaryBitmap.bytes[address] = (byte) color.b;
+                address++;
+                texture.primaryBitmap.bytes[address] = (byte) color.g;
+                address++;
+                texture.primaryBitmap.bytes[address] = (byte) color.r;
+            }
+
+        return texture;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java
new file mode 100644 (file)
index 0000000..9a71bb0
--- /dev/null
@@ -0,0 +1,342 @@
+/*
+ * 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.line;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+
+
+/**
+ * A 3D line segment with perspective-correct width and alpha blending.
+ * <p>
+ * This class represents a line between two 3D points, rendered with a specified
+ * width that adjusts based on perspective (distance from the viewer).
+ * The line is drawn using interpolators to handle edge cases and alpha blending for
+ * transparency effects.
+ * <p>
+ * The rendering algorithm:
+ * 1. For thin lines (below a threshold), draws single-pixel lines with alpha
+ *    adjustment based on perspective.
+ * 2. For thicker lines, creates four interpolators to define the line's
+ *    rectangular area and fills it scanline by scanline.
+ * <p>
+ * Note: The width is scaled by the LINE_WIDTH_MULTIPLIER and adjusted based on
+ * the distance from the viewer (z-coordinate) to maintain a consistent visual size.
+ */
+public class Line extends AbstractCoordinateShape {
+
+    private static final double MINIMUM_WIDTH_THRESHOLD = 1;
+
+    private static final double LINE_WIDTH_MULTIPLIER = 0.2d;
+
+    /**
+     * width of the line.
+     */
+    public final double width;
+    final LineInterpolator[] lineInterpolators = new LineInterpolator[4];
+
+    /**
+     * Color of the line.
+     */
+    public Color color;
+
+    public Line(final Line parentLine) {
+        this(parentLine.coordinates[0].coordinate.clone(),
+                parentLine.coordinates[1].coordinate.clone(),
+                new Color(parentLine.color), parentLine.width);
+    }
+
+    public Line(final Point3D point1, final Point3D point2, final Color color,
+                final double width) {
+
+        super(
+                new Vertex(point1),
+                new Vertex(point2)
+        );
+
+        this.color = color;
+        this.width = width;
+
+        for (int i = 0; i < lineInterpolators.length; i++)
+            lineInterpolators[i] = new LineInterpolator();
+
+    }
+
+    private void drawHorizontalLine(final LineInterpolator line1,
+                                    final LineInterpolator line2, final int y,
+                                    final RenderingContext renderBuffer) {
+
+        int x1 = line1.getX(y);
+        int x2 = line2.getX(y);
+
+        double d1 = line1.getD();
+        double d2 = line2.getD();
+
+        if (x1 > x2) {
+            final int tmp = x1;
+            x1 = x2;
+            x2 = tmp;
+
+            final double tmp2 = d1;
+            d1 = d2;
+            d2 = tmp2;
+        }
+
+        final int unclippedWidth = x2 - x1;
+        final double dinc = (d2 - d1) / unclippedWidth;
+
+        if (x1 < 0) {
+            d1 += (dinc * (-x1));
+            x1 = 0;
+        }
+
+        if (x2 >= renderBuffer.width)
+            x2 = renderBuffer.width - 1;
+
+        final int drawnWidth = x2 - x1;
+
+        int offset = ((y * renderBuffer.width) + x1) * 4;
+        final byte[] offSreenBufferBytes = renderBuffer.pixels;
+
+        final int lineAlpha = color.a;
+
+        final int colorB = color.b;
+        final int colorG = color.g;
+        final int colorR = color.r;
+
+        for (int i = 0; i < drawnWidth; i++) {
+
+            final double alphaMultiplier = 1d - Math.abs(d1);
+
+            final int realLineAlpha = (int) (lineAlpha * alphaMultiplier);
+            final int backgroundAlpha = 255 - realLineAlpha;
+
+            offSreenBufferBytes[offset] = (byte) 255;
+            offset++;
+            offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorB * realLineAlpha)) / 256);
+            offset++;
+            offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorG * realLineAlpha)) / 256);
+            offset++;
+            offSreenBufferBytes[offset] = (byte) ((((offSreenBufferBytes[offset] & 0xff) * backgroundAlpha) + (colorR * realLineAlpha)) / 256);
+            offset++;
+
+            d1 += dinc;
+        }
+
+    }
+
+    private void drawSinglePixelHorizontalLine(final RenderingContext buffer,
+                                               final int alpha) {
+
+        final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+
+        int xStart = (int) onScreenPoint1.x;
+        int xEnd = (int) onScreenPoint2.x;
+
+        int lineHeight;
+        int yBase;
+
+        if (xStart > xEnd) {
+            final int tmp = xStart;
+            xStart = xEnd;
+            xEnd = tmp;
+            lineHeight = (int) (onScreenPoint1.y - onScreenPoint2.y);
+            yBase = (int) onScreenPoint2.y;
+        } else {
+            yBase = (int) onScreenPoint1.y;
+            lineHeight = (int) (onScreenPoint2.y - onScreenPoint1.y);
+        }
+
+        final int lineWidth = xEnd - xStart;
+        if (lineWidth == 0)
+            return;
+
+        final byte[] offSreenBufferBytes = buffer.pixels;
+        final int backgroundAlpha = 255 - alpha;
+
+        final int blueWithAlpha = color.b * alpha;
+        final int greenWithAplha = color.g * alpha;
+        final int redWithAlpha = color.r * alpha;
+
+        for (int relativeX = 0; relativeX <= lineWidth; relativeX++) {
+            final int x = xStart + relativeX;
+
+            if ((x >= 0) && (x < buffer.width)) {
+
+                final int y = yBase + ((relativeX * lineHeight) / lineWidth);
+                if ((y >= 0) && (y < buffer.height)) {
+                    int ramOffset = ((y * buffer.width) + x) * 4;
+
+                    offSreenBufferBytes[ramOffset] = (byte) 255;
+                    ramOffset++;
+                    offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256);
+                    ramOffset++;
+                    offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + greenWithAplha) / 256);
+                    ramOffset++;
+                    offSreenBufferBytes[ramOffset] = (byte) ((((offSreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256);
+                }
+            }
+        }
+
+    }
+
+    private void drawSinglePixelVerticalLine(final RenderingContext buffer,
+                                             final int alpha) {
+
+        final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+
+        int yStart = (int) onScreenPoint1.y;
+        int yEnd = (int) onScreenPoint2.y;
+
+        int lineWidth;
+        int xBase;
+
+        if (yStart > yEnd) {
+            final int tmp = yStart;
+            yStart = yEnd;
+            yEnd = tmp;
+            lineWidth = (int) (onScreenPoint1.x - onScreenPoint2.x);
+            xBase = (int) onScreenPoint2.x;
+        } else {
+            xBase = (int) onScreenPoint1.x;
+            lineWidth = (int) (onScreenPoint2.x - onScreenPoint1.x);
+        }
+
+        final int lineHeight = yEnd - yStart;
+        if (lineHeight == 0)
+            return;
+
+        final byte[] offScreenBufferBytes = buffer.pixels;
+        final int backgroundAlpha = 255 - alpha;
+
+        final int blueWithAlpha = color.b * alpha;
+        final int greenWithAlpha = color.g * alpha;
+        final int redWithAlpha = color.r * alpha;
+
+        for (int relativeY = 0; relativeY <= lineHeight; relativeY++) {
+            final int y = yStart + relativeY;
+
+            if ((y >= 0) && (y < buffer.height)) {
+
+                final int x = xBase + ((relativeY * lineWidth) / lineHeight);
+                if ((x >= 0) && (x < buffer.width)) {
+                    int ramOffset = ((y * buffer.width) + x) * 4;
+
+                    offScreenBufferBytes[ramOffset] = (byte) 255;
+                    ramOffset++;
+                    offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256);
+                    ramOffset++;
+                    offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + greenWithAlpha) / 256);
+                    ramOffset++;
+                    offScreenBufferBytes[ramOffset] = (byte) ((((offScreenBufferBytes[ramOffset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256);
+                }
+            }
+        }
+    }
+
+    private int getLineInterpolator(final int startPointer, final int y) {
+
+        for (int i = startPointer; i < lineInterpolators.length; i++)
+            if (lineInterpolators[i].containsY(y))
+                return i;
+        return -1;
+    }
+
+    @Override
+    public void paint(final RenderingContext buffer) {
+
+        final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D onScreenPoint2 = coordinates[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)
+                / coordinates[0].transformedCoordinate.z;
+        final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width)
+                / coordinates[1].transformedCoordinate.z;
+
+        if ((point1radius < MINIMUM_WIDTH_THRESHOLD)
+                || (point2radius < MINIMUM_WIDTH_THRESHOLD)) {
+
+            double averageRadius = (point1radius + point2radius) / 2;
+
+            if (averageRadius > 1)
+                averageRadius = 1;
+
+            final int alpha = (int) (color.a * averageRadius);
+            if (alpha < 2)
+                return;
+
+            if (Math.abs(xp) > Math.abs(yp))
+                drawSinglePixelHorizontalLine(buffer, alpha);
+            else
+                drawSinglePixelVerticalLine(buffer, alpha);
+            return;
+        }
+
+        final double lineLength = Math.sqrt((xp * xp) + (yp * yp));
+
+        final double yinc1 = (point1radius * xp) / lineLength;
+        final double yinc2 = (point2radius * xp) / lineLength;
+
+        final double xdec1 = (point1radius * yp) / lineLength;
+        final double xdec2 = (point2radius * yp) / lineLength;
+
+        final double p1x1 = onScreenPoint1.x - xdec1;
+        final double p1y1 = onScreenPoint1.y + yinc1;
+
+        final double p1x2 = onScreenPoint1.x + xdec1;
+        final double p1y2 = onScreenPoint1.y - yinc1;
+
+        final double p2x1 = onScreenPoint2.x - xdec2;
+        final double p2y1 = onScreenPoint2.y + yinc2;
+
+        final double p2x2 = onScreenPoint2.x + xdec2;
+        final double p2y2 = onScreenPoint2.y - yinc2;
+
+        lineInterpolators[0].setPoints(p1x1, p1y1, 1d, p2x1, p2y1, 1d);
+        lineInterpolators[1].setPoints(p1x2, p1y2, -1d, p2x2, p2y2, -1d);
+
+        lineInterpolators[2].setPoints(p1x1, p1y1, 1d, p1x2, p1y2, -1d);
+        lineInterpolators[3].setPoints(p2x1, p2y1, 1d, p2x2, p2y2, -1d);
+
+        double ymin = p1y1;
+        if (p1y2 < ymin)
+            ymin = p1y2;
+        if (p2y1 < ymin)
+            ymin = p2y1;
+        if (p2y2 < ymin)
+            ymin = p2y2;
+        if (ymin < 0)
+            ymin = 0;
+
+        double ymax = p1y1;
+        if (p1y2 > ymax)
+            ymax = p1y2;
+        if (p2y1 > ymax)
+            ymax = p2y1;
+        if (p2y2 > ymax)
+            ymax = p2y2;
+        if (ymax >= buffer.height)
+            ymax = buffer.height - 1;
+
+        for (int y = (int) ymin; y <= ymax; y++) {
+            final int li1 = getLineInterpolator(0, y);
+            if (li1 != -1) {
+                final int li2 = getLineInterpolator(li1 + 1, y);
+                if (li2 != -1)
+                    drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, buffer);
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.java
new file mode 100644 (file)
index 0000000..c27aad6
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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.line;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+
+/**
+ * Factory for creating Line objects with consistent appearance settings.
+ * <p>
+ * This class encapsulates common line styling parameters (width and color) to
+ * avoid redundant configuration. It provides multiple constructors for
+ * flexibility and ensures default values are used when not specified.
+ */
+public class LineAppearance {
+
+    private final double lineWidth;
+
+    private Color color = new Color(100, 100, 255, 255);
+
+    public LineAppearance() {
+        lineWidth = 1;
+    }
+
+    public LineAppearance(final double lineWidth) {
+        this.lineWidth = lineWidth;
+    }
+
+    public LineAppearance(final double lineWidth, final Color color) {
+        this.lineWidth = lineWidth;
+        this.color = color;
+    }
+
+    public Line getLine(final Point3D point1, final Point3D point2) {
+        return new Line(point1, point2, color, lineWidth);
+    }
+
+    public Line getLine(final Point3D point1, final Point3D point2,
+                        final Color color) {
+        return new Line(point1, point2, color, lineWidth);
+    }
+
+    public double getLineWidth() {
+        return lineWidth;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java
new file mode 100644 (file)
index 0000000..57f8dee
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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.line;
+
+/**
+ * Interpolates between two points along a line for scanline rendering.
+ * <p>
+ * This class calculates screen coordinates and depth values (d) for a given Y
+ * position. It supports perspective-correct interpolation by tracking the
+ * distance between points and using it to compute step increments.
+ * <p>
+ * The comparison logic prioritizes interpolators with greater vertical coverage
+ * to optimize scanline ordering.
+ */
+public class LineInterpolator {
+
+    private double x1, y1, d1, x2, y2, d2;
+
+    private double d;
+    private int height;
+    private int width;
+    private double dinc;
+
+    public boolean containsY(final int y) {
+
+        if (y1 < y2) {
+            if (y >= y1)
+                return y <= y2;
+        } else if (y >= y2)
+            return y <= y1;
+
+        return false;
+    }
+
+    public double getD() {
+        return d;
+    }
+
+    public int getX(final int y) {
+        if (height == 0)
+            return (int) (x2 + x1) / 2;
+
+        final int distanceFromY1 = y - (int) y1;
+
+        d = d1 + ((dinc * distanceFromY1) / height);
+
+        return (int) x1 + ((width * distanceFromY1) / height);
+    }
+
+    public void setPoints(final double x1, final double y1, final double d1,
+                          final double x2, final double y2, final double d2) {
+
+        this.x1 = x1;
+        this.y1 = y1;
+        this.d1 = d1;
+
+        this.x2 = x2;
+        this.y2 = y2;
+        this.d2 = d2;
+
+        height = (int) y2 - (int) y1;
+        width = (int) x2 - (int) x1;
+
+        dinc = d2 - d1;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java
new file mode 100644 (file)
index 0000000..20fb6f4
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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.solidpolygon;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+
+/**
+ * Interpolates the x coordinate along a 2D line edge for scanline-based polygon rasterization.
+ *
+ * <p>{@code LineInterpolator} represents one edge of a polygon in screen space, defined by
+ * two {@link Point2D} endpoints. Given a scanline y coordinate, it computes the corresponding
+ * x coordinate via linear interpolation. This is a core building block for the solid polygon
+ * rasterizer, which fills triangles by sweeping horizontal scanlines and using two
+ * {@code LineInterpolator} instances to find the left and right x boundaries at each y level.</p>
+ *
+ * <p>Instances are {@link Comparable}, sorted by absolute height (tallest first) and then
+ * by width. This ordering is used during rasterization to select the primary (longest) edge
+ * of the triangle for the outer scanline loop.</p>
+ *
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon
+ * @see Point2D
+ */
+public class LineInterpolator implements Comparable<LineInterpolator> {
+
+    /**
+     * The first endpoint of this edge.
+     */
+    Point2D p1;
+
+    /**
+     * The second endpoint of this edge.
+     */
+    Point2D p2;
+
+    /**
+     * The vertical span (p2.y - p1.y), which may be negative.
+     */
+    private int height;
+
+    /**
+     * The horizontal span (p2.x - p1.x), which may be negative.
+     */
+    private int width;
+
+    /**
+     * The absolute value of the vertical span, used for sorting.
+     */
+    private int absoluteHeight;
+
+    @Override
+    public boolean equals(final Object o) {
+        if (o == null) return false;
+
+        return o instanceof LineInterpolator && compareTo((LineInterpolator) o) == 0;
+    }
+
+    @Override
+    public int compareTo(final LineInterpolator o) {
+        if (absoluteHeight < o.absoluteHeight)
+            return 1;
+        if (absoluteHeight > o.absoluteHeight)
+            return -1;
+
+        return Integer.compare(o.width, width);
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = width;
+        result = 31 * result + absoluteHeight;
+        return result;
+    }
+
+    /**
+     * Tests whether the given y coordinate falls within the vertical span of this edge.
+     *
+     * @param y the scanline y coordinate to test
+     * @return {@code true} if {@code y} is between the y coordinates of the two endpoints (inclusive)
+     */
+    public boolean containsY(final int y) {
+
+        if (p1.y <= p2.y) {
+            if (y >= p1.y)
+                return y <= p2.y;
+        } else if (y >= p2.y)
+            return y <= p1.y;
+
+        return false;
+    }
+
+    /**
+     * Computes the interpolated x coordinate for the given scanline y value.
+     *
+     * <p>If the edge is horizontal (height is zero), returns the average of the
+     * two endpoint x coordinates.</p>
+     *
+     * @param y the scanline y coordinate
+     * @return the interpolated x coordinate on this edge at the given y
+     */
+    public int getX(final int y) {
+
+        if (height == 0)
+            return (int) (p2.x + p1.x) / 2;
+
+        return (int) (p1.x + ((width * (y - p1.y)) / height));
+    }
+
+    /**
+     * Sets the two endpoints of this edge and precomputes the width, height, and absolute height.
+     *
+     * @param p1 the first endpoint
+     * @param p2 the second endpoint
+     */
+    public void setPoints(final Point2D p1, final Point2D p2) {
+        this.p1 = p1;
+        this.p2 = p2;
+        height = (int) (p2.y - p1.y);
+        width = (int) (p2.x - p1.x);
+
+        absoluteHeight = Math.abs(height);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
new file mode 100644 (file)
index 0000000..eb53029
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+ * 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.solidpolygon;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+
+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.
+ */
+public class SolidPolygon extends AbstractCoordinateShape {
+
+    private final static LineInterpolator polygonBoundary1 = new LineInterpolator();
+    private final static LineInterpolator polygonBoundary2 = new LineInterpolator();
+    private final static LineInterpolator polygonBoundary3 = new LineInterpolator();
+    private final Point3D cachedNormal = new Point3D();
+    private final Point3D cachedCenter = new Point3D();
+    private Color color;
+    private boolean shadingEnabled = false;
+    private LightingManager lightingManager;
+    private boolean backfaceCulling = false;
+
+    public SolidPolygon(final Point3D point1, final Point3D point2,
+                        final Point3D point3, final Color color) {
+        super(
+                new Vertex(point1),
+                new Vertex(point2),
+                new Vertex(point3)
+        );
+        this.color = color;
+    }
+
+    public static void drawHorizontalLine(final LineInterpolator line1,
+                                          final LineInterpolator line2, final int y,
+                                          final RenderingContext renderBuffer, final Color color) {
+
+        int x1 = line1.getX(y);
+        int x2 = line2.getX(y);
+
+        if (x1 > x2) {
+            final int tmp = x1;
+            x1 = x2;
+            x2 = tmp;
+        }
+
+        if (x1 < 0)
+            x1 = 0;
+
+        if (x2 >= renderBuffer.width)
+            x2 = renderBuffer.width - 1;
+
+        final int width = x2 - x1;
+
+        int offset = ((y * renderBuffer.width) + x1) * 4;
+        final byte[] offScreenBufferBytes = renderBuffer.pixels;
+
+        final int polygonAlpha = color.a;
+        final int b = color.b;
+        final int g = color.g;
+        final int r = color.r;
+
+        if (polygonAlpha == 255)
+            for (int i = 0; i < width; i++) {
+                offScreenBufferBytes[offset] = (byte) 255;
+                offset++;
+                offScreenBufferBytes[offset] = (byte) b;
+                offset++;
+                offScreenBufferBytes[offset] = (byte) g;
+                offset++;
+                offScreenBufferBytes[offset] = (byte) r;
+                offset++;
+            }
+        else {
+            final int backgroundAlpha = 255 - polygonAlpha;
+
+            final int blueWithAlpha = b * polygonAlpha;
+            final int greenWithAlpha = g * polygonAlpha;
+            final int redWithAlpha = r * polygonAlpha;
+
+            for (int i = 0; i < width; i++) {
+                offScreenBufferBytes[offset] = (byte) 255;
+                offset++;
+                offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + blueWithAlpha) / 256);
+                offset++;
+                offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + greenWithAlpha) / 256);
+                offset++;
+                offScreenBufferBytes[offset] = (byte) ((((offScreenBufferBytes[offset] & 0xff) * backgroundAlpha) + redWithAlpha) / 256);
+                offset++;
+            }
+
+        }
+
+    }
+
+    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)
+                if (pointWithinPolygon(context.getMouseEvent().coordinate,
+                        onScreenPoint1, onScreenPoint2, onScreenPoint3))
+                    context.setCurrentObjectUnderMouseCursor(mouseInteractionController);
+
+        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)
+            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)
+            yBottom = context.height - 1;
+
+        // paint
+        polygonBoundary1.setPoints(onScreenPoint1, onScreenPoint2);
+        polygonBoundary2.setPoints(onScreenPoint1, onScreenPoint3);
+        polygonBoundary3.setPoints(onScreenPoint2, onScreenPoint3);
+
+        // Inline sort for 3 elements to avoid array allocation
+        LineInterpolator a = polygonBoundary1;
+        LineInterpolator b = polygonBoundary2;
+        LineInterpolator c = polygonBoundary3;
+        LineInterpolator 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, context, color);
+                else if (c.containsY(y))
+                    drawHorizontalLine(a, c, y, context, color);
+            } else if (b.containsY(y))
+                if (c.containsY(y))
+                    drawHorizontalLine(b, c, y, context, color);
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(final Color color) {
+        this.color = color;
+    }
+
+    /**
+     * Returns the lighting manager used for shading calculations.
+     *
+     * @return the lighting manager, or null if shading is not enabled
+     */
+    public LightingManager getLightingManager() {
+        return lightingManager;
+    }
+
+    /**
+     * 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
+     * @param lightingManager the lighting manager to use for shading calculations
+     */
+    public void setShadingEnabled(final boolean shadingEnabled, final LightingManager lightingManager) {
+        this.shadingEnabled = shadingEnabled;
+        this.lightingManager = lightingManager;
+    }
+
+    public boolean isBackfaceCullingEnabled() {
+        return backfaceCulling;
+    }
+
+    public void setBackfaceCulling(final boolean backfaceCulling) {
+        this.backfaceCulling = backfaceCulling;
+    }
+
+    private void calculateNormal(final Point3D result) {
+        final Point3D v1 = coordinates[0].coordinate;
+        final Point3D v2 = coordinates[1].coordinate;
+        final Point3D v3 = coordinates[2].coordinate;
+
+        final double ax = v2.x - v1.x;
+        final double ay = v2.y - v1.y;
+        final double az = v2.z - v1.z;
+
+        final double bx = v3.x - v1.x;
+        final double by = v3.y - v1.y;
+        final double bz = v3.z - v1.z;
+
+        double nx = ay * bz - az * by;
+        double ny = az * bx - ax * bz;
+        double nz = ax * by - ay * bx;
+
+        final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
+        if (length > 0.0001) {
+            nx /= length;
+            ny /= length;
+            nz /= length;
+        }
+
+        result.x = nx;
+        result.y = ny;
+        result.z = nz;
+    }
+
+    private void calculateCenter(final Point3D result) {
+        final Point3D v1 = coordinates[0].coordinate;
+        final Point3D v2 = coordinates[1].coordinate;
+        final Point3D v3 = coordinates[2].coordinate;
+
+        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;
+    }
+
+    @Override
+    public void paint(final RenderingContext renderBuffer) {
+
+        final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+        final Point2D onScreenPoint3 = coordinates[2].onScreenCoordinate;
+
+        if (backfaceCulling) {
+            final double signedArea = (onScreenPoint2.x - onScreenPoint1.x)
+                    * (onScreenPoint3.y - onScreenPoint1.y)
+                    - (onScreenPoint3.x - onScreenPoint1.x)
+                    * (onScreenPoint2.y - onScreenPoint1.y);
+            if (signedArea >= 0)
+                return;
+        }
+
+        Color paintColor = color;
+
+        if (shadingEnabled && lightingManager != null) {
+            calculateCenter(cachedCenter);
+            calculateNormal(cachedNormal);
+            paintColor = lightingManager.calculateLighting(cachedCenter, cachedNormal, color);
+        }
+
+        drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2,
+                onScreenPoint3, mouseInteractionController, paintColor);
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java
new file mode 100644 (file)
index 0000000..5b6c198
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * 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 static java.lang.Math.abs;
+
+/**
+ * Interpolator for textured polygon edges with perspective correction.
+ * <p>
+ * This class maps screen coordinates to texture coordinates while maintaining
+ * perspective accuracy.
+ * It's used to create texture-mapped scanlines that adjust for depth (z) to
+ * prevent texture distortion.
+ * <p>
+ * The comparison logic ensures proper scanline ordering based on vertical
+ * coverage and horizontal span.
+ */
+public class PolygonBorderInterpolator implements
+        Comparable<PolygonBorderInterpolator> {
+
+    // on-screen coordinates
+    Point2D onScreenPoint1;
+    Point2D onScreenPoint2;
+
+    double distanceFromY1;
+    private int onScreenHeight;
+    private int onScreenWidth;
+    private int onscreenAbsoluteHeight;
+    private double textureWidth;
+    private double textureHeight;
+    // texture coordinates
+    private Point2D texturePoint1;
+    private Point2D texturePoint2;
+
+
+    @Override
+    public boolean equals(final Object o) {
+        if (o == null) return false;
+
+        return o instanceof PolygonBorderInterpolator && compareTo((PolygonBorderInterpolator) o) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = onScreenWidth;
+        result = 31 * result + onscreenAbsoluteHeight;
+        return result;
+    }
+
+    @Override
+    public int compareTo(final PolygonBorderInterpolator otherInterpolator) {
+        if (onscreenAbsoluteHeight < otherInterpolator.onscreenAbsoluteHeight)
+            return 1;
+        if (onscreenAbsoluteHeight > otherInterpolator.onscreenAbsoluteHeight)
+            return -1;
+
+        if (onScreenWidth < otherInterpolator.onScreenWidth)
+            return 1;
+        if (onScreenWidth > otherInterpolator.onScreenWidth)
+            return -1;
+
+        return 0;
+    }
+
+    public boolean containsY(final int y) {
+
+        if (onScreenPoint1.y < onScreenPoint2.y) {
+            if (y >= onScreenPoint1.y)
+                return y <= onScreenPoint2.y;
+        } else if (y >= onScreenPoint2.y)
+            return y <= onScreenPoint1.y;
+
+        return false;
+    }
+
+    public double getTX() {
+
+        if (onScreenHeight == 0)
+            return (texturePoint2.x + texturePoint1.x) / 2d;
+
+        return texturePoint1.x + ((textureWidth * distanceFromY1) / onScreenHeight);
+    }
+
+    public double getTY() {
+
+        if (onScreenHeight == 0)
+            return (texturePoint2.y + texturePoint1.y) / 2d;
+
+        return texturePoint1.y + ((textureHeight * distanceFromY1) / onScreenHeight);
+    }
+
+    public int getX() {
+
+        if (onScreenHeight == 0)
+            return (int) ((onScreenPoint2.x + onScreenPoint1.x) / 2d);
+
+        return (int) (onScreenPoint1.x + ((onScreenWidth * distanceFromY1) / onScreenHeight));
+    }
+
+    public void setCurrentY(final int y) {
+        distanceFromY1 = y - onScreenPoint1.y;
+    }
+
+    public void setPoints(final Point2D onScreenPoint1, final Point2D onScreenPoint2,
+                          final Point2D texturePoint1, final Point2D texturePoint2) {
+
+        this.onScreenPoint1 = onScreenPoint1;
+        this.onScreenPoint2 = onScreenPoint2;
+        this.texturePoint1 = texturePoint1;
+        this.texturePoint2 = texturePoint2;
+
+        onScreenHeight = (int) (onScreenPoint2.y - onScreenPoint1.y);
+        onScreenWidth = (int) (onScreenPoint2.x - onScreenPoint1.x);
+        onscreenAbsoluteHeight = abs(onScreenHeight);
+
+        textureWidth = texturePoint2.x - texturePoint1.x;
+        textureHeight = texturePoint2.y - texturePoint1.y;
+    }
+
+}
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
new file mode 100644 (file)
index 0000000..45e9b98
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * 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.Color;
+
+import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
+
+/**
+ * Textured polygon.
+ * <p>
+ *
+ * <pre>
+ * This is how perspective-correct texture rendering is implemented:
+ * If polygon is sufficiently small, it is rendered without perspective correction.
+ * Otherwise, it is sliced into smaller polygons.
+ * </pre>
+ */
+
+public class TexturedPolygon extends AbstractCoordinateShape {
+
+    private static final PolygonBorderInterpolator polygonBorder1 = new PolygonBorderInterpolator();
+    private static final PolygonBorderInterpolator polygonBorder2 = new PolygonBorderInterpolator();
+    private static final PolygonBorderInterpolator polygonBorder3 = new PolygonBorderInterpolator();
+
+    public final Texture texture;
+
+    /**
+     * If <code>true</code> then polygon borders will be drawn.
+     * It is used for debugging purposes.
+     */
+    public boolean showBorders = false;
+    private boolean backfaceCulling = false;
+
+    private double totalTextureDistance = -1;
+
+    public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
+
+        super(p1, p2, p3);
+        this.texture = texture;
+    }
+
+    private void computeTotalTextureDistance() {
+        // compute total texture distance
+        totalTextureDistance = coordinates[0].textureCoordinate.getDistanceTo(coordinates[1].textureCoordinate);
+        totalTextureDistance += coordinates[0].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate);
+        totalTextureDistance += coordinates[1].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate);
+    }
+
+    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) * 4;
+        final byte[] renderBufferBytes = renderBuffer.pixels;
+
+        final double twidth = tx2 - tx1;
+        final double theight = ty2 - ty1;
+
+        for (int x = x1; x < x2; x++) {
+
+            final double distance = x - realX1;
+
+            final double tx = tx1 + ((twidth * distance) / realWidth);
+            final double ty = ty1 + ((theight * distance) / realWidth);
+
+            final int textureOffset = textureBitmap.getAddress((int) tx,
+                    (int) ty);
+
+            textureBitmap.drawPixel(textureOffset, renderBufferBytes,
+                    renderBufferOffset);
+
+            renderBufferOffset += 4;
+        }
+
+    }
+
+    @Override
+    public void paint(final RenderingContext renderBuffer) {
+
+        final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
+        final Point2D projectedPoint3 = coordinates[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 (showBorders)
+            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;
+
+        // 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);
+
+        polygonBorder1.setPoints(projectedPoint1, projectedPoint2,
+                coordinates[0].textureCoordinate,
+                coordinates[1].textureCoordinate);
+        polygonBorder2.setPoints(projectedPoint1, projectedPoint3,
+                coordinates[0].textureCoordinate,
+                coordinates[2].textureCoordinate);
+        polygonBorder3.setPoints(projectedPoint2, projectedPoint3,
+                coordinates[1].textureCoordinate,
+                coordinates[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);
+
+    }
+
+    public boolean isBackfaceCullingEnabled() {
+        return backfaceCulling;
+    }
+
+    public void setBackfaceCulling(final boolean backfaceCulling) {
+        this.backfaceCulling = backfaceCulling;
+    }
+
+    private void showBorders(final RenderingContext renderBuffer) {
+
+        final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
+        final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
+        final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
+
+        renderBuffer.graphics.setColor(Color.YELLOW);
+        renderBuffer.graphics.drawLine((int) projectedPoint1.x,
+                (int) projectedPoint1.y, (int) projectedPoint2.x,
+                (int) projectedPoint2.y);
+        renderBuffer.graphics.drawLine((int) projectedPoint3.x,
+                (int) projectedPoint3.y, (int) projectedPoint2.x,
+                (int) projectedPoint2.y);
+        renderBuffer.graphics.drawLine((int) projectedPoint1.x,
+                (int) projectedPoint1.y, (int) projectedPoint3.x,
+                (int) projectedPoint3.y);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java
new file mode 100644 (file)
index 0000000..f394a29
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.Billboard;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
+import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
+
+/**
+ * A text label rendered as a billboard texture that always faces the camera.
+ *
+ * <p>This shape renders a single line of text onto a {@link Texture} using the font metrics
+ * defined in {@link TextCanvas} ({@link TextCanvas#FONT}, {@link TextCanvas#FONT_CHAR_WIDTH_TEXTURE_PIXELS},
+ * {@link TextCanvas#FONT_CHAR_HEIGHT_TEXTURE_PIXELS}), then displays the texture as a
+ * forward-oriented billboard via its {@link Billboard} superclass. The result
+ * is a text label that remains readable from any viewing angle.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a red text label at position (0, -50, 300)
+ * ForwardOrientedTextBlock label = new ForwardOrientedTextBlock(
+ *     new Point3D(0, -50, 300),
+ *     1.0,
+ *     2,
+ *     "Hello, World!",
+ *     Color.RED
+ * );
+ * shapeCollection.addShape(label);
+ * }</pre>
+ *
+ * @see Billboard
+ * @see TextCanvas
+ * @see Texture
+ */
+public class ForwardOrientedTextBlock extends Billboard {
+
+    /**
+     * Creates a new forward-oriented text block at the given 3D position.
+     *
+     * @param point           the 3D position where the text label is placed
+     * @param scale           the scale factor controlling the rendered size of the text
+     * @param maxUpscaleFactor the maximum mipmap upscale factor for the backing texture
+     * @param text            the text string to render
+     * @param textColor       the color of the rendered text
+     */
+    public ForwardOrientedTextBlock(final Point3D point, final double scale,
+                                    final int maxUpscaleFactor, final String text,
+                                    final eu.svjatoslav.sixth.e3d.renderer.raster.Color textColor) {
+        super(point, scale, getTexture(text, maxUpscaleFactor, textColor));
+
+    }
+
+    /**
+     * Creates a {@link Texture} containing the rendered text string.
+     *
+     * <p>The texture dimensions are calculated from the text length and the font metrics
+     * defined in {@link TextCanvas}. Each character is drawn individually at the appropriate
+     * horizontal offset using {@link TextCanvas#FONT}.</p>
+     *
+     * @param text            the text string to render into the texture
+     * @param maxUpscaleFactor the maximum mipmap upscale factor for the texture
+     * @param textColor       the color of the rendered text
+     * @return a new {@link Texture} containing the rendered text
+     */
+    public static Texture getTexture(final String text,
+                                     final int maxUpscaleFactor,
+                                     final eu.svjatoslav.sixth.e3d.renderer.raster.Color textColor) {
+
+        final Texture texture = new Texture(text.length()
+                * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS, TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS,
+                maxUpscaleFactor);
+
+        // texture.graphics.setColor(Color.BLUE);
+        // texture.graphics.fillRect(0, 0, texture.primaryBitmap.width,
+        // texture.primaryBitmap.width);
+
+        texture.graphics.setFont(TextCanvas.FONT);
+        texture.graphics.setColor(textColor.toAwtColor());
+
+        for (int c = 0; c < text.length(); c++)
+            texture.graphics.drawChars(new char[]{text.charAt(c),}, 0, 1,
+                    (c * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS),
+                    (int) (TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.45));
+
+        return texture;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java
new file mode 100644 (file)
index 0000000..c7f71d7
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite;
+
+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.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas;
+
+import java.util.List;
+
+/**
+ * A 2D graph visualization rendered in 3D space.
+ *
+ * <p>Plots a series of {@link Point2D} data points as a connected line graph, overlaid on a
+ * grid with horizontal and vertical grid lines, axis labels, and a title. The graph is
+ * rendered in the XY plane at the specified 3D location, with all dimensions scaled by
+ * a configurable scale factor.</p>
+ *
+ * <p>The graph uses the following default configuration:</p>
+ * <ul>
+ *   <li>X-axis range: {@code 0} to {@code 20} (world units before scaling)</li>
+ *   <li>Y-axis range: {@code -2} to {@code 2}</li>
+ *   <li>Grid spacing: {@code 0.5} in both horizontal and vertical directions</li>
+ *   <li>Grid color: semi-transparent blue ({@code rgba(100, 100, 250, 100)})</li>
+ *   <li>Plot color: semi-transparent red ({@code rgba(255, 0, 0, 100)})</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Prepare data points
+ * List<Point2D> data = new ArrayList<>();
+ * for (double x = 0; x <= 20; x += 0.1) {
+ *     data.add(new Point2D(x, Math.sin(x)));
+ * }
+ *
+ * // Create a graph at position (0, 0, 500) with scale factor 10
+ * Graph graph = new Graph(10.0, data, "sin(x)", new Point3D(0, 0, 500));
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(graph);
+ * }</pre>
+ *
+ * @see Line
+ * @see TextCanvas
+ * @see AbstractCompositeShape
+ */
+public class Graph extends AbstractCompositeShape {
+
+    /** The width of the graph in unscaled world units. */
+    private final double width;
+    /** The minimum Y-axis value. */
+    private final double yMin;
+    /** The maximum Y-axis value. */
+    private final double yMax;
+    /** The spacing between vertical grid lines along the X-axis. */
+    private final double horizontalStep;
+    /** The spacing between horizontal grid lines along the Y-axis. */
+    private final double verticalStep;
+    /** The color used for grid lines. */
+    private final Color gridColor;
+    /** The width of grid lines in world units (after scaling). */
+    private final double lineWidth;
+    /** The color used for the data plot line. */
+    private final Color plotColor;
+
+    /**
+     * Creates a new graph visualization at the specified 3D location.
+     *
+     * <p>The graph is constructed with grid lines, axis labels, plotted data, and a title
+     * label. All spatial dimensions are multiplied by the given scale factor.</p>
+     *
+     * @param scale    the scale factor applied to all spatial dimensions of the graph
+     * @param data     the list of 2D data points to plot; consecutive points are connected by lines
+     * @param label    the title text displayed above the graph
+     * @param location the 3D position of the graph's origin in the scene
+     */
+    public Graph(final double scale, final List<Point2D> data,
+                 final String label, final Point3D location) {
+        super(location);
+
+        width = 20;
+
+        yMin = -2;
+        yMax = 2;
+
+        horizontalStep = 0.5;
+        verticalStep = 0.5;
+
+        gridColor = new Color(100, 100, 250, 100);
+
+        lineWidth = 0.1 * scale;
+        plotColor = new Color(255, 0, 0, 100);
+
+        addVerticalLines(scale);
+        addXLabels(scale);
+        addHorizontalLinesAndLabels(scale);
+        plotData(scale, data);
+
+        final Point3D labelLocation = new Point3D(width / 2, yMax + 0.5, 0)
+                .scaleUp(scale);
+
+        final TextCanvas labelCanvas = new TextCanvas(new Transform(
+                labelLocation), label, Color.WHITE, Color.TRANSPARENT);
+
+        addShape(labelCanvas);
+    }
+
+    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 p2 = new Point3D(width, y, 0).scaleUp(scale);
+
+            final Line line = new Line(p1, p2, gridColor, lineWidth);
+
+            addShape(line);
+
+            final Point3D labelLocation = new Point3D(-0.5, y, 0)
+                    .scaleUp(scale);
+
+            final TextCanvas label = new TextCanvas(
+                    new Transform(labelLocation), String.valueOf(y),
+                    Color.WHITE, Color.TRANSPARENT);
+
+            addShape(label);
+
+        }
+    }
+
+    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 Line line = new Line(p1, p2, gridColor, lineWidth);
+
+            addShape(line);
+
+        }
+    }
+
+    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);
+
+            final TextCanvas label = new TextCanvas(
+                    new Transform(labelLocation), String.valueOf(x),
+                    Color.WHITE, Color.TRANSPARENT);
+
+            addShape(label);
+        }
+    }
+
+    private void plotData(final double scale, final List<Point2D> data) {
+        Point3D previousPoint = null;
+        for (final Point2D point : data) {
+
+            final Point3D p3d = new Point3D(point.x, point.y, 0).scaleUp(scale);
+
+            if (previousPoint != null) {
+
+                final Line line = new Line(previousPoint, p3d, plotColor,
+                        0.4 * scale);
+
+                addShape(line);
+            }
+
+            previousPoint = p3d;
+        }
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/LightSourceMarker.java
new file mode 100755 (executable)
index 0000000..a33c52d
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.GlowingPoint;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A visual marker that indicates a light source position in the 3D scene.
+ *
+ * <p>Rendered as a glowing point that provides a clear, lightweight visual
+ * indicator useful for debugging light placement in the scene.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Place a yellow light source marker at position (100, -50, 200)
+ * LightSourceMarker marker = new LightSourceMarker(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW
+ * );
+ * shapeCollection.addShape(marker);
+ * }</pre>
+ *
+ * @see GlowingPoint
+ * @see AbstractCompositeShape
+ */
+public class LightSourceMarker extends AbstractCompositeShape {
+
+    /**
+     * Creates a new light source marker at the specified location.
+     *
+     * @param location the 3D position of the marker in the scene
+     * @param color    the color of the glowing point
+     */
+    public LightSourceMarker(final Point3D location, final Color color) {
+        super(location);
+        addShape(new GlowingPoint(new Point3D(0, 0, 0), 15, color));
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java
new file mode 100644 (file)
index 0000000..1ababb2
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite;
+
+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.composite.base.AbstractCompositeShape;
+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
+ * is centered at the origin of its local coordinate system, with configurable world-space
+ * dimensions and independent texture resolution.</p>
+ *
+ * <p>The contained {@link Texture} object is accessible via {@link #getTexture()}, allowing
+ * dynamic rendering to the texture surface (e.g., drawing text, images, or procedural content)
+ * after construction.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a 200x100 textured rectangle at position (0, 0, 300)
+ * Transform transform = new Transform(new Point3D(0, 0, 300));
+ * TexturedRectangle rect = new TexturedRectangle(transform, 200, 100, 2);
+ *
+ * // Draw onto the texture dynamically
+ * Texture tex = rect.getTexture();
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 50, 50);
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(rect);
+ * }</pre>
+ *
+ * @see TexturedPolygon
+ * @see Texture
+ * @see AbstractCompositeShape
+ */
+public class TexturedRectangle extends AbstractCompositeShape {
+
+    /** Top-left corner position in local 3D coordinates. */
+    public Point3D topLeft;
+    /** Top-right corner position in local 3D coordinates. */
+    public Point3D topRight;
+    /** Bottom-right corner position in local 3D coordinates. */
+    public Point3D bottomRight;
+    /** Bottom-left corner position in local 3D coordinates. */
+    public Point3D bottomLeft;
+    /** Top-left corner mapping in texture coordinates (pixels). */
+    public Point2D textureTopLeft;
+    /** Top-right corner mapping in texture coordinates (pixels). */
+    public Point2D textureTopRight;
+    /** Bottom-right corner mapping in texture coordinates (pixels). */
+    public Point2D textureBottomRight;
+    /** Bottom-left corner mapping in texture coordinates (pixels). */
+    public Point2D textureBottomLeft;
+    private Texture texture;
+
+    /**
+     * Creates a textured rectangle with only a transform, without initializing geometry.
+     *
+     * <p>After construction, call {@link #initialize(double, double, int, int, int)} to
+     * set up the rectangle's dimensions, texture, and triangle geometry.</p>
+     *
+     * @param transform the position and orientation of this rectangle in the scene
+     */
+    public TexturedRectangle(final Transform transform) {
+        super(transform);
+    }
+
+    /**
+     * Creates a textured rectangle where the texture resolution matches the world-space size.
+     *
+     * <p>This is a convenience constructor equivalent to calling
+     * {@link #TexturedRectangle(Transform, int, int, int, int, int)} with
+     * {@code textureWidth = width} and {@code textureHeight = height}.</p>
+     *
+     * @param transform         the position and orientation of this rectangle in the scene
+     * @param width             the width of the rectangle in world units (also used as texture width in pixels)
+     * @param height            the height of the rectangle in world units (also used as texture height in pixels)
+     * @param maxTextureUpscale the maximum mipmap upscale factor for the texture
+     */
+    public TexturedRectangle(final Transform transform, final int width,
+                             final int height, final int maxTextureUpscale) {
+        this(transform, width, height, width, height, maxTextureUpscale);
+    }
+
+    /**
+     * Creates a fully initialized textured rectangle with independent world-space size and texture resolution.
+     *
+     * @param transform         the position and orientation of this rectangle in the scene
+     * @param width             the width of the rectangle in world units
+     * @param height            the height of the rectangle in world units
+     * @param textureWidth      the width of the backing texture in pixels
+     * @param textureHeight     the height of the backing texture in pixels
+     * @param maxTextureUpscale the maximum mipmap upscale factor for the texture
+     */
+    public TexturedRectangle(final Transform transform, final int width,
+                             final int height, final int textureWidth, final int textureHeight,
+                             final int maxTextureUpscale) {
+
+        super(transform);
+
+        initialize(width, height, textureWidth, textureHeight,
+                maxTextureUpscale);
+    }
+
+    /**
+     * Returns the backing texture for this rectangle.
+     *
+     * <p>The returned {@link Texture} can be used to draw dynamic content onto the
+     * rectangle's surface via its {@code graphics} field (a {@link java.awt.Graphics2D} instance).</p>
+     *
+     * @return the texture mapped onto this rectangle
+     */
+    public Texture getTexture() {
+        return texture;
+    }
+
+    /**
+     * Initializes the rectangle geometry, texture, and the two constituent textured triangles.
+     *
+     * <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,
+     * sharing a single {@link Texture} instance.</p>
+     *
+     * @param width             the width of the rectangle in world units
+     * @param height            the height of the rectangle in world units
+     * @param textureWidth      the width of the backing texture in pixels
+     * @param textureHeight     the height of the backing texture in pixels
+     * @param maxTextureUpscale the maximum mipmap upscale factor for the texture
+     */
+    public void initialize(final double width, final double height,
+                           final int textureWidth, final int textureHeight,
+                           final int maxTextureUpscale) {
+
+        topLeft = new Point3D(-width / 2, -height / 2, 0);
+        topRight = new Point3D(width / 2, -height / 2, 0);
+        bottomRight = new Point3D(width / 2, height / 2, 0);
+        bottomLeft = new Point3D(-width / 2, height / 2, 0);
+
+        texture = new Texture(textureWidth, textureHeight, maxTextureUpscale);
+
+        textureTopRight = new Point2D(textureWidth, 0);
+        textureTopLeft = new Point2D(0, 0);
+        textureBottomRight = new Point2D(textureWidth, textureHeight);
+        textureBottomLeft = new Point2D(0, textureHeight);
+
+
+
+
+        final TexturedPolygon texturedPolygon1 = new TexturedPolygon(
+                new Vertex(topLeft, textureTopLeft),
+                new Vertex(topRight, textureTopRight),
+                new Vertex(bottomRight, textureBottomRight), texture);
+
+        texturedPolygon1
+                .setMouseInteractionController(mouseInteractionController);
+
+        final TexturedPolygon texturedPolygon2 = new TexturedPolygon(
+                new Vertex(topLeft, textureTopLeft),
+                new Vertex(bottomLeft, textureBottomLeft),
+                new Vertex(bottomRight, textureBottomRight), texture);
+
+        texturedPolygon2
+                .setMouseInteractionController(mouseInteractionController);
+
+        addShape(texturedPolygon1);
+        addShape(texturedPolygon2);
+    }
+
+//    public void initialize(final int width, final int height,
+//                           final int maxTextureUpscale) {
+//        initialize(width, height, width, height, maxTextureUpscale);
+//    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
new file mode 100644 (file)
index 0000000..e35c451
--- /dev/null
@@ -0,0 +1,399 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker;
+import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.math.TransformStack;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
+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.slicer.Slicer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A composite shape that groups multiple sub-shapes into a single logical unit.
+ *
+ * <p>Use {@code AbstractCompositeShape} to build complex 3D objects by combining
+ * primitive shapes (lines, polygons, textured polygons) into a group that can be
+ * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized
+ * into named groups for selective visibility toggling.</p>
+ *
+ * <p><b>Usage example - creating a custom composite shape:</b></p>
+ * <pre>{@code
+ * // Create a composite shape at position (0, 0, 200)
+ * AbstractCompositeShape myObject = new AbstractCompositeShape(
+ *     new Point3D(0, 0, 200)
+ * );
+ *
+ * // Add sub-shapes
+ * myObject.addShape(new Line(
+ *     new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * // Add shapes to a named group for toggling visibility
+ * myObject.addShape(labelShape, "labels");
+ * myObject.hideGroup("labels");  // hide all shapes in "labels" group
+ * myObject.showGroup("labels");  // show them again
+ *
+ * // Add to scene
+ * viewPanel.getRootShapeCollection().addShape(myObject);
+ * }</pre>
+ *
+ * <p><b>Level-of-detail slicing:</b></p>
+ * <p>Textured polygons within the composite shape are automatically sliced into smaller
+ * triangles based on distance from the viewer. This provides perspective-correct texture
+ * mapping without requiring hardware support. The slicing factor adapts dynamically.</p>
+ *
+ * <p><b>Extending this class:</b></p>
+ * <p>Override {@link #beforeTransformHook} to customize shape appearance or behavior
+ * on each frame (e.g., animations, dynamic geometry updates).</p>
+ *
+ * @see SubShape wrapper for individual sub-shapes with group and visibility support
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape the base shape class
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer
+ */
+public class AbstractCompositeShape extends AbstractShape {
+    private final List<SubShape> originalSubShapes = new ArrayList<>();
+    private final ViewSpaceTracker viewSpaceTracker;
+    double currentSliceFactor = 5;
+    private List<AbstractShape> renderedSubShapes = new ArrayList<>();
+    private boolean slicingOutdated = true;
+    private Transform transform;
+    private LightingManager lightingManager;
+
+    /**
+     * Creates a composite shape at the world origin with no rotation.
+     */
+    public AbstractCompositeShape() {
+        this(new Transform());
+    }
+
+    /**
+     * Creates a composite shape at the specified location with no rotation.
+     *
+     * @param location the position in world space
+     */
+    public AbstractCompositeShape(final Point3D location) {
+        this(new Transform(location));
+    }
+
+    /**
+     * Creates a composite shape with the specified transform (position and orientation).
+     *
+     * @param transform the initial transform defining position and rotation
+     */
+    public AbstractCompositeShape(final Transform transform) {
+        this.transform = transform;
+        viewSpaceTracker = new ViewSpaceTracker();
+    }
+
+    /**
+     * Adds a sub-shape to this composite shape without a group identifier.
+     *
+     * @param shape the shape to add
+     */
+    public void addShape(final AbstractShape shape) {
+        addShape(shape, null);
+    }
+
+    /**
+     * Adds a sub-shape to this composite shape with an optional group identifier.
+     *
+     * <p>Grouped shapes can be shown, hidden, or removed together using
+     * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
+     *
+     * @param shape   the shape to add
+     * @param groupId the group identifier, or {@code null} for ungrouped shapes
+     */
+    public void addShape(final AbstractShape shape, final String groupId) {
+        final SubShape subShape = new SubShape(shape);
+        subShape.setGroup(groupId);
+        subShape.setVisible(true);
+        originalSubShapes.add(subShape);
+        slicingOutdated = true;
+    }
+
+    /**
+     * This method should be overridden by anyone wanting to customize shape
+     * before it is rendered.
+     */
+    public void beforeTransformHook(final TransformStack transformPipe,
+                                    final RenderingContext context) {
+    }
+
+    /**
+     * Returns the world-space position of this composite shape.
+     *
+     * @return the translation component of this shape's transform
+     */
+    public Point3D getLocation() {
+        return transform.getTranslation();
+    }
+
+    /**
+     * Returns the list of all sub-shapes (including hidden ones).
+     *
+     * @return the internal list of sub-shapes
+     */
+    public List<SubShape> getOriginalSubShapes() {
+        return originalSubShapes;
+    }
+
+    /**
+     * Returns the view-space tracker that monitors the distance
+     * and angle between the camera and this shape for level-of-detail adjustments.
+     *
+     * @return the view-space tracker for this shape
+     */
+    public ViewSpaceTracker getViewSpaceTracker() {
+        return viewSpaceTracker;
+    }
+
+    /**
+     * Hides all sub-shapes belonging to the specified group.
+     * Hidden shapes are not rendered but remain in the collection.
+     *
+     * @param groupIdentifier the group to hide
+     * @see #showGroup(String)
+     * @see #removeGroup(String)
+     */
+    public void hideGroup(final String groupIdentifier) {
+        for (int i = 0; i < originalSubShapes.size(); i++) {
+            final SubShape subShape = originalSubShapes.get(i);
+            if (subShape.matchesGroup(groupIdentifier)) {
+                subShape.setVisible(false);
+                slicingOutdated = true;
+            }
+        }
+    }
+
+    private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) {
+
+        if (slicingOutdated)
+            return true;
+
+        // reslice if there is significant difference between proposed and current slice factor
+        if (proposedNewSliceFactor > currentSliceFactor) {
+            final double tmp = proposedNewSliceFactor;
+            proposedNewSliceFactor = currentSliceFactor;
+            currentSliceFactor = tmp;
+        }
+
+        return (currentSliceFactor / proposedNewSliceFactor) > 1.5d;
+    }
+
+    /**
+     * Permanently removes all sub-shapes belonging to the specified group.
+     *
+     * @param groupIdentifier the group to remove
+     * @see #hideGroup(String)
+     */
+    public void removeGroup(final String groupIdentifier) {
+        final java.util.Iterator<SubShape> iterator = originalSubShapes
+                .iterator();
+
+        while (iterator.hasNext()) {
+            final SubShape subShape = iterator.next();
+            if (subShape.matchesGroup(groupIdentifier)) {
+                iterator.remove();
+                slicingOutdated = true;
+            }
+        }
+    }
+
+    /**
+     * Returns all sub-shapes belonging to the specified group.
+     *
+     * @param groupIdentifier the group identifier to match
+     * @return list of matching sub-shapes
+     */
+    public List<SubShape> getGroup(final String groupIdentifier) {
+        final List<SubShape> result = new ArrayList<>();
+        for (int i = 0; i < originalSubShapes.size(); i++) {
+            final SubShape subShape = originalSubShapes.get(i);
+            if (subShape.matchesGroup(groupIdentifier))
+                result.add(subShape);
+        }
+        return result;
+    }
+
+    private void resliceIfNeeded() {
+
+        final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
+
+        if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
+            currentSliceFactor = proposedSliceFactor;
+            reslice();
+        }
+    }
+
+    /**
+     * Paint solid elements of this composite shape into given color.
+     */
+    public void setColor(final Color color) {
+        for (final SubShape subShape : getOriginalSubShapes()) {
+            final AbstractShape shape = subShape.getShape();
+
+            if (shape instanceof SolidPolygon)
+                ((SolidPolygon) shape).setColor(color);
+
+            if (shape instanceof Line)
+                ((Line) shape).color = color;
+        }
+    }
+
+    /**
+     * Assigns a group identifier to all sub-shapes that currently have no group.
+     *
+     * @param groupIdentifier the group to assign to ungrouped shapes
+     */
+    public void setGroupForUngrouped(final String groupIdentifier) {
+        for (int i = 0; i < originalSubShapes.size(); i++) {
+            final SubShape subShape = originalSubShapes.get(i);
+            if (subShape.isUngrouped())
+                subShape.setGroup(groupIdentifier);
+        }
+    }
+
+    @Override
+    public void setMouseInteractionController(
+            final MouseInteractionController mouseInteractionController) {
+        super.setMouseInteractionController(mouseInteractionController);
+
+        for (final SubShape subShape : originalSubShapes)
+            subShape.getShape().setMouseInteractionController(
+                    mouseInteractionController);
+
+        slicingOutdated = true;
+
+    }
+
+    /**
+     * Replaces this shape's transform (position and orientation).
+     *
+     * @param transform the new transform to apply
+     */
+    public void setTransform(final Transform transform) {
+        this.transform = transform;
+    }
+
+    /**
+     * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes.
+     *
+     * @param lightingManager the lighting manager to use for shading calculations
+     */
+    public void setLightingManager(final LightingManager lightingManager) {
+        this.lightingManager = lightingManager;
+        applyShadingToPolygons();
+    }
+
+    /**
+     * Enables or disables shading for all SolidPolygon sub-shapes.
+     *
+     * @param shadingEnabled true to enable shading, false to disable
+     */
+    public void setShadingEnabled(final boolean shadingEnabled) {
+        for (final SubShape subShape : getOriginalSubShapes()) {
+            final AbstractShape shape = subShape.getShape();
+            if (shape instanceof SolidPolygon) {
+                ((SolidPolygon) shape).setShadingEnabled(shadingEnabled, lightingManager);
+            }
+        }
+    }
+
+    private void applyShadingToPolygons() {
+        if (lightingManager == null)
+            return;
+
+        for (final SubShape subShape : getOriginalSubShapes()) {
+            final AbstractShape shape = subShape.getShape();
+            if (shape instanceof SolidPolygon) {
+                ((SolidPolygon) shape).setShadingEnabled(true, lightingManager);
+            }
+        }
+    }
+
+    public void 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);
+            }
+        }
+    }
+
+    /**
+     * Makes all sub-shapes belonging to the specified group visible.
+     *
+     * @param groupIdentifier the group to show
+     * @see #hideGroup(String)
+     */
+    public void showGroup(final String groupIdentifier) {
+        for (int i = 0; i < originalSubShapes.size(); i++) {
+            final SubShape subShape = originalSubShapes.get(i);
+            if (subShape.matchesGroup(groupIdentifier)) {
+                subShape.setVisible(true);
+                slicingOutdated = true;
+            }
+        }
+    }
+
+    private void reslice() {
+        slicingOutdated = false;
+
+        final List<AbstractShape> result = new ArrayList<>();
+
+        final Slicer slicer = new Slicer(currentSliceFactor);
+        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());
+                else
+                    result.add(subShape.getShape());
+            }
+        }
+
+        result.addAll(slicer.getResult());
+
+        renderedSubShapes = result;
+    }
+
+    @Override
+    public void transform(final TransformStack transformPipe,
+                          final RenderAggregator aggregator, final RenderingContext context) {
+
+        // add current composite shape transform to the end of the transform
+        // pipeline
+        transformPipe.addTransform(transform);
+
+        viewSpaceTracker.analyze(transformPipe, context);
+
+        beforeTransformHook(transformPipe, context);
+
+        // hack, to get somewhat perspective correct textures
+        resliceIfNeeded();
+
+        // transform rendered subshapes
+        for (final AbstractShape shape : renderedSubShapes)
+            shape.transform(transformPipe, aggregator, context);
+
+        transformPipe.dropTransform();
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
new file mode 100644 (file)
index 0000000..6530b2d
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
+
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
+
+/**
+ * Wrapper around an {@link AbstractShape} within an {@link AbstractCompositeShape},
+ * adding group membership and visibility control.
+ *
+ * <p>Sub-shapes can be organized into named groups so they can be shown, hidden,
+ * or removed together. This is useful for toggling parts of a composite shape,
+ * such as showing/hiding labels, highlights, or selection borders.</p>
+ *
+ * @see AbstractCompositeShape#addShape(AbstractShape, String)
+ * @see AbstractCompositeShape#hideGroup(String)
+ * @see AbstractCompositeShape#showGroup(String)
+ */
+public class SubShape {
+
+    private final AbstractShape shape;
+    private boolean visible = true;
+    private String groupIdentifier;
+
+    /**
+     * Creates a sub-shape wrapper around the given shape.
+     *
+     * @param shape the shape to wrap
+     */
+    public SubShape(AbstractShape shape) {
+        this.shape = shape;
+    }
+
+    /**
+     * Returns {@code true} if this sub-shape has no group assigned.
+     *
+     * @return {@code true} if ungrouped
+     */
+    public boolean isUngrouped() {
+        return groupIdentifier == null;
+    }
+
+    /**
+     * Checks whether this sub-shape belongs to the specified group.
+     *
+     * @param groupIdentifier the group identifier to match against, or {@code null} to match ungrouped shapes
+     * @return {@code true} if this sub-shape belongs to the specified group
+     */
+    public boolean matchesGroup(final String groupIdentifier) {
+        if (this.groupIdentifier == null)
+            return groupIdentifier == null;
+
+        return this.groupIdentifier.equals(groupIdentifier);
+    }
+
+    /**
+     * Assigns this sub-shape to a group.
+     *
+     * @param groupIdentifier the group identifier, or {@code null} to make it ungrouped
+     */
+    public void setGroup(final String groupIdentifier) {
+        this.groupIdentifier = groupIdentifier;
+    }
+
+    /**
+     * Returns the wrapped shape.
+     *
+     * @return the underlying shape
+     */
+    public AbstractShape getShape() {
+        return shape;
+    }
+
+    /**
+     * Returns whether this sub-shape is currently visible and will be rendered.
+     *
+     * @return {@code true} if visible
+     */
+    public boolean isVisible() {
+        return visible;
+    }
+
+    /**
+     * Sets the visibility of this sub-shape.
+     *
+     * @param visible {@code true} to make the shape visible, {@code false} to hide it
+     */
+    public void setVisible(boolean visible) {
+        this.visible = visible;
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java
new file mode 100755 (executable)
index 0000000..6fa3482
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+/**
+ * A solid cube centered at a given point with equal side length along all axes.
+ * This is a convenience subclass of {@link SolidPolygonRectangularBox} that
+ * constructs a cube from a center point and a half-side length.
+ *
+ * <p>The cube extends {@code size} units in each direction from the center,
+ * resulting in a total edge length of {@code 2 * size}.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * SolidPolygonCube cube = new SolidPolygonCube(
+ *         new Point3D(0, 0, 300), 50, Color.GREEN);
+ * shapeCollection.addShape(cube);
+ * }</pre>
+ *
+ * @see SolidPolygonRectangularBox
+ * @see Color
+ */
+public class SolidPolygonCube extends SolidPolygonRectangularBox {
+
+    /**
+     * Constructs a solid cube centered at the given point.
+     *
+     * @param center the center point of the cube in 3D space
+     * @param size   the half-side length; the cube extends this distance from
+     *               the center along each axis, giving a total edge length of
+     *               {@code 2 * size}
+     * @param color  the fill color applied to all faces of the cube
+     */
+    public SolidPolygonCube(final Point3D center, final double size,
+                            final Color color) {
+        super(new Point3D(center.x - size, center.y - size, center.z - size),
+                new Point3D(center.x + size, center.y + size, center.z + size),
+                color);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java
new file mode 100644 (file)
index 0000000..a926a94
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+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;
+
+/**
+ * A solid cylinder oriented along the Y-axis.
+ *
+ * <p>The cylinder has circular top and bottom caps connected by a curved side
+ * surface made of rectangular panels. The number of segments determines the
+ * smoothness of the curved surface.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a cylinder with radius 50, height 100, and 16 segments
+ * SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
+ *     new Point3D(0, 0, 200), 50, 100, 16, Color.RED);
+ * shapeCollection.addShape(cylinder);
+ * }</pre>
+ *
+ * @see SolidPolygonCube
+ * @see SolidPolygonSphere
+ * @see SolidPolygon
+ */
+public class SolidPolygonCylinder extends AbstractCompositeShape {
+
+    /**
+     * Constructs a solid cylinder centered at the given point.
+     *
+     * @param center   the center point of the cylinder in 3D space.
+     *                 The cylinder is centered on the Y-axis, extending
+     *                 {@code height/2} above and below this point.
+     * @param radius   the radius of the cylinder
+     * @param height   the total height of the cylinder
+     * @param segments the number of segments around the circumference.
+     *                 Higher values create smoother cylinders. Minimum is 3.
+     * @param color    the fill color applied to all polygons
+     */
+    public SolidPolygonCylinder(final Point3D center, final double radius,
+                                final double height, final int segments,
+                                final Color color) {
+        super();
+
+        final double halfHeight = height / 2.0;
+        final double bottomY = center.y - halfHeight;
+        final double topY = center.y + halfHeight;
+
+        Point3D bottomCenter = new Point3D(center.x, bottomY, center.z);
+        Point3D topCenter = new Point3D(center.x, topY, center.z);
+
+        Point3D[] bottomRing = new Point3D[segments];
+        Point3D[] topRing = new Point3D[segments];
+
+        for (int i = 0; i < segments; i++) {
+            double angle = 2.0 * Math.PI * i / segments;
+            double x = center.x + radius * Math.cos(angle);
+            double z = center.z + radius * Math.sin(angle);
+            bottomRing[i] = new Point3D(x, bottomY, z);
+            topRing[i] = new Point3D(x, topY, z);
+        }
+
+        for (int i = 0; i < segments; i++) {
+            int next = (i + 1) % segments;
+
+            addShape(new SolidPolygon(
+                    new Point3D(bottomCenter.x, bottomCenter.y, bottomCenter.z),
+                    new Point3D(bottomRing[i].x, bottomRing[i].y, bottomRing[i].z),
+                    new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z),
+                    color));
+
+            addShape(new SolidPolygon(
+                    new Point3D(topCenter.x, topCenter.y, topCenter.z),
+                    new Point3D(topRing[next].x, topRing[next].y, topRing[next].z),
+                    new Point3D(topRing[i].x, topRing[i].y, topRing[i].z),
+                    color));
+
+            addShape(new SolidPolygon(
+                    new Point3D(bottomRing[i].x, bottomRing[i].y, bottomRing[i].z),
+                    new Point3D(topRing[i].x, topRing[i].y, topRing[i].z),
+                    new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z),
+                    color));
+
+            addShape(new SolidPolygon(
+                    new Point3D(bottomRing[next].x, bottomRing[next].y, bottomRing[next].z),
+                    new Point3D(topRing[i].x, topRing[i].y, topRing[i].z),
+                    new Point3D(topRing[next].x, topRing[next].y, topRing[next].z),
+                    color));
+        }
+
+        setBackfaceCulling(true);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java
new file mode 100644 (file)
index 0000000..97b18d8
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+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;
+
+/**
+ * A solid square-based pyramid with the base centered at a given point.
+ *
+ * <p>The pyramid has a square base and four triangular faces meeting at an apex.
+ * The base has side length of {@code 2 * baseSize} and the height extends
+ * {@code height} units above the base center to the apex.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
+ *         new Point3D(0, 0, 300), 50, 100, Color.BLUE);
+ * shapeCollection.addShape(pyramid);
+ * }</pre>
+ *
+ * @see SolidPolygonCube
+ * @see SolidPolygonSphere
+ * @see SolidPolygon
+ */
+public class SolidPolygonPyramid extends AbstractCompositeShape {
+
+    /**
+     * Constructs a solid square-based pyramid with base centered at the given point.
+     *
+     * @param baseCenter the center point of the pyramid's base in 3D space
+     * @param baseSize   the half-width of the square base; the base extends
+     *                   this distance from the center along X and Z axes,
+     *                   giving a total base edge length of {@code 2 * baseSize}
+     * @param height     the height of the pyramid from base center to apex
+     * @param color      the fill color applied to all faces of the pyramid
+     */
+    public SolidPolygonPyramid(final Point3D baseCenter, final double baseSize,
+                               final double height, final Color color) {
+        super();
+
+        final double halfBase = baseSize;
+        final double apexY = baseCenter.y - height;
+        final double baseY = baseCenter.y;
+
+        Point3D frontLeft = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase);
+        Point3D frontRight = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase);
+        Point3D backRight = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase);
+        Point3D backLeft = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase);
+        Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z);
+
+        // Polygons that touch apex
+        addShape(new SolidPolygon(frontLeft, frontRight, apex, color));
+        addShape(new SolidPolygon(frontRight, backRight, apex, color));
+        addShape(new SolidPolygon(backRight, backLeft, apex, color));
+        addShape(new SolidPolygon(backLeft, frontLeft, apex, color));
+
+        // Pyramid bottom
+        addShape(new SolidPolygon( backLeft, backRight, frontLeft, color));
+        addShape(new SolidPolygon( frontRight, frontLeft, backRight, color));
+
+        setBackfaceCulling(true);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java
new file mode 100755 (executable)
index 0000000..800def5
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+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;
+
+/**
+ * A solid (filled) rectangular box composed of 12 triangular polygons (2 per face,
+ * covering all 6 faces). Each face is rendered as a pair of {@link SolidPolygon}
+ * triangles with the same color.
+ *
+ * <p>The box can be constructed either from a center point and a uniform size
+ * (producing a cube), or from two diagonally opposite corner points (producing
+ * an arbitrary axis-aligned rectangular box).</p>
+ *
+ * <p>The vertices are labeled p1 through p8, representing the eight corners of
+ * the box. The triangles are arranged to cover the bottom, top, front, back,
+ * left, and right faces.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // From center and size:
+ * SolidPolygonRectangularBox box1 = new SolidPolygonRectangularBox(
+ *         new Point3D(0, 0, 200), 100, Color.RED);
+ *
+ * // From two corner points:
+ * SolidPolygonRectangularBox box2 = new SolidPolygonRectangularBox(
+ *         new Point3D(-50, -25, 100), new Point3D(50, 25, 200), Color.BLUE);
+ *
+ * shapeCollection.addShape(box1);
+ * }</pre>
+ *
+ * @see SolidPolygonCube
+ * @see SolidPolygon
+ * @see AbstractCompositeShape
+ */
+public class SolidPolygonRectangularBox extends AbstractCompositeShape {
+
+    /**
+     * Constructs a solid rectangular box between two diagonally opposite corner
+     * points in 3D space. The eight vertices of the box are derived from the
+     * coordinate components of {@code p1} and {@code p7}. All six faces are
+     * tessellated into two triangles each, for a total of 12 solid polygons.
+     *
+     * @param p1    the first corner point (minimum coordinates by convention)
+     * @param p7    the diagonally opposite corner point (maximum coordinates)
+     * @param color the fill color applied to all 12 triangular polygons
+     */
+    public SolidPolygonRectangularBox(final Point3D p1, final Point3D p7, final Color color) {
+        super();
+
+        final Point3D p2 = new Point3D(p7.x, p1.y, p1.z);
+        final Point3D p3 = new Point3D(p7.x, p1.y, p7.z);
+        final Point3D p4 = new Point3D(p1.x, p1.y, p7.z);
+
+        final Point3D p5 = new Point3D(p1.x, p7.y, p1.z);
+        final Point3D p6 = new Point3D(p7.x, p7.y, p1.z);
+        final Point3D p8 = new Point3D(p1.x, p7.y, p7.z);
+
+        // Bottom face (y = minY)
+        addShape(new SolidPolygon(p1, p2, p3, color));
+        addShape(new SolidPolygon(p1, p3, p4, color));
+
+        // Top face (y = maxY)
+        addShape(new SolidPolygon(p5, p8, p7, color));
+        addShape(new SolidPolygon(p5, p7, p6, color));
+
+        // Front face (z = minZ)
+        addShape(new SolidPolygon(p1, p5, p6, color));
+        addShape(new SolidPolygon(p1, p6, p2, color));
+
+        // Back face (z = maxZ)
+        addShape(new SolidPolygon(p3, p7, p8, color));
+        addShape(new SolidPolygon(p3, p8, p4, color));
+
+        // Left face (x = minX)
+        addShape(new SolidPolygon(p1, p4, p8, color));
+        addShape(new SolidPolygon(p1, p8, p5, color));
+
+        // Right face (x = maxX)
+        addShape(new SolidPolygon(p2, p6, p7, color));
+        addShape(new SolidPolygon(p2, p7, p3, color));
+
+        setBackfaceCulling(true);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java
new file mode 100644 (file)
index 0000000..6f0d64d
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+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;
+
+/**
+ * A solid sphere composed of triangular polygons.
+ *
+ * <p>The sphere is constructed using a latitude-longitude grid (UV sphere).
+ * The number of segments determines the smoothness - more segments create
+ * a smoother sphere but require more polygons.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a sphere with radius 50 and 16 segments (smooth)
+ * SolidPolygonSphere sphere = new SolidPolygonSphere(
+ *     new Point3D(0, 0, 200), 50, 16, Color.RED);
+ * shapeCollection.addShape(sphere);
+ * }</pre>
+ *
+ * @see SolidPolygonCube
+ * @see SolidPolygon
+ * @see AbstractCompositeShape
+ */
+public class SolidPolygonSphere extends AbstractCompositeShape {
+
+    /**
+     * Constructs a solid sphere centered at the given point.
+     *
+     * @param center   the center point of the sphere in 3D space
+     * @param radius   the radius of the sphere
+     * @param segments the number of segments (latitude/longitude divisions).
+     *                 Higher values create smoother spheres. Minimum is 3.
+     * @param color    the fill color applied to all triangular polygons
+     */
+    public SolidPolygonSphere(final Point3D center, final double radius,
+                              final int segments, final Color color) {
+        super();
+
+        final int rings = segments;
+        final int sectors = segments * 2;
+
+        for (int i = 0; i < rings; i++) {
+            double lat0 = Math.PI * (-0.5 + (double) i / rings);
+            double lat1 = Math.PI * (-0.5 + (double) (i + 1) / rings);
+
+            for (int j = 0; j < sectors; j++) {
+                double lon0 = 2 * Math.PI * (double) j / sectors;
+                double lon1 = 2 * Math.PI * (double) (j + 1) / sectors;
+
+                Point3D p0 = sphericalToCartesian(center, radius, lat0, lon0);
+                Point3D p1 = sphericalToCartesian(center, radius, lat0, lon1);
+                Point3D p2 = sphericalToCartesian(center, radius, lat1, lon0);
+                Point3D p3 = sphericalToCartesian(center, radius, lat1, lon1);
+
+                if (i > 0) {
+                    addShape(new SolidPolygon(p0, p2, p1, color));
+                }
+
+                if (i < rings - 1) {
+                    addShape(new SolidPolygon(p2, p3, p1, color));
+                }
+            }
+        }
+
+        setBackfaceCulling(true);
+    }
+
+    private Point3D sphericalToCartesian(final Point3D center,
+                                          final double radius,
+                                          final double lat,
+                                          final double lon) {
+        double x = center.x + radius * Math.cos(lat) * Math.cos(lon);
+        double y = center.y + radius * Math.sin(lat);
+        double z = center.z + radius * Math.cos(lat) * Math.sin(lon);
+        return new Point3D(x, y, z);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java
new file mode 100644 (file)
index 0000000..a616dae
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+
+import java.awt.*;
+
+import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawPolygon;
+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;
+
+/**
+ * Represents a single character on the text canvas.
+ */
+public class CanvasCharacter extends AbstractCoordinateShape {
+
+    private static final int MAX_FONT_SIZE = 500;
+
+    /**
+     * Cached fonts.
+     */
+    private static final Font[] fonts = new Font[MAX_FONT_SIZE];
+
+    /**
+     * The character to be rendered.
+     */
+    private char value;
+
+    /**
+     * The foreground color of the character.
+     */
+    private eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor;
+
+    /**
+     * The background color of the character.
+     */
+    private eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor;
+
+    public CanvasCharacter(final Point3D centerLocation, final char character,
+                           final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor,
+                           final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) {
+
+        // There are 5 coordinates: center, upper left, upper right, lower right, lower left
+        super(5);
+
+        value = character;
+        this.foregroundColor = foregroundColor;
+        this.backgroundColor = backgroundColor;
+
+
+        coordinates[0].coordinate = centerLocation;
+
+        final double halfWidth = FONT_CHAR_WIDTH / 2d;
+        final double halfHeight = FONT_CHAR_HEIGHT / 2d;
+
+        // upper left
+        coordinates[1].coordinate = centerLocation.clone().translateX(-halfWidth)
+                .translateY(-halfHeight);
+
+        // upper right
+        coordinates[2].coordinate = centerLocation.clone().translateX(halfWidth)
+                .translateY(-halfHeight);
+
+        // lower right
+        coordinates[3].coordinate = centerLocation.clone().translateX(halfWidth)
+                .translateY(halfHeight);
+
+        // lower left
+        coordinates[4].coordinate = centerLocation.clone().translateX(-halfWidth)
+                .translateY(halfHeight);
+    }
+
+    /**
+     * Returns a font of the specified size.
+     * <p>
+     * If the font of the specified size is already cached, it will be
+     * returned. Otherwise, a new font will be created, cached and returned.
+     *
+     * @param size the size of the font
+     * @return the font
+     */
+    public static Font getFont(final int size) {
+        if (fonts[size] != null)
+            return fonts[size];
+
+        final Font font = new Font("Courier", Font.BOLD, size);
+        fonts[size] = font;
+        return font;
+    }
+
+    /**
+     * Returns color of the background.
+     */
+    public eu.svjatoslav.sixth.e3d.renderer.raster.Color getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    /**
+     * Sets color of the background.
+     */
+    public void setBackgroundColor(
+            final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) {
+        this.backgroundColor = backgroundColor;
+    }
+
+    /**
+     * Returns color of the foreground.
+     *
+     * @return the color
+     */
+    public eu.svjatoslav.sixth.e3d.renderer.raster.Color getForegroundColor() {
+        return foregroundColor;
+    }
+
+    /**
+     * Sets color of the foreground.
+     *
+     * @param foregroundColor the color
+     */
+    public void setForegroundColor(
+            final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor) {
+        this.foregroundColor = foregroundColor;
+    }
+
+    /**
+     * Paints the character on the screen.
+     * @param renderingContext the rendering context
+     */
+    @Override
+    public void paint(final RenderingContext renderingContext) {
+
+        // Draw background rectangle first. It is composed of two triangles.
+        drawPolygon(renderingContext,
+                coordinates[1].onScreenCoordinate,
+                coordinates[2].onScreenCoordinate,
+                coordinates[3].onScreenCoordinate,
+                mouseInteractionController,
+                backgroundColor);
+
+        drawPolygon(renderingContext,
+                coordinates[1].onScreenCoordinate,
+                coordinates[3].onScreenCoordinate,
+                coordinates[4].onScreenCoordinate,
+                mouseInteractionController,
+                backgroundColor);
+
+        final int desiredFontSize = (int) ((renderingContext.width * 4.5) / onScreenZ);
+
+        // do not render too large characters
+        if (desiredFontSize >= MAX_FONT_SIZE)
+            return;
+
+        final Point2D onScreenLocation = coordinates[0].onScreenCoordinate;
+
+        // screen borders check
+        if (onScreenLocation.x < 0)
+            return;
+        if (onScreenLocation.y < 0)
+            return;
+
+        if (onScreenLocation.x > renderingContext.width)
+            return;
+        if (onScreenLocation.y > renderingContext.height)
+            return;
+
+        // draw the character
+        renderingContext.graphics.setFont(getFont(desiredFontSize));
+        renderingContext.graphics.setColor(foregroundColor.toAwtColor());
+        renderingContext.graphics.drawString(
+                valueOf(value),
+                (int) onScreenLocation.x - (int) (desiredFontSize / 3.2),
+                (int) onScreenLocation.y + (int) (desiredFontSize / 2.5));
+
+    }
+
+    public void setValue(final char value) {
+        this.value = value;
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/RenderMode.java
new file mode 100644 (file)
index 0000000..b4fc5e7
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas;
+
+/**
+ * Defines how text is rendered on a {@link TextCanvas}.
+ *
+ * <p>The render mode controls the trade-off between rendering quality and performance.
+ * {@link TextCanvas} automatically selects the optimal mode based on the viewer's
+ * distance and viewing angle relative to the text surface.</p>
+ *
+ * @see TextCanvas
+ */
+public enum RenderMode {
+    /**
+     * Text is rendered as pixels on textured polygon.
+     * This mode works in any orientation. Even if polygon is rotated.
+     */
+    TEXTURE,
+
+    /**
+     * Text is rendered as high quality, anti-aliased tiles.
+     * This mode works only if text is facing the camera almost directly.
+     */
+    CHARACTERS
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java
new file mode 100644 (file)
index 0000000..14e563b
--- /dev/null
@@ -0,0 +1,463 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+import eu.svjatoslav.sixth.e3d.gui.TextPointer;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.math.TransformStack;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.TexturedRectangle;
+
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.BLACK;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.WHITE;
+import static java.lang.Math.PI;
+import static java.lang.Math.abs;
+
+/**
+ * A text rendering surface in 3D space that displays a grid of characters.
+ *
+ * <p>{@code TextCanvas} extends {@link TexturedRectangle} and renders a 2D grid of
+ * characters (rows and columns) onto a texture-mapped rectangle. Each character cell
+ * supports independent foreground and background colors through {@link CanvasCharacter}.</p>
+ *
+ * <p>Characters are rendered using a monospace font at a fixed size
+ * ({@value #FONT_CHAR_WIDTH_TEXTURE_PIXELS} x {@value #FONT_CHAR_HEIGHT_TEXTURE_PIXELS}
+ * texture pixels per character). The canvas automatically switches between two
+ * {@linkplain RenderMode render modes} based on the viewer's distance and viewing angle:</p>
+ * <ul>
+ *   <li>{@link RenderMode#TEXTURE} -- renders all characters to a shared texture bitmap.
+ *       This is efficient for distant or obliquely viewed text.</li>
+ *   <li>{@link RenderMode#CHARACTERS} -- renders each character as an individual textured
+ *       polygon with higher quality anti-aliased tiles. Used when the viewer is close
+ *       and looking at the text nearly head-on.</li>
+ * </ul>
+ *
+ * <p><b>Usage example</b></p>
+ * <pre>{@code
+ * Transform location = new Transform(new Point3D(0, 0, 500));
+ * TextCanvas canvas = new TextCanvas(location, "Hello, World!",
+ *         Color.WHITE, Color.BLACK);
+ * shapeCollection.addShape(canvas);
+ *
+ * // Or create a blank canvas and write to it
+ * TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40),
+ *         Color.GREEN, Color.BLACK);
+ * blank.locate(0, 0);
+ * blank.print("Line 1");
+ * blank.locate(1, 0);
+ * blank.print("Line 2");
+ * }</pre>
+ *
+ * @see RenderMode
+ * @see CanvasCharacter
+ * @see TexturedRectangle
+ */
+public class TextCanvas extends TexturedRectangle {
+
+    /**
+     * Font character width in world coordinates.
+     */
+    public static final int FONT_CHAR_WIDTH = 8;
+
+    /**
+     * Font character height in world coordinates.
+     */
+    public static final int FONT_CHAR_HEIGHT = 16;
+
+    /**
+      * Font character width in texture pixels.
+      */
+    public static final int FONT_CHAR_WIDTH_TEXTURE_PIXELS = 16;
+
+    /**
+     * Font character height in texture pixels.
+     */
+    public static final int FONT_CHAR_HEIGHT_TEXTURE_PIXELS = 32;
+
+
+    public static final Font FONT = CanvasCharacter.getFont((int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.066));
+    private static final String GROUP_TEXTURE = "texture";
+    private static final String GROUP_CHARACTERS = "characters";
+    private final TextPointer size;
+    private final TextPointer cursorLocation = new TextPointer();
+    CanvasCharacter[][] lines;
+    private RenderMode renderMode = null;
+    private Color backgroundColor = BLACK;
+    private Color foregroundColor = WHITE;
+
+    /**
+     * Creates a text canvas initialized with the given text string.
+     *
+     * <p>The canvas dimensions are automatically computed from the text content
+     * (number of lines determines rows, the longest line determines columns).</p>
+     *
+     * @param location        the 3D transform positioning this canvas in the scene
+     * @param text            the initial text content (may contain newlines for multiple rows)
+     * @param foregroundColor the default text color
+     * @param backgroundColor the default background color
+     */
+    public TextCanvas(final Transform location, final String text,
+                      final Color foregroundColor, final Color backgroundColor) {
+        this(location, getTextDimensions(text), foregroundColor,
+                backgroundColor);
+        setText(text);
+    }
+
+    /**
+     * Creates a blank text canvas with the specified dimensions.
+     *
+     * <p>The canvas is initialized with spaces in every cell, filled with the
+     * specified background color. Characters can be written using
+     * {@link #putChar(char)}, {@link #print(String)}, or {@link #setText(String)}.</p>
+     *
+     * @param dimensions      the grid size as a {@link TextPointer} where
+     *                        {@code row} is the number of rows and {@code column} is the number of columns
+     * @param location        the 3D transform positioning this canvas in the scene
+     * @param foregroundColor the default text color
+     * @param backgroundColor the default background color
+     */
+    public TextCanvas(final Transform location, final TextPointer dimensions,
+                      final Color foregroundColor, final Color backgroundColor) {
+        super(location);
+        getViewSpaceTracker().enableOrientationTracking();
+
+        size = dimensions;
+        final int columns = dimensions.column;
+        final int rows = dimensions.row;
+
+        this.backgroundColor = backgroundColor;
+        this.foregroundColor = foregroundColor;
+
+        // initialize underlying textured rectangle
+        initialize(
+                columns * FONT_CHAR_WIDTH,
+                rows * FONT_CHAR_HEIGHT,
+                columns * FONT_CHAR_WIDTH_TEXTURE_PIXELS,
+                rows * FONT_CHAR_HEIGHT_TEXTURE_PIXELS,
+                0);
+
+        getTexture().primaryBitmap.fillColor(backgroundColor);
+        getTexture().resetResampledBitmapCache();
+
+        setGroupForUngrouped(GROUP_TEXTURE);
+
+        lines = new CanvasCharacter[rows][];
+        for (int row = 0; row < rows; row++) {
+            lines[row] = new CanvasCharacter[columns];
+
+            for (int column = 0; column < columns; column++) {
+                final Point3D characterCoordinate = getCharLocation(row, column);
+
+                final CanvasCharacter character = new CanvasCharacter(
+                        characterCoordinate, ' ', foregroundColor,
+                        backgroundColor);
+                addShape(character);
+                lines[row][column] = character;
+            }
+
+        }
+
+        setGroupForUngrouped(GROUP_CHARACTERS);
+
+        setRenderMode(RenderMode.TEXTURE);
+    }
+
+    /**
+     * Computes the row and column dimensions needed to fit the given text.
+     *
+     * @param text the text content (may contain newlines)
+     * @return a {@link TextPointer} where {@code row} is the number of lines and
+     *         {@code column} is the length of the longest line
+     */
+    public static TextPointer getTextDimensions(final String text) {
+
+        final BufferedReader reader = new BufferedReader(new StringReader(text));
+
+        int rows = 0;
+        int columns = 0;
+
+        while (true) {
+            final String line;
+            try {
+                line = reader.readLine();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+
+            if (line == null)
+                return new TextPointer(rows, columns);
+
+            rows++;
+            columns = Math.max(columns, line.length());
+        }
+    }
+
+    @Override
+    public void beforeTransformHook(final TransformStack transformPipe,
+                                    final RenderingContext context) {
+        ensureOptimalRenderMode(context);
+    }
+
+    private void ensureOptimalRenderMode(RenderingContext context) {
+
+        // if the text is too far away, use texture
+        final double textRelativeSize = context.width / getViewSpaceTracker().getDistanceToCamera();
+        if (textRelativeSize < 2d) {
+            setRenderMode(RenderMode.TEXTURE);
+            return;
+        }
+
+        // if user is looking at the text from the side, use texture
+        final double piHalf = PI / 2;
+        final double deviation = abs(getViewSpaceTracker().getAngleXZ()
+                + piHalf)
+                + abs(getViewSpaceTracker().getAngleYZ() + piHalf);
+
+        final double maxDeviation = 0.5;
+        setRenderMode(deviation > maxDeviation ? RenderMode.TEXTURE : RenderMode.CHARACTERS);
+    }
+
+    /**
+     * Clears the entire canvas, resetting all characters to spaces with the default colors.
+     *
+     * <p>Both the character grid and the backing texture bitmap are reset.</p>
+     */
+    public void clear() {
+        for (final CanvasCharacter[] line : lines)
+            for (final CanvasCharacter character : line) {
+                character.setValue(' ');
+                character.setBackgroundColor(backgroundColor);
+                character.setForegroundColor(foregroundColor);
+            }
+
+        // set background color
+        getTexture().primaryBitmap.fillColor(backgroundColor);
+        getTexture().resetResampledBitmapCache();
+    }
+
+    private void drawCharToTexture(final int row, final int column,
+                                   final char character, final Color foreground) {
+        final Graphics2D graphics = getTexture().graphics;
+
+        getTexture().primaryBitmap.drawRectangle(
+                column * FONT_CHAR_WIDTH_TEXTURE_PIXELS,
+                row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS,
+                (column * FONT_CHAR_WIDTH_TEXTURE_PIXELS) + FONT_CHAR_WIDTH_TEXTURE_PIXELS,
+                (row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS) + FONT_CHAR_HEIGHT_TEXTURE_PIXELS,
+                backgroundColor);
+
+        graphics.setFont(FONT);
+        graphics.setColor(foreground.toAwtColor());
+        graphics.drawChars(
+                new char[]{character,}, 0, 1,
+                (column * FONT_CHAR_WIDTH_TEXTURE_PIXELS),
+                (row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS) + (int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.23f));
+        getTexture().resetResampledBitmapCache();
+    }
+
+    /**
+     * Computes the 3D world coordinate for the center of the character cell at the given row and column.
+     *
+     * @param row    the row index (0-based, from the top)
+     * @param column the column index (0-based, from the left)
+     * @return the 3D coordinate of the character cell center, relative to the canvas origin
+     */
+    public Point3D getCharLocation(final int row, final int column) {
+        final Point3D coordinate = topLeft.clone();
+
+        coordinate.translateY((row * FONT_CHAR_HEIGHT)
+                + (FONT_CHAR_HEIGHT / 3.2));
+
+        coordinate.translateX((column * FONT_CHAR_WIDTH)
+                + (FONT_CHAR_WIDTH / 2));
+
+        return coordinate;
+    }
+
+    /**
+     * Returns the dimensions of this text canvas.
+     *
+     * @return a {@link TextPointer} where {@code row} is the number of rows
+     *         and {@code column} is the number of columns
+     */
+    public TextPointer getSize() {
+        return size;
+    }
+
+    /**
+     * Moves the internal cursor to the specified row and column.
+     *
+     * <p>Subsequent calls to {@link #putChar(char)} and {@link #print(String)} will
+     * begin writing at this position.</p>
+     *
+     * @param row    the target row (0-based)
+     * @param column the target column (0-based)
+     */
+    public void locate(final int row, final int column) {
+        cursorLocation.row = row;
+        cursorLocation.column = column;
+    }
+
+    /**
+     * Prints a string starting at the current cursor location, advancing the cursor after each character.
+     *
+     * <p>When the cursor reaches the end of a row, it wraps to the beginning of the next row.</p>
+     *
+     * @param text the text to print
+     * @see #locate(int, int)
+     */
+    public void print(final String text) {
+        for (int i = 0; i < text.length(); i++)
+            putChar(text.charAt(i));
+    }
+
+    /**
+     * Writes a character at the current cursor location and advances the cursor.
+     *
+     * <p>The cursor moves one column to the right. If it exceeds the row width,
+     * it wraps to column 0 of the next row.</p>
+     *
+     * @param character the character to write
+     */
+    public void putChar(final char character) {
+        putChar(cursorLocation, character);
+
+        cursorLocation.column++;
+        if (cursorLocation.column >= size.column) {
+            cursorLocation.column = 0;
+            cursorLocation.row++;
+        }
+    }
+
+    /**
+     * Writes a character at the specified row and column using the current foreground and background colors.
+     *
+     * <p>If the row or column is out of bounds, the call is silently ignored.</p>
+     *
+     * @param row       the row index (0-based)
+     * @param column    the column index (0-based)
+     * @param character the character to write
+     */
+    public void putChar(final int row, final int column, final char character) {
+        if ((row >= lines.length) || (row < 0))
+            return;
+
+        final CanvasCharacter[] line = lines[row];
+
+        if ((column >= line.length) || (column < 0))
+            return;
+
+        final CanvasCharacter canvasCharacter = line[column];
+        canvasCharacter.setValue(character);
+        canvasCharacter.setBackgroundColor(backgroundColor);
+        canvasCharacter.setForegroundColor(foregroundColor);
+        drawCharToTexture(row, column, character,
+                foregroundColor);
+    }
+
+    /**
+     * Writes a character at the position specified by a {@link TextPointer}.
+     *
+     * @param location  the row and column position
+     * @param character the character to write
+     */
+    public void putChar(final TextPointer location, final char character) {
+        putChar(location.row, location.column, character);
+    }
+
+    /**
+     * Sets the default background color for subsequent character writes.
+     *
+     * @param backgroundColor the new background color
+     */
+    public void setBackgroundColor(
+            final eu.svjatoslav.sixth.e3d.renderer.raster.Color backgroundColor) {
+        this.backgroundColor = backgroundColor;
+    }
+
+    /**
+     * Sets the default foreground (text) color for subsequent character writes.
+     *
+     * @param foregroundColor the new foreground color
+     */
+    public void setForegroundColor(
+            final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor) {
+        this.foregroundColor = foregroundColor;
+    }
+
+    private void setRenderMode(final RenderMode mode) {
+        if (mode == renderMode)
+            return;
+
+        switch (mode) {
+            case CHARACTERS:
+                hideGroup(GROUP_TEXTURE);
+                showGroup(GROUP_CHARACTERS);
+                break;
+            case TEXTURE:
+                hideGroup(GROUP_CHARACTERS);
+                showGroup(GROUP_TEXTURE);
+                break;
+        }
+
+        renderMode = mode;
+    }
+
+    /**
+     * Replaces the entire canvas content with the given multi-line text string.
+     *
+     * <p>Each line of text (separated by newlines) is written to consecutive rows,
+     * starting from row 0. Characters beyond the canvas width are ignored.</p>
+     *
+     * @param text the text to display (may contain newline characters)
+     */
+    public void setText(final String text) {
+        final BufferedReader reader = new BufferedReader(new StringReader(text));
+
+        int row = 0;
+
+        while (true) {
+            final String line;
+            try {
+                line = reader.readLine();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+
+            if (line == null)
+                return;
+
+            int column = 0;
+            for (int i = 0; i < line.length(); i++) {
+                putChar(row, column, line.charAt(i));
+                column++;
+            }
+            row++;
+        }
+    }
+
+    /**
+     * Sets the foreground color of all existing characters on the canvas.
+     *
+     * <p>This updates the color of every {@link CanvasCharacter} in the grid,
+     * but does not affect the backing texture. It is primarily useful in
+     * {@link RenderMode#CHARACTERS} mode.</p>
+     *
+     * @param color the new foreground color for all characters
+     */
+    public void setTextColor(final Color color) {
+        for (final CanvasCharacter[] line : lines)
+            for (final CanvasCharacter character : line)
+                character.setForegroundColor(color);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java
new file mode 100644 (file)
index 0000000..cb4f6be
--- /dev/null
@@ -0,0 +1,9 @@
+/**
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ * <p>
+ *
+ * Text canvas is a 2D canvas that can be used to render text.
+ */
+
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas;
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java
new file mode 100644 (file)
index 0000000..333d920
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.geometry.Rectangle;
+import eu.svjatoslav.sixth.e3d.math.Transform;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A 2D grid of line segments lying in the XY plane (Z = 0 in local space).
+ * The grid is divided into configurable numbers of cells along the X and Y axes,
+ * producing a regular rectangular mesh of lines.
+ *
+ * <p>This shape is useful for rendering floors, walls, reference planes, or any
+ * flat surface that needs a grid overlay. The grid is positioned and oriented
+ * in world space using a {@link Transform}.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * Transform transform = new Transform(new Point3D(0, 100, 0));
+ * Rectangle rect = new Rectangle(new Point2D(-500, -500), new Point2D(500, 500));
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Grid2D grid = new Grid2D(transform, rect, 10, 10, appearance);
+ * shapeCollection.addShape(grid);
+ * }</pre>
+ *
+ * @see Grid3D
+ * @see LineAppearance
+ * @see AbstractCompositeShape
+ */
+public class Grid2D extends AbstractCompositeShape {
+
+    /**
+     * Constructs a 2D grid in the XY plane with the specified dimensions and
+     * number of divisions.
+     *
+     * @param transform      the transform defining the grid's position and orientation
+     *                       in world space
+     * @param rectangle      the rectangular dimensions of the grid in local XY space
+     * @param xDivisionCount the number of divisions (cells) along the X axis;
+     *                       produces {@code xDivisionCount + 1} vertical lines
+     * @param yDivisionCount the number of divisions (cells) along the Y axis;
+     *                       produces {@code yDivisionCount + 1} horizontal lines
+     * @param appearance     the line appearance (color, width) used for all grid lines
+     */
+    public Grid2D(final Transform transform, final Rectangle rectangle,
+                  final int xDivisionCount, final int yDivisionCount,
+                  final LineAppearance appearance) {
+
+        super(transform);
+
+        final double stepY = rectangle.getHeight() / yDivisionCount;
+        final double stepX = rectangle.getWidth() / xDivisionCount;
+
+        for (int ySlice = 0; ySlice <= yDivisionCount; ySlice++) {
+            final double y = (ySlice * stepY) + rectangle.getLowerY();
+
+            for (int xSlice = 0; xSlice <= xDivisionCount; xSlice++) {
+                final double x = (xSlice * stepX) + rectangle.getLowerX();
+
+                final Point3D p1 = new Point3D(x, y, 0);
+                final Point3D p2 = new Point3D(x + stepX, y, 0);
+                final Point3D p3 = new Point3D(x, y + stepY, 0);
+
+                if (xSlice < xDivisionCount)
+                    addShape(appearance.getLine(p1, p2));
+
+                if (ySlice < yDivisionCount)
+                    addShape(appearance.getLine(p1, p3));
+            }
+
+        }
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java
new file mode 100755 (executable)
index 0000000..f7ea422
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A 3D grid of line segments filling a rectangular volume defined by two
+ * opposite corner points. Lines run along all three axes (X, Y, and Z) at
+ * regular intervals determined by the step size.
+ *
+ * <p>At each grid intersection point, up to three line segments are created
+ * (one along each axis), forming a three-dimensional lattice. The corner
+ * points are automatically normalized so that {@code p1} holds the minimum
+ * coordinates and {@code p2} holds the maximum coordinates.</p>
+ *
+ * <p>This shape is useful for visualizing 3D space, voxel boundaries, or
+ * spatial reference grids in a scene.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Point3D corner1 = new Point3D(-100, -100, -100);
+ * Point3D corner2 = new Point3D(100, 100, 100);
+ * Grid3D grid = new Grid3D(corner1, corner2, 50, appearance);
+ * shapeCollection.addShape(grid);
+ * }</pre>
+ *
+ * @see Grid2D
+ * @see LineAppearance
+ * @see AbstractCompositeShape
+ */
+public class Grid3D extends AbstractCompositeShape {
+
+    /**
+     * Constructs a 3D grid filling the volume between two corner points.
+     * The corner points are copied and normalized internally so that grid
+     * generation always proceeds from minimum to maximum coordinates.
+     *
+     * @param p1t        the first corner point defining the volume (copied, not modified)
+     * @param p2t        the diagonally opposite corner point (copied, not modified)
+     * @param step       the spacing between grid lines along each axis; must be
+     *                   positive
+     * @param appearance the line appearance (color, width) used for all grid lines
+     */
+    public Grid3D(final Point3D p1t, final Point3D p2t, final double step,
+                  final LineAppearance appearance) {
+
+        super();
+
+        final Point3D p1 = new Point3D(p1t);
+        final Point3D p2 = new Point3D(p2t);
+
+        if (p1.x > p2.x) {
+            final double tmp = p1.x;
+            p1.x = p2.x;
+            p2.x = tmp;
+        }
+
+        if (p1.y > p2.y) {
+            final double tmp = p1.y;
+            p1.y = p2.y;
+            p2.y = tmp;
+        }
+
+        if (p1.z > p2.z) {
+            final double tmp = p1.z;
+            p1.z = p2.z;
+            p2.z = tmp;
+        }
+
+        for (double x = p1.x; x <= p2.x; x += step)
+            for (double y = p1.y; y <= p2.y; y += step)
+                for (double z = p1.z; z <= p2.z; z += step) {
+
+                    final Point3D p3 = new Point3D(x, y, z);
+
+                    if ((x + step) <= p2.x) {
+                        final Point3D point3d2 = new Point3D(x + step, y, z);
+                        addShape(appearance.getLine(p3, point3d2));
+                    }
+
+                    if ((y + step) <= p2.y) {
+                        final Point3D point3d3 = new Point3D(x, y + step, z);
+                        addShape(appearance.getLine(p3, point3d3));
+                    }
+
+                    if ((z + step) <= p2.z) {
+                        final Point3D point3d4 = new Point3D(x, y, z + step);
+                        addShape(appearance.getLine(p3, point3d4));
+                    }
+
+                }
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java
new file mode 100755 (executable)
index 0000000..b776db4
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Box;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A wireframe box (rectangular parallelepiped) composed of 12 line segments
+ * representing the edges of the box. The box is axis-aligned, defined by two
+ * opposite corner points.
+ *
+ * <p>The wireframe consists of four edges along each axis: four edges parallel
+ * to X, four parallel to Y, and four parallel to Z.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LineAppearance appearance = new LineAppearance(2, Color.GREEN);
+ * Point3D corner1 = new Point3D(-50, -50, -50);
+ * Point3D corner2 = new Point3D(50, 50, 50);
+ * WireframeBox box = new WireframeBox(corner1, corner2, appearance);
+ * shapeCollection.addShape(box);
+ * }</pre>
+ *
+ * @see WireframeCube
+ * @see Box
+ * @see LineAppearance
+ * @see AbstractCompositeShape
+ */
+public class WireframeBox extends AbstractCompositeShape {
+
+    /**
+     * Constructs a wireframe box from a {@link Box} geometry object.
+     *
+     * @param box        the axis-aligned box defining the two opposite corners
+     * @param appearance the line appearance (color, width) used for all 12 edges
+     */
+    public WireframeBox(final Box box,
+                        final LineAppearance appearance) {
+
+        this(box.p1, box.p2, appearance);
+    }
+
+    /**
+     * Constructs a wireframe box from two opposite corner points. The corners
+     * do not need to be in any particular min/max order; the constructor uses
+     * each coordinate independently to form all eight vertices of the box.
+     *
+     * @param p1         the first corner point of the box
+     * @param p2         the diagonally opposite corner point of the box
+     * @param appearance the line appearance (color, width) used for all 12 edges
+     */
+    public WireframeBox(final Point3D p1, final Point3D p2,
+                        final LineAppearance appearance) {
+        super();
+
+        addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
+                p2.x, p1.y, p1.z)));
+        addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D(
+                p2.x, p2.y, p1.z)));
+        addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
+                p1.x, p2.y, p1.z)));
+        addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D(
+                p2.x, p2.y, p1.z)));
+
+        addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D(
+                p2.x, p1.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p1.x, p2.y, p2.z), new Point3D(
+                p2.x, p2.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D(
+                p1.x, p2.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p2.x, p1.y, p2.z), new Point3D(
+                p2.x, p2.y, p2.z)));
+
+        addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
+                p1.x, p1.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D(
+                p1.x, p2.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D(
+                p2.x, p1.y, p2.z)));
+        addShape(appearance.getLine(new Point3D(p2.x, p2.y, p1.z), new Point3D(
+                p2.x, p2.y, p2.z)));
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java
new file mode 100755 (executable)
index 0000000..5f31fd3
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+
+/**
+ * A wireframe cube (equal-length sides) centered at a given point in 3D space.
+ * This is a convenience subclass of {@link WireframeBox} that constructs an
+ * axis-aligned cube from a center point and a half-side length.
+ *
+ * <p>The cube extends {@code size} units in each direction from the center,
+ * resulting in a total edge length of {@code 2 * size}.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.CYAN);
+ * WireframeCube cube = new WireframeCube(new Point3D(0, 0, 200), 50, appearance);
+ * shapeCollection.addShape(cube);
+ * }</pre>
+ *
+ * @see WireframeBox
+ * @see LineAppearance
+ */
+public class WireframeCube extends WireframeBox {
+
+    /**
+     * Constructs a wireframe cube centered at the given point.
+     *
+     * @param center     the center point of the cube in 3D space
+     * @param size       the half-side length; the cube extends this distance from
+     *                   the center along each axis, giving a total edge length
+     *                   of {@code 2 * size}
+     * @param appearance the line appearance (color, width) used for all 12 edges
+     */
+    public WireframeCube(final Point3D center, final double size,
+                         final LineAppearance appearance) {
+        super(new Point3D(center.x - size, center.y - size, center.z - size),
+                new Point3D(center.x + size, center.y + size, center.z + size),
+                appearance);
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeDrawing.java
new file mode 100755 (executable)
index 0000000..a015852
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A freeform polyline drawing tool that connects sequential points with line
+ * segments. Points are added one at a time via {@link #addPoint(Point3D)};
+ * each new point is connected to the previously added point by a line.
+ *
+ * <p>The first point added establishes the starting position without drawing
+ * a line. Each subsequent point creates a new line segment from the previous
+ * point to the new one.</p>
+ *
+ * <p>This shape is useful for drawing paths, trails, trajectories, or
+ * arbitrary wireframe shapes that are defined as a sequence of vertices.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LineAppearance appearance = new LineAppearance(2, Color.YELLOW);
+ * WireframeDrawing drawing = new WireframeDrawing(appearance);
+ * drawing.addPoint(new Point3D(0, 0, 0));
+ * drawing.addPoint(new Point3D(100, 50, 0));
+ * drawing.addPoint(new Point3D(200, 0, 0));
+ * shapeCollection.addShape(drawing);
+ * }</pre>
+ *
+ * @see LineAppearance
+ * @see AbstractCompositeShape
+ */
+public class WireframeDrawing extends AbstractCompositeShape {
+
+    /** The line appearance used for all segments in this drawing. */
+    final private LineAppearance lineAppearance;
+
+    /** The most recently added point, used as the start of the next line segment. */
+    Point3D currentPoint;
+
+    /**
+     * Constructs a new empty wireframe drawing with the given line appearance.
+     *
+     * @param lineAppearance the line appearance (color, width) used for all
+     *                       line segments added to this drawing
+     */
+    public WireframeDrawing(final LineAppearance lineAppearance) {
+        super();
+        this.lineAppearance = lineAppearance;
+    }
+
+    /**
+     * Adds a new point to the drawing. If this is the first point, it sets the
+     * starting position. Otherwise, a line segment is created from the previous
+     * point to this new point.
+     *
+     * <p>The point is defensively copied, so subsequent modifications to the
+     * passed {@code point3d} object will not affect the drawing.</p>
+     *
+     * @param point3d the point to add to the polyline
+     */
+    public void addPoint(final Point3D point3d) {
+        if (currentPoint != null) {
+            final Line line = lineAppearance.getLine(currentPoint, point3d);
+            addShape(line);
+        }
+
+        currentPoint = new Point3D(point3d);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java
new file mode 100755 (executable)
index 0000000..6c885f4
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+import java.util.ArrayList;
+
+/**
+ * A wireframe sphere approximation built from rings of connected line segments.
+ * The sphere is generated using parametric spherical coordinates, producing a
+ * latitude-longitude grid of vertices connected by lines.
+ *
+ * <p>The sphere is divided into 20 longitudinal slices and 20 latitudinal rings
+ * (using a step of {@code PI / 10} radians). Adjacent vertices within each ring
+ * are connected, and corresponding vertices between consecutive rings are also
+ * connected, forming a mesh that approximates a sphere surface.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.WHITE);
+ * WireframeSphere sphere = new WireframeSphere(new Point3D(0, 0, 300), 100f, appearance);
+ * shapeCollection.addShape(sphere);
+ * }</pre>
+ *
+ * @see LineAppearance
+ * @see AbstractCompositeShape
+ */
+public class WireframeSphere extends AbstractCompositeShape {
+
+    /** Stores the vertices of the previously generated ring for inter-ring connections. */
+    ArrayList<Point3D> previousRing = new ArrayList<>();
+
+    /**
+     * Constructs a wireframe sphere at the given location with the specified radius.
+     * The sphere is approximated by a grid of line segments generated from
+     * parametric spherical coordinates.
+     *
+     * @param location    the center point of the sphere in 3D space
+     * @param radius      the radius of the sphere
+     * @param lineFactory the line appearance (color, width) used for all line segments
+     */
+    public WireframeSphere(final Point3D location, final float radius,
+                           final LineAppearance lineFactory) {
+        super(location);
+
+        final double step = Math.PI / 10;
+
+        final Point3D center = new Point3D();
+
+        int ringIndex = 0;
+
+        for (double j = 0d; j <= (Math.PI * 2); j += step) {
+
+            Point3D oldPoint = null;
+            int pointIndex = 0;
+
+            for (double i = 0; i <= (Math.PI * 2); i += step) {
+                final Point3D newPoint = new Point3D(0, 0, radius);
+                newPoint.rotate(center, i, j);
+
+                if (oldPoint != null)
+                    addShape(lineFactory.getLine(newPoint, oldPoint));
+
+                if (ringIndex > 0) {
+                    final Point3D previousRingPoint = previousRing
+                            .get(pointIndex);
+                    addShape(lineFactory.getLine(newPoint, previousRingPoint));
+
+                    previousRing.set(pointIndex, newPoint);
+                } else
+                    previousRing.add(newPoint);
+
+                oldPoint = newPoint;
+                pointIndex++;
+            }
+
+            ringIndex++;
+        }
+
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/BorderLine.java
new file mode 100644 (file)
index 0000000..1e5e1ff
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.slicer;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point2D;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+
+/**
+ * Represents an edge (border line) of a triangle in the polygon slicing pipeline.
+ *
+ * <p>A {@code BorderLine} connects two {@link Vertex} endpoints and carries an
+ * identification {@link #count} used by the {@link Slicer} to determine which edge
+ * of the original triangle this line corresponds to (1, 2, or 3). This identification
+ * is essential for correct recursive subdivision -- when the longest edge is split,
+ * the slicer uses the count to decide how to partition the triangle into two
+ * smaller triangles.</p>
+ *
+ * @see Slicer
+ * @see Vertex
+ */
+public class BorderLine {
+
+    /**
+     * The edge identifier (1, 2, or 3) indicating which edge of the original triangle
+     * this border line represents. Used by {@link Slicer} during recursive subdivision.
+     */
+    public int count;
+
+    /**
+     * The first vertex endpoint of this edge.
+     */
+    Vertex c1;
+
+    /**
+     * The second vertex endpoint of this edge.
+     */
+    Vertex c2;
+
+    /**
+     * Creates an uninitialized border line for reuse.
+     */
+    public BorderLine() {
+    }
+
+    /**
+     * Sets the endpoints and edge identifier for this border line.
+     *
+     * @param c1    the first vertex endpoint
+     * @param c2    the second vertex endpoint
+     * @param count the edge identifier (1, 2, or 3)
+     */
+    public void set(final Vertex c1, final Vertex c2, final int count) {
+        this.c1 = c1;
+        this.c2 = c2;
+        this.count = count;
+    }
+
+    /**
+     * Computes the 3D Euclidean distance between the two endpoint vertices.
+     *
+     * @return the length of this edge in world-space units
+     */
+    public double getLength() {
+        return c1.coordinate.getDistanceTo(c2.coordinate);
+    }
+
+    /**
+     * Computes the midpoint vertex of this edge by averaging both the 3D coordinates
+     * and the 2D texture coordinates of the two endpoints.
+     *
+     * @return a new {@link Vertex} at the midpoint, with interpolated texture coordinates
+     */
+    public Vertex getMiddlePoint() {
+        return new Vertex(
+                new Point3D().computeMiddlePoint(c1.coordinate, c2.coordinate),
+                new Point2D().setToMiddle(c1.textureCoordinate, c2.textureCoordinate));
+    }
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java
new file mode 100644 (file)
index 0000000..20b80e9
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Recursively subdivides textured polygons into smaller triangles for
+ * perspective-correct rendering and level-of-detail management.
+ *
+ * <p>When a textured polygon covers a large area of the screen, rendering it as
+ * a single triangle can produce visible texture distortion due to affine (non-perspective)
+ * texture interpolation. The {@code Slicer} addresses this by recursively splitting
+ * triangles along their longest edge until no edge exceeds {@link #maxDistance}.</p>
+ *
+ * <p>The subdivision algorithm works as follows:</p>
+ * <ol>
+ *   <li>For a given triangle, compute the lengths of all three edges.</li>
+ *   <li>Sort edges by length and find the longest one.</li>
+ *   <li>If the longest edge is shorter than {@code maxDistance}, emit the triangle as-is.</li>
+ *   <li>Otherwise, split the longest edge at its midpoint (interpolating both 3D and
+ *       texture coordinates) and recurse on the two resulting sub-triangles.</li>
+ * </ol>
+ *
+ * <p>This class is used by
+ * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape}
+ * to break large composite shapes into appropriately-sized sub-polygons.</p>
+ *
+ * @see BorderLine
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ */
+public class Slicer {
+
+    private static final BorderLine line1 = new BorderLine();
+    private static final BorderLine line2 = new BorderLine();
+    private static final BorderLine line3 = new BorderLine();
+
+    /**
+     * Maximum distance between two points.
+     * If the distance is greater than this value, the polygon will be sliced.
+     * Otherwise, it will be added to the result.
+     */
+    private final double maxDistance;
+
+    /**
+     * Result of slicing.
+     */
+    private final List<TexturedPolygon> result = new ArrayList<>();
+
+    /**
+     * Creates a new slicer with the specified maximum edge length.
+     *
+     * @param maxDistance the maximum allowed edge length in world-space units;
+     *                    edges longer than this will be subdivided
+     */
+    public Slicer(final double maxDistance) {
+        this.maxDistance = maxDistance;
+    }
+
+    private void considerSlicing(final Vertex c1,
+                                 final Vertex c2,
+                                 final Vertex c3,
+                                 final TexturedPolygon originalPolygon) {
+
+        line1.set(c1, c2, 1);
+        line2.set(c2, c3, 2);
+        line3.set(c3, c1, 3);
+
+        // Inline sort for 3 elements by length to avoid array allocation
+        BorderLine a = line1;
+        BorderLine b = line2;
+        BorderLine c = line3;
+        BorderLine t;
+        if (a.getLength() > b.getLength()) { t = a; a = b; b = t; }
+        if (b.getLength() > c.getLength()) { t = b; b = c; c = t; }
+        if (a.getLength() > b.getLength()) { t = a; a = b; b = t; }
+
+        final BorderLine longestLine = c;
+
+        if (longestLine.getLength() < maxDistance) {
+            final TexturedPolygon polygon = new TexturedPolygon(c1, c2, c3,
+                    originalPolygon.texture);
+
+            polygon.setMouseInteractionController(originalPolygon.mouseInteractionController);
+
+            getResult().add(polygon);
+            return;
+        }
+
+        final Vertex middle = longestLine.getMiddlePoint();
+
+        switch (longestLine.count) {
+            case 1:
+                considerSlicing(c1, middle, c3, originalPolygon);
+                considerSlicing(middle, c2, c3, originalPolygon);
+                return;
+            case 2:
+                considerSlicing(c1, c2, middle, originalPolygon);
+                considerSlicing(middle, c3, c1, originalPolygon);
+                return;
+            case 3:
+                considerSlicing(c1, c2, middle, originalPolygon);
+                considerSlicing(middle, c2, c3, originalPolygon);
+        }
+
+    }
+
+    /**
+     * Returns the list of subdivided polygons produced by the slicing process.
+     *
+     * @return an unmodifiable view of the resulting {@link TexturedPolygon} list
+     */
+    public List<TexturedPolygon> getResult() {
+        return result;
+    }
+
+    /**
+     * Slices the given textured polygon into smaller triangles.
+     *
+     * <p>After calling this method, retrieve the resulting sub-polygons via
+     * {@link #getResult()}. The original polygon's texture reference and
+     * mouse interaction controller are preserved on all sub-polygons.</p>
+     *
+     * @param originalPolygon the polygon to subdivide
+     */
+    public void slice(final TexturedPolygon originalPolygon) {
+
+        considerSlicing(
+                originalPolygon.coordinates[0],
+                originalPolygon.coordinates[1],
+                originalPolygon.coordinates[2],
+                originalPolygon);
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
new file mode 100644 (file)
index 0000000..bf319b7
--- /dev/null
@@ -0,0 +1,383 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.texture;
+
+import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.awt.image.WritableRaster;
+
+import static java.util.Arrays.fill;
+
+/**
+ * Represents a 2D texture with mipmap support for level-of-detail rendering.
+ *
+ * <p>A {@code Texture} contains a primary bitmap at native resolution, along with
+ * cached upscaled and downscaled versions (mipmaps) that are lazily generated on demand.
+ * This mipmap chain enables efficient texture sampling at varying distances from the camera,
+ * avoiding aliasing artifacts for distant surfaces and pixelation for close-up views.</p>
+ *
+ * <p>The texture also exposes a {@link java.awt.Graphics2D} context backed by the primary
+ * bitmap's {@link java.awt.image.BufferedImage}, allowing dynamic rendering of text,
+ * shapes, or other 2D content directly onto the texture surface. Anti-aliasing is
+ * enabled by default on this graphics context.</p>
+ *
+ * <p><b>Mipmap levels</b></p>
+ * <ul>
+ *   <li><b>Primary bitmap</b> -- the native resolution; always available.</li>
+ *   <li><b>Downsampled bitmaps</b> -- up to 8 levels, each half the size of the previous.
+ *       Used when the texture is rendered at zoom levels below 1.0.</li>
+ *   <li><b>Upsampled bitmaps</b> -- configurable count (set at construction time), each
+ *       double the size of the previous. Used when the texture is rendered at zoom levels
+ *       above 2.0.</li>
+ * </ul>
+ *
+ * <p><b>Usage example</b></p>
+ * <pre>{@code
+ * Texture tex = new Texture(256, 256, 3);
+ * // Draw content using the Graphics2D context
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 256, 256);
+ * // Invalidate cached mipmaps after modifying the primary bitmap
+ * tex.resetResampledBitmapCache();
+ * // Retrieve the appropriate mipmap for a given zoom level
+ * TextureBitmap bitmap = tex.getZoomedBitmap(0.5);
+ * }</pre>
+ *
+ * @see TextureBitmap
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon
+ */
+public class Texture {
+
+    /**
+     * The primary (native resolution) bitmap for this texture.
+     * All dynamic drawing via {@link #graphics} modifies this bitmap's backing data.
+     */
+    public final TextureBitmap primaryBitmap;
+
+    /**
+     * A {@link java.awt.Graphics2D} context for drawing 2D content onto the primary bitmap.
+     * Anti-aliasing for both geometry and text is enabled by default.
+     */
+    public final java.awt.Graphics2D graphics;
+
+    /**
+     * Cached upsampled (enlarged) versions of the primary bitmap.
+     * Index 0 is 2x the primary, index 1 is 4x, and so on.
+     * Entries are lazily populated on first access.
+     */
+    TextureBitmap[] upSampled;
+
+    /**
+     * Cached downsampled (reduced) versions of the primary bitmap.
+     * Index 0 is 1/2 the primary, index 1 is 1/4, and so on.
+     * Entries are lazily populated on first access.
+     */
+    TextureBitmap[] downSampled = new TextureBitmap[8];
+
+    /**
+     * Creates a new texture with the specified dimensions and upscale capacity.
+     *
+     * <p>The underlying {@link java.awt.image.BufferedImage} is created using
+     * {@link eu.svjatoslav.sixth.e3d.gui.RenderingContext#bufferedImageType} for
+     * compatibility with the raster rendering pipeline.</p>
+     *
+     * @param width      the width of the primary bitmap in pixels
+     * @param height     the height of the primary bitmap in pixels
+     * @param maxUpscale the maximum number of upscaled mipmap levels to support
+     *                   (each level doubles the resolution)
+     */
+    public Texture(final int width, final int height, final int maxUpscale) {
+        upSampled = new TextureBitmap[maxUpscale];
+
+        final BufferedImage bufferedImage = new BufferedImage(width, height,
+                RenderingContext.bufferedImageType);
+
+        final WritableRaster raster = bufferedImage.getRaster();
+        final DataBufferByte dbi = (DataBufferByte) raster.getDataBuffer();
+        graphics = (Graphics2D) bufferedImage.getGraphics();
+
+        graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+        primaryBitmap = new TextureBitmap(width, height, dbi.getData(), 1);
+    }
+
+    /**
+     * Determines the appropriate downscale factor index for a given zoom level.
+     *
+     * <p>Iterates through the downsampled mipmap levels (each halving the size)
+     * and returns the index of the first level whose effective size falls below
+     * the requested zoom.</p>
+     *
+     * @param zoom the zoom level (typically less than 1.0 for downscaling)
+     * @return the index into the {@code downSampled} array to use, clamped to the
+     *         maximum available level
+     */
+    public int detectDownscaleFactorForZoom(final double zoom) {
+        double size = 1;
+        for (int i = 0; i < downSampled.length; i++) {
+            size = size / 2;
+            if (size < zoom)
+                return i;
+        }
+
+        return downSampled.length - 1;
+    }
+
+    /**
+     * Determines the appropriate upscale factor index for a given zoom level.
+     *
+     * <p>Iterates through the upsampled mipmap levels (each doubling the size)
+     * and returns the index of the first level whose effective size exceeds
+     * the requested zoom.</p>
+     *
+     * @param zoom the zoom level (typically greater than 2.0 for upscaling)
+     * @return the index into the {@code upSampled} array to use, clamped to the
+     *         maximum available level
+     */
+    public int detectUpscaleFactorForZoom(final double zoom) {
+        double size = 2;
+        for (int i = 0; i < upSampled.length; i++) {
+            size = size * 2;
+            if (size > zoom)
+                return i;
+        }
+
+        return upSampled.length - 1;
+    }
+
+    /**
+     * Downscale given bitmap by factor of 2.
+     *
+     * @param originalBitmap Bitmap to downscale.
+     * @return Downscaled bitmap.
+     */
+    public TextureBitmap downscaleBitmap(final TextureBitmap originalBitmap) {
+        int newWidth = originalBitmap.width / 2;
+        int newHeight = originalBitmap.height / 2;
+
+        // Enforce minimum width and height
+        if (newWidth < 1)
+            newWidth = 1;
+        if (newHeight < 1)
+            newHeight = 1;
+
+        final TextureBitmap downScaled = new TextureBitmap(newWidth, newHeight,
+                originalBitmap.multiplicationFactor / 2d);
+
+        final ColorAccumulator accumulator = new ColorAccumulator();
+
+        for (int y = 0; y < newHeight; y++)
+            for (int x = 0; x < newWidth; x++) {
+                accumulator.reset();
+                accumulator.accumulate(originalBitmap, x * 2, y * 2);
+                accumulator.accumulate(originalBitmap, (x * 2) + 1, y * 2);
+                accumulator.accumulate(originalBitmap, x * 2, (y * 2) + 1);
+                accumulator
+                        .accumulate(originalBitmap, (x * 2) + 1, (y * 2) + 1);
+                accumulator.storeResult(downScaled, x, y);
+            }
+
+        return downScaled;
+    }
+
+    /**
+     * Returns a downscaled bitmap at the specified mipmap level, creating it lazily if needed.
+     *
+     * <p>Level 0 is half the primary resolution, level 1 is a quarter, and so on.
+     * Each level is derived by downscaling the previous level by a factor of 2.</p>
+     *
+     * @param scaleFactor the downscale level index (0 = 1/2 size, 1 = 1/4 size, etc.)
+     * @return the cached or newly created downscaled {@link TextureBitmap}
+     * @see #downscaleBitmap(TextureBitmap)
+     */
+    public TextureBitmap getDownscaledBitmap(final int scaleFactor) {
+        if (downSampled[scaleFactor] == null) {
+
+            TextureBitmap largerBitmap;
+            if (scaleFactor == 0)
+                largerBitmap = primaryBitmap;
+            else
+                largerBitmap = getDownscaledBitmap(scaleFactor - 1);
+
+            downSampled[scaleFactor] = downscaleBitmap(largerBitmap);
+        }
+
+        return downSampled[scaleFactor];
+    }
+
+    /**
+     * Returns the bitmap that should be used for rendering at the given zoom
+     *
+     * @param scaleFactor The upscale factor
+     * @return The bitmap
+     */
+    public TextureBitmap getUpscaledBitmap(final int scaleFactor) {
+        if (upSampled[scaleFactor] == null) {
+
+            TextureBitmap smallerBitmap;
+            if (scaleFactor == 0)
+                smallerBitmap = primaryBitmap;
+            else
+                smallerBitmap = getUpscaledBitmap(scaleFactor - 1);
+
+            upSampled[scaleFactor] = upscaleBitmap(smallerBitmap);
+        }
+
+        return upSampled[scaleFactor];
+    }
+
+    /**
+     * Returns the bitmap that should be used for rendering at the given zoom
+     *
+     * @param zoomLevel The zoom level
+     * @return The bitmap
+     */
+    public TextureBitmap getZoomedBitmap(final double zoomLevel) {
+
+        if (zoomLevel < 1) {
+            final int downscaleFactor = detectDownscaleFactorForZoom(zoomLevel);
+            return getDownscaledBitmap(downscaleFactor);
+        } else if (zoomLevel > 2) {
+            final int upscaleFactor = detectUpscaleFactorForZoom(zoomLevel);
+
+            if (upscaleFactor < 0)
+                return primaryBitmap;
+
+            return getUpscaledBitmap(upscaleFactor);
+        }
+
+        // System.out.println(zoomLevel);
+        return primaryBitmap;
+    }
+
+    /**
+     * Resets the cache of resampled bitmaps
+     */
+    public void resetResampledBitmapCache() {
+        fill(upSampled, null);
+
+        fill(downSampled, null);
+    }
+
+    /**
+     * Upscales the given bitmap by a factor of 2
+     *
+     * @param originalBitmap The bitmap to upscale
+     * @return The upscaled bitmap
+     */
+    public TextureBitmap upscaleBitmap(final TextureBitmap originalBitmap) {
+        final int newWidth = originalBitmap.width * 2;
+        final int newHeight = originalBitmap.height * 2;
+
+        final TextureBitmap upScaled = new TextureBitmap(newWidth, newHeight,
+                originalBitmap.multiplicationFactor * 2d);
+
+        final ColorAccumulator accumulator = new ColorAccumulator();
+
+        for (int y = 0; y < originalBitmap.height; y++)
+            for (int x = 0; x < originalBitmap.width; x++) {
+                accumulator.reset();
+                accumulator.accumulate(originalBitmap, x, y);
+                accumulator.storeResult(upScaled, x * 2, y * 2);
+
+                accumulator.reset();
+                accumulator.accumulate(originalBitmap, x, y);
+                accumulator.accumulate(originalBitmap, x + 1, y);
+                accumulator.storeResult(upScaled, (x * 2) + 1, y * 2);
+
+                accumulator.reset();
+                accumulator.accumulate(originalBitmap, x, y);
+                accumulator.accumulate(originalBitmap, x, y + 1);
+                accumulator.storeResult(upScaled, x * 2, (y * 2) + 1);
+
+                accumulator.reset();
+                accumulator.accumulate(originalBitmap, x, y);
+                accumulator.accumulate(originalBitmap, x + 1, y);
+                accumulator.accumulate(originalBitmap, x, y + 1);
+                accumulator.accumulate(originalBitmap, x + 1, y + 1);
+                accumulator.storeResult(upScaled, (x * 2) + 1, (y * 2) + 1);
+            }
+
+        return upScaled;
+    }
+
+    /**
+     * A helper class that accumulates color values for a given area of a bitmap
+     */
+    public static class ColorAccumulator {
+        // Accumulated color values
+        public int r, g, b, a;
+
+        // Number of pixels that have been accumulated
+        public int pixelCount = 0;
+
+        /**
+         * Accumulates the color values of the given pixel
+         *
+         * @param bitmap The bitmap
+         * @param x      The x coordinate of the pixel
+         * @param y      The y coordinate of the pixel
+         */
+        public void accumulate(final TextureBitmap bitmap, final int x,
+                               final int y) {
+            int address = bitmap.getAddress(x, y);
+
+            a += bitmap.bytes[address] & 0xff;
+            address++;
+
+            b += bitmap.bytes[address] & 0xff;
+            address++;
+
+            g += bitmap.bytes[address] & 0xff;
+            address++;
+
+            r += bitmap.bytes[address] & 0xff;
+
+            pixelCount++;
+        }
+
+        /**
+         * Resets the accumulator
+         */
+        public void reset() {
+            a = 0;
+            r = 0;
+            g = 0;
+            b = 0;
+            pixelCount = 0;
+        }
+
+        /**
+         * Stores the accumulated color values in the given bitmap
+         *
+         * @param bitmap The bitmap
+         * @param x      The x coordinate of the pixel
+         * @param y      The y coordinate of the pixel
+         */
+        public void storeResult(final TextureBitmap bitmap, final int x,
+                                final int y) {
+            int address = bitmap.getAddress(x, y);
+
+            bitmap.bytes[address] = (byte) (a / pixelCount);
+            address++;
+
+            bitmap.bytes[address] = (byte) (b / pixelCount);
+            address++;
+
+            bitmap.bytes[address] = (byte) (g / pixelCount);
+            address++;
+
+            bitmap.bytes[address] = (byte) (r / pixelCount);
+        }
+    }
+
+}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.java
new file mode 100644 (file)
index 0000000..a2d253d
--- /dev/null
@@ -0,0 +1,253 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.renderer.raster.texture;
+
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+/**
+ * Represents a single resolution level of a texture as a raw byte array.
+ *
+ * <p>Each pixel is stored as 4 consecutive bytes in ABGR order:
+ * Alpha, Blue, Green, Red. This byte ordering matches the
+ * {@link java.awt.image.BufferedImage#TYPE_4BYTE_ABGR} format used by
+ * the rendering pipeline.</p>
+ *
+ * <p>{@code TextureBitmap} is used internally by {@link Texture} to represent
+ * individual mipmap levels. The {@link #multiplicationFactor} records the
+ * scale ratio relative to the primary (native) resolution -- for example,
+ * a value of 0.5 means this bitmap is half the original size, and 2.0
+ * means it is double.</p>
+ *
+ * <p>This class provides low-level pixel operations including:</p>
+ * <ul>
+ *   <li>Alpha-blended pixel transfer to a target raster ({@link #drawPixel(int, byte[], int)})</li>
+ *   <li>Direct pixel writes using engine {@link Color} ({@link #drawPixel(int, int, Color)})</li>
+ *   <li>Filled rectangle drawing ({@link #drawRectangle(int, int, int, int, Color)})</li>
+ *   <li>Full-surface color fill ({@link #fillColor(Color)})</li>
+ * </ul>
+ *
+ * @see Texture
+ * @see Color
+ */
+public class TextureBitmap {
+
+    /**
+     * Raw pixel data in ABGR byte order (Alpha, Blue, Green, Red).
+     * The array length is {@code width * height * 4}.
+     */
+    public final byte[] bytes;
+
+    /**
+     * The width of this bitmap in pixels.
+     */
+    public final int width;
+
+    /**
+     * The height of this bitmap in pixels.
+     */
+    public final int height;
+
+    /**
+     * The scale factor of this bitmap relative to the primary (native) texture resolution.
+     * A value of 1.0 indicates the native resolution, 0.5 indicates half-size, 2.0 indicates double-size, etc.
+     */
+    public double multiplicationFactor;
+
+    /**
+     * Creates a texture bitmap backed by an existing byte array.
+     *
+     * <p>This constructor is typically used when the bitmap data is obtained from
+     * a {@link java.awt.image.BufferedImage}'s raster, allowing direct access to
+     * the image's pixel data without copying.</p>
+     *
+     * @param width                the bitmap width in pixels
+     * @param height               the bitmap height in pixels
+     * @param bytes                the raw pixel data array (must be at least {@code width * height * 4} bytes)
+     * @param multiplicationFactor the scale factor relative to the native texture resolution
+     */
+    public TextureBitmap(final int width, final int height, final byte[] bytes,
+                         final double multiplicationFactor) {
+
+        this.width = width;
+        this.height = height;
+        this.bytes = bytes;
+        this.multiplicationFactor = multiplicationFactor;
+    }
+
+    /**
+     * Creates a texture bitmap with a newly allocated byte array.
+     *
+     * <p>The pixel data array is initialized to all zeros (fully transparent black).</p>
+     *
+     * @param width                the bitmap width in pixels
+     * @param height               the bitmap height in pixels
+     * @param multiplicationFactor the scale factor relative to the native texture resolution
+     */
+    public TextureBitmap(final int width, final int height,
+                         final double multiplicationFactor) {
+
+        this(width, height, new byte[width * height * 4], multiplicationFactor);
+    }
+
+    /**
+     * Transfer (render) one pixel from current {@link TextureBitmap} to target raster bitmap.
+     *
+     * @param sourceBitmapPixelAddress Pixel address within current {@link TextureBitmap} as indicated by its offset.
+     * @param targetBitmap             Bitmap of the target image where pixel should be rendered to.
+     * @param targetBitmapPixelAddress Pixel location within target image where pixel should be rendered to.
+     */
+    public void drawPixel(int sourceBitmapPixelAddress,
+                          final byte[] targetBitmap, int targetBitmapPixelAddress) {
+
+        final int textureAlpha = bytes[sourceBitmapPixelAddress] & 0xff;
+
+        if (textureAlpha == 0)
+            return;
+
+        if (textureAlpha == 255) {
+            // skip reading of background for fully opaque pixels
+            targetBitmap[targetBitmapPixelAddress] = (byte) 255;
+
+            targetBitmapPixelAddress++;
+            sourceBitmapPixelAddress++;
+            targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress];
+
+            targetBitmapPixelAddress++;
+            sourceBitmapPixelAddress++;
+            targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress];
+
+            targetBitmapPixelAddress++;
+            sourceBitmapPixelAddress++;
+            targetBitmap[targetBitmapPixelAddress] = bytes[sourceBitmapPixelAddress];
+            return;
+        }
+
+        final int backgroundAlpha = 255 - textureAlpha;
+        sourceBitmapPixelAddress++;
+
+        targetBitmap[targetBitmapPixelAddress] = (byte) 255;
+        targetBitmapPixelAddress++;
+
+        targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256);
+        sourceBitmapPixelAddress++;
+        targetBitmapPixelAddress++;
+
+        targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256);
+        sourceBitmapPixelAddress++;
+        targetBitmapPixelAddress++;
+
+        targetBitmap[targetBitmapPixelAddress] = (byte) ((((targetBitmap[targetBitmapPixelAddress] & 0xff) * backgroundAlpha) + ((bytes[sourceBitmapPixelAddress] & 0xff) * textureAlpha)) / 256);
+    }
+
+    /**
+     * Draws a single pixel at the specified coordinates using the given color.
+     *
+     * <p>The color components are written directly without alpha blending.
+     * Coordinates are clamped to the bitmap bounds by {@link #getAddress(int, int)}.</p>
+     *
+     * @param x     the x coordinate of the pixel
+     * @param y     the y coordinate of the pixel
+     * @param color the color to write
+     */
+    public void drawPixel(final int x, final int y, final Color color) {
+        int address = getAddress(x, y);
+
+        bytes[address] = (byte) color.a;
+
+        address++;
+        bytes[address] = (byte) color.b;
+
+        address++;
+        bytes[address] = (byte) color.g;
+
+        address++;
+        bytes[address] = (byte) color.r;
+    }
+
+    /**
+     * Fills a rectangular region with the specified color.
+     *
+     * <p>If {@code x1 > x2}, the coordinates are swapped to ensure correct rendering.
+     * The same applies to {@code y1} and {@code y2}. The rectangle is exclusive of the
+     * right and bottom edges.</p>
+     *
+     * @param x1    the left x coordinate
+     * @param y1    the top y coordinate
+     * @param x2    the right x coordinate (exclusive)
+     * @param y2    the bottom y coordinate (exclusive)
+     * @param color the fill color
+     */
+    public void drawRectangle(int x1, final int y1, int x2, final int y2,
+                              final Color color) {
+
+        if (x1 > x2) {
+            final int tmp = x1;
+            x1 = x2;
+            x2 = tmp;
+        }
+
+        if (y1 > y2) {
+            final int tmp = x1;
+            x1 = x2;
+            x2 = tmp;
+        }
+
+        for (int y = y1; y < y2; y++)
+            for (int x = x1; x < x2; x++)
+                drawPixel(x, y, color);
+    }
+
+    /**
+     * Fills the entire bitmap with the specified color.
+     *
+     * <p>Every pixel in the bitmap is set to the given color value,
+     * overwriting all existing content.</p>
+     *
+     * @param color the color to fill the entire bitmap with
+     */
+    public void fillColor(final Color color) {
+        int address = 0;
+        while (address < bytes.length) {
+            bytes[address] = (byte) color.a;
+            address++;
+
+            bytes[address] = (byte) color.b;
+            address++;
+
+            bytes[address] = (byte) color.g;
+            address++;
+
+            bytes[address] = (byte) color.r;
+            address++;
+        }
+    }
+
+    /**
+     * Computes the byte offset into the {@link #bytes} array for the pixel at ({@code x}, {@code y}).
+     *
+     * <p>Coordinates are clamped to the valid range {@code [0, width-1]} and
+     * {@code [0, height-1]} so that out-of-bounds accesses are safely handled
+     * by sampling the nearest edge pixel.</p>
+     *
+     * @param x the x coordinate of the pixel
+     * @param y the y coordinate of the pixel
+     * @return the byte offset of the first component (alpha) for the specified pixel
+     */
+    public int getAddress(int x, int y) {
+        if (x < 0)
+            x = 0;
+
+        if (x >= width)
+            x = width - 1;
+
+        if (y < 0)
+            y = 0;
+
+        if (y >= height)
+            y = height - 1;
+
+        return ((y * width) + x) * 4;
+    }
+}
diff --git a/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png b/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png
new file mode 100644 (file)
index 0000000..47a1638
Binary files /dev/null and b/src/main/resources/eu/svjatoslav/sixth/e3d/examples/hourglass.png differ
diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/gui/textEditorComponent/TextLineTest.java
new file mode 100644 (file)
index 0000000..d4c3069
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Sixth - System for data storage, computation, exploration and interaction.
+ * Author: Svjatoslav Agejenko. 
+ * This project is released under Creative Commons Zero (CC0) license.
+ *
+*/
+
+package eu.svjatoslav.sixth.e3d.gui.textEditorComponent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TextLineTest {
+
+    @Test
+    public void testAddIdent() {
+        TextLine textLine = new TextLine("test");
+        textLine.addIdent(4);
+        assertEquals("    test", textLine.toString());
+
+        textLine = new TextLine();
+        textLine.addIdent(4);
+        assertEquals("", textLine.toString());
+    }
+
+    @Test
+    public void testCutFromBeginning() {
+        TextLine textLine = new TextLine("test");
+        textLine.cutFromBeginning(2);
+        assertEquals("st", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.cutFromBeginning(4);
+        assertEquals("", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.cutFromBeginning(5);
+        assertEquals("", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.cutFromBeginning(100);
+        assertEquals("", textLine.toString());
+    }
+
+    @Test
+    public void testCutSubString() {
+        TextLine textLine = new TextLine("test");
+        assertEquals("es", textLine.cutSubString(1, 3));
+        assertEquals("tt", textLine.toString());
+
+        textLine = new TextLine("test");
+        assertEquals("st ", textLine.cutSubString(2, 5));
+        assertEquals("te", textLine.toString());
+    }
+
+    @Test
+    public void testGetCharForLocation() {
+        final TextLine textLine = new TextLine("test");
+        assertEquals('s', textLine.getCharForLocation(2));
+        assertEquals('t', textLine.getCharForLocation(3));
+        assertEquals(' ', textLine.getCharForLocation(4));
+    }
+
+    @Test
+    public void testGetIdent() {
+        final TextLine textLine = new TextLine("   test");
+        assertEquals(3, textLine.getIdent());
+    }
+
+    @Test
+    public void testGetLength() {
+        final TextLine textLine = new TextLine("test");
+        assertEquals(4, textLine.getLength());
+    }
+
+    @Test
+    public void testInsertCharacter() {
+        TextLine textLine = new TextLine("test");
+        textLine.insertCharacter(1, 'o');
+        assertEquals("toest", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.insertCharacter(5, 'o');
+        assertEquals("test o", textLine.toString());
+
+    }
+
+    @Test
+    public void testIsEmpty() {
+        TextLine textLine = new TextLine("");
+        assertEquals(true, textLine.isEmpty());
+
+        textLine = new TextLine("     ");
+        assertEquals(true, textLine.isEmpty());
+
+        textLine = new TextLine("l");
+        assertEquals(false, textLine.isEmpty());
+    }
+
+    @Test
+    public void testRemoveCharacter() {
+        TextLine textLine = new TextLine("test");
+        textLine.removeCharacter(0);
+        assertEquals("est", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.removeCharacter(3);
+        assertEquals("tes", textLine.toString());
+
+        textLine = new TextLine("test");
+        textLine.removeCharacter(4);
+        assertEquals("test", textLine.toString());
+    }
+
+}