Make dedicated tutorial about shading algorithm with screenshot and
what are available parameters.
+** Document boolean operations
+
* Add 3D mouse support
:PROPERTIES:
:CUSTOM_ID: add-3d-mouse-support
:PROPERTIES:
:CUSTOM_ID: add-more-math-formula-examples
:END:
+
** Allow manual thread count specification in performance test demo
:PROPERTIES:
:CUSTOM_ID: allow-manual-thread-count-specification
:PROPERTIES:
:CUSTOM_ID: add-polygon-reduction-lod
:END:
+
** Add object fading based on view distance
:PROPERTIES:
:CUSTOM_ID: add-object-fading-view-distance
:PROPERTIES:
:CUSTOM_ID: add-csg-support
:END:
+
** Add shadow casting
:PROPERTIES:
:CUSTOM_ID: add-shadow-casting
image resolution if needed to maintain desired FPS.
** Explore possibility for implementing better perspective correct textured polygons
-** Add arrow shape: cone + cylinder
+
** Add X, Y, Z axis indicators
Will use different colored arrows + text label
+** Add collision detection (physics engine)
+
* Add clickable vertexes
:PROPERTIES:
:CUSTOM_ID: add-clickable-vertexes
http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html
+ Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/
-
-** Fix camera rotation for voxel raytracer
-:PROPERTIES:
-:CUSTOM_ID: fix-camera-rotation-voxel-raytracer
-:END:
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.csg;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonMesh;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a solid for Constructive Solid Geometry (CSG) operations.
+ *
+ * <p>CSG allows combining 3D shapes using boolean operations:</p>
+ * <ul>
+ * <li><b>Union:</b> Combine two shapes into one</li>
+ * <li><b>Subtract:</b> Carve one shape out of another</li>
+ * <li><b>Intersect:</b> Keep only the overlapping volume</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create shapes from existing composite shapes
+ * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 80, Color.RED);
+ * SolidPolygonSphere sphere = new SolidPolygonSphere(new Point3D(0, 0, 0), 96, 12, Color.BLUE);
+ *
+ * // Convert to CSG solids
+ * CSG cubeCSG = CSG.fromCompositeShape(cube);
+ * CSG sphereCSG = CSG.fromCompositeShape(sphere);
+ *
+ * // Perform boolean operation
+ * CSG result = cubeCSG.subtract(sphereCSG);
+ *
+ * // Render the result
+ * SolidPolygonMesh mesh = result.toMesh(new Color(255, 100, 100), new Point3D(0, 0, 0));
+ * shapes.addShape(mesh);
+ * }</pre>
+ *
+ * @see CSGNode the BSP tree node used internally
+ * @see CSGPolygon the N-gon polygon type used for BSP operations
+ * @see SolidPolygonMesh the renderable mesh created from CSG results
+ */
+public class CSG {
+
+ /**
+ * The list of polygons that make up this solid.
+ */
+ public final List<CSGPolygon> polygons = new ArrayList<>();
+
+ /**
+ * Creates an empty CSG solid.
+ */
+ public CSG() {
+ }
+
+ /**
+ * Creates a CSG solid from a list of CSG polygons.
+ *
+ * @param polygonList the polygons to include
+ * @return a new CSG solid
+ */
+ public static CSG fromPolygons(final List<CSGPolygon> polygonList) {
+ final CSG csg = new CSG();
+ csg.polygons.addAll(polygonList);
+ return csg;
+ }
+
+ /**
+ * Creates a CSG solid from a list of SolidPolygon triangles.
+ *
+ * <p>Each SolidPolygon is converted to a CSGPolygon (3-vertex N-gon).
+ * The color from each SolidPolygon is preserved.</p>
+ *
+ * @param solidPolygons the triangles to convert
+ * @return a new CSG solid
+ */
+ public static CSG fromSolidPolygons(final List<SolidPolygon> solidPolygons) {
+ final List<CSGPolygon> csgPolygons = new ArrayList<>(solidPolygons.size());
+
+ for (final SolidPolygon sp : solidPolygons) {
+ final List<Vertex> vertices = new ArrayList<>(3);
+ for (int i = 0; i < 3; i++) {
+ final Vertex v = new Vertex(sp.vertices[i].coordinate);
+ v.normal = sp.vertices[i].normal;
+ vertices.add(v);
+ }
+
+ final CSGPolygon csgPoly = new CSGPolygon(vertices, sp.getColor());
+ csgPolygons.add(csgPoly);
+ }
+
+ return fromPolygons(csgPolygons);
+ }
+
+ /**
+ * Creates a CSG solid from a composite shape.
+ *
+ * <p>Extracts all SolidPolygon triangles from the composite shape
+ * and converts them to CSGPolygons. This allows using shapes like
+ * {@code SolidPolygonCube}, {@code SolidPolygonSphere}, etc. with CSG operations.</p>
+ *
+ * @param shape the composite shape to convert
+ * @return a new CSG solid containing all triangles from the shape
+ */
+ public static CSG fromCompositeShape(final AbstractCompositeShape shape) {
+ return fromSolidPolygons(shape.extractSolidPolygons());
+ }
+
+ /**
+ * Creates a deep clone of this CSG solid.
+ *
+ * @return a new CSG solid with cloned polygons
+ */
+ public CSG clone() {
+ final CSG csg = new CSG();
+ for (final CSGPolygon p : polygons) {
+ csg.polygons.add(p.clone());
+ }
+ return csg;
+ }
+
+ /**
+ * Returns the list of polygons in this solid.
+ *
+ * @return the polygon list
+ */
+ public List<CSGPolygon> toPolygons() {
+ return polygons;
+ }
+
+ /**
+ * Performs a union operation with another CSG solid.
+ *
+ * <p>The result contains all points that are in either solid.</p>
+ *
+ * <h3>Algorithm:</h3>
+ * <pre>
+ * Union(A, B) = clip(A to outside B) + clip(B to outside A)
+ * </pre>
+ * <ol>
+ * <li>Clip A's polygons to keep only parts outside B</li>
+ * <li>Clip B's polygons to keep only parts outside A</li>
+ * <li>Invert B, clip to A, invert again (keeps B's surface inside A)</li>
+ * <li>Build final tree from all remaining polygons</li>
+ * </ol>
+ *
+ * @param csg the other solid to union with
+ * @return a new CSG solid representing the union
+ */
+ public CSG union(final CSG csg) {
+ // Create BSP trees from both solids
+ final CSGNode a = new CSGNode(clone().polygons);
+ final CSGNode b = new CSGNode(csg.clone().polygons);
+
+ // Remove from A any parts that are inside B
+ a.clipTo(b);
+
+ // Remove from B any parts that are inside A
+ b.clipTo(a);
+
+ // Invert B temporarily to capture B's interior surface that touches A
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+
+ // Combine all polygons into A's tree
+ a.build(b.allPolygons());
+
+ return CSG.fromPolygons(a.allPolygons());
+ }
+
+ /**
+ * Performs a subtraction operation with another CSG solid.
+ *
+ * <p>The result contains all points that are in this solid but not in the other.
+ * This effectively carves the other solid out of this one.</p>
+ *
+ * <h3>Algorithm:</h3>
+ * <pre>
+ * Subtract(A, B) = A - B = clip(inverted A to B) inverted
+ * </pre>
+ * <ol>
+ * <li>Invert A (turning solid into cavity, cavity into solid)</li>
+ * <li>Clip inverted A to keep only parts inside B</li>
+ * <li>Clip B to keep only parts inside inverted A</li>
+ * <li>Invert B twice to get B's cavity surface</li>
+ * <li>Combine and invert final result</li>
+ * </ol>
+ *
+ * <p>The inversion trick converts "subtract B from A" into "intersect A
+ * with the inverse of B", which the BSP algorithm handles naturally.</p>
+ *
+ * @param csg the solid to subtract
+ * @return a new CSG solid representing the difference
+ */
+ public CSG subtract(final CSG csg) {
+ // Create BSP trees from both solids
+ final CSGNode a = new CSGNode(clone().polygons);
+ final CSGNode b = new CSGNode(csg.clone().polygons);
+
+ // Invert A: what was solid becomes empty, what was empty becomes solid
+ // This transforms the problem into finding the intersection of inverted-A and B
+ a.invert();
+
+ // Remove from inverted-A any parts outside B (keep intersection)
+ a.clipTo(b);
+
+ // Remove from B any parts outside inverted-A (keep intersection)
+ b.clipTo(a);
+
+ // Capture B's interior surface
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+
+ // Combine B's interior surface with A
+ a.build(b.allPolygons());
+
+ // Invert result to convert back from "intersection with inverse" to "subtraction"
+ a.invert();
+
+ return CSG.fromPolygons(a.allPolygons());
+ }
+
+ /**
+ * Performs an intersection operation with another CSG solid.
+ *
+ * <p>The result contains only the points that are in both solids.</p>
+ *
+ * <h3>Algorithm:</h3>
+ * <pre>
+ * Intersect(A, B) = clip(inverted A to outside B) inverted
+ * </pre>
+ * <ol>
+ * <li>Invert A (swap inside/outside)</li>
+ * <li>Clip inverted-A to B, keeping parts outside B</li>
+ * <li>Invert B, clip to A (captures B's interior surface)</li>
+ * <li>Clip B again to ensure proper boundaries</li>
+ * <li>Combine and invert final result</li>
+ * </ol>
+ *
+ * <p>This uses the principle: A ∩ B = ¬(¬A ∪ ¬B)</p>
+ *
+ * @param csg the other solid to intersect with
+ * @return a new CSG solid representing the intersection
+ */
+ public CSG intersect(final CSG csg) {
+ // Create BSP trees from both solids
+ final CSGNode a = new CSGNode(clone().polygons);
+ final CSGNode b = new CSGNode(csg.clone().polygons);
+
+ // Invert A to transform intersection into a union-like operation
+ a.invert();
+
+ // Clip B to keep only parts inside inverted-A (outside original A)
+ b.clipTo(a);
+
+ // Invert B to capture its interior surface
+ b.invert();
+
+ // Clip A to keep only parts inside inverted-B (outside original B)
+ a.clipTo(b);
+
+ // Clip B again to ensure proper boundary handling
+ b.clipTo(a);
+
+ // Combine B's interior surface with A
+ a.build(b.allPolygons());
+
+ // Invert result to get the actual intersection
+ a.invert();
+
+ return CSG.fromPolygons(a.allPolygons());
+ }
+
+ /**
+ * Returns the inverse of this solid.
+ *
+ * <p>The inverse has all polygons flipped, effectively turning the solid inside-out.</p>
+ *
+ * @return a new CSG solid representing the inverse
+ */
+ public CSG inverse() {
+ final CSG csg = clone();
+ for (final CSGPolygon p : csg.polygons) {
+ p.flip();
+ }
+ return csg;
+ }
+
+ /**
+ * Converts this CSG solid to a renderable mesh with a uniform color.
+ *
+ * <p>All polygons are rendered with the specified color, ignoring
+ * any colors stored in the CSGPolygons.</p>
+ *
+ * @param color the color to apply to all triangles
+ * @param location the position in 3D space for the mesh
+ * @return a renderable mesh containing triangles
+ */
+ public SolidPolygonMesh toMesh(final Color color, final Point3D location) {
+ final List<SolidPolygon> triangles = new ArrayList<>();
+
+ for (final CSGPolygon polygon : polygons) {
+ triangulatePolygon(polygon, color, triangles);
+ }
+
+ return new SolidPolygonMesh(triangles, location);
+ }
+
+ /**
+ * Triangulates a CSGPolygon using fan triangulation.
+ *
+ * <p>Fan triangulation works by selecting the first vertex as a central point
+ * and connecting it to each pair of consecutive vertices. For an N-gon,
+ * this produces (N-2) triangles:</p>
+ *
+ * <pre>
+ * Original N-gon: v0-v1-v2-v3-v4...
+ * Triangles: v0-v1-v2, v0-v2-v3, v0-v3-v4, ...
+ * </pre>
+ *
+ * <p>This method is suitable for convex polygons. For concave polygons,
+ * it may produce overlapping triangles, but CSG operations typically
+ * generate convex polygon fragments.</p>
+ *
+ * @param polygon the polygon to triangulate (may have 3+ vertices)
+ * @param color the color to apply to all resulting triangles
+ * @param triangles the list to add the resulting SolidPolygon triangles to
+ */
+ private void triangulatePolygon(final CSGPolygon polygon, final Color color,
+ final List<SolidPolygon> triangles) {
+ final int vertexCount = polygon.vertices.size();
+
+ // Skip degenerate polygons (less than 3 vertices cannot form a triangle)
+ if (vertexCount < 3) {
+ return;
+ }
+
+ // Use the first vertex as the "pivot" of the fan
+ final Point3D v0 = polygon.vertices.get(0).coordinate;
+
+ // Create triangles by connecting v0 to each consecutive pair of vertices
+ // For a polygon with vertices [v0, v1, v2, v3], we create:
+ // - Triangle 1: v0, v1, v2 (i=1)
+ // - Triangle 2: v0, v2, v3 (i=2)
+ for (int i = 1; i < vertexCount - 1; i++) {
+ final Point3D v1 = polygon.vertices.get(i).coordinate;
+ final Point3D v2 = polygon.vertices.get(i + 1).coordinate;
+
+ // Clone the points to avoid sharing references with the original polygon
+ final SolidPolygon triangle = new SolidPolygon(
+ new Point3D(v0),
+ new Point3D(v1),
+ new Point3D(v2),
+ color
+ );
+
+ triangles.add(triangle);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.csg;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A node in a Binary Space Partitioning (BSP) tree used for CSG operations.
+ *
+ * <p>BSP trees are the data structure that makes CSG boolean operations possible.
+ * Each node divides 3D space into two half-spaces using a plane, enabling
+ * efficient spatial queries and polygon clipping.</p>
+ *
+ * <h3>BSP Tree Structure:</h3>
+ * <pre>
+ * [Node: plane P]
+ * / \
+ * [Front subtree] [Back subtree]
+ * (same side as P's (opposite side
+ * normal) of P's normal)
+ * </pre>
+ *
+ * <h3>Key Properties:</h3>
+ * <ul>
+ * <li><b>polygons:</b> Polygons coplanar with this node's partitioning plane</li>
+ * <li><b>plane:</b> The partitioning plane that divides space</li>
+ * <li><b>front:</b> Subtree for the half-space the plane normal points toward</li>
+ * <li><b>back:</b> Subtree for the opposite half-space</li>
+ * </ul>
+ *
+ * <h3>CSG Algorithm Overview:</h3>
+ * <p>CSG boolean operations (union, subtraction, intersection) work by:</p>
+ * <ol>
+ * <li>Building BSP trees from both input solids</li>
+ * <li>Clipping each tree against the other (removing overlapping geometry)</li>
+ * <li>Optionally inverting trees (for subtraction and intersection)</li>
+ * <li>Collecting the resulting polygons</li>
+ * </ol>
+ *
+ * @see CSG the main CSG class that provides the boolean operation API
+ * @see CSGPlane the plane type used for spatial partitioning
+ * @see CSGPolygon the polygon type stored in BSP nodes
+ */
+public class CSGNode {
+
+ /**
+ * Polygons that lie on this node's partitioning plane.
+ *
+ * <p>These polygons are coplanar with the plane and are stored directly
+ * in this node rather than being pushed down to child nodes. This includes
+ * both polygons originally on this plane and polygons split by planes above
+ * that ended up coplanar here.</p>
+ */
+ public final List<CSGPolygon> polygons = new ArrayList<>();
+
+ /**
+ * The partitioning plane for this node.
+ *
+ * <p>This plane divides 3D space into two half-spaces: front (where the
+ * normal points) and back. All polygons in this node are coplanar with
+ * this plane. Child nodes contain polygons on their respective sides.</p>
+ *
+ * <p>Null for leaf nodes (empty subtrees).</p>
+ */
+ public CSGPlane plane;
+
+ /**
+ * The front child subtree.
+ *
+ * <p>Contains polygons that lie in the front half-space of this node's plane
+ * (the side the normal points toward). May be null if no polygons exist
+ * in the front half-space.</p>
+ */
+ public CSGNode front;
+
+ /**
+ * The back child subtree.
+ *
+ * <p>Contains polygons that lie in the back half-space of this node's plane
+ * (the side opposite the normal direction). May be null if no polygons exist
+ * in the back half-space.</p>
+ */
+ public CSGNode back;
+
+ /**
+ * Creates an empty BSP node with no plane or children.
+ *
+ * <p>This constructor creates a leaf node. The plane, front, and back
+ * fields will be populated when polygons are added via {@link #build(List)}.</p>
+ */
+ public CSGNode() {
+ }
+
+ /**
+ * Creates a BSP tree from a list of polygons.
+ *
+ * <p>Delegates to {@link #build(List)} to construct the tree.</p>
+ *
+ * @param polygons the polygons to partition into a BSP tree
+ */
+ public CSGNode(final List<CSGPolygon> polygons) {
+ build(polygons);
+ }
+
+ /**
+ * Creates a deep clone of this BSP tree.
+ *
+ * <p>Recursively clones all child nodes and polygons. The resulting tree
+ * is completely independent of the original.</p>
+ *
+ * @return a new CSGNode tree with cloned data
+ */
+ public CSGNode clone() {
+ final CSGNode node = new CSGNode();
+
+ // Clone the plane if present
+ node.plane = plane != null ? plane.clone() : null;
+
+ // Recursively clone child subtrees
+ node.front = front != null ? front.clone() : null;
+ node.back = back != null ? back.clone() : null;
+
+ // Clone each polygon in this node
+ for (final CSGPolygon p : polygons) {
+ node.polygons.add(p.clone());
+ }
+
+ return node;
+ }
+
+ /**
+ * Inverts this BSP tree, converting "inside" to "outside" and vice versa.
+ *
+ * <p>This operation is fundamental to CSG subtraction and intersection:</p>
+ * <ul>
+ * <li>All polygon normals are flipped (reversing their facing direction)</li>
+ * <li>All plane normals are flipped</li>
+ * <li>Front and back subtrees are swapped</li>
+ * </ul>
+ *
+ * <p>After inversion:</p>
+ * <ul>
+ * <li>What was solid becomes empty space</li>
+ * <li>What was empty space becomes solid</li>
+ * <li>Front/back relationships are reversed throughout the tree</li>
+ * </ul>
+ *
+ * <p>This is used in CSG subtraction where solid B "carves out" of solid A
+ * by inverting B, unioning, then inverting the result.</p>
+ */
+ public void invert() {
+ // Flip all polygons at this node
+ for (final CSGPolygon polygon : polygons) {
+ polygon.flip();
+ }
+
+ // Flip the partitioning plane
+ if (plane != null) {
+ plane.flip();
+ }
+
+ // Recursively invert child subtrees
+ if (front != null) {
+ front.invert();
+ }
+ if (back != null) {
+ back.invert();
+ }
+
+ // Swap front and back children since the half-spaces are now reversed
+ final CSGNode temp = front;
+ front = back;
+ back = temp;
+ }
+
+ /**
+ * Clips a list of polygons against this BSP tree.
+ *
+ * <p>This recursively removes the portions of the input polygons that lie
+ * inside the solid represented by this BSP tree. The result contains only
+ * the portions that are outside this solid.</p>
+ *
+ * <h3>Algorithm:</h3>
+ * <ol>
+ * <li>At each node, split input polygons by the node's plane</li>
+ * <li>Polygons in front go to front child for further clipping</li>
+ * <li>Polygons in back go to back child for further clipping</li>
+ * <li>Coplanar polygons are kept (they're on the surface)</li>
+ * <li>If no back child exists, back polygons are discarded (they're inside)</li>
+ * </ol>
+ *
+ * <p>This is used during CSG operations to remove overlapping geometry.</p>
+ *
+ * @param polygons the polygons to clip against this BSP tree
+ * @return a new list containing only the portions outside this solid
+ */
+ public List<CSGPolygon> clipPolygons(final List<CSGPolygon> polygons) {
+ // Base case: if this is a leaf node, return copies of all polygons
+ if (plane == null) {
+ return new ArrayList<>(polygons);
+ }
+
+ // Split all input polygons by this node's plane
+ final List<CSGPolygon> frontList = new ArrayList<>();
+ final List<CSGPolygon> backList = new ArrayList<>();
+
+ for (final CSGPolygon polygon : polygons) {
+ // Split polygon into front/back/coplanar parts
+ // Note: coplanar polygons go into both front and back lists
+ plane.splitPolygon(polygon, frontList, backList, frontList, backList);
+ }
+
+ // Recursively clip front polygons against front subtree
+ List<CSGPolygon> resultFront = frontList;
+ if (front != null) {
+ resultFront = front.clipPolygons(frontList);
+ }
+
+ // Recursively clip back polygons against back subtree
+ List<CSGPolygon> resultBack = backList;
+ if (back != null) {
+ resultBack = back.clipPolygons(backList);
+ } else {
+ // No back child means this is a boundary - discard back polygons
+ // (they would be inside the solid we're clipping against)
+ resultBack = new ArrayList<>();
+ }
+
+ // Combine the clipped results
+ final List<CSGPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
+ result.addAll(resultFront);
+ result.addAll(resultBack);
+ return result;
+ }
+
+ /**
+ * Clips this BSP tree against another BSP tree.
+ *
+ * <p>This removes from this tree all polygons that lie inside the solid
+ * represented by the other BSP tree. Used during CSG operations to
+ * eliminate overlapping geometry.</p>
+ *
+ * <p>The operation modifies this tree in place, replacing all polygons
+ * with their clipped versions.</p>
+ *
+ * @param bsp the BSP tree to clip against (the "cutter")
+ */
+ public void clipTo(final CSGNode bsp) {
+ // Clip all polygons at this node against the other BSP tree
+ final List<CSGPolygon> newPolygons = bsp.clipPolygons(polygons);
+ polygons.clear();
+ polygons.addAll(newPolygons);
+
+ // Recursively clip child subtrees
+ if (front != null) {
+ front.clipTo(bsp);
+ }
+ if (back != null) {
+ back.clipTo(bsp);
+ }
+ }
+
+ /**
+ * Collects all polygons from this BSP tree into a flat list.
+ *
+ * <p>Recursively traverses the entire tree and collects all polygons
+ * from all nodes. This is used after CSG operations to extract the
+ * final result as a simple polygon list.</p>
+ *
+ * @return a new list containing all polygons in this tree
+ */
+ public List<CSGPolygon> allPolygons() {
+ final List<CSGPolygon> result = new ArrayList<>(polygons);
+
+ // Recursively collect polygons from child subtrees
+ if (front != null) {
+ result.addAll(front.allPolygons());
+ }
+ if (back != null) {
+ result.addAll(back.allPolygons());
+ }
+
+ return result;
+ }
+
+ /**
+ * Builds or extends this BSP tree from a list of polygons.
+ *
+ * <p>This is the core BSP tree construction algorithm. It partitions
+ * space by selecting a splitting plane and recursively building subtrees.</p>
+ *
+ * <h3>Algorithm:</h3>
+ * <ol>
+ * <li>If this node has no plane, use the first polygon's plane as the partitioning plane</li>
+ * <li>For each polygon:
+ * <ul>
+ * <li>Coplanar polygons are stored in this node</li>
+ * <li>Front polygons go to the front list</li>
+ * <li>Back polygons go to the back list</li>
+ * <li>Spanning polygons are split into front and back parts</li>
+ * </ul>
+ * </li>
+ * <li>Recursively build front subtree with front polygons</li>
+ * <li>Recursively build back subtree with back polygons</li>
+ * </ol>
+ *
+ * <h3>Calling Conventions:</h3>
+ * <ul>
+ * <li>Can be called multiple times to add more polygons to an existing tree</li>
+ * <li>Empty polygon list is a no-op</li>
+ * <li>Creates child nodes as needed</li>
+ * </ul>
+ *
+ * @param polygonList the polygons to add to this BSP tree
+ */
+ public void build(final List<CSGPolygon> polygonList) {
+ // Base case: no polygons to add
+ if (polygonList.isEmpty()) {
+ return;
+ }
+
+ // Initialize the partitioning plane if this is a new node
+ // Use the first polygon's plane as the splitting plane
+ if (plane == null) {
+ plane = polygonList.get(0).plane.clone();
+ }
+
+ // Classify each polygon relative to this node's plane
+ final List<CSGPolygon> frontList = new ArrayList<>();
+ final List<CSGPolygon> backList = new ArrayList<>();
+
+ for (final CSGPolygon polygon : polygonList) {
+ // Split the polygon and distribute to appropriate lists:
+ // - coplanarFront/coplanarBack → this node's polygons list
+ // - front → frontList (for front subtree)
+ // - back → backList (for back subtree)
+ plane.splitPolygon(polygon, polygons, polygons, frontList, backList);
+ }
+
+ // Recursively build front subtree
+ if (!frontList.isEmpty()) {
+ if (front == null) {
+ front = new CSGNode();
+ }
+ front.build(frontList);
+ }
+
+ // Recursively build back subtree
+ if (!backList.isEmpty()) {
+ if (back == null) {
+ back = new CSGNode();
+ }
+ back.build(backList);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.csg;
+
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an infinite plane in 3D space using the Hesse normal form.
+ *
+ * <p>A plane is defined by a normal vector (perpendicular to the plane surface)
+ * and a scalar value 'w' representing the signed distance from the origin.
+ * The plane equation is: {@code normal.x * x + normal.y * y + normal.z * z = w}</p>
+ *
+ * <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
+ * in CSG. They divide 3D space into two half-spaces:</p>
+ * <ul>
+ * <li><b>Front half-space:</b> Points where {@code normal · point > w}</li>
+ * <li><b>Back half-space:</b> Points where {@code normal · point < w}</li>
+ * </ul>
+ *
+ * <p>Planes are used to:</p>
+ * <ul>
+ * <li>Define the surface orientation of {@link CSGPolygon} faces</li>
+ * <li>Split polygons that cross BSP partition boundaries</li>
+ * <li>Determine which side of a BSP node a polygon lies on</li>
+ * </ul>
+ *
+ * @see CSGPolygon polygons that reference their containing plane
+ * @see CSGNode BSP tree nodes that use planes for spatial partitioning
+ */
+public class CSGPlane {
+
+ /**
+ * Epsilon value used for floating-point comparisons.
+ *
+ * <p>When determining which side of a plane a point lies on, values within
+ * this threshold are considered coplanar (on the plane). This prevents
+ * numerical instability from causing infinite recursion or degenerate
+ * polygons during BSP operations.</p>
+ */
+ public static final double EPSILON = 0.01;
+
+ /**
+ * The unit normal vector perpendicular to the plane surface.
+ *
+ * <p>The direction of the normal determines which side is "front"
+ * and which is "back". The front is the side the normal points toward.</p>
+ */
+ public Point3D normal;
+
+ /**
+ * The signed distance from the origin to the plane along the normal.
+ *
+ * <p>This is equivalent to the dot product of the normal with any point
+ * on the plane. For a plane defined by point P and normal N:
+ * {@code w = N · P}</p>
+ */
+ public double w;
+
+ /**
+ * Creates a plane with the given normal and distance.
+ *
+ * @param normal the unit normal vector (caller must ensure it's normalized)
+ * @param w the signed distance from origin to the plane
+ */
+ public CSGPlane(final Point3D normal, final double w) {
+ this.normal = normal;
+ this.w = w;
+ }
+
+ /**
+ * Creates a plane from three non-collinear points.
+ *
+ * <p>The plane passes through all three points. The normal is computed
+ * using the cross product of vectors (b-a) and (c-a), then normalized.
+ * The winding order of the points determines the normal direction:</p>
+ * <ul>
+ * <li>Counter-clockwise (CCW) winding → normal points toward viewer</li>
+ * <li>Clockwise (CW) winding → normal points away from viewer</li>
+ * </ul>
+ *
+ * @param a the first point on the plane
+ * @param b the second point on the plane
+ * @param c the third point on the plane
+ * @return a new CSGPlane passing through the three points
+ * @throws ArithmeticException if the points are collinear (cross product is zero)
+ */
+ public static CSGPlane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
+ // Compute two edge vectors from point a
+ final Point3D edge1 = b.minus(a);
+ final Point3D edge2 = c.minus(a);
+
+ // Cross product gives the normal direction (perpendicular to both edges)
+ final Point3D n = edge1.cross(edge2).unit();
+
+ // Distance from origin is the projection of any point on the plane onto the normal
+ return new CSGPlane(n, n.dot(a));
+ }
+
+ /**
+ * Creates a deep clone of this plane.
+ *
+ * <p>The normal vector is cloned to avoid shared references.</p>
+ *
+ * @return a new CSGPlane with the same normal and distance
+ */
+ public CSGPlane clone() {
+ return new CSGPlane(new Point3D(normal.x, normal.y, normal.z), w);
+ }
+
+ /**
+ * Flips the plane orientation by negating the normal and distance.
+ *
+ * <p>This effectively swaps the front and back half-spaces. After flipping:</p>
+ * <ul>
+ * <li>Points that were in front are now in back</li>
+ * <li>Points that were in back are now in front</li>
+ * <li>Coplanar points remain coplanar</li>
+ * </ul>
+ *
+ * <p>Used during CSG operations when inverting solids (converting "inside"
+ * to "outside" and vice versa).</p>
+ */
+ public void flip() {
+ normal = normal.negated();
+ w = -w;
+ }
+
+ /**
+ * Splits a polygon by this plane, classifying and potentially dividing it.
+ *
+ * <p>This is the core operation for BSP tree construction. The polygon is
+ * classified based on where its vertices lie relative to the plane:</p>
+ *
+ * <h3>Classification types:</h3>
+ * <ul>
+ * <li><b>COPLANAR (0):</b> All vertices lie on the plane (within EPSILON)</li>
+ * <li><b>FRONT (1):</b> All vertices are in the front half-space</li>
+ * <li><b>BACK (2):</b> All vertices are in the back half-space</li>
+ * <li><b>SPANNING (3):</b> Vertices are on both sides (polygon crosses the plane)</li>
+ * </ul>
+ *
+ * <h3>Destination lists:</h3>
+ * <ul>
+ * <li><b>coplanarFront:</b> Coplanar polygons with same-facing normals</li>
+ * <li><b>coplanarBack:</b> Coplanar polygons with opposite-facing normals</li>
+ * <li><b>front:</b> Polygons entirely in front half-space</li>
+ * <li><b>back:</b> Polygons entirely in back half-space</li>
+ * </ul>
+ *
+ * <h3>Spanning polygon handling:</h3>
+ * <p>When a polygon spans the plane, it is split into two polygons:</p>
+ * <ol>
+ * <li>Vertices on the front side become a new polygon (added to front list)</li>
+ * <li>Vertices on the back side become a new polygon (added to back list)</li>
+ * <li>Intersection points are computed and added to both polygons</li>
+ * </ol>
+ *
+ * @param polygon the polygon to classify and potentially split
+ * @param coplanarFront list to receive coplanar polygons with same-facing normals
+ * @param coplanarBack list to receive coplanar polygons with opposite-facing normals
+ * @param front list to receive polygons in the front half-space
+ * @param back list to receive polygons in the back half-space
+ */
+ public void splitPolygon(final CSGPolygon polygon,
+ final List<CSGPolygon> coplanarFront,
+ final List<CSGPolygon> coplanarBack,
+ final List<CSGPolygon> front,
+ final List<CSGPolygon> back) {
+
+ PolygonType polygonType = PolygonType.COPLANAR;
+ final PolygonType[] types = new PolygonType[polygon.vertices.size()];
+
+ for (int i = 0; i < polygon.vertices.size(); i++) {
+ final Vertex v = polygon.vertices.get(i);
+ final double t = normal.dot(v.coordinate) - w;
+ final PolygonType type = (t < -EPSILON) ? PolygonType.BACK
+ : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR;
+ polygonType = polygonType.combine(type);
+ types[i] = type;
+ }
+
+ switch (polygonType) {
+ case COPLANAR:
+ ((normal.dot(polygon.plane.normal) > 0) ? coplanarFront : coplanarBack).add(polygon);
+ break;
+
+ case FRONT:
+ front.add(polygon);
+ break;
+
+ case BACK:
+ back.add(polygon);
+ break;
+
+ case SPANNING:
+ final List<Vertex> f = new ArrayList<>();
+ final List<Vertex> b = new ArrayList<>();
+
+ for (int i = 0; i < polygon.vertices.size(); i++) {
+ final int j = (i + 1) % polygon.vertices.size();
+ final PolygonType ti = types[i];
+ final PolygonType tj = types[j];
+ final Vertex vi = polygon.vertices.get(i);
+ final Vertex vj = polygon.vertices.get(j);
+
+ if (ti.isFront()) {
+ f.add(vi);
+ }
+ if (ti.isBack()) {
+ b.add(ti == PolygonType.COPLANAR ? vi.clone() : vi);
+ }
+ if (ti != tj && ti != PolygonType.COPLANAR && tj != PolygonType.COPLANAR) {
+ final double t = (w - normal.dot(vi.coordinate))
+ / normal.dot(vj.coordinate.minus(vi.coordinate));
+ final Vertex v = vi.interpolate(vj, t);
+ f.add(v);
+ b.add(v.clone());
+ }
+ }
+
+ if (f.size() >= 3) {
+ final CSGPolygon frontPoly = new CSGPolygon(f, polygon.color);
+ front.add(frontPoly);
+ }
+ if (b.size() >= 3) {
+ final CSGPolygon backPoly = new CSGPolygon(b, polygon.color);
+ back.add(backPoly);
+ }
+ break;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.csg;
+
+import eu.svjatoslav.sixth.e3d.math.Vertex;
+import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An N-gon polygon used for CSG BSP tree operations.
+ *
+ * <p>During BSP tree traversal, polygons may be split by planes, resulting
+ * in polygons with varying vertex counts (3 or more). The polygon stores
+ * its vertices, the plane it lies on, and material properties (color).</p>
+ *
+ * <p>The color is preserved through CSG operations - split polygons inherit
+ * the color from their parent.</p>
+ *
+ * @see CSG the main CSG solid class
+ * @see CSGPlane used for splitting polygons
+ */
+public class CSGPolygon {
+
+ /**
+ * The vertices defining this polygon's geometry.
+ * For CSG operations, this can be 3 or more vertices (N-gon).
+ */
+ public final List<Vertex> vertices;
+
+ /**
+ * The plane that contains this polygon.
+ * Cached for efficient BSP operations.
+ */
+ public final CSGPlane plane;
+
+ /**
+ * The color of this polygon.
+ * Preserved through CSG operations; split polygons inherit this color.
+ */
+ public Color color;
+
+ /**
+ * Creates a polygon with vertices and a color.
+ *
+ * @param vertices the vertices defining this polygon (must be at least 3)
+ * @param color the color of this polygon
+ */
+ public CSGPolygon(final List<Vertex> vertices, final Color color) {
+ this.vertices = vertices;
+ this.color = color;
+ this.plane = CSGPlane.fromPoints(
+ vertices.get(0).coordinate,
+ vertices.get(1).coordinate,
+ vertices.get(2).coordinate
+ );
+ }
+
+ /**
+ * Creates a deep clone of this polygon.
+ *
+ * <p>Clones all vertices and preserves the color.</p>
+ *
+ * @return a new CSGPolygon with cloned data
+ */
+ public CSGPolygon clone() {
+ final List<Vertex> clonedVertices = new ArrayList<>(vertices.size());
+ for (final Vertex v : vertices) {
+ clonedVertices.add(v.clone());
+ }
+ return new CSGPolygon(clonedVertices, this.color);
+ }
+
+ /**
+ * Flips the orientation of this polygon.
+ *
+ * <p>Reverses the vertex order and negates vertex normals.
+ * Also flips the plane. Used during CSG operations when inverting solids.</p>
+ */
+ public void flip() {
+ Collections.reverse(vertices);
+ for (final Vertex v : vertices) {
+ v.flip();
+ }
+ plane.flip();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+package eu.svjatoslav.sixth.e3d.csg;
+
+/**
+ * Classification of a polygon's position relative to a plane.
+ * Used in CSG operations to determine how polygons should be split.
+ */
+enum PolygonType {
+ /** Polygon lies on the plane. */
+ COPLANAR,
+ /** Polygon is entirely in front of the plane. */
+ FRONT,
+ /** Polygon is entirely behind the plane. */
+ BACK,
+ /** Polygon straddles the plane (vertices on both sides). */
+ SPANNING;
+
+ /**
+ * Combines this type with another to compute the aggregate classification.
+ * When vertices are on both sides of a plane, the result is SPANNING.
+ *
+ * @param other the other polygon type to combine with
+ * @return the combined classification
+ */
+ PolygonType combine(final PolygonType other) {
+ if (this == other || other == COPLANAR) {
+ return this;
+ }
+ if (this == COPLANAR) {
+ return other;
+ }
+ // FRONT + BACK = SPANNING
+ return SPANNING;
+ }
+
+ /**
+ * Checks if this type represents a vertex in front of the plane.
+ *
+ * @return true if FRONT or COPLANAR (treated as front for classification)
+ */
+ boolean isFront() {
+ return this == FRONT || this == COPLANAR;
+ }
+
+ /**
+ * Checks if this type represents a vertex behind the plane.
+ *
+ * @return true if BACK or COPLANAR (treated as back for classification)
+ */
+ boolean isBack() {
+ return this == BACK || this == COPLANAR;
+ }
+}
\ No newline at end of file
return this;
}
+ // ========== Non-mutating vector operations (return new Point3D) ==========
+
+ /**
+ * Computes the dot product of this vector with another.
+ *
+ * @param other the other vector
+ * @return the dot product (scalar)
+ */
+ public double dot(final Point3D other) {
+ return x * other.x + y * other.y + z * other.z;
+ }
+
+ /**
+ * Computes the cross product of this vector with another.
+ * Returns a new vector perpendicular to both input vectors.
+ *
+ * @param other the other vector
+ * @return a new Point3D representing the cross product
+ */
+ public Point3D cross(final Point3D other) {
+ return new Point3D(
+ y * other.z - z * other.y,
+ z * other.x - x * other.z,
+ x * other.y - y * other.x
+ );
+ }
+
+ /**
+ * Returns a new point that is the sum of this point and another.
+ * Neither point is modified.
+ *
+ * @param other the point to add
+ * @return a new Point3D representing the sum
+ */
+ public Point3D plus(final Point3D other) {
+ return new Point3D(x + other.x, y + other.y, z + other.z);
+ }
+
+ /**
+ * Returns a new point that is this point minus another.
+ * Neither point is modified.
+ *
+ * @param other the point to subtract
+ * @return a new Point3D representing the difference
+ */
+ public Point3D minus(final Point3D other) {
+ return new Point3D(x - other.x, y - other.y, z - other.z);
+ }
+
+ /**
+ * Returns a new point with negated coordinates.
+ * This point is not modified.
+ *
+ * @return a new Point3D with negated coordinates
+ */
+ public Point3D negated() {
+ return new Point3D(-x, -y, -z);
+ }
+
+ /**
+ * Returns a new unit vector (normalized) in the same direction.
+ * This point is not modified.
+ *
+ * @return a new Point3D with unit length
+ */
+ public Point3D unit() {
+ final double len = getVectorLength();
+ if (len == 0) {
+ return new Point3D(0, 0, 0);
+ }
+ return new Point3D(x / len, y / len, z / len);
+ }
+
+ /**
+ * Returns a new point that is a linear interpolation between this point and another.
+ * When t=0, returns this point. When t=1, returns the other point.
+ *
+ * @param other the other point
+ * @param t the interpolation parameter (0 to 1)
+ * @return a new Point3D representing the interpolated position
+ */
+ public Point3D lerp(final Point3D other, final double t) {
+ return new Point3D(
+ x + (other.x - x) * t,
+ y + (other.y - y) * t,
+ z + (other.z - z) * t
+ );
+ }
+
+ /**
+ * Returns a new point with coordinates multiplied by a factor.
+ * This point is not modified. Unlike {@link #scaleUp}, this returns a new instance.
+ *
+ * @param factor the scaling factor
+ * @return a new scaled Point3D
+ */
+ public Point3D times(final double factor) {
+ return new Point3D(x * factor, y * factor, z * factor);
+ }
+
+ /**
+ * Returns a new point with coordinates divided by a factor.
+ * This point is not modified. Unlike {@link #scaleDown}, this returns a new instance.
+ *
+ * @param factor the divisor
+ * @return a new scaled Point3D
+ */
+ public Point3D dividedBy(final double factor) {
+ return new Point3D(x / factor, y / factor, z / factor);
+ }
+
}
/**
* Texture coordinate for UV mapping (optional).
*/
- public Point2D textureCoordinate; // TODO: is this proper term ?
+ public Point2D textureCoordinate;
+
+ /**
+ * Normal vector for this vertex (optional).
+ * Used by CSG operations for smooth interpolation during polygon splitting.
+ * Null for non-CSG usage; existing rendering code ignores this field.
+ */
+ public Point3D normal;
/**
/**
* Creates a vertex at the specified position with no texture coordinate.
*
- * @param location the 3D position of this vertex
+ * @param coordinate the 3D position of this vertex
*/
- public Vertex(final Point3D location) {
- this(location, null);
+ public Vertex(final Point3D coordinate) {
+ this(coordinate, null);
}
/**
* Creates a vertex at the specified position with an optional texture coordinate.
*
- * @param location the 3D position of this vertex
+ * @param coordinate the 3D position of this vertex
* @param textureCoordinate the UV texture coordinate, or {@code null} for none
*/
- public Vertex(final Point3D location, Point2D textureCoordinate) {
- coordinate = location;
+ public Vertex(final Point3D coordinate, final Point2D textureCoordinate) {
+ this.coordinate = coordinate;
transformedCoordinate = new Point3D();
onScreenCoordinate = new Point2D();
this.textureCoordinate = textureCoordinate;
onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale);
onScreenCoordinate.add(renderContext.centerCoordinate);
}
+
+ // ========== CSG support methods ==========
+
+ /**
+ * Creates a deep copy of this vertex.
+ * Clones the coordinate, normal (if present), and texture coordinate (if present).
+ * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame).
+ *
+ * @return a new Vertex with cloned data
+ */
+ public Vertex clone() {
+ final Vertex result = new Vertex(new Point3D(coordinate),
+ textureCoordinate != null ? new Point2D(textureCoordinate) : null);
+ if (normal != null) {
+ result.normal = new Point3D(normal);
+ }
+ return result;
+ }
+
+ /**
+ * Flips the orientation of this vertex by negating the normal vector.
+ * Called when the orientation of a polygon is flipped during CSG operations.
+ * If normal is null, this method does nothing.
+ */
+ public void flip() {
+ if (normal != null) {
+ normal = normal.negated();
+ }
+ }
+
+ /**
+ * Creates a new vertex between this vertex and another by linearly interpolating
+ * all properties using parameter t.
+ *
+ * <p>Interpolates: position, normal (if present), and texture coordinate (if present).</p>
+ *
+ * @param other the other vertex to interpolate towards
+ * @param t the interpolation parameter (0 = this vertex, 1 = other vertex)
+ * @return a new Vertex representing the interpolated position
+ */
+ public Vertex interpolate(final Vertex other, final double t) {
+ final Vertex result = new Vertex(
+ coordinate.lerp(other.coordinate, t),
+ (textureCoordinate != null && other.textureCoordinate != null)
+ ? new Point2D(
+ textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
+ textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t)
+ : null
+ );
+ if (normal != null && other.normal != null) {
+ result.normal = normal.lerp(other.normal, t);
+ }
+ return result;
+ }
}
* Each vertex contains both the original world-space coordinate and
* a transformed screen-space coordinate computed during {@link #transform}.
*/
- public final Vertex[] coordinates;
+ public final Vertex[] vertices;
/**
* Average Z-depth of this shape in screen space after transformation.
* 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
+ * @param vertexCount 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();
+ public AbstractCoordinateShape(final int vertexCount) {
+ vertices = new Vertex[vertexCount];
+ for (int i = 0; i < vertexCount; i++)
+ vertices[i] = new Vertex();
shapeId = lastShapeId.getAndIncrement();
}
/**
* Creates a shape from the given vertices.
*
- * @param vertexes the vertices defining this shape's geometry
+ * @param vertices the vertices defining this shape's geometry
*/
- public AbstractCoordinateShape(final Vertex... vertexes) {
- coordinates = vertexes;
+ public AbstractCoordinateShape(final Vertex... vertices) {
+ this.vertices = vertices;
shapeId = lastShapeId.getAndIncrement();
}
double accumulatedZ = 0;
boolean paint = true;
- for (final Vertex geometryPoint : coordinates) {
+ for (final Vertex geometryPoint : vertices) {
geometryPoint.calculateLocationRelativeToViewer(transforms, renderingContext);
accumulatedZ += geometryPoint.transformedCoordinate.z;
}
if (paint) {
- onScreenZ = accumulatedZ / coordinates.length;
+ onScreenZ = accumulatedZ / vertices.length;
aggregator.queueShapeForRendering(this);
}
}
public void paint(final RenderingContext targetRenderingArea) {
// distance from camera/viewer to center of the texture
- final double z = coordinates[0].transformedCoordinate.z;
+ final double z = vertices[0].transformedCoordinate.z;
// compute forward oriented texture visible distance from center
final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width
final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom);
- final Point2D onScreenCoordinate = coordinates[0].onScreenCoordinate;
+ final Point2D onScreenCoordinate = vertices[0].onScreenCoordinate;
// compute Y
final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter);
* @return the center position in world coordinates
*/
public Point3D getLocation() {
- return coordinates[0].coordinate;
+ return vertices[0].coordinate;
}
}
* <p>
* The rendering algorithm:
* 1. For thin lines (below a threshold), draws single-pixel lines with alpha
- * adjustment based on perspective.
+ * adjustment based on perspective.
* 2. For thicker lines, creates four interpolators to define the line's
- * rectangular area and fills it scanline by scanline.
+ * 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.
* @param parentLine the line to copy
*/
public Line(final Line parentLine) {
- this(parentLine.coordinates[0].coordinate.clone(),
- parentLine.coordinates[1].coordinate.clone(),
+ this(parentLine.vertices[0].coordinate.clone(),
+ parentLine.vertices[1].coordinate.clone(),
new Color(parentLine.color), parentLine.width);
}
private void drawSinglePixelHorizontalLine(final RenderingContext buffer,
final int alpha) {
- final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
int xStart = (int) onScreenPoint1.x;
int xEnd = (int) onScreenPoint2.x;
final int y = yBase + ((relativeX * lineHeight) / lineWidth);
if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) {
- if ((y >= 0) && (y < buffer.height)) {
- int offset = (y * buffer.width) + x;
+ if ((y >= 0) && (y < buffer.height)) {
+ int offset = (y * buffer.width) + x;
- final int dest = pixels[offset];
- final int destR = (dest >> 16) & 0xff;
- final int destG = (dest >> 8) & 0xff;
- final int destB = dest & 0xff;
+ final int dest = pixels[offset];
+ final int destR = (dest >> 16) & 0xff;
+ final int destG = (dest >> 8) & 0xff;
+ final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
- final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
- final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
+ final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+ final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+ final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
- pixels[offset] = (newR << 16) | (newG << 8) | newB;
- }
+ pixels[offset] = (newR << 16) | (newG << 8) | newB;
}
+ }
}
}
private void drawSinglePixelVerticalLine(final RenderingContext buffer,
final int alpha) {
- final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
int yStart = (int) onScreenPoint1.y;
int yEnd = (int) onScreenPoint2.y;
final int y = yStart + relativeY;
if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) {
- if ((y >= 0) && (y < buffer.height)) {
+ if ((y >= 0) && (y < buffer.height)) {
- final int x = xBase + ((relativeY * lineWidth) / lineHeight);
- if ((x >= 0) && (x < buffer.width)) {
- int offset = (y * buffer.width) + x;
+ final int x = xBase + ((relativeY * lineWidth) / lineHeight);
+ if ((x >= 0) && (x < buffer.width)) {
+ int offset = (y * buffer.width) + x;
- final int dest = pixels[offset];
- final int destR = (dest >> 16) & 0xff;
- final int destG = (dest >> 8) & 0xff;
- final int destB = dest & 0xff;
+ final int dest = pixels[offset];
+ final int destR = (dest >> 16) & 0xff;
+ final int destG = (dest >> 8) & 0xff;
+ final int destB = dest & 0xff;
- final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
- final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
- final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
+ final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8;
+ final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8;
+ final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8;
- pixels[offset] = (newR << 16) | (newG << 8) | newB;
- }
+ pixels[offset] = (newR << 16) | (newG << 8) | newB;
}
}
+ }
}
}
@Override
public void paint(final RenderingContext buffer) {
- final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate;
- final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate;
+ final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices[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;
+ / vertices[0].transformedCoordinate.z;
final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width)
- / coordinates[1].transformedCoordinate.z;
+ / vertices[1].transformedCoordinate.z;
if ((point1radius < MINIMUM_WIDTH_THRESHOLD)
|| (point2radius < MINIMUM_WIDTH_THRESHOLD)) {
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;
* <li>Scanline rasterization with alpha blending</li>
* </ul>
*
- * @param context the rendering context
- * @param onScreenPoint1 the first vertex in screen coordinates
- * @param onScreenPoint2 the second vertex in screen coordinates
- * @param onScreenPoint3 the third vertex in screen coordinates
- * @param mouseInteractionController optional controller for mouse events, or null
- * @param color the fill color
+ * @param context the rendering context
+ * @param onScreenPoint1 the first vertex in screen coordinates
+ * @param onScreenPoint2 the second vertex in screen coordinates
+ * @param onScreenPoint3 the third vertex in screen coordinates
+ * @param mouseInteractionController optional controller for mouse events, or null
+ * @param color the fill color
*/
public static void drawPolygon(final RenderingContext context,
final Point2D onScreenPoint1, final Point2D onScreenPoint2,
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; }
+ 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)) {
* @param result the point to store the normal vector in
*/
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 Point3D v1 = vertices[0].coordinate;
+ final Point3D v2 = vertices[1].coordinate;
+ final Point3D v3 = vertices[2].coordinate;
final double ax = v2.x - v1.x;
final double ay = v2.y - v1.y;
* @param result the point to store the center in
*/
private void calculateCenter(final Point3D result) {
- final Point3D v1 = coordinates[0].coordinate;
- final Point3D v2 = coordinates[1].coordinate;
- final Point3D v3 = coordinates[2].coordinate;
+ final Point3D v1 = vertices[0].coordinate;
+ final Point3D v2 = vertices[1].coordinate;
+ final Point3D v3 = vertices[2].coordinate;
result.x = (v1.x + v2.x + v3.x) / 3.0;
result.y = (v1.y + v2.y + v3.y) / 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;
+ final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D onScreenPoint3 = vertices[2].onScreenCoordinate;
if (backfaceCulling) {
final double signedArea = (onScreenPoint2.x - onScreenPoint1.x)
import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap;
-import java.awt.Color;
+import java.awt.*;
import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
*/
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);
+ totalTextureDistance = vertices[0].textureCoordinate.getDistanceTo(vertices[1].textureCoordinate);
+ totalTextureDistance += vertices[0].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate);
+ totalTextureDistance += vertices[1].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate);
}
/**
@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;
+ final Point2D projectedPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D projectedPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D projectedPoint3 = vertices[2].onScreenCoordinate;
if (backfaceCulling) {
final double signedArea = (projectedPoint2.x - projectedPoint1.x)
final PolygonBorderInterpolator polygonBorder3 = interp[2];
polygonBorder1.setPoints(projectedPoint1, projectedPoint2,
- coordinates[0].textureCoordinate,
- coordinates[1].textureCoordinate);
+ vertices[0].textureCoordinate,
+ vertices[1].textureCoordinate);
polygonBorder2.setPoints(projectedPoint1, projectedPoint3,
- coordinates[0].textureCoordinate,
- coordinates[2].textureCoordinate);
+ vertices[0].textureCoordinate,
+ vertices[2].textureCoordinate);
polygonBorder3.setPoints(projectedPoint2, projectedPoint3,
- coordinates[1].textureCoordinate,
- coordinates[2].textureCoordinate);
+ vertices[1].textureCoordinate,
+ vertices[2].textureCoordinate);
// Inline sort for 3 elements to avoid array allocation
PolygonBorderInterpolator a = polygonBorder1;
PolygonBorderInterpolator b = polygonBorder2;
PolygonBorderInterpolator c = polygonBorder3;
PolygonBorderInterpolator t;
- if (a.compareTo(b) > 0) { t = a; a = b; b = t; }
- if (b.compareTo(c) > 0) { t = b; b = c; c = t; }
- if (a.compareTo(b) > 0) { t = a; a = b; b = t; }
+ 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)) {
*/
private void showBorders(final RenderingContext renderBuffer) {
- final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
- final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
- final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
+ final Point2D projectedPoint1 = vertices[0].onScreenCoordinate;
+ final Point2D projectedPoint2 = vertices[1].onScreenCoordinate;
+ final Point2D projectedPoint3 = vertices[2].onScreenCoordinate;
final int x1 = (int) projectedPoint1.x;
final int y1 = (int) projectedPoint1.y;
/**
* 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 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
+ * @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,
* 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 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
+ * @param textColor the color of the rendered text
* @return a new {@link Texture} containing the rendered text
*/
public static Texture getTexture(final String text,
* TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS, TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS,
maxUpscaleFactor);
+ // Put blue background to test if texture has correct size
// texture.graphics.setColor(Color.BLUE);
// texture.graphics.fillRect(0, 0, texture.primaryBitmap.width,
- // texture.primaryBitmap.width);
+ // texture.primaryBitmap.width);
texture.graphics.setFont(TextCanvas.FONT);
texture.graphics.setColor(textColor.toAwtColor());
return originalSubShapes;
}
+ /**
+ * Extracts all SolidPolygon triangles from this composite shape.
+ *
+ * <p>Recursively traverses the shape hierarchy and collects all
+ * {@link SolidPolygon} instances. Useful for CSG operations where
+ * you need the raw triangles from a composite shape like
+ * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube}
+ * or {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere}.</p>
+ *
+ * <p><b>Example:</b></p>
+ * <pre>{@code
+ * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
+ * List<SolidPolygon> triangles = cube.extractSolidPolygons();
+ * CSG csg = CSG.fromSolidPolygons(triangles);
+ * }</pre>
+ *
+ * @return list of all SolidPolygon sub-shapes
+ */
+ public List<SolidPolygon> extractSolidPolygons() {
+ final List<SolidPolygon> result = new ArrayList<>();
+ for (final SubShape subShape : originalSubShapes) {
+ final AbstractShape shape = subShape.getShape();
+ if (shape instanceof SolidPolygon) {
+ result.add((SolidPolygon) shape);
+ } else if (shape instanceof AbstractCompositeShape) {
+ result.addAll(((AbstractCompositeShape) shape).extractSolidPolygons());
+ }
+ }
+ return result;
+ }
+
/**
* Returns the view-space tracker that monitors the distance
* and angle between the camera and this shape for level-of-detail adjustments.
import eu.svjatoslav.sixth.e3d.geometry.Point3D;
import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
import eu.svjatoslav.sixth.e3d.math.Quaternion;
-import eu.svjatoslav.sixth.e3d.math.Transform;
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;
public class SolidPolygonArrow extends AbstractCompositeShape {
/**
- * Constructs a 3D arrow pointing from start to end.
*
- * <p>The arrow consists of a cylindrical body extending from the start point
- * towards the end, and a conical tip at the end point. If the distance between
- * start and end is less than or equal to the tip length, only the cone tip
- * is rendered.</p>
+ * Number of segments for arrow smoothness.
+ */
+ private static final int SEGMENTS = 12;
+
+ /**
+ * Arrow tip radius as a fraction of body radius (2.5x).
+ */
+ private static final double TIP_RADIUS_FACTOR = 2.5;
+
+ /**
+ * Arrow tip length as a fraction of body radius (5.0x).
+ */
+ private static final double TIP_LENGTH_FACTOR = 5.0;
+
+ /**
+ * Constructs a 3D arrow pointing from start to end with sensible defaults.
+ *
+ * <p>This simplified constructor automatically calculates the tip radius as
+ * 2.5 times the body radius, the tip length as 5 times the body radius, and
+ * uses 12 segments for smoothness. For custom tip dimensions or segment count,
+ * use the full constructor.</p>
*
- * @param startPoint the origin point of the arrow (where the body starts)
- * @param endPoint the destination point of the arrow (where the tip points to)
- * @param bodyRadius the radius of the cylindrical body
- * @param tipRadius the radius of the cone base at the tip
- * @param tipLength the length of the conical tip
- * @param segments the number of segments for cylinder and cone smoothness.
- * Higher values create smoother arrows. Minimum is 3.
- * @param color the fill color (RGBA; alpha controls transparency)
+ * @param startPoint the origin point of the arrow (where the body starts)
+ * @param endPoint the destination point of the arrow (where the tip points to)
+ * @param bodyRadius the radius of the cylindrical body; tip dimensions are
+ * calculated automatically from this value
+ * @param color the fill color (RGBA; alpha controls transparency)
*/
public SolidPolygonArrow(final Point3D startPoint, final Point3D endPoint,
- final double bodyRadius, final double tipRadius,
- final double tipLength, final int segments,
- final Color color) {
+ final double bodyRadius, final Color color) {
super();
// Calculate direction and distance
final Matrix3x3 rotMatrix = rotation.toMatrix();
// Calculate body length (distance minus tip)
- final double bodyLength = Math.max(0, distance - tipLength);
+ final double bodyLength = Math.max(0, distance - bodyRadius * TIP_LENGTH_FACTOR);
// Build the arrow components
if (bodyLength > 0) {
- addCylinderBody(startPoint, bodyRadius, bodyLength, segments, color, rotMatrix, nx, ny, nz);
+ addCylinderBody(startPoint, bodyRadius, bodyLength, SEGMENTS, color, rotMatrix, nx, ny, nz);
}
- addConeTip(endPoint, tipRadius, tipLength, segments, color, rotMatrix, nx, ny, nz);
+ addConeTip(endPoint, bodyRadius * TIP_RADIUS_FACTOR, bodyRadius * TIP_LENGTH_FACTOR, SEGMENTS, color, rotMatrix, nx, ny, nz);
setBackfaceCulling(true);
}
* <p><b>Local coordinate system:</b> In local space, the cone points in -Y direction
* (apex at lower Y). The base ring is at Y=0, and the apex is at Y=-length.</p>
*
- * @param endPoint the position of the arrow tip (cone apex)
- * @param radius the radius of the cone base
- * @param length the length of the cone
- * @param segments the number of segments around the circumference
- * @param color the fill color
- * @param rotMatrix the rotation matrix to apply
- * @param dirX direction X component
- * @param dirY direction Y component
- * @param dirZ direction Z component
+ * @param endPoint the position of the arrow tip (cone apex)
+ * @param radius the radius of the cone base
+ * @param length the length of the cone
+ * @param segments the number of segments around the circumference
+ * @param color the fill color
+ * @param rotMatrix the rotation matrix to apply
+ * @param dirX direction X component
+ * @param dirY direction Y component
+ * @param dirZ direction Z component
*/
private void addConeTip(final Point3D endPoint, final double radius,
final double length, final int segments,
--- /dev/null
+/*
+ * 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;
+
+import java.util.List;
+
+/**
+ * A renderable mesh composed of SolidPolygon triangles.
+ *
+ * <p>This is a generic composite shape that holds a collection of triangles.
+ * It can be constructed from any source of triangles, such as CSG operation
+ * results or procedural geometry generation.</p>
+ *
+ * <p><b>Usage:</b></p>
+ * <pre>{@code
+ * // From CSG result
+ * CSG result = cubeCSG.subtract(sphereCSG);
+ * SolidPolygonMesh mesh = result.toMesh(new Point3D(0, 0, 0));
+ * mesh.setShadingEnabled(true);
+ * mesh.setBackfaceCulling(true);
+ * shapes.addShape(mesh);
+ *
+ * // From list of triangles
+ * List<SolidPolygon> triangles = ...;
+ * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, new Point3D(0, 0, 0));
+ * }</pre>
+ *
+ * @see SolidPolygon the triangle type for rendering
+ */
+public class SolidPolygonMesh extends AbstractCompositeShape {
+
+ private int triangleCount;
+
+ /**
+ * Creates a mesh from a list of SolidPolygon triangles.
+ *
+ * @param triangles the triangles to include in the mesh
+ * @param location the position in 3D space
+ */
+ public SolidPolygonMesh(final List<SolidPolygon> triangles, final Point3D location) {
+ super(location);
+ this.triangleCount = 0;
+
+ for (final SolidPolygon triangle : triangles) {
+ addShape(triangle);
+ triangleCount++;
+ }
+ }
+
+ /**
+ * Returns the number of triangles in this mesh.
+ *
+ * @return the triangle count
+ */
+ public int getTriangleCount() {
+ return triangleCount;
+ }
+}
\ No newline at end of file
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.*;
/**
* Creates a canvas character at the specified location with given colors.
*
- * @param centerLocation the center position in 3D space
- * @param character the character to render
- * @param foregroundColor the foreground (text) color
- * @param backgroundColor the background color
+ * @param centerLocation the center position in 3D space
+ * @param character the character to render
+ * @param foregroundColor the foreground (text) color
+ * @param backgroundColor the background color
*/
public CanvasCharacter(final Point3D centerLocation, final char character,
final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor,
this.backgroundColor = backgroundColor;
- coordinates[0].coordinate = centerLocation;
+ vertices[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)
+ vertices[1].coordinate = centerLocation.clone().translateX(-halfWidth)
.translateY(-halfHeight);
// upper right
- coordinates[2].coordinate = centerLocation.clone().translateX(halfWidth)
+ vertices[2].coordinate = centerLocation.clone().translateX(halfWidth)
.translateY(-halfHeight);
// lower right
- coordinates[3].coordinate = centerLocation.clone().translateX(halfWidth)
+ vertices[3].coordinate = centerLocation.clone().translateX(halfWidth)
.translateY(halfHeight);
// lower left
- coordinates[4].coordinate = centerLocation.clone().translateX(-halfWidth)
+ vertices[4].coordinate = centerLocation.clone().translateX(-halfWidth)
.translateY(halfHeight);
}
/**
* Paints the character on the screen.
+ *
* @param renderingContext the rendering context
*/
@Override
// Draw background rectangle first. It is composed of two triangles.
drawPolygon(renderingContext,
- coordinates[1].onScreenCoordinate,
- coordinates[2].onScreenCoordinate,
- coordinates[3].onScreenCoordinate,
+ vertices[1].onScreenCoordinate,
+ vertices[2].onScreenCoordinate,
+ vertices[3].onScreenCoordinate,
mouseInteractionController,
backgroundColor);
drawPolygon(renderingContext,
- coordinates[1].onScreenCoordinate,
- coordinates[3].onScreenCoordinate,
- coordinates[4].onScreenCoordinate,
+ vertices[1].onScreenCoordinate,
+ vertices[3].onScreenCoordinate,
+ vertices[4].onScreenCoordinate,
mouseInteractionController,
backgroundColor);
if (desiredFontSize >= MAX_FONT_SIZE)
return;
- final Point2D onScreenLocation = coordinates[0].onScreenCoordinate;
+ final Point2D onScreenLocation = vertices[0].onScreenCoordinate;
// screen borders check
if (onScreenLocation.x < 0)
*/
public class WireframeArrow extends AbstractCompositeShape {
- /**
- * Constructs a 3D wireframe arrow pointing from start to end.
- *
- * <p>The arrow consists of a cylindrical body extending from the start point
- * towards the end, and a conical tip at the end point. If the distance between
- * start and end is less than or equal to the tip length, only the cone tip
- * is rendered.</p>
- *
- * @param startPoint the origin point of the arrow (where the body starts)
- * @param endPoint the destination point of the arrow (where the tip points to)
- * @param bodyRadius the radius of the cylindrical body
- * @param tipRadius the radius of the cone base at the tip
- * @param tipLength the length of the conical tip
- * @param segments the number of segments for cylinder and cone smoothness.
- * Higher values create smoother arrows. Minimum is 3.
- * @param appearance the line appearance (color, width) used for all lines
- */
- public WireframeArrow(final Point3D startPoint, final Point3D endPoint,
- final double bodyRadius, final double tipRadius,
- final double tipLength, final int segments,
- final LineAppearance appearance) {
+/**
+ * Default number of segments for arrow smoothness.
+ */
+private static final int DEFAULT_SEGMENTS = 12;
+
+/**
+ * Default tip radius as a fraction of body radius (2.5x).
+ */
+private static final double TIP_RADIUS_FACTOR = 2.5;
+
+/**
+ * Default tip length as a fraction of body radius (5.0x).
+ */
+private static final double TIP_LENGTH_FACTOR = 5.0;
+
+/**
+ * Constructs a 3D wireframe arrow pointing from start to end with sensible defaults.
+ *
+ * <p>This simplified constructor automatically calculates the tip radius as
+ * 2.5 times the body radius, the tip length as 5 times the body radius, and
+ * uses 12 segments for smoothness. For custom tip dimensions or segment count,
+ * use the full constructor.</p>
+ *
+ * @param startPoint the origin point of the arrow (where the body starts)
+ * @param endPoint the destination point of the arrow (where the tip points to)
+ * @param bodyRadius the radius of the cylindrical body; tip dimensions are
+ * calculated automatically from this value
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+public WireframeArrow(final Point3D startPoint, final Point3D endPoint,
+ final double bodyRadius, final LineAppearance appearance) {
+ this(startPoint, endPoint, bodyRadius,
+ bodyRadius * TIP_RADIUS_FACTOR,
+ bodyRadius * TIP_LENGTH_FACTOR,
+ DEFAULT_SEGMENTS, appearance);
+}
+
+/**
+ * Constructs a 3D wireframe arrow pointing from start to end with full control over all dimensions.
+ *
+ * <p>The arrow consists of a cylindrical body extending from the start point
+ * towards the end, and a conical tip at the end point. If the distance between
+ * start and end is less than or equal to the tip length, only the cone tip
+ * is rendered.</p>
+ *
+ * @param startPoint the origin point of the arrow (where the body starts)
+ * @param endPoint the destination point of the arrow (where the tip points to)
+ * @param bodyRadius the radius of the cylindrical body
+ * @param tipRadius the radius of the cone base at the tip
+ * @param tipLength the length of the conical tip
+ * @param segments the number of segments for cylinder and cone smoothness.
+ * Higher values create smoother arrows. Minimum is 3.
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+public WireframeArrow(final Point3D startPoint, final Point3D endPoint,
+ final double bodyRadius, final double tipRadius,
+ final double tipLength, final int segments,
+ final LineAppearance appearance) {
super();
// Calculate direction and distance
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; }
+ 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;
public void slice(final TexturedPolygon originalPolygon) {
considerSlicing(
- originalPolygon.coordinates[0],
- originalPolygon.coordinates[1],
- originalPolygon.coordinates[2],
+ originalPolygon.vertices[0],
+ originalPolygon.vertices[1],
+ originalPolygon.vertices[2],
originalPolygon);
}